DOS头
一个PE文件起始的部分开始就是DOS头,不要觉得难理解,这和GIF,BMP,JPG,RAR等等都一样,只是在文件起始部位标识这个文件属于什么文件类型。
下面是DOS头在winnt.h中的定义:
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; // Maximum extra paragraphs needed
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;
定义这些结构就是为了计算在DOS头中这些东西的偏移,要修改PE偏移就需要修改DOS头中的e_lfanew
,你可以将文件内容读取到内存中,然后将char*强转为PIMAGE_DOS_HEADER类型,通过指针就可以方便的修改,写回文件即可。
本篇内容因为要更直观的看结构,所以不会使用代码方式修改DOS头或PE头,所以我这里使用C32。
PE头
PE文件的全称是Portable Executable,所以人们常常把NT头叫PE头。
winnt.h中定义的结构体名称为IMAGE_NT_HEADERS
,我个人觉得叫PE头可能还一个原因。就是在内存中NT头的Signature
(NT头起始位置)是个0x50 0x45,转为字符串就是PE,所以叫PE头了。文章中我就叫PE头了。
上面的DOS头结尾部分e_lfanew
的内容是0x1001,我们知道windows中使用小端存储,e_lfanew
不是一个字符串数组,所以要区分低位高位,0x1001存储时实际的long应该为0x0110。
我们得到了DOS头偏移多少是PE头位置
,实际上这个偏移不需要计算,因为DOS头本身就是文件开始的位置,0x0+0x110还是0x110,我们直接找到0x110的地址就是PE头了。
下面是winnt.h中PE头的定义:
//x86
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
//x64
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
其中FileHeader
中包含了这些:
//x86 x64相同
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
IMAGE_OPTIONAL_HEADER
太长不贴了,有兴趣自己看一眼,其中有链接器版本,(AddressOfEntryPoint) 程序执行入口RVA,(BaseOfCode)代码的区块的起始RVA,(BaseOfData)数据的区块的起始RVA等等。
PE头移位
首先我们知道了PE头的偏移位置,我们还要知道PE头大小,否则移位时到底移哪块内容呢?PE头大小其实就在NT头中的FileHeader
的SizeOfOptionalHeader
中。经过计算是PE头向后偏移0x14
因为它是个WORD,所以是2字节,0x00F0=240,我们将PE头开始到之后的240个字节拷贝一下。
因为我演示的程序是x64,所以在RVA计算时地址大小是8字节,所以我得移动8字节的倍数,事实上4字节也可以,并不影响,但是为了区分x64和x86,我向前移动8个字节。
完成了移动后,我们还要去修改e_lfanew
,因为他记录了PE头偏移还记得吧?
0x110-0x8= 0x108
我们修改为0801
现在PE头移动就完成了,但是不出意外,这程序是无法运行了,因为我们仅仅改了PE头的位置和DOS头记录的偏移,这将导致IMAGE_SECTION_HEADER
的位置(section)出错,因为我们向上偏移了8个字节。
我们现在需要将PE头大小+8,实际上PE头大小没有变,+8的原因是为了让他计算section位置时+8,因为我们向上移动了8个字节。还记得PE头大小在哪里吧?