我们先写一个win32的小程序,vc++6.0,选择release模式,我简单的写了一个对话框的程序
Tiny.h是我在黑客防线找到的一个库函数,可以简化程序的大小,本来大小36kb的程序变为2.5kb,我们用这个程序做对照。
写PE文件,首先了解一下PE文件的格式
MS-DOS头(_IMAGE_DOS_HEADER ) (64type)————LONG e_lfanew
|
|
MS-DOS实模式残余程序 112byte
|
|
NT头 | -- PE文件头(_IMAGE_FILE_HEADER) 20byte
(_IMAGE_NT_HEADERS) ---------------------|
| | -- PE文件可选头(_IMAGE_OPTIONAL_HEADER) 224byte
|
节表(_IMAGE_SECTION_HEADER) n*40byte
|
|
节数据
创建PE文件,并用C32打开,并参照我们之前写的,注意数据的存储方式高高低低的原则,接下来写DOS头和PE文件标志
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maxim
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
第一个成员占2个字节,它被用于表示一个MS-DOS兼容的文件类型,他的值是固定的----“4D5A”。 (注意:因为我们是在十六进制编辑器下写数据,所以所有的数据格式都是十六进制式的。但是我们在开发环境中默认 都是十进制的,所以必须在数据前加 0x ,即:0x4D5A。而我为了方便,就直接写成“4D5A”,也就是直接输入到编辑器中的值,是十六进制)
第2个成员到第18个成员总共58个字节,是对DOS程序环境的初始化等操作,对于我们这个程序来说,没什么影响, 我们通通用“00”来填充注意:因为我们不可能把PE结构所。(如果读者想对其进行详细了解,请查阅相关书籍。) 有的东西都面面俱到,他十分的庞大。当然也没有必要都去记他,只需掌握关键的地方就可以了。以后我们都将把不 影响程序执行的成员填充为零,这样做,一方面使程序看起来简洁,另一方面可以使快速定位PE结构中要重点掌握的地方。
第19个成员非常重要,他占4个字节,用来表示“PE文件标志”在文件中的偏移,单位是byte。而从上面的示意图可以看到 “PE文件标志”紧随“MS-DOS 实模式残余程序”其后。
知道这一点,我们就可以计算一下了,我们的“DOS MZ header”总 共64 byte,后面的“MS-DOS 实模式残余程序”占112 byte, 64 + 112 = 176 byte,但是要注意,我们这里的176可是十进制 的,转化成十六进制是B0,对了,就是这个值,因为是4个字节,所以我们应该填“B0000000”。 到这里你先别急我们看一下我们的参考程序是多少?他的地址是B8000000,这里我们为了同他一致就多填写8个字节然后这里填入B8000000 .
查看_IMAGE_NT_HEADERS 结构:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
第一个成员表示“PE文件标志”,可以看到他是一个DWORD类型,因此占4个字节,它是PE开始的标记,对Windows程 序这个值必须为“50450000”,即PE字符。
认识_IMAGE_FILE_HEADER结构:
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; //运行平台 intel i386一般为14Ch
WORD NumberOfSections; //块(section)数目
DWORD TimeDateStamp; //时间日期标记
DWORD PointerToSymbolTable; //COFF符号指针,
这是程序调试信息
DWORD NumberOfSymbols; //符号数
WORD SizeOfOptionalHeader; //可选部首长度,
是IMAGE_OPTIONAL_HEADER的长度
WORD Characteristics; //文件属性
}
写入_IMAGE_FILE_HEADER数据:
成员1,占2个字节,表示该文件运行所要求的CPU。对于Intel平台,该值是“4C01”。
成员2,占2个字节,表示该文件中段的总数,我们这里计划写3个段(用Pelorder查看参考程序)。(.text(代码段)、.rdata(只读数据段)、 .data(全局变量数据段))。所以此处值是“0300”。
成员3,占4个字节,表示文件创建日期和时间,从1970.1.1 00:00:00以来的秒数,我们这里填“0000”即可。
成员4,占4个字节,表示符号表的指针,主要用于调试,在这里填“0000”。
成员5,占4个字节,表示符号的数目,主要用于调试,在这里填“0000”。
成员6,占2个字节,表示后面的“PE文件可选头 ”部分所占空间大小,我们已经知道“PE文件可选头 ”的大小是 224 byte,转换成十六进制就是E0,所以这里的值为“E000”。
成员7,占2个字节,表示关于文件信息的标记,比如文件是exe还是dll。这个值实际上是二进制位进行或运算得到的值。
各二进制位表示的意义如下:
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,其他的按照需要置位即可,由此得到成员7的值为”0200”
写入_IMAGE_OPTIONAL_HEADER数据
//IMAGE_OPTIONAL_HEADER结构(可选映像头)
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic; //幻数,一般为10BH
BYTE MajorLinkerVersion; //链接程序的主版本号
BYTE MinorLinkerVersion; //链接程序的次版本号
DWORD SizeOfCode; //代码段大小
DWORD SizeOfInitializedData; //已初始化数据块的大小
DWORD SizeOfUninitializedData; //未初始化数据库的大小
DWORD AddressOfEntryPoint; //程序开始执行的入口地址,这是一个RVA(相对虚拟地址)
DWORD BaseOfCode; //代码段的起始RVA
DWORD BaseOfData; //数据段的起始RVA
//
// NT additional fields.
//
DWORD ImageBase; //可执行文件默认装入的基地址
DWORD SectionAlignment; //内存中块的对齐值(默认的块对齐值为1000H,4KB个字节)
DWORD FileAlignment;//文件中块的对齐值(默认值为200H字节,为了保证块总是从磁盘的扇区开始的)
WORD MajorOperatingSystemVersion;//要求操作系统的最低版本号的主版本号
WORD MinorOperatingSystemVersion;//要求操作系统的最低版本号的次版本号
WORD MajorImageVersion;//该可执行文件的主版本号
WORD MinorImageVersion;//该可执行文件的次版本号
WORD MajorSubsystemVersion;//要求最低之子系统版本的主版本号
WORD MinorSubsystemVersion;//要求最低之子系统版本的次版本号
DWORD Win32VersionValue;//保留字
DWORD SizeOfImage;//映像装入内存后的总尺寸
DWORD SizeOfHeaders;//部首及块表的大小
DWORD CheckSum;//CRC检验和
WORD Subsystem;//程序使用的用户接口子系统
WORD DllCharacteristics;//DLLmain函数何时被调用,默认为0
DWORD SizeOfStackReserve;//初始化时堆栈大小
DWORD SizeOfStackCommit;//初始化时实际提交的堆栈大小
DWORD SizeOfHeapReserve;//初始化时保留的堆大小
DWORD SizeOfHeapCommit;//初始化时实际提交的对大小
DWORD LoaderFlags;//与调试有关,默认为0
DWORD NumberOfRvaAndSizes;//数据目录结构的数目
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];//数据目录表
}
具有31个成员
成员1,占2个字节,表示文件的格式,值为0x010B表示.EXE文件,为0x0107表示ROM映像,因为我们写的是一个可执行程序,所以此值应该为“0B01”
成员2,占1个字节,表示链接器的主版本号,此值不会影响程序的执行,我们这里填充零,此值为“00”
成员3,占1个字节,表示链接器的幅版本号,此值不会影响程序的执行,我们这里填充零,此值为“00”。
成员4,占4个字节,表示可执行代码的长度,此值不会影响程序的执行,我们这里填充零,此值为“00000000”。
成员5,占4个字节,表示初始化数据的长度(数据段)。此值不会影响程序的执行,我们这里填充零,此值为“00000000”。
成员6,占4个字节,表示未初始化数据的长度(bss段)。此值不会影响程序的执行,我们这里填充零,此值为“00000000”。
成员7 ,4个字节,表示代码的入口RVA(文件映射到内存的偏移地址)地址,程序从这儿开始执行。PE装载器准备运行的PE文件的第一个指令的RVA。若您要改变整个执行的流程,可以将该值指定到新的RVA,这样新RVA处的指令首先被执行。
那么这个值我们怎么得到呢?我们知道在文件中有个.text段,他包含了所有的代码,我们可以从中找到我们的入口地址,在这里就是.text段里的第一行代码,也就是.text段的首地址,而在.text段头部就给出了他映射到内存后的首地址的偏移,我们查看已经做好的demo找到他取出添到此处,这里为“00100000”。
成员8,4个字节,表示可执行代码起始位置。当然就是.text段的首地址,此值不会影响程序的执行,我们这里填充零,此值为“00000000”。
成员9,4个字节,表示初始化数据的起始位置,此值不会影响程序的执行,我们这里填充零,此值为“00000000”。
成员10,4个字节,就是文件映射到内存是的基地址。PE文件的优先装载地址。通常设为“00400000”,PE装载器将尝试把文件装到虚拟地址空间的00400000h处。“优先”表示若该地址区域已被其他模块占用,那PE装载器会选用其他空闲地址。我们这里的值设为“00400000”。
成员11,4个字节,表示段加载后在内存中的对齐方式。内存中节对齐的粒度。例如,如果该值是4096 (1000h),那么每节的起始地址必须是4096的倍数。若第一节从401000h开始且大小是10个字节,则下一节必定从402000h开始, 即使401000h和402000h之间还有很多空间没被使用。因为Windows管理内存采用分页管理的方式,而每页的大小为4k,也就是1000h,所以我们这个值为”00100000”。
成员12,4个字节,表示段在文件中的对齐方式。文件中节对齐的粒度。例如,如果该值是(200h),,那么每节的起始地址必须是512的倍数。若第一节从文件偏移量200h开始且大小是10个字节,则下一节必定位于偏移量400h: 即使偏移 量512和1024之间还有很多空间没被使用。此值最好设为200h,所以该成员的值为“00020000”。
成员13,2个字节,表示操作系统主版本号,此值不会影响程序的执行,我们这里填充零,此值为“0000”。 成员14,2个字节,表示操作系统副版本号,此值不会影响程序的执行,我们这里填充零,此值为“0000”。 成员15,2个字节,表示程序主版本号,此值不会影响程序的执行,我们这里填充零,此值为“0000”。 成员16,2个字节,表示程序副版本号,此值不会影响程序的执行,我们这里填充零,此值为“0000”。
成员17,2个字节,表示子系统主版本号。win32子系统版本。PE文件是专门为Win32设计的,该子系统版本必定是4.0那么此处值为“04 00”。 成员18,2个字节,表示子系统副版本号,此值应为“00 00”。 成员19,4个字节,此值一般为“00 00 00 00”。
成员20,4个字节,表示程序调入后占用内存大小(字节),等于所有段的长度之和。所有头和节经过节对齐处理后 的大小。我们知道,我们文件PE结构总长小于1000h,但是内存中的对齐粒度是1000h,所以PE结构被映射后要占1000h,尽管很多空间没有使用,另外我们有3个段,每个段的长度小于1000h,但是被映射后同样要占1000h,所以总共占用内存的大小为1000h + 3 * 1000h = 4000h,因此此值为“00400000”。
成员21,4个字节,表示所有文件头的长度之和,这里指的是文件大小非映射到内存的大小(即从文件开始到第一个段之间的大小)。也就是所有头+节表的大小,也就等于文件尺寸减去文件中所有节的尺寸。那么我们怎么得到这个值呢?我们的可以以此值作为PE文件第一节的文件偏移量。PE文件结构总大小为:
64 + 112 + 4 + 20 + 224 + 3 * 40 = 544 byte 转化成十六进制为220h,那么此值就是220h吗? 不是的,因为我们文件中的对齐粒度是200h,那么220h实际上要占用400h的空间,所以此值为“00040000”。
成员22,4个字节,表示校验和。它仅用在驱动程序中,在可执行文件中可能为0。它的计算方法Microsoft不公开,在imagehelp.dll中的CheckSumMappedFile()函数可以计算它,此处我们设为填充零,此值为“00000000”。
成员23,2个字节,表示NT子系统,可能是以下的值:
IMAGE_SUBSYSTEM_NATIVE (1) 不需要子系统。用在驱动程序中。 IMAGE_SUBSYSTEM_WINDOWS_GUI(2) WIN32 graphical程序(它可用AllocConsole()来打开一个控制台,但是不能在一开始自动得到)。 IMAGE_SUBSYSTEM_WINDOWS_CUI(3) WIN32 console程序(它可以一开始自动建立)。 IMAGE_SUBSYSTEM_OS2_CUI(5) OS/2 console程序(因为程序是OS/2格式,所以它很少用在PE)。 IMAGE_SUBSYSTEM_POSIX_CUI(7) POSIX console程序。 Windows程序总是用WIN32子系统,所以只有2和3是合法的值。也就是说此值必须为2或3,如果是3,那么程序运行后会自动打开一个控制台,我们这里设为2,此值为“0200“。
成员24,2个字节,表示Dll状态,我们这里填充零,此值为“0000”。
成员25,4个字节,保留堆栈大小,我们这里填充零,此值为“00000000”。
成员26,4个字节,启动后实际申请的堆栈数,可随实际情况变大,我们这里填充零,此值为“00000000”。
成员27,4个字节,保留堆大小,我们这里填充零,此值为“00000000”。
成员28,4个字节,实际堆大小,我们这里填充零,此值为“00000000”。
成员29,4个字节,装载标志,我们这里填充零,此值为“00000000”.
成员30,4个字节,在讲这个成员之前,我们应该先了解成员31,成员31实际上是一个IMAGE_DATA_DIRECTORY结构的数组,成员30的值就是表示该数组的大小。通常有16个元素,所以此值为:“10000000”。
如图所示
写入_IMAGE_DATA_DIRECTORY成员
打开PE文件结构图文件,通过看可选头我们可以看到可选头的结尾是16个_IMAGE_DATA_DIRECTORY数组,这就是我们为什么在第30个成员中填入十六16个进制的10h,也就是十进制的16的原因了,代表有16个_IMAGE_DATA_DIRECTORY数组。
typedef struct _IMAGE_DATA_DIRECTORY{
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
成员1表示该目录所指向的数据的内存偏移即RVA。
成员2表示该目录所指向的内存数据的大小。
成员31,128个字节,上面说过他是一个IMAGE_DATA_DIRECTORY结构的数组,通常具有16个元素。
IMAGE_DATA_DIRECTORY结构有两个成员,各占4个字节,那么也就得到成员31的总大小:2 * 4 * 16 = 128byte。
写入_IMAGE_SECTION_HEADER 数据:
接下来我们动手来写.text段
成员1,8个字节,表识该段的名称,我们这里是.text,那么此值是他的ASKII码应该为“2E74657874000000”。
成员2,4个字节,表示有效代码所占的字节数。我们这里所有代码数一下总共16h个,固此值为“16000000”。
成员3,4个字节,表示在.text段映射到内存中的起始地址,那么这个值如何得来呢?我们知道.text是紧跟PE结构后的,然后整个PE结构映射到内存后占的大小为1000h(因为PE结构小于1000h个字节,而对齐力度粒度是1000h),那么此值便得到了,“00100000”。
成员4,4个字节,表示.text段在文件中所占的大小。我们的实际代码只有16h个字节,那么这个值是不是16h呢?并不是,一定要注意段在文件中的对齐粒度是200h,所以此值为“00020000”。
成员5,4个字节,表示.text段在文件中的起始地址,上面已经计算过PE文件的总长度为400h,他实际上也就是.text的起始偏移地址,此值为“00040000”。
成员6,7,8,9,均占4个字节,都仅用于目标文件,我们这里统统填为零。
成员10,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 6 (IMAGE_SCN_CNT_INITIALIZED_DATA)位置1,有因为代码段的代码可以执行的,所以bit 29 (IMAGE_SCN_MEM_EXECUTE) 置1,那么这3个二进制位进行或运算最终得到此成员值“20000060”。
下一篇写其他两个段的信息