作者分析的是32位的notepad.exe,我们通过模仿分析64位的notepad.exe来认识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位的notepad.exe的dos存根大小为A8h,为了重现执行dos存根的效果,我们安装了DOSBox.exe,在其中成功执行notepad.exe得到。“This program cannot be run in DOS mode.”
如图:
*******************************************************************************
第三部分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 40 01 00 00 00 0140000000
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 90 03 00 039000内存映像大小
60 DWORD SizeOfHeaders; //00 04 00 00 0400 头部大小
64 DWORD CheckSum;
68 WORD Subsystem; //02 00 2 gui文件 窗口应用程序
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 内存节区起始地址 |
||
..... ..... ..... 26200 |
25e00 |
text |
..... ..... ..... 27000 |
25ccf
|
..... 2fc00 |
9a00 |
rdata |
..... 31000 |
98d6 |
...... 0a00 |
0e00 |
data |
...... 34000 |
2788 |
...... 31c00 |
1200 |
pdata |
...... 36000 |
1188 |
...... 31e00 |
0200 |
didat |
...... 37000 |
0178 |
...... 32a00 |
0c00 |
rsrc |
...... 38000 |
0bdb |
...... 32DE0 |
0400 |
reloc |
...... 39000 |
02e8 |
注意上面表格两边虽然格式上对齐,但是数值上是不一样的 |
我们查看notepad.exe的文件大小为:203 KB (208,384 字节),和这里的32DE0h是一样的。
如图所示:
内存映像大小和 结构体_IMAGE_OPTIONAL_HEADER64的如下字段相对应
56 DWORD SizeOfImage; //00 90 03 00 039000内存映像大小
*******************************************************************************
经过上面的铺垫,我们现在来看DataDirectory数组中的第二个元素导入库的存在,
对比我们实际的数据:D8 E6 02 00 44 02 00 00
typedef struct _IMAGE_DATA_DIRECTORY {
0 DWORD VirtualAddress; //2E6D8h ->(2e6d8-27000+26200=2d8d8)
4 DWORD Size; //244h
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
首先认识一下导入库的结构体,共20个字节,
对应的实际数据为:F0 E9 02 00 00 00 00 00 00 00 00 00 46 F8 02 00 F8 78 02 00
27000-26200=E00
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;
// INT(import name table) 地址 RVA F0 E9 02 00 02E9F0->(02e9f0-e00=2dbf0)
//2dbf0保存的内容为5C F4 02 00即2f45c ->(2f45c-e00=2e65c)
} ;
DWORD TimeDateStamp; // 00 00 00 00
DWORD ForwarderChain; //00 00 00 00
DWORD Name; //库名称字符串的地址 RVA 46 F8 02 00 02F846->(2F846-e00=2ea46)
//2ea46保存的地址内容为 4B 45 52 4E 45 4C 33 32 2E 64 6C 6C即字符串KERNEL32.dll
DWORD FirstThunk; // IAT地址 RVA F8 78 02 00 0278F8 ->(278f8-e00=26af8)
//26af8保存的内容为 5C F4 02 00 即2f45c ->(2f45c-e00=2e65c),和上面字段INT一样
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; //B8 02 02B8
BYTE Name[1]; //变长字符串
//47 65 74 50 72 6F 63 41 64 64 72 65 73 73 即为GetProcAddress
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
根据上述分析,所以引入的第一个dll是KERNEL32.dll,KERNEL32.dll的第一个函数是GetProcAddress。类似的我们可以分析引入的第二个dll是GDI32.dll,GDI32.dll的第一个函数是CreateDCW。以此类推,至此分析完毕。
*******************************************************************************