前言
PE结构
DOS头
IMAGE_DOS_HEADER
PE头
介绍
总大小【248字节】
结构体含义
标记(4字节)0x4550
文件头(20字节)
扩展头(224字节)
为程序添加ExitProcess函数
前言
- 最近,学习了PE知识,为了更深层的掌握PE结构,使用纯手工写一个PE小程序,只弹出一个MessageBox的小程序
- 内容:Hello World!
PE结构
DOS头
IMAGE_DOS_HEADER
每一个PE文件是以一个DOS程序开始的,有了它,一旦程序在DOS下执行,DOS就能识别出这是有效的执行体,然后运行紧随MZ Header之后的DOS stub(DOS残余块,其实是一个有效的EXE,就是我们可以看到的一个错误提示:This program cannot be run in MS-DOS mode)。我们对于这个DOS stub可以忽略/删除,所以我在下面手写PE的时候,将DOS stub处删除。下面来看看IMAGE_DOS_HEADER的结构体定义,我将一些手写PE需要注意的几项给注释了下:
struct _IMAGE_DOS_HEADER {
WORD e_magic; // DOS可执行文件标记“MZ”,被#define IMAGE_DOS_SIGNATURE 0x5A4Dh
WORD e_cblp;
WORD e_cp;
WORD e_crlc;
WORD e_cparhdr;
WORD e_minalloc;
WORD e_maxalloc;
WORD e_ss;
WORD e_sp;
WORD e_csum;
WORD e_ip; // DOS代码入口IP
WORD e_cs; // DOS代码入口CS
WORD e_lfarlc;
WORD e_ovno;
WORD e_res[4];
WORD e_oemid;
WORD e_oeminfo;
WORD e_res2[10];
LONG e_lfanew; // 指向PE文件头“PE”,0,0
} ;
- MZ-DOS MZ头部(大小:64字节)
- 我们在010Editor中选择插入64个0
在IMAGE_DOS_HEADER中,有两个字段比较重要,分别是e_magic和e_lfanew字段(一个字大小)需要被设置为5A4Dh,这个值是#define的,在ASCII里,为“MZ”,是MS-DOS的最初创建者之一Mark Zbikowski字母的缩写,e_lfanew字段是真正PE文件头的想对偏移(RVA),作用是指出真正PE头的文件偏移位置(如下图):
从上面的结构体可以看出,它占4个字节,位于文件开始偏移3Ch字节中。 “PE文件标志”紧随“MS-DOS 实模式残余程序”其后。知道这一点,我们就可以计算一下了,我们的“DOS MZ header”总共64 byte,后面的“MS-DOS 实模式残余程序”占112 byte, 64 + 112 = 176 byte,但是要注意,我们这里的176可是十进制的,转化成十六进制是B0,对了,就是这个值,因为是4个字节,所以我们应该填“B0000000”。
由于我们是要将MS-DOS实模式残留程序部分删除,因此MZ-DOS紧接着是PE头,这里应该填写40000000。
所以我们现在将前两个字节填充为4D5A,在3C处填充为40000000。如图:
- MS-DOS (大小112字节)
在之前已经说过我们要删除MS-DOS部分,所以这部分就不必填写,如果填写,3C出的值应该修改。
PE头
介绍
在将准备工作做完以后,我们开始进入我们的重要部分,开始写真正的PE结构部分:
微软将“PE文件标志”,“PE文件头”,“PE文件扩展头”这三个部分用一个结构来定义,即:IMAGE_NT_HEADERS32(WINNT.H中有定义,后面象这样的结构均在WINNT.H中有定义)。
struct _IMAGE_NT_HEADERS {
0x00 DWORD Signature; ;PE文件标识
0x04 _IMAGE_FILE_HEADER FileHeader;
0x18 _IMAGE_OPTIONAL_HEADER OptionalHeader;
};
总大小【248字节】
结构体含义
标记(4字节)0x4550
第一个成员表示“PE文件标识”,可以看到他是一个DWORD类型,因此占4个字节,它是PE开始的标记,是一个#define IMAGE_NT_SIGNATURE定义了这个值,对Windows程序这个值必须为“50450000”。DOS头部的e_lfanew字段正是指向“PE\0\0”:#define IMAGE_NT_SIGNATURE 0x00004550
- 在010Editor编写后如下图:
文件头(20字节)
第二个成员表示“PE文件头 ”,他的类型是一个IMAGE_FILE_HEADER的结构。也就是说“PE文件头”的20个字节被定义为IMAGE_FILE_HEADER结构,第二个成员表示“PE文件头 ”,他的类型是一个IMAGE_FILE_HEADER的结构。也就是说“PE文件头”的20个字节被定义为IMAGE_FILE_HEADER结构。
struct _IMAGE_FILE_HEADER {
0x00 WORD Machine; ;运行平台
0x02 WORD NumberOfSections; ;文件的区块数目
0x04 DWORD TimeDateStamp; ;文件创建日期和时间
0x08 DWORD PointerToSymbolTable; ;指向符号表(用于调试)
0x0c DWORD NumberOfSymbols; ;符号表中符号个数(用于调试)
0x10 WORD SizeOfOptionalHeader; ;IMAGE_OPTIONAL_HEADER32结构的大小
0x12 WORD Characteristics; ;文件属性
};
- 这个结构具有7个成员(如图):
成员Machine:占2个字节,表示该文件运行所要求的CPU。对于Intel i386平台,该值是“4C01”。
成员NumberOfSections:占2个字节,表示该文件中段的总数,我们这里计划写3个段,(.text(代码段)、.rdata(只读数据段)、 .data(全局变量数据段))。所以此处值是“0300”。
成员TimeDateStamp:占4个字节,表示文件创建日期和时间,从1970.1.1 00:00:00以来的秒数,我们这里填“0000”即可。
成员PointerToSymbolTable:占4个字节,表示符号表的指针,主要用于调试,在这里填“0000”。
成员NumberOfSymbols:占4个字节,表示符号的数目,主要用于调试,在这里填“0000”。
成员SizeOfOptionalHeader:占2个字节,表示后面的“PE文件可选头 ”部分所占空间大小,我们已经知道“PE文件扩展头 ”的大小是224 byte,转换成十六进制就是E0,所以这里的值为“E000”
成员Characteristics:占2个字节,表示关于文件信息的标记,比如文件是exe还是dll。这个值实际上是二进制位进行或运算得到的值。
- 成员Characteristics各个二进制位表示的意义如下
Bit 0 :置1表示文件中没有重定向信息。每个段都有它们自己的重定向信息。这个标志在可执行文件中没有使用,在可执行文件中是用一个叫做基址重定向目录表来表示重定向信息的,这将在下面介绍。
Bit 1 :置1表示该文件是可执行文件(也就是说不是一个目标文件或库文件)。
Bit 2 :置1表示没有行数信息;在可执行文件中没有使用。
Bit 3 :置1表示没有局部符号信息;在可执行文件中没有使用。
Bit 4 :
Bit 7
Bit 8 :表示希望机器为32位机。这个值永远为1。
Bit 9 :表示没有调试信息,在可执行文件中没有使用。
Bit 10:置1表示该程序不能运行于可移动介质中(如软驱或CD-ROM)。在这种情况下,OS必须把文件拷贝到交换文件中执行。
Bit 11:置1表示程序不能在网上运行。在这种情况下,OS必须把文件拷贝到交换文件中执行。
Bit 12:置1表示文件是一个系统文件例如驱动程序。在可执行文件中没有使用。
Bit 13:置1表示文件是一个动态链接库(DLL)。
Bit 14:表示文件被设计成不能运行于多处理器系统中。
Bit 15:表示文件的字节顺序如果不是机器所期望的,那么在读出之前要进行交换。在可执行文件中它们是不可信的(操作系统期望按正确的字节顺序执行程序)。
- 注意:
- 因为我们写的是可执行程序,所以Bit 1 必须为1
- 因为我们写的是32位程序,所以Bit 8 必须为1
- 其它的按照需要置位即可
这里我们只将第2、8位置1,由此得到成员7的16进制为“0x0102”,转换成小端存储,其值为“0201”
- 文件头编写如下图:
扩展头(224字节)
第三个成员,表示“PE文件扩展头 ”,他的类型是一个IMAGE_OPTIONAL_HEADER32结构。也就是说“PE文件头 ”的224个字节被定义为IMAGE_OPTIONAL_HEADER32结构。
struct _IMAGE_OPTIONAL_HEADER {
0x00 WORD Magic; ;标志字
0x02 BYTE MajorLinkerVersion; ;链接器主版本号
0x03 BYTE MinorLinkerVersion; ;链接器次版本号
0x04 DWORD SizeOfCode; ;所有含有代码区块的总大小
0x08 DWORD SizeOfInitializedData; ;所有初始化数据区块总大小
0x0c DWORD SizeOfUninitializedData; ;所有未初始化数据区块总大小
0x10 DWORD AddressOfEntryPoint; ;程序执行入口的RVA
0x14 DWORD BaseOfCode; ;代码区块起始RVA
0x18 DWORD BaseOfData; ;数据区块起始RVA
0x1c DWORD ImageBase; ;程序默认装入基地址
0x20 DWORD SectionAlignment; ;内存中区块的对齐值
0x24 DWORD FileAlignment; ;文件中区块的对齐值
0x28 WORD MajorOperatingSystemVersion; ;操作系统主版本号
0x2a WORD MinorOperatingSystemVersion; ;操作系统副版本号
0x2c WORD MajorImageVersion; ;用户自定义主版本号
0x2e WORD MinorImageVersion; ;用户自定义副版本号
0x30 WORD MajorSubsystemVersion; ;所需要子系统主版本号
0x32 WORD MinorSubsystemVersion; ;所需要子系统次版本号
0x34 DWORD Win32VersionValue; ;保留,通常被设置为0
0x38 DWORD SizeOfImage; ;影响装入内存后的总尺寸
0x3c DWORD SizeOfHeaders; ;DOS头、PE头部、区块表总大小
0x40 DWORD CheckSum; ;影响校验和
0x44 WORD Subsystem; ;文件子系统
0x46 WORD DllCharacteristics; ;显示DLL特性的旗标
0x48 DWORD SizeOfStackReserve; ;初始化栈大小
0x4c DWORD SizeOfStackCommit; ;初始化实际提交栈大小
0x50 DWORD SizeOfHeapReserve; ;初始化保留堆大小
0x54 DWORD SizeOfHeapCommit; ;初始化实际保留堆大小
0x58 DWORD LoaderFlags; ;与调试有关,默认值为0
0x5c DWORD NumberOfRvaAndSizes; ;数据目录表的项数
0x60 _IMAGE_DATA_DIRECTORY DataDirectory[16];
};
- 这个结构公有31个成员:
- 成员1→Magic:占用2个字节,表示文件的格式
- 32为exe其值为:0x10b
- 64为exe其值为:0x20b
- ROM映像其值为:0x107
- 我们要写的是32位exe,所以应该“为0b01”
- 成员2→MajorLinkerVersion:占用1个字节,表示链接器的主版本号,此值不会影响程序的执行,我们这里填充0,其值为“00”
- 成员3→MinorLinkerVersion:占用1个字节,表示链接器的副版本号,此值不会影响程序的执行,我们这里填充0,其值为“00”
- 成员4→SizeOfCode:占用4个字节,表示可执行代码的长度,此值不会影响程序的执行,我们这里填充0,其值为“0000 0000”
- 成员5→SizeOfInitializedData:占用4个字节,表示初始化数据的长度(数据段),此值不会影响程序的执行,我们这里填充0,其值为“0000 0000”
- 成员6→SizeOfUninitializedData:占用4个字节,表示未初始化数据的长度(bss段),此值不会影响程序的执行,我们这里填充0,其值为“0000 0000”
(在介绍成员7之前,有必要了解一个很重要的知识------文件映射到内存。在可执行程序运行之前,PE加载器将把PE文件加载到进程空间的内存中去,并且初始化每个段实体。那么加载到内存中的哪个地址去呢?这将由IMAGE_OPTIONAL_HEADER32结构的成员10的值指出加载的起始地址(又叫基地址)。这个值通常是“00400000”, 那么PE文件的首地址“00000”就被映射到内存地址“00400000”处,那么相对于文件偏移10个字节的地址为“00010”,被映射到内存后的偏移也应该是10个字节,映射后的地址应该为“00400010”。)
- 成员7→AddressOfEntryPoint:占用4个字节,表示代码的入口RVA(文件映射到内存的偏移地址)地址,程序从这儿开始执行。PE装载器准备运行的PE文件的第一个指令的RVA。若您要改变整个执行的流程,可以将该值指定到新的RVA,这样新RVA处的指令首先被执行。那么这个值我们怎么得到呢?我们知道在文件中有个.text段,他包含了所有的代码,我们可以从中找到我们的入口地址,在这里就是.text段里的第一行代码,也就是.text段的首地址,而在.text段头部就给出了他映射到内存后的首地址的偏移,我们找到他取出添到此处,这里为“00100000”。(此处不理解没关系,我们讲完段结构后自能迎刃而解。)
- 成员8→BaseOfCode:占用4个字节,表示可执行代码起始位置。当然就是.text段的首地址,此值不会影响程序的执行,我们这里填充零,此值为“00000000”。
- 成员9→BaseOfData:占用4个字节,表示初始化数据的起始位置,此值不会影响程序的执行,我们这里填充零,此值为“00000000”。
- 成员10→ImageBase:占用4个字节,就是上面所讲的文件映射到内存是的基地址。PE文件的优先装载地址。通常设为“00400000”,PE装载器将尝试把文件装到虚拟地址空间的00400000h处。字眼"优先"表示若该地址区域已被其他模块占用,那PE装载器会选用其他空闲地址。我们这里的值设为“00004000”。
- 成员11→SectionAlignment:占用4个字节,表示段加载后在内存中的对齐方式。内存中节对齐的粒度。例如,如果该值是4096 (1000h),那么每节的起始地址必须是4096的倍数。若第一节从401000h开始且大小是10个字节,则下一节必定从402000h开始,即使401000h和402000h之间还有很多空间没被使用。因为Windows管理内存采用分页管理的方式,而每页的大小为4k,也就是1000h,所以我们这个值为“00100000”。
- 成员12→FileAlignment:占用4个字节,表示段在文件中的对齐方式。文件中节对齐的粒度。例如,如果该值是(200h),,那么每节的起始地址必须是512的倍数。若第一节从文件偏移量200h开始且大小是10个字节,则下一节必定位于偏移量400h: 即使偏移量512和1024之间还有很多空间没被使用。此值最好设为200h,所以该成员的值为“00020000”。
- 成员13→MajorOperatingSystemVersion:占用2个字节,表示操作系统主版本号,此值不会影响程序的执行,我们这里填充零,此值为“0000”。
- 成员14→MinorOperatingSystemVersion:占用2个字节,表示操作系统副版本号,此值不会影响程序的执行,我们这里填充零,此值为“0000”。
- 成员15→MajorImageVersion:占用2个字节,表示程序主版本号,此值不会影响程序的执行,我们这里填充零,此值为“0000”。
- 成员16→MinorImageVersion:占用2个字节,表示程序副版本号,此值不会影响程序的执行,我们这里填充零,此值为“0000”。
- 成员17→MajorSubsystemVersion:占用2个字节,表示子系统主版本号。win32子系统版本。通常对于Windows NT 3.10而言,这个值被设为3, 这里写做“0600”。
- 成员18→MinorSubsystemVersion:占用2个字节,表示子系统副版本号。win32子系统版本。通常对于Windows NT 3.10而言,这个值被设为10, 这里写做“0000”。
- 成员19→Win32VersionValue:占用4个字节,保留,这个值一般设置为“00000000”。
- 成员20→SizeOfImage:占用4个字节,表示程序调入后占用内存大小(字节),等于所有段的长度之和。所有头和节经过节对齐处理后的大小。我们知道,我们文件PE结构总长小于1000h,但是内存中的对齐粒度是1000h,所以PE结构被映射后要占1000h,尽管很多空间没有使用,另外我们有3个段,每个段的长度小于1000h,但是被映射后同样要占1000h,所以总共占用内存的大小为1000h + 3 * 1000h = 4000h,因此此值为“00400000”。
- 成员21→SizeOfHeaders:占用4个字节,表示所有文件头的长度之和(从文件开始到第一个段之间的大小)。所有头+节表的大小,也就等于文件尺寸减去文件中所有节的尺寸。可以以此值作为PE文件第一节的文件偏移量。那么我们怎么得到这个值呢?我们的PE文件结构总大小为:64 + 4 + 20 + 224 + 3 * 40 = 432 byte 转化成十六进制为1B0h,那么此值就是1B0h吗?+ 112已经砍掉
不是的,因为我们文件中的对齐粒度是200h,那么1B0h实际上要占用200h的空间,所以此值为“00020000”。
- 成员22→CheckSum:占用4个字节,保留,这个值一般设置为“00000000”。
- 成员23→Subsystem:占用2个字节,表示NT子系统,可能是以下的值:
Windows程序总是用WIN32子系统,所以只有2和3是合法的值。也就是说此值必须为2或3,如果是3,那么程序运行后会自动打开一个控制台,我们为了看一下效果,这里设为3,此值为“0300“。
- 成员24→DllCharacteristics:占用2个字节,表示Dll状态,我们这里填充零,此值为“0000”。
- 成员25→SizeOfStackReserve:占用4个字节,保留栈大小,我们这里填充零,此值为“00000000”。
- 成员26→SizeOfStackCommit:占用4个字节,启动后实际申请的栈数,可随实际情况变大,我们这里填充零,此值为“00000000”。
- 成员27→SizeOfHeapReserve:占用4个字节,保留堆大小,我们这里填充零,此值为“00000000”。
- 成员28→SizeOfHeapCommit:占用4个字节,实际堆大小,我们这里填充零,此值为“00000000”。
- 成员29→LoaderFlags:占用4个字节,装载标志,我们这里填充零,此值为“00000000”。
- 成员30→NumberOfRvaAndSizes:占用4个字节,在讲这个成员之前,我们应该先了解成员31,成员31实际上是一个IMAGE_DATA_DIRECTORY结构的数组,成员30的值就是表示该数组的大小。通常有16个元素,所以此值为:“10000000”。
- 成员31→IMAGE_DATA_DIRECTORY:它是一个结构体数组。通常有16个元素
struct _IMAGE_DATA_DIRECTORY {
0x00 DWORD VirtualAddress; // 数据的起始RVA
0x04 DWORD Size; // 数据块的长度
};
- IMAGE_DATA_DIRECTORY结构有两个成员,各占4个字节(可由上图清新的看出),那么也就得到成员31【IMAGE_DATA_DIRECTORY】的总大小:2 * 4 * 16 = 128byte。
- 其中每个元素代表一个目录表,每个目录表表示目录如下:
IMAGE_DIRECTORY_ENTRY_EXPORT 0 // 导出表
IMAGE_DIRECTORY_ENTRY_IMPORT 1 // 导入表
IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // 资源表
IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // 异常目录
IMAGE_DIRECTORY_ENTRY_SECURITY 4 // 安全目录
IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // 重定位
IMAGE_DIRECTORY_ENTRY_DEBUG 6 // 调试目录
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // 描述版权串
IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // 描述版权串
IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // 机器值
IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS目录
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load configuration 目录
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import 目录
IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table输入地址表目录
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
-
- 是不是所有的目录表都要关心呢?其实要把这些目录表都研究清楚是个很大的课题,对于我们这个程序,只需关心第2个元素,导入目录,它标识了我们的程序从其他模块导入的函数信息。因为我们要显示一个消息框,所以要导入user32.dll库中的MessageBoxA函数,程序退出,又要导入kernel32.dll库中的ExitProcess函数,这个目录表需要使用。然而上面已说明每个目录是一个IMAGE_DATA_DIRECTORY结构,该结构具有两个成员,第一个成员表示目录表的起始RVA地址,第二个成员表示目录表的长度。这两个值要根据.rdata段实体来确定,暂时先不填写。为了记录该位置,我们先都填写为x(0x78),即: “xxxxxxxx","xxxxxxxx"。其余的统统添零即可。
typedef struct _IMAGE_SECTION_HEADER {
0x00 BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
0x08 DWORD PhysicalAddress;
0x08 DWORD VirtualSize; // 节区尺寸
} Misc;
0x0c DWORD VirtualAddress; // 区块的RVA地址
0x10 DWORD SizeOfRawData; // 在文件中对齐后的尺寸
0x14 DWORD PointerToRawData; // 在文件中偏移
0x18 DWORD PointerToRelocations; // 在OBJ文件中使用,重定位的偏移
0x1c DWORD PointerToLinenumbers; // 行号表的便宜(供调试用)
0x20 WORD NumberOfRelocations; // 在OBJ文件中使用,重定位项数目
0x22 WORD NumberOfLinenumbers; // 行号表中行号的数目
0x24 DWORD Characteristics; // 区块的属性
};
- 节表大小(0x28)
- 创建0x28大小的数据
- 各个成员分布如下图:
- 成员1→Name:8个字节,表识该段的名称,我们这里是.text,那么此值是他的ASCII码应该为“2E74657874000000”。
- 成员2→PhysicalAddress/VirtualSize:4个字节,表示有效代码所占的字节数。我们这里所有代码数一下总共12h个(该值可在编写完代码后确定其大小),固此值为“12000000”。
- 成员3→VirtualAddress:4个字节,表示在.text段映射到内存中的起始地址,那么这个值如何得来呢?我们知道.text是紧跟PE结构后的,然后整个PE结构映射到内存后占的大小为1000h(因为PE结构小于1000h个字节,而对齐力度粒度是1000h),那么此值便得到了,为“00100000”。
- 成员4→SizeOfRawData:4个字节,表示.text段在文件中所占的大小。因为我们的实际代码只有16h个字节,那么这个值是不是26h呢?并不是,一定要注意段在文件中的对齐粒度是200h,所以此值为“00020000”。
- 成员5→PointerToRawData:4个字节,表示.text段在文件中的起始地址,上面已经计算过PE文件的总长度为200h,他实际上也就是.text的起始偏移地址,此值为“00020000”。
- 成员6→PointerToRelocations:4个字节,仅用于目标文件,我们这里填为零。
- 成员7→PointerToLinenumbers:4个字节,仅用于目标文件,我们这里填为零。
- 成员8→NumberOfRelocations:2个字节,仅用于目标文件,我们这里填为零。
- 成员9→NumberOfLinenumbers:2个字节,仅用于目标文件,我们这里填为零。
- 成员10→Characteristics:4个字节。包含标记以指示节属性,比如节是否含有可执行代码、初始化数据、未初始数据,是否可写、可读等。这个值实际上是二进制位进行或运算得到的值。各二进制位表示的意义如下:
bit 5 (IMAGE_SCN_CNT_CODE),置1,节内包含可执行代码。
bit 6 (IMAGE_SCN_CNT_INITIALIZED_DATA)置1,节内包含的数据在执行前是确定的。
bit 7 (IMAGE_SCN_CNT_UNINITIALIZED_DATA) 置1,本节包含未初始化的数据,执行前即将被初始化为0。一般是BSS.
bit 9 (IMAGE_SCN_LNK_INFO) 置1,节内不包含映象数据除了注释,描述或者其他文档外,是一个目标文件的一部分,可能是针对链接器的信息。比如哪个库被需要。
bit 11 (IMAGE_SCN_LNK_REMOVE) 置1,在可执行文件链接后,作为文件一部分的数据被清除。
bit 12 (IMAGE_SCN_LNK_COMDAT) 置1,节包含公共块数据,是某个顺序的打包的函数。
bit 15 (IMAGE_SCN_MEM_FARDATA) 置1,不确定。
bit 17 (IMAGE_SCN_MEM_PURGEABLE) 置1,节的数据是可清除的。
bit 18 (IMAGE_SCN_MEM_LOCKED) 置1,节不可以在内存内移动。
bit 19 (IMAGE_SCN_MEM_PRELOAD)置1, 节必须在执行开始前调入。
Bits 20 to 23指定对齐。一般是库文件的对象对齐。
bit 24 (IMAGE_SCN_LNK_NRELOC_OVFL) 置1, 节包含扩展的重定位。
bit 25 (IMAGE_SCN_MEM_DISCARDABLE) 置1,进程开始后节的数据不再需要。
bit 26 (IMAGE_SCN_MEM_NOT_CACHED) 置1,节的 数据不得缓存。
bit 27 (IMAGE_SCN_MEM_NOT_PAGED) 置1,节的 数据不得交换出去。
bit 28 (IMAGE_SCN_MEM_SHARED) 置1,节的数据在所有映象例程内共享,如DLL的初始化数据。
bit 29 (IMAGE_SCN_MEM_EXECUTE) 置1,进程得到“执行”访问节内存。
bit 30 (IMAGE_SCN_MEM_READ) 置1,进程得到“读出”访问节内存。
bit 31 (IMAGE_SCN_MEM_WRITE)置1,进程得到“写入”访问节内存。
在我们这里,因为这是代码段,所以bit 5 (IMAGE_SCN_CNT_CODE)位置1,一般代码段都含需要执行权限,那么bit 30 (IMAGE_SCN_CNT_INITIALIZED_DATA)位置1,又因为代码段的代码可以执行的,所以bit 29 (IMAGE_SCN_MEM_EXECUTE) 置1,那么这3个二进制位进行或运算最终得到此成员值“20000060”。
这样,整个.text段就编写完毕,按照上面的方法,分别在编写.rdata段和.data段。因为要对齐,所以后面的代码用0补齐。
最后编写的结果如下图:
至此,我们已经完成了PE结构的编写,但是,此时的程序还不能够运行,我们还需要耐心的不上点东西。
为了让我们写的程序可以运行,我们还要完成.text(代码段)、.rdata(只读数据段)、.data(全局变量数据段)三个段的实体部分。
首先编写.text段,他紧接着PE结构后面,但是我们如何编写这些内容呢?前面已经说过,.text段中存放所有的可执行代码(机器码),我们可以通过先编写汇编指令(调用MessageBoxA和ExitProcess两个函数),然后反汇编出机器码抄到这里就可以了。这里有一点要注意,我们在为MessageBoxA函数传递参数时,如何将“Hello World!”字符串传递进去,这就要用到我们的.data(全局变量数据段),我们把这两个字符串放到这个段中,然后把字符串的偏移首地址作为参数传给MessageBoxA即可。因为要以200h对齐,所以剩余部分用0补齐,最终的到的代码如下:
接下来完成.rdata段,这个段非常重要,也有些繁琐。要写入导入表(_IMAGE_IMPORT_DESCRIPTOR),相当的繁琐,我们找一个汇编编译过的程序,使用010Editor中,分析下它的导入表(这里只导入了一个MessageBoxA),如图:
由此,我们可以完成.rdata段的编写了,写好的代码如下:
最后一个是.data段,这个非常简单,就是MessageBoxA所需要的参数,消息框的内容,最终代码如下(注意对齐问题【200h】):
此时程序还不能运行,修改导入表偏移和大小:xxxxxxxx处
可使用LoadPE,根据文件偏移处计算出来
注意:去除IAT后的文件偏移位置
修改后的导入表位置及大小,如下图:
至此,手写PE就完成了。
为程序添加ExitProcess函数
- 还是以HelloWorld程序为例,使用汇编语言添加ExitProcess函数,进行导入表分析
- 将数据写到自己的导入表中,如下:
- 修改导入表位置及大小
- 通过导入表文件偏移(排除IAT),计算出导入表的位置,修改如下图:
- 至此,添加ExitProcess函数完成
附件列表
- 通过导入表文件偏移(排除IAT),计算出导入表的位置,修改如下图:
原文地址:https://www.cnblogs.com/PhantomW/p/10566067.html