首页
社区
课程
招聘
[原创]PE解析思路
2024-1-26 11:12 17137

[原创]PE解析思路

2024-1-26 11:12
17137

PE解析思路


本文的目的是编写解析器,故本文叙述的PE细节不像其他文章那么详细,想学习更加详细的解析可以翻到文本最后的参考文章


0x01 Headers 解析

头部分分为5部分:

  • Dos Header
  • NT Header
  • FileHeader
  • Optional Header
  • Section Header

Dos Header

Dos部分为兼容古老的DOS 16位系统,目前来说用处已经不大,整体结构在winnt.h中定义为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;

目前只用到两个参数,一个是e_magic,该参数为整个PE文件的开头部分,可用于区别ELF和PE

在PE中,一般表现为0x5A4D,字符串表现为MZ

ELF文件中,表现为:

第二个使用到的参数为DOS Header中的最后一个参数e_lfanew,该参数用来指向exe文件的开始位置,也就是

第二部分Nt Header的位置从Dos Header 到 Nt Header中间空出来的部分数据可以理解为历史残留,都是一些脏数据,一般都会有这样一段话:This program cannot be run in DOS mode

Nt Header

Nt Header 结构体(winnt.h)

1
2
3
4
5
6
7
8
9
10
11
typedef struct _IMAGE_NT_HEADERS64 {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
 
typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

Nt Header的开头为一个Signature,该值通过宏进行定义为PE文件的固定值,不受32位或者64位影响:

1
#define IMAGE_NT_SIGNATURE                  0x00004550  // PE

File Header

File Header 结构体(winnt.h)

1
2
3
4
5
6
7
8
9
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,在wint.h中定义了宏,目前常见的应该只有IMAGE_FILE_MACHINE_I38632位程序和IMAGE_FILE_MACHINE_AMD6464位程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#define IMAGE_FILE_MACHINE_UNKNOWN           0
#define IMAGE_FILE_MACHINE_TARGET_HOST       0x0001  // Useful for indicating we want to interact with the host and not a WoW guest.
#define IMAGE_FILE_MACHINE_I386              0x014c  // Intel 386.
#define IMAGE_FILE_MACHINE_R3000             0x0162  // MIPS little-endian, 0x160 big-endian
#define IMAGE_FILE_MACHINE_R4000             0x0166  // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000            0x0168  // MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2         0x0169  // MIPS little-endian WCE v2
#define IMAGE_FILE_MACHINE_ALPHA             0x0184  // Alpha_AXP
#define IMAGE_FILE_MACHINE_SH3               0x01a2  // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3DSP            0x01a3
#define IMAGE_FILE_MACHINE_SH3E              0x01a4  // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4               0x01a6  // SH4 little-endian
#define IMAGE_FILE_MACHINE_SH5               0x01a8  // SH5
#define IMAGE_FILE_MACHINE_ARM               0x01c0  // ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB             0x01c2  // ARM Thumb/Thumb-2 Little-Endian
#define IMAGE_FILE_MACHINE_ARMNT             0x01c4  // ARM Thumb-2 Little-Endian
#define IMAGE_FILE_MACHINE_AM33              0x01d3
#define IMAGE_FILE_MACHINE_POWERPC           0x01F0  // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_POWERPCFP         0x01f1
#define IMAGE_FILE_MACHINE_IA64              0x0200  // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16            0x0266  // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64           0x0284  // ALPHA64
#define IMAGE_FILE_MACHINE_MIPSFPU           0x0366  // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16         0x0466  // MIPS
#define IMAGE_FILE_MACHINE_AXP64             IMAGE_FILE_MACHINE_ALPHA64
#define IMAGE_FILE_MACHINE_TRICORE           0x0520  // Infineon
#define IMAGE_FILE_MACHINE_CEF               0x0CEF
#define IMAGE_FILE_MACHINE_EBC               0x0EBC  // EFI Byte Code
#define IMAGE_FILE_MACHINE_AMD64             0x8664  // AMD64 (K8)
#define IMAGE_FILE_MACHINE_M32R              0x9041  // M32R little-endian
#define IMAGE_FILE_MACHINE_ARM64             0xAA64  // ARM64 Little-Endian
#define IMAGE_FILE_MACHINE_CEE               0xC0EE

可用来区分Windows 下的32位程序和64位程序:

第二个是NumberOfSections,就是节的数量,关于节遍历时需要用到,还有对于节的增加和合并时需要对该参数的值进行修改

第三个是SizeOfOptionalHeader,关于可选PE头的大小,默认情况下,32位程序该值为E0,64位程序该值为F0

最后一个参数Characteristic可以理解为操作权限

Optional Header

Optional Header 结构体(winnt.h)

#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16

pe32:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
typedef struct _IMAGE_OPTIONAL_HEADER {
    //
    // Standard fields.
    //
 
    WORD    Magic;
    BYTE    MajorLinkerVersion;
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;
    DWORD   SizeOfInitializedData;
    DWORD   SizeOfUninitializedData;
    DWORD   AddressOfEntryPoint;
    DWORD   BaseOfCode;
    DWORD   BaseOfData;
 
    //
    // NT additional fields.
    //
 
    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;

pe64:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
typedef struct _IMAGE_OPTIONAL_HEADER64 {
    WORD        Magic;
    BYTE        MajorLinkerVersion;
    BYTE        MinorLinkerVersion;
    DWORD       SizeOfCode;
    DWORD       SizeOfInitializedData;
    DWORD       SizeOfUninitializedData;
    DWORD       AddressOfEntryPoint;
    DWORD       BaseOfCode;
    ULONGLONG   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;
    ULONGLONG   SizeOfStackReserve;
    ULONGLONG   SizeOfStackCommit;
    ULONGLONG   SizeOfHeapReserve;
    ULONGLONG   SizeOfHeapCommit;
    DWORD       LoaderFlags;
    DWORD       NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;

通过对比可以发现在32位的Optional Header中多了一个DWORD BaseOfData

在不做修改的情况下,OptionalHeader的长度是固定的,所以32位程序在File Header中的SizeOfOptionalHeader为0xE8,而64位程序的SizeOfOptionalHeader为0xF0,通过上图也可以看出在ImageBaseSIzeOfStackReserveSizeOfStackeCommitSizeOfHeapReserve, SizeOfHeapCommit这五个参数中,64位程序的参数长度为ULONGLONG,就是8字节QDWORD,加上少掉的一个DWORD,刚好默认情况下64位程序的OptionalHeader比32位的多0x04*5-0x04=16个字节

在OptionalHeader的开头Magic参数会再一次显示32位和64位的区别:

在winnt.h中也定义了相关的宏:

1
2
#define IMAGE_NT_OPTIONAL_HDR32_MAGIC      0x10b
#define IMAGE_NT_OPTIONAL_HDR64_MAGIC      0x20b

所以总结之后可以得到区分32位和64位程序的方式:

  • 通过OptionalHeader->Magic
  • 通过FileHeader->Machine
  • 通过FileHeader->SizeOfOptionalHeader

OptionalHeader中会用到的参数:

  • AddressOfEntryPoint:程序入口地址,简称为OEP
  • ImageBase:镜像基址
  • SectionAlignment:内存对齐大小
  • FileAlignment:文件对齐大小

OptionalHeader中解析到的OEP为相对偏移并且这个偏移应该是内存对齐后的偏移

载入x32dbg查看,此时发现OEP的地址为A80233,这时候就要提到ImageBase了,可以看到在OptionalHeader中的ImageBase为0x400000,但是程序此时的ImageBase为0xA80233 - 0x10233 = 0xA70000,这个原因是OptionalHeader中解析到的ImageBase为理想状况下的,内存的地址是连续,0x40000只有一个,所以有且只有一个程序可以刚好分配到0x40000作为镜像基址,那么其他程序怎么办,这时候就会产生一个重定位的情况,这个后面会说到,总之,当程序载入内存时,相对偏移是不会变的,会发生变化的是镜像基址,当然这里指的相对偏移,是指内存对齐后的相对偏移,在PE解析中,存在着文件对齐的情况,特别是该程序中,文件对齐与内存对齐是不一致的,所以当程序从FileBuffer加载到内存成为ImageBuffer后,需要对程序进行拉伸,对齐成内存对齐,那么相应的某些本身处于文件对齐的地址,就需要通过转化,转换成内存对齐地址

  • SizeOfImage:经过内存对齐后整个镜像大小

  • SizeOfCode:文件对齐下镜像大小

  • SizeOfHeaders: 头+节表文件对齐下的大小

  • BaseOfCode: 内存对齐下代码的开始(一般为SectionAlignment的整数倍)

  • BaseOfData:内存对齐下数据段的开始(一般为SectionAlignment的整数倍)

Section Header

SectionHeader(winnt.h):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define IMAGE_SIZEOF_SHORT_NAME              8
#define IMAGE_SIZEOF_SECTION_HEADER          40
 
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;

节表是整个PE文件最重要的一部分,因为只有通过他进行内存地址的转化,以及各种节都是通过节表进行索引,

  • Name:一个Char数组,用来存储节名

  • Misc:这是一个union联合体,具体的含义这里不作解释,一般表现为VirtualSize,或者直接把它当作VirtualSize也行,这里指的是每个节的未对齐大小

  • VirtualAddress:指的是内存对齐后节的开始地址,这里也是一个相对地址

  • SizeOfRawData:指的是进行文件对齐后的节大小

  • 在该文件中,FileAlignment(文件对齐)的大小为0x200,所以这里RawSize就会对齐为0x200的整数倍

  • PointerToRawData:文件对齐后该节的起始地址

    • 通过sizeofHeaders可以看到整个Header的文件对齐长度为0x400,所以第一个节的起始地址就是0x400
  • PointerToRelocations\PointerToLinenumbers\NumberOfRelocations\NumberOfLinenumbers,暂时用不到:

  • Characteristics:为每个节的权限

    • 这里数值的结果是通过|得到的,参考微软官方的Section Flags:
      • 0x60000020 -> 0x20000000 | 0x40000000 | 0x00000020
        • IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ | IMAGE_SCN_CNT_CODE
      • 0x40000040-> 0x40000000 | 0x00000040
        • IMAGE_SCN_MEM_READ | IMAGE_SCN_CNT_INITIALIZED_DATA
      • 0xC00000040 -> 0x80000000 | 0x40000000 | 0x00000040
        • IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_READ | IMAGE_SCN_CNT_INITIALIZED_DATA

0x02 Data Directory 解析

Data Directory位于Optional Header,结构体(winnt.h):

1
2
3
4
typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

在Optional中该结构体数组数量为16个:#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16

各个表的序号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT          1   // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3   // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY        4   // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC       5   // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG           6   // Debug Directory
//      IMAGE_DIRECTORY_ENTRY_COPYRIGHT       7   // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    7   // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR       8   // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS             9   // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG    10   // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT   11   // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT            12   // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13   // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14   // COM Runtime descriptor

该结构体中的Size不影响对于各个表的解析,影响表的解析是VirtualAddress

Export Directory

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;
    DWORD   Base;
    DWORD   NumberOfFunctions;
    DWORD   NumberOfNames;
    DWORD   AddressOfFunctions;     // RVA from base of image
    DWORD   AddressOfNames;         // RVA from base of image
    DWORD   AddressOfNameOrdinals;  // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

前面4个参数可以不用管,重点在后面的6个参数:

  • Name: 指向导出表文件名
  • Base:导出函数的起始序号
  • NumberOfFunctions:导出函数的个数
  • NumberOfNames:以函数名称导出的函数个数
  • AddressOfFunctions:导出函数的地址表,既然是地址,那么宽度就是DWORD
  • AddressOfNames:导出函数的名称表,表中存放的是函数名称字符串的首地址
  • AddressOfNameOrdinals:导出函数的序号表,表中存放的是函数的序号,宽度为Word

PE文件中函数导出有两种方式:

  • 序号导出
  • 函数名称导出

AddressOfFunctions指向的是一张导出函数表,而NumberOfFunctions是这张表的长度

AddressOfNames指向的是导出函数名称表,NumberOfNames是该表的长度

AddressOfNameOrdinals指向的是导出函数的序号表

这里需要注意导出函数表和其余两张表的长度是不一定相同的,而AddressOfNames和AddressOfNameOrdinals指向的表的长度是一样长度

注意Ordinals序号表中,序号为偏移值,真的序号应该是要加上导出表中的Base参数的值,作为起始序号:

例:DWORD base = 10;

NameOrdinals Table:

1
3
4
6
7

那么真正的序号应该为:

11
13
14
16
17

当导出表的DataDirectory中的VirtualAddress为0时,说明该PE文件不存在导出函数,当然这个可以人为修改,在PE解析中需要通过该VirtualAddress找到导出表存储的位置,如果该值为0,则无法找到

函数名称表AddressOfNames和函数序号表AddressOfNameOrdinals的数量是一样的,当某个函数在导出时写了NoName不以名称导出时,那么此时导出函数序号表中也不会有该函数的函数序号

通过函数名称找函数地址的步骤:

  1. 遍历函数名称表,找到对应的函数名称,将其下标作为索引1
  2. 通过1得到的索引1在函数序号表中读取索引值,该值再次作为索引2
  3. 通过2得到的索引2在函数地址表中读取索引值,该值即为函数地址

通过函数序号找函数地址的步骤:

  1. 通过提供的序号,减去导出表结构体中的Base参数,得到的值作为索引
  2. 通过索引去函数地址表中读取索引值,该值即为函数地址

通过对比两种方式的索引可以发现,通过函数序号找函数地址的时候根本不需要用到AddressOfNameOrdinals,Ordinals是跟Names绑定的一张表,所以才会发现这两张表的成员数量是相同的

Import Directory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;                  // 0 if not bound,
                                            // -1 if bound, and real date\time stamp
                                            //     in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                            // O.W. date/time stamp of DLL bound to (Old BIND)
 
    DWORD   ForwarderChain;                 // -1 if no forwarders
    DWORD   Name;
    DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

通过DataDirectory索引到的VirtuallAddress为导入表的描述符,Name为该导入dll的名称,为偏移量,这里所有的描述符的值都可以理解为偏移,偏移到的是真正存储的位置,这里通过ForwarderChain来确定是否是最后一张导入表描述符,通过,如果该值为-1,则说明该描述符为最后一个导入表描述符

INT 和 IAT

INT:Import Name Table,根据名称找函数

IAT:Import Address Table,根据地址找函数

文件加载前:

OriginalFirstThunk指向的是INT表,INT表中就会涉及到新的结构体:IMAGE_THUNK_DATA,该结构体同样是属于偏移,所以会区分32位和64位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef struct _IMAGE_THUNK_DATA64 {
    union {
        ULONGLONG ForwarderString;  // PBYTE
        ULONGLONG Function;         // PDWORD
        ULONGLONG Ordinal;
        ULONGLONG AddressOfData;    // PIMAGE_IMPORT_BY_NAME
    } u1;
} IMAGE_THUNK_DATA64;
typedef IMAGE_THUNK_DATA64 * PIMAGE_THUNK_DATA64;
 
typedef struct _IMAGE_THUNK_DATA32 {
    union {
        DWORD ForwarderString;      // PBYTE
        DWORD Function;             // PDWORD
        DWORD Ordinal;
        DWORD AddressOfData;        // PIMAGE_IMPORT_BY_NAME
    } u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;

FirstThunk指向的IAT表,IAT表中的结构体跟INT表中的结构体完全相同(ps:文件加载前)

其原因跟THUNK结构体中的类型有关,可以看到该结构体实际上内部是一个union包着4个参数,长度为DWORD,只需要看后面两个,Ordinal和AddressOfData

  • Ordinal为序号,通过序号去查找函数位置
  • AddressOfData为函数名称的地址,通过导入表中函数的导出名称去查找

在程序加载前,对于每个导入表的导出函数而言,是无法知道其是否可以通过函数名称进行查找的,所以此时才有了INT和IAT

查找IAT表有两种办法,一个是通过DataDirectory[13],第二个是通过导入表中的FirstThunk偏移找到IAT表的起始位置

文件加载后:

INT中的数据不变,IAT中的值替换成函数地址,函数地址通过INT或者IAT进行替换

在查找时,如果文件不存在

Relocation Directory

重定位表关乎到程序能否正常运行,该表的解析很有意思

通过Data Directory[IMAGE_DIRECTORY_ENTRY_BASERELOC]定位到第一张表的指针位置,同样需要通过RVAToRAW拿到文件中存放该表的位置

Relocation结构体:

1
2
3
4
5
6
typedef struct _IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress;
    DWORD   SizeOfBlock;
//  WORD    TypeOffset[1];
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;

既然是表,就说明不止有这一个结构体,这里的索引也是同样的,通过当前的指针来索引下一个表的位置,最后通过结构体全0来判断该表的结束:

1
VirtualAddress == 0 && SizeOfBlock == 0

SizeOfBlock表明指向的Relocation结构体的实际大小,该大小是包括已知两个参数的大小:所以实际的大小应该为SizeOfBlock - 8,在32位程序中,块大小为2的12次方,也就是4096,所以只需要12位就可以遍历整个块,根据字节对齐需要,编译器对程序进行分配时不会直接分12位,而是会分16位,也就是2个字节,所以对于该2字节,低12位为偏移,高4位为填充,当高4位的值为3时,说明该地址需要被重定位,如果为其他值,则说明该2字节的值可作为填充数据处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
this->pRelocation = (PIMAGE_BASE_RELOCATION) & (((PBYTE)lpBuffer)[this->RVAToRAW((this->pDataDirectory + 5)->VirtualAddress)]);
     
PIMAGE_BASE_RELOCATION pTmpRelocation = pe->pRelocation;
while (pTmpRelocation->VirtualAddress != 0 && pTmpRelocation->SizeOfBlock != 0) {
    PWORD pBlock = (PWORD)(pTmpRelocation + 1);
    cout << "VirtualAddr: " << pTmpRelocation->VirtualAddress << " Size: " << pTmpRelocation->SizeOfBlock;
 
    int items = (pTmpRelocation->SizeOfBlock - 8) / 2;
    cout << " Items: " << items << endl;
 
    for (int i = 0; i < items ; i++) {
        cout << "Item: "  << * pBlock << " RVA: " << pTmpRelocation->VirtualAddress + ( * pBlock & 0x7F) << endl;
        pBlock++;
    }
    cout << endl;
    pTmpRelocation = (PIMAGE_BASE_RELOCATION)((PBYTE)pTmpRelocation + pTmpRelocation->SizeOfBlock);
}

打印结果:

通过这几张表的解析也可以发现PE的特点,由于程序的大小不确定,所以无法静态or动态的分配空间给各种表,所以通过自索引的方式来实现表的遍历,实现了程序的可扩展性,这确实是一种好的方式,这个结构的优点也一直延续至今

Resource Directory

该表分成三层进行解析

First Layer:

第一层为资源样式,最后两个参数之和决定资源数:

DWORD resourceNum = NumberOfNamedEntries + NumberOfIdEntries

1
2
3
4
5
6
7
8
9
typedef struct _IMAGE_RESOURCE_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    WORD    NumberOfNamedEntries;
    WORD    NumberOfIdEntries;
//  IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[];
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;

在该结构体后紧跟的就是第二层,resourceNum决定后面跟着多少个_IMAGE_RESOURCE_DIRECTORY_ENTRY

PIMAGE_RESOURCE_DIRECTORY_ENTRY = (PIMAGE_RESOURCE_DIRECTORY_ENTRY)(PIMAGE_RESOURCE_DIRECTORY + 1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
    union {
        struct {
            DWORD NameOffset:31;
            DWORD NameIsString:1;
        } DUMMYSTRUCTNAME;
        DWORD   Name;
        WORD    Id;
    } DUMMYUNIONNAME;
    union {
        DWORD   OffsetToData;
        struct {
            DWORD   OffsetToDirectory:31;
            DWORD   DataIsDirectory:1;
        } DUMMYSTRUCTNAME2;
    } DUMMYUNIONNAME2;
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;

该结构体为两个2联合体,第一个union中的NameIsString表示该资源是否通过字符串进行索引,该值为1时说明通过字符串进行索引,说明该资源为开发者资源,该值为0说明通过Id进行索引,说明该资源为系统资源,第二个union中的DataIsDirectory表示该数据是否是一个目录,值为1说明该数据为目录

Windows预定义了一些资源,位于winuser.h,一般只用到前面的11个资源,预定义的资源都是通过ID进行索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/*
 * Predefined Resource Types
 */
#define RT_CURSOR           MAKEINTRESOURCE(1)
#define RT_BITMAP           MAKEINTRESOURCE(2)
#define RT_ICON             MAKEINTRESOURCE(3)
#define RT_MENU             MAKEINTRESOURCE(4)
#define RT_DIALOG           MAKEINTRESOURCE(5)
#define RT_STRING           MAKEINTRESOURCE(6)
#define RT_FONTDIR          MAKEINTRESOURCE(7)
#define RT_FONT             MAKEINTRESOURCE(8)
#define RT_ACCELERATOR      MAKEINTRESOURCE(9)
#define RT_RCDATA           MAKEINTRESOURCE(10)
#define RT_MESSAGETABLE     MAKEINTRESOURCE(11)
 
#define DIFFERENCE     11
#define RT_GROUP_CURSOR MAKEINTRESOURCE((ULONG_PTR)(RT_CURSOR) + DIFFERENCE)
#define RT_GROUP_ICON   MAKEINTRESOURCE((ULONG_PTR)(RT_ICON) + DIFFERENCE)
#define RT_VERSION      MAKEINTRESOURCE(16)
#define RT_DLGINCLUDE   MAKEINTRESOURCE(17)
#if(WINVER >= 0x0400)
#define RT_PLUGPLAY     MAKEINTRESOURCE(19)
#define RT_VXD          MAKEINTRESOURCE(20)
#define RT_ANICURSOR    MAKEINTRESOURCE(21)
#define RT_ANIICON      MAKEINTRESOURCE(22)
#endif /* WINVER >= 0x0400 */
#define RT_HTML         MAKEINTRESOURCE(23)
#ifdef RC_INVOKED
#define RT_MANIFEST                        24
#define CREATEPROCESS_MANIFEST_RESOURCE_ID  1
#define ISOLATIONAWARE_MANIFEST_RESOURCE_ID 2
#define ISOLATIONAWARE_NOSTATICIMPORT_MANIFEST_RESOURCE_ID 3
#define ISOLATIONPOLICY_MANIFEST_RESOURCE_ID 4
#define ISOLATIONPOLICY_BROWSER_MANIFEST_RESOURCE_ID 5
#define MINIMUM_RESERVED_MANIFEST_RESOURCE_ID 1   /* inclusive */
#define MAXIMUM_RESERVED_MANIFEST_RESOURCE_ID 16  /* inclusive */
#else  /* RC_INVOKED */
#define RT_MANIFEST                        MAKEINTRESOURCE(24)
#define CREATEPROCESS_MANIFEST_RESOURCE_ID MAKEINTRESOURCE( 1)
#define ISOLATIONAWARE_MANIFEST_RESOURCE_ID MAKEINTRESOURCE(2)
#define ISOLATIONAWARE_NOSTATICIMPORT_MANIFEST_RESOURCE_ID MAKEINTRESOURCE(3)
#define ISOLATIONPOLICY_MANIFEST_RESOURCE_ID MAKEINTRESOURCE(4)
#define ISOLATIONPOLICY_BROWSER_MANIFEST_RESOURCE_ID MAKEINTRESOURCE(5)
#define MINIMUM_RESERVED_MANIFEST_RESOURCE_ID MAKEINTRESOURCE( 1 /*inclusive*/)
#define MAXIMUM_RESERVED_MANIFEST_RESOURCE_ID MAKEINTRESOURCE(16 /*inclusive*/)
#endif /* RC_INVOKED */
#endif /* !NORESOURCE */

关于资源表的结构可以理解为资源管理器,在文件夹中即可以创建文件也可以创建文件夹,所以在解析该结构的时候可以通过递归去做解析

参考文章

[1]PE整体结构体:https://docs.microsoft.com/en-us/windows/win32/debug/pe-format

[2]资源表解析:https://www.cnblogs.com/iBinary/p/7712932.html


[竞赛]2024 KCTF 大赛征题截止日期08月10日!

最后于 2024-1-26 11:42 被Gushang编辑 ,原因:
收藏
免费 12
打赏
分享
最新回复 (13)
雪    币: 1879
活跃值: (1774)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
五毒女 2024-1-26 11:31
2
0
第二张图和第三张图贴反了
雪    币: 2887
活跃值: (995)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
Gushang 2 2024-1-26 11:42
3
0
五毒女 第二张图和第三张图贴反了
好的我更正一下
雪    币: 10
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_ldbucrik 2024-1-26 12:24
4
0
编写解析器,yyds
雪    币: 21022
活跃值: (30261)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2024-1-29 09:54
5
1
感谢分享
雪    币: 462
活跃值: (733)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
_emmm_ 2024-1-29 17:56
6
4
重定位表的遍历错了。。。。。。。。。网上说的都是错的,你这里写的还是错的。
不能用VirtualAddress和SizeofBlock来判断重定位表的结尾。而应该用整个重定位表的大小。

你这里能运行是因为刚好重定位表没有对齐,后面的是一段0x00.

你找一个刚好对齐的DLL去加载,后面的内存是无效的,就会segment fault了。

详细的看我这里https://github.com/fancycode/MemoryModule/pull/116
雪    币: 2887
活跃值: (995)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
Gushang 2 2024-2-5 09:28
7
0
_emmm_ 重定位表的遍历错了。。。。。。。。。网上说的都是错的,你这里写的还是错的。 不能用VirtualAddress和SizeofBlock来判断重定位表的结尾。而应该用整个重定位表的大小。 你这里 ...
感谢纠正!
雪    币: 25
活跃值: (580)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
夺命黑裤衩 2024-2-6 10:18
8
0
加油
雪    币: 322
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
4essence 2024-2-17 16:43
9
0
感谢分享
雪    币: 224
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
Dircou 2024-2-18 18:39
10
0
开始还能看懂点,留个记号、、、、
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_vqfzmcme 2024-2-27 14:40
11
0
小白学习中
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
shaonianguai 2024-3-13 10:33
12
0
mark
雪    币: 1041
活跃值: (733)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
鸭子咯咯哒 2024-3-13 13:07
13
0
大佬,有没有解析elf的
雪    币: 2887
活跃值: (995)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
Gushang 2 2024-3-14 00:56
14
0
鸭子咯咯哒 大佬,有没有解析elf的
有,还没整理完,后续会发ELF和Mach-O的
游客
登录 | 注册 方可回帖
返回