- IMAGE_DOS_HEADER结构(DOS头部)
typedef struct _IMAGE_DOS_HEADER {
WORD e_magic; // MZ标志
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;
WORD e_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文件头偏移
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
上述为IMAGE_DOS_HEADER结构结构体,最重要的参数只有两个—— e_magic, e_lfanew。 e_magic是MZ标志位,为0x5A4D, e_lfanew放着PE文件开头的位置0x000000f8(0x3c-0x3f),根据e_lfanew可以跳过IMAGE_DOS_HEADER来到PE头部
- IMAGE_NT_HEADERS结构学习(PE头部)
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
从上文中的e_lfanew(0x000000b8),我们可以直接找到PE文件头部,即IMAGE_NT_HEADERS中的Signature,为0x50450000(0xb8-0xbb)。IMAGE_NT_HEADERS中第二个元素为IMAGE_FILE_HEADER FileHeader,标明PE文件的一些属性,其结构体及其内部元素解释如下
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;
Machine指明PE文件支持的CPU类型,0x014c表明是Intel i386的CPU
NumberOfSections指明PE文件包含的节区数量,0x0004表明有4块
TimeDateStamp字段指明文件的创建时间,为1970年1月1日以来用格林威治时间计算的秒数
SizeOfOptionalHeader字段,指明在IMAGE_NT_HEADERS中紧跟在FileHeader后的OptionalHeader的大小,对于32位的PE文件而言,这个值的大小通常为0x00E0;
Characteristics字段,表明文件的属性。如果Characteristics & 0x2000 = 0x2000,那么表明这是一个DLL文件
IMAGE_NT_HEADERS中第三个元素为 IMAGE_OPTIONAL_HEADER32 OptionalHeader,用于为加载器提供加载信息,注意OptionalHeader的大小并不固定,位置在0xb8(e_lfanew)+0x4(size(Signature))+0x14(size(IMAGE_FILE_HEADER))=0xd0处,其大小由FileHeader中的SizeOfOptionalHeader字段来决定。其结构体及其内部元素解释如下:
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
Magic,表明是ROM映像还是普通的可执行映像,普通的可执行文件的值为0x010B,PE32+可执行文件的值为0x020B(PE32+即64位的PE文件,这种文件只能运行在64位的Windows操作系统下)(0xd0-0xd1)
SizeOfCode,代码区块的大小,通常而言可执行文件仅有一个代码区块.text,所以通常就是.text区块的大小(磁盘对齐);(0xd4-0xd7)
AddressOfEntryPoint,重要字段,程序执行的入口点地址,是一个RVA值;(0xe0-0xe3)
BaseOfCode,代码段的起始地址,是一个RVA值,通常而言就是.text区块的起始RVA;(0xe4-0xe7)
BaseOfData,数据段的起始地址,是一个RVA值,通常而言就是.data区块的起始RVA;(0xe8-0xeb)
ImageBase,重要字段,PE文件在内存中首选的装载基地址,文件真实装载地址位于ImageBase+AddressOfEntryPoint;(0xec-0xef)
SectionAlignment,当PE文件装载到内存时区块的对齐大小,假设.text区块的大小为0x7748,而SectionAlignment的大小为0x1000,那么对齐后的大小为0x8000字节;(0xf0-0xf3)
FileAlignment,磁盘上PE文件中区块的对齐大小,对齐方式类似SectionAlignment;(0xf4-0xf7)
SizeOfImage,PE文件被装载到内存空间后总的大小,指从ImageBase到最后一个区块的大小(内存中开头+几个段在内存中的大小,是SectionAlignment的倍数);(0x108-0x10b)
SizeOfHeaders,Dos头、DosStub、PE头以及区块头的总大小,并进行FileAlignment对齐后的大小(exe文件中程序正式开始的地方,为FileAlignment的倍数)(0x10c-0x10f);
NumberOfRvaAndSizes,数据目录项的个数,固定为16,即后面一个成员DataDirectory的数组元素个数;(0x12c-0x12f)
DataDirectory,数据目录表,包含有输入表、输出表等表项的具体信息;
DataDirectory是一个数据目录表数组,数组的元素个数为16,各个元素所对应的表项如下图所示:
IMAGE_DATA_DIRECTORY结构体在WinNt.h头文件中的定义如下:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
其中VirtualAddress为数据块的起始RVA地址,Size为数据块的长度。数据目录表中比较重要的表项有输出表、输入表、资源表以及重定位表等。
这里共有16 项,大小为16*0x8=0x80
- 节表头解析以及RVA与文件偏移地址的转换
PE头之后就是节表头,节表头通常为 _IMAGE_SECTION_HEADER 类型。节表头位于0xb8(e_lfanew)+0x4(size(Signature))+0x14(size(IMAGE_FILE_HEADER))+0xe0(SizeOfOptionalHeader)=0x1f0,且每一个节表头占40bit,下面对节表头数据类型和每个变量的含义进行解释
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER
Name,为对应节区的名字,其中IMAGE_SIZEOF_SHORT_NAME的值固定为8。如果名字的长度小于8,则以NULL字符结束;如果名字的长度等于8,则没有NULL字符,因为数组长度为8。节区的名字通常以点号开头,如.text、.data等,通常而言,这个名字可以随便修改。
VirtualSize,在未对齐的情况下,区块所有数据的大小。
VirtualAddress,区块被装载到内存时的RVA,这个值总是SectionAlignment的整数倍。
SizeOfRawData,区块数据在磁盘文件中按照FileAlignment对齐后的大小。
PointerToRawData,区块数据在磁盘文件中的偏移地址。
PointerToRelocations、PointerToLinenumbers、NumberOfRelocations、NumberOfLinenumbers:不是很重要的字段,这里不作研究。
Characteristics,区块的属性值,表明区块的可读、可写、可执行等相关属性。
- IMAGE_IMPORT_DESCRIPTOR结构分析(导入表分析)
从DataDirectory数据结构中可以找到导入表开头在内存中的RVA为0x20d4,则输入表开头位于.rdate向后偏移0xd4,而.rdate在磁盘的起始地址在0xc00,则导入表开头位于0xc00+0xd4=0xcd4(数据对应数据所在段偏移=数据在内存中的位置-数据所在段在内存的基地址=数据在磁盘中的位置-数据所在段在磁盘的基地址)
,大小为0x64。下面将对IMAGE_IMPORT_DESCRIPTOR结构和数据类型进行分析
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;
} DUMMYUNIONNAME;
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name;
DWORD FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;
OriginalFirstThunk,RVA,指向输入名称表(简称INT),INT是一个类型为IMAGE_THUNK_DATA结构的数组,同样的,通过在数组末尾附加一个空的IMAGE_THUNK_DATA结构来表示数组的结束,每一个输入的函数都有一个对应的IMAGE_THUNK_DATA结构;
Name,RVA,指向DLL的名字,如“User32.dll”;
FirstThunk,RVA,指向输入地址表(简称IAT),IAT也是一个类型为IMAGE_THUNK_DATA结构的数组,同样的,通过在数组末尾附加一个空的IMAGE_THUNK_DATA结构来表示数组的结束,每一个输入的函数都有一个对应的IMAGE_THUNK_DATA结构;
我们发现导入表一共有四项,即有四个IMAGE_THUNK_DATA结构,在结尾还有一个空的IMAGE_THUNK_DATA结构表示结束,一个IMAGE_THUNK_DATA结构大小为0x14,则导入表大小为0x14*5=0x64,下面将对第一个导入表(User32.dll)对导入表进行解释
输入表的结构如下图所示:
IAT以及INT结构的数组类型均为IMAGE_THUNK_DATA,该结构体在WinNT.h头文件中的定义如下:
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString;
DWORD Function;
DWORD Ordinal;
DWORD AddressOfData;
} u1;
} IMAGE_THUNK_DATA32;
可以看出,该类型仅有的一个成员u1是一个联合体(union),而联合体内的类型都是DWORD,所以IMAGE_THUNK_DATA的大小是4字节。当该类型的最高位为1时,表示函数以序号的方式进行输入,这时候低31位的值就表示函数的序号;当该类型的最高位为0时,表示函数以名字的方式进行输入,这时候值就表示一个指向IMAGE_IMPORT_BY_NAME结构的RVA。
IMAGE_IMPORT_BY_NAME结构体在WinNT.h头文件中的定义如下:
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
其中Hint是WORD类型,表示函数在其原始DLL文件的输出表中的序号;Name是一个BYTE类型的数组,数组的内容为函数的名称字符串,以NULL字符结尾。需要注意的是,尽管结构体中Name的大小被指定为1,其实这里是一个可变的大小。
还有一个遗留问题是:OriginalFirstThunk指向的INT数组和FirstThunk指向的IAT数组到底有什么区别呢?答案是当PE文件被加载时,PE加载器会遍历INT结构中的数组项,通过其指向的IMAGE_IMPORT_BY_NAME结构来找到函数的名字,PE加载器可以通过函数的名字找到函数的地址,随后把得到的函数地址填充到IAT结构中,此后,通过IAT结构中的函数地址就可以进行函数调用了。
IAT被PE装载器填充后的输入表结构如下图所示。
我们从上面可以看到OriginalFirstThunk的值为0x21a4,和上面计算方式相同,其指向磁盘的位置在0xc00+(0x21a4-0x2000)=0xda4,在0xda4我们找到了0x000022c8,由于最高位为0,那么该值是指向IMAGE_IMPORT_BY_NAME结构的RVA。该RVA指向磁盘的地址为0xc00+(0x22c8-0x2000)=0xec8,我们可以看到在这里放着函数的序号0x1b3和该函数的名称。同理可以找到其他函数
我们从上面知道函数名字RVA为0x2352,其位于磁盘位置为0xc00+(0x2352-0x2000)=0xf52,在此处存储着导入库的名称
FirstThunk存储的RVA为0x206c,其指向磁盘的位置为0xc00+(0x206c-0x2000)=0xc6c,我们在0xc6c发现数值0x000022c8,按照前面的方法,我们就可以找到第一个函数。