逆向工程核心原理之第13章PE文件格式导出表的初步理解


作者分析的是32位的kernel32.dll,我们通过模仿分析64位的kernel32.dll来认识PE文件格式,跟随作者的脚步,下面我们开始分析:

PE的头由 1DOS2DOS存根 3NT4各种节区头 组成。

*******************************************************************************

第一部分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  不同于32E064位的开始位置为E8

} IMAGE_DOS_HEADER,*PIMAGE_DOS_HEADER;

*******************************************************************************

第二部分DOS存根,大小不固定,我们可以用 E8h - 40h = A8H, 64位的kernel32.dlldos存根大小为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  20B64PE标志

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 0009e1df-1600=9cbdf

第二个地址: 18 E2 09 0009e218-1600=9cc18

第三个地址: 4B E2 09 0009e24b-1600=9cc4b

第四个地址: 5A E2 09 0009e25a-1600=9cc5a

第五个地址: 6F E2 09 0009e26f-1600=9cc6f

第六个地址: 78 E2 09 0009e278-1600=9cc78

我们在这里找到了函数AddAtomW,所以它的索引是6

函数地址为DWORD AddressOfFunctions;  //08 A2 09 00->09a208    

09a208  - 1600  =  98c0898c08指向的内容为

开始地址: F7 E1 09 00 09e1f7 - 1600 = 9cbf7

多以对应的第6个索引的内容为 F0 28 01 00 0128f0 RVA

从加载入内存的结果看为 01800128f0 = ImageBase(0180000000)+  0128f0

核实无误,至此完毕。