逆向工程核心原理之第13章PE文件格式导出表的初步理解
作者分析的是32位的kernel32.dll,我们通过模仿分析64位的kernel32.dll来认识PE文件格式,跟随作者的脚步,下面我们开始分析:
PE的头由 1DOS头 2DOS存根 3NT头 4各种节区头 组成。
*******************************************************************************
第一部分DOS头结构,总共是64个字节
typedef struct _IMAGE_DOS_HEADER
{
0 WORD e_magic ; // DOS signature:4D5A('MZ')
2 WORD e_cblp ;
4 WORD e_cp ;
6 WORD e_crlc ;
8 WORD e_cparhdr ;
10 WORD e_minalloc ;
12 WORD e_maxalloc ;
14 WORD e_ss ;
16 WORD e_sp ;
18 WORD e_csum ;
20 WORD e_ip ;
22 WORD e_cs ;
24 WORD e_lfarlc ;
26 WORD e_ovno ;
28 WORD e_res[4] ;
36 WORD e_oemid ;
38 WORD e_oeminfo ;
40 WORD e_res2[10] ;
60 LONG e_lfanew ; // Offset to NT header 不同于32位E0,64位的开始位置为E8
} IMAGE_DOS_HEADER,*PIMAGE_DOS_HEADER;
*******************************************************************************
第二部分DOS存根,大小不固定,我们可以用 E8h - 40h = A8H, 64位的kernel32.dll的dos存根大小为A8h
*******************************************************************************
第三部分NT头,NT头由3部分组成: 1签名 2文件头 3可选头
32位的NT头和64位的NT头有点不同,如下:
//32位的NT头
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
//64位的NT头
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
在于结构体的第三个参数不同,下面我们从签名开始分析
签名是固定的4个字节"PE"00
文件头的结构体如下:共20个字节
typedef struct _IMAGE_FILE_HEADER {
0 WORD Machine; //64 86 8664 IMAGE_FILE_MACHINE_AMD64 AMD64
2 WORD NumberOfSections; //07 00 有7个节区 作者书中只有3个节区
4 DWORD TimeDateStamp;
8 DWORD PointerToSymbolTable;
12 DWORD NumberOfSymbols;
16 WORD SizeOfOptionalHeader; //F0 00 可选头的大小为F0
18 WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
*******************************************************************************
可选头的结构体如下: 共F0(240)个字节
typedef struct _IMAGE_OPTIONAL_HEADER64 {
0 WORD Magic; // 0B 02 20B是64位PE标志
2 BYTE MajorLinkerVersion;
3 BYTE MinorLinkerVersion;
4 DWORD SizeOfCode;
8 DWORD SizeOfInitializedData;
12 DWORD SizeOfUninitializedData;
16 DWORD AddressOfEntryPoint;
20 DWORD BaseOfCode;
24 ULONGLONG ImageBase; //00 00 00 80 01 00 00 00 0180000000
32 DWORD SectionAlignment; //00 10 00 00 在内存最小单位 1000
36 DWORD FileAlignment; //00 02 00 00 在磁盘最小单位 0200
40 WORD MajorOperatingSystemVersion;
42 WORD MinorOperatingSystemVersion;
44 WORD MajorImageVersion;
46 WORD MinorImageVersion;
48 WORD MajorSubsystemVersion;
50 WORD MinorSubsystemVersion;
52 DWORD Win32VersionValue;
56 DWORD SizeOfImage; //00 E0 0B 00 0be000内存映像大小
60 DWORD SizeOfHeaders; //00 04 00 00 0400 头部大小
64 DWORD CheckSum;
68 WORD Subsystem; //03 00 3 cui文件 控制台应用程序
70 WORD DllCharacteristics;
72 ULONGLONG SizeOfStackReserve;
80 ULONGLONG SizeOfStackCommit;
88 ULONGLONG SizeOfHeapReserve;
96 ULONGLONG SizeOfHeapCommit;
104 DWORD LoaderFlags;
108 DWORD NumberOfRvaAndSizes;
// 下面数组DataDirectory的实际大小 10 00 00 00 10h=16
112 IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
保存导入导出函数,后面重点要介绍
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;
IMAGE_NUMBEROF_DIRECTORY_ENTRIES 的值为16
IMAGE_DATA_DIRECTORY的结构体如下: 共8个字节
typedef struct _IMAGE_DATA_DIRECTORY {
0 DWORD VirtualAddress;
4 DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
所以最后的数组字节大小为: 16 * 8 = 128个字节
*******************************************************************************
第四部分节区头,我们这里有7个节区头 text rdata data pdata didat rsrc reloc
#define IMAGE_SIZEOF_SHORT_NAME 8
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;
下面是对节区头中包含节区的内存展示:
文件RAW地址 |
内存RVA地址 |
|||
400 磁盘节区起始地址 |
|
1000 内存节区起始地址 |
||
..... ..... ..... 7ea00 |
7e600 |
text |
..... ..... ..... 080000 |
7e4bb |
..... b1a00 |
33000 |
rdata |
..... B3000 |
32ff6 |
...... B2000 |
600 |
data |
...... B5000 |
126c |
...... B7800 |
5800 |
pdata |
...... Bb000 |
5604 |
...... B7a00 |
0200 |
didat |
...... bc000 |
68 |
...... B8000 |
0600 |
rsrc |
...... bd000 |
0520 |
...... B8400 |
0400 |
reloc |
...... be000 |
030c |
注意上面表格两边虽然格式上对齐,但是数值上是不一样的 |
我们查看kernel32.dll的文件大小为:753 KB (771,192 字节),和这里的0b8400h是不太一样的。因为这里多了一些内容(SizeOfImage所代表的内存镜像大小没有包含属性证书表和调试信息),查看开始的信息,得知增加的长度为:78 40 00 00 即4078h,所以总长度为
0b8400h + 4078h = bc478h,这个值和771,192是一致的。
如图所示:
内存映像大小和 结构体_IMAGE_OPTIONAL_HEADER64的如下字段相对应
56 DWORD SizeOfImage; //00 E0 0B 00 0be000内存映像大小
因为多余的证书和调试属性不会加载到内存,所以内存中和上面我们分析的节区的长度是一致的。
*******************************************************************************
经过上面的铺垫,我们现在来看DataDirectory数组中的第一个元素导出库的存在,
对比我们实际的数据: E0 A1 09 00 0C DF 00 00
080000-07ea00 = 1600
typedef struct _IMAGE_DATA_DIRECTORY {
0 DWORD VirtualAddress; //09a1e0h ->(09a1e0-080000+07ea00=98be0)
4 DWORD Size; //DFC0h
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
首先认识一下导入库的结构体,共40个字节,
对应的实际数据为:
00 00 00 00 47 57 96 1A 00 00 00 00 D2 E1 09 00
01 00 00 00 61 06 00 00 61 06 00 00 08 A2 09 00
8C BB 09 00 10 D5 09 00
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name; //D2 E1 09 00 即 09e1d2 - 1600 = 9cbd2
// 4B 45 52 4E 45 4C 33 32 2E 64 6C 6Cj即字符串 KERNEL32.dll
DWORD Base; //01
DWORD NumberOfFunctions; //61 06 00 00 ->0661
DWORD NumberOfNames; //61 06 00 00->0661
//这里我们知道每个函数都时有名字的,所以对应的索引保持一致
DWORD AddressOfFunctions; //08 A2 09 00->09a208
DWORD AddressOfNames; //8C BB 09 00->09bb8c
DWORD AddressOfNameOrdinals; //10 D5 09 00-> 09d510
} IMAGE_EXPORT_DIRECTORYM, *pIMAGE_EXPORT_DIRECTORY
下面我们来查找 KERNEL32.dll中的函数 AddAtomW.
首先找到名字开始地址:DWORD AddressOfNames; //8C BB 09 00->09bb8c
09bb8c - 1600 = 9a58c , 9a58c指向的内容是
第一个地址: DF E1 09 00即09e1df-1600=9cbdf
第二个地址: 18 E2 09 00即09e218-1600=9cc18
第三个地址: 4B E2 09 00即09e24b-1600=9cc4b
第四个地址: 5A E2 09 00即09e25a-1600=9cc5a
第五个地址: 6F E2 09 00即09e26f-1600=9cc6f
第六个地址: 78 E2 09 00即09e278-1600=9cc78
我们在这里找到了函数AddAtomW,所以它的索引是6。
函数地址为DWORD AddressOfFunctions; //08 A2 09 00->09a208
09a208 - 1600 = 98c08,98c08指向的内容为
开始地址: F7 E1 09 00 即09e1f7 - 1600 = 9cbf7
多以对应的第6个索引的内容为 F0 28 01 00 即 0128f0 RVA
从加载入内存的结果看为 01800128f0 = ImageBase(0180000000)+ 0128f0
核实无误,至此完毕。