内核前置——PE文件格式

stu_PE:[原创]【2020年5月1日更新】StudyPE (x86 / x64) 1.11 版-安全工具-看雪-安全社区|安全招聘|kanxue.com

加密与解密 第十章 PE文件格式 - 0Xor’ CTF WriteUp

PE的基本结构

刚开始的我放到另一篇文章里了,那个观PE文件有感所以没什么好说的了

基地址ImageBase

当PE文件传入到内存中,内存中的版本叫模块,映射文件的起始地址叫模块句柄,这个初始内存地址叫及地址

这里可以进阶的去理解GetModuleHandle函数了,调用该函数时,传参传入一个文件或者一个名字,函数会返回对应模块的句柄(也就是那个文件被映射的基地址),如果传入的时NULL参数的话,则返回调用的函数的可执行文件

虚拟地址VA

每个PE文件被映射到内存中,都有自己的虚拟空间,这个空间里的地址是虚拟地址

相对虚拟地址RVA

虚拟地址(VA)=基地址+相对虚拟地址(RVA)

文件偏移地址

PE文件在磁盘中的时候,数据相对于文件头地址的地址是文件偏移地址

文件头

MS-DOS头部 00 00-00 B0

每个PE文件都是以一个DOS程序开始的,有了它,DOS就可以识别它为有效的执行体,然后运行紧随的DOSstub。DOSstub也相当于一个.exe文件了,在不支持PE文件格式的操作系统中,它会简单的提示一个不可执行的错误提示,DOS一般由编译软件编译成

PE文件的第一个字节位于一个传统的MS-DOS头部,称为IMAGE_DOS_HEADER,其结构体中比较重要的字段分别是

e_magic和e_lfanew。e_magic字段的值一般需要被设置为5A4Dh,意味着这是一个可执行的文件。这个值有一个#define,名为IMAGE_DOS_SIGNTURE(0x5A4D),在ascii表示法里它的值为“MZ”,他在PE文件中起到定义宏的作用,而有这个宏定义,操作系统才会把这个文件认定为可执行的,而e_lfanew字段是真正的PE文件头的相对偏移,指出真正的PE头的文件偏移位置,占用4字节,位于从文件开始偏移3Ch字节处

第一个划红线的地方是e_magic,值为MZ,第二个是e_flanew,值经过大小端序之后是00 00 00 B0,所以B0处才是真正的文件偏移位置,并且还可以看到右边有一串字符串,!This program cannot be run in DOS mode.这个就是前面提到的不可执行错误

PE文件头(IMAGE_NT_HEADER) 00 B0-01 20

紧跟着DOSstub的是PE文件头,其中包含很多PE转载器会用到的重要字段,当执行体在支持PE文件结构的操作系统中执行时,PE装载器将从IMAGE_DOS_HEADER结构的e_lfanew字段里找到PE_Header的起始偏移量,用其加上基址,得到PE文件头的指针

PNTHeader=ImageBase+dosHeader->e_lfanew

Signature字段

在一个有效的PE文件里,Signature字段被设置为0x 00 00 45 50,ACSII码字符是”PE\0\0“,定义宏为#define IMAGE_NT_SIGNATURE

“PE\0\0”是一个PE文件的开始,而e_flanew字段正是指向”PE\0\0”

IMAGE_FILE_HEADER结构

IMAGE_FILE_HEADER(映射文件头)结构包含PE文件的一些基本信息,最重要的是,其中一个域指出了IMAGE_OPTIONAL_HEADER的大小,这里按照红色框框划分,依次介绍

Machine:可执行文件的目标CPU类型。PE文件可以在多种机器上使用,不同平台上指令的机器码不同

NumberOfSestions:区块数目,块表紧跟在IMAGE_NT_HEADER后面

TimeDateStamp:表示文件的创建时间。这个值是自1970年1月1日以来用格林威治时间计算的秒数,是一个比文件系统的日期/时间更精确的文件创建时间指示器,将这个值翻译成易读的字符串需要_ctime函数。另一个对此字段有用的函数是gmtime

PointerToSymbolTable:COFF符号表的偏移地址位置,如果没有就是0

NumberOfSymbols:如果有COFF符号表,这个代表符号表的数目

SizeOfOptionalHeader:紧跟IMAGE_FILE_HEADER,表示数据的大小。在PE文件中,这个数据结构叫做IMAGE_OPTIONAL_HEADER,其大小依赖于当前文件是32位还是64位文件。对32位文件这个域通常是00E0h;对64位PE32+文件,这个域是00F0h。不管怎样,这些事要求的最小值,较大的值也可能出现

Characteristics:文件属性,有选择地通过几个值的运算得到。这些标志的有效只是定义于winnt.h内的IMAGE_FILE_XXX值。普通EXE文件的这个字段的值一般是010h,DLL文件里这个字段一般是2102h

IMAGE_OPTIONAL_HEADER结构-可选映像头

IMAGE_FILE_HEADER与IMAGE_OPTIONAL_HEADER看起来组成了完整的“PE文件头结构”。IMAGE_OPTINAL_HEADER结构也分为32与32+,不过他们大差不差,如下表示

:::success
typedef struct _IMAGE_OPTIONAL_HEADER
{
//
// Standard fields.
//
+18h WORD Magic; // 标志字, ROM 映像(0107h),普通可执行文件(010Bh)
+1Ah BYTE MajorLinkerVersion; // 链接程序的主版本号
+1Bh BYTE MinorLinkerVersion; // 链接程序的次版本号
+1Ch DWORD SizeOfCode; // 所有含代码的节的总大小
+20h DWORD SizeOfInitializedData; // 所有含已初始化数据的节的总大小
+24h DWORD SizeOfUninitializedData; // 所有含未初始化数据的节的大小
+28h DWORD AddressOfEntryPoint; // 程序执行入口RVA
+2Ch DWORD BaseOfCode; // 代码的区块的起始RVA
+30h DWORD BaseOfData; // 数据的区块的起始RVA
//
// NT additional fields. 以下是属于NT结构增加的领域。
//
+34h DWORD ImageBase; // 程序的首选装载地址
+38h DWORD SectionAlignment; // 内存中的区块的对齐大小
+3Ch DWORD FileAlignment; // 文件中的区块的对齐大小
+40h WORD MajorOperatingSystemVersion; // 要求操作系统最低版本号的主版本号
+42h WORD MinorOperatingSystemVersion; // 要求操作系统最低版本号的副版本号
+44h WORD MajorImageVersion; // 可运行于操作系统的主版本号
+46h WORD MinorImageVersion; // 可运行于操作系统的次版本号
+48h WORD MajorSubsystemVersion; // 要求最低子系统版本的主版本号
+4Ah WORD MinorSubsystemVersion; // 要求最低子系统版本的次版本号
+4Ch DWORD Win32VersionValue; // 莫须有字段,不被病毒利用的话一般为0
+50h DWORD SizeOfImage; // 映像装入内存后的总尺寸
+54h DWORD SizeOfHeaders; // 所有头 + 区块表的尺寸大小
+58h DWORD CheckSum; // 映像的校检和
+5Ch WORD Subsystem; // 可执行文件期望的子系统
+5Eh WORD DllCharacteristics; // DllMain()函数何时被调用,默认为 0
+60h DWORD SizeOfStackReserve; // 初始化时的栈大小
+64h DWORD SizeOfStackCommit; // 初始化时实际提交的栈大小
+68h DWORD SizeOfHeapReserve; // 初始化时保留的堆大小
+6Ch DWORD SizeOfHeapCommit; // 初始化时实际提交的堆大小
+70h DWORD LoaderFlags; // 与调试有关,默认为 0
+74h DWORD NumberOfRvaAndSizes; // 下边数据目录的项数,这个字段自Windows NT 发布以来一直是16
+78h IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
// 数据目录表
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

:::

#define IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10b // 32位PE可选头
#defineIMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b // 64位PE可选头
#defineIMAGE_ROM_OPTIONAL_HDR_MAGIC 0x107
Magic:标识可选头位数。
AddressOfEntryPoint:持有EP的RVA(地址),指出程序最先执行的代码起始地址。
ImageBase:优先装填区域,有点像段地址的感觉。
SectionAlignmentFileAlignment:File——节区在文件中的最小单位,Section——节区在内存中的最小单位。
SizeOfimage:指定了PE Image在虚拟内存中所占空间大小。
SizeOfHeaders:指出整个PE头的大小。
Subststem:区分文件类型(如:sys,exe,dll)。
NumberOfRvaAndSizes:指定DataDirectory数组的个数。
DataDirectory:由IMAGE_DATA_DIRECTORY结构体组成的数组,指向输出表,输入表等等数据(这里在所有的红圈圈之后)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedefstruct_IMAGE_DATA_DIRECTORY{
DWORDVirtualAddress;//数据起始的RVA
DWORDSize;//数据块的长度
}IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;VirtualAddress
#defineIMAGE_DIRECTORY_ENTRY_EXPORT 0 //0 导出表
#defineIMAGE_DIRECTORY_ENTRY_IMPORT 1 //1 导入表
#defineIMAGE_DIRECTORY_ENTRY_RESOURCE 2 //2 资源目录
#defineIMAGE_DIRECTORY_ENTRY_EXCEPTION 3 //3 异常目录
#defineIMAGE_DIRECTORY_ENTRY_SECURITY 4 //4 安全目录
#defineIMAGE_DIRECTORY_ENTRY_BASERELOC 5 /5 重定位基本表
#defineIMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#defineIMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#defineIMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#defineIMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#defineIMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#defineIMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#defineIMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#defineIMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#defineIMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor

区块

区块表

在pe文件头后面便是区块表,是一个IMAGE_SECTION_HEADER结构数组。每个结构都包含了他所关联的区块信息,该数组个数由IMAGE_SECTION_HEADER.FileHeader.NumberOfSections提出([想起来了,都想起来了].jpg)

:::success
typedef struct _IMAGE_SECTION_HEADER

{

BYTE Name[IMAGE_SIZEOF_SHORT_NAME];   // 节表名称,如“.text” 

                                          //IMAGE_SIZEOF_SHORT_NAME=8

union{

   DWORD PhysicalAddress; // 在文件中的物理地址

DWORD VirtualSize; // 真实长度,这两个值是一个联合结构,可以使用其中的任何一个,一般是取后一个

} Misc;

DWORD VirtualAddress; // 节区的 RVA 地址

DWORD SizeOfRawData; // 在文件中对齐后的尺寸

DWORD PointerToRawData; // 在文件中的偏移量

DWORD PointerToRelocations; // 在OBJ文件中使用,重定位的偏移

DWORD PointerToLinenumbers; // 行号表的偏移(供调试使用地)

WORD NumberOfRelocations; // 在OBJ文件中使用,重定位项数目

WORD NumberOfLinenumbers; // 行号表中行号的数目

DWORD Characteristics; // 节属性如可读,可写,可执行等

} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

:::

(1)Name:区块名。这是一个由8个ASCII码组成,用来定义区块的名称的数组。多数区块名都习惯性以一个“.”作为开头(例如:.text),这个“.”实际上是不是必须的。值得我们注意的是,如果区块名达到8 个字节,后面就没有0字符了。前边带有一个$ 的区块名字会从连接器那里得到特殊的待遇,前边带有$ 的相同名字的区块在载入时候将会被合并,在合并之后的区块中,他们是按照“$”后边的字符的字母顺序进行合并的。每个区块的名称都是唯一的,不能有同名的两个区块。但事实上节的名称不代表任何含义,他的存在仅仅是为了正规统一编程的时候方便程序员查看方便而设置的一个标记而已。所以将包含代码的区块命名为“.Data”或者说将包含数据的区块命名为“.Code”都是合法的。当我们要从PE 文件中读取需要的区块时候,不能以区块的名称作为定位的标准和依据,正确的方法是按照IMAGE_OPTIONAL_HEADER32 结构中的数据目录字段结合进行定位。

(2) VirtualSize:对表对应的区块的大小,这是区块的数据在没有进行对齐处理前的实际大小。如果VirtualSize > SizeOfRawData,那么SizeOfRawData是可执行文件初始化数据的大小(SizeOfRawData – VirtualSize)的字节用0来填充。这个字段在OBJ文件中被设为0。

(3)VirtualAddress:该块时装载到内存中的RVA,注意这个地址是按内存页对齐的,她总是SectionAlignment的整数倍,在工具中第一个块默认RVA为1000,在OBJ中为0。

(4)SizeofRawData:该块在磁盘中所占的大小,在可执行文件中,该字段包括经过FileAlignment调整后块的长度。例如FileAlignment的大小为200h,如果VirtualSize中的块长度为19Ah个字节,这一块保存的长度为200h个字节。

(5) PointerToRawData:该块是在磁盘文件中的偏移,程序编译或汇编后生成原始数据,这个字段用于给出原始数据块在文件的偏移,如果程序自装载PE或COFF文件(而不是由OS装载),这种情况,必须完全使用线性映像方法装入文件,需要在该块处找到块的数据。

上述四个数据很重要,在分析的时候经常用到,因此需要熟悉其代表的具体意义。

PointerToRawDate+SizeofRawData=下一块偏移地址

(6) PointerToRelocations 在PE中无意义

(7) PointerToLinenumbers 行号表在文件中的偏移值,文件调试的信息

(8) NumberOfRelocations 在PE中无意义

(9) NumberOfLinenumbers 该块在行号表中的行号数目

(10) Characteristics 块属性,(如代码/数据/可读/可写)的标志,这个值可通过链接器的/SECTION选项设置.下面是比较重要的标志:

常见区块与区块合并

区块在映像中是按照RVA划分的,常见区块如下

链接器的一个有趣特征就是能够合并区块。如果两个区块有相似、一致性的属性,那么它们在链接的时候能被合并成一个单一的区块。这取决于是否开启编译器的 /merge 开关。事实上合并区块有一个好处就是可以节省磁盘的内存空间……注意:我们不应该将.rsrc、.reloc、.pdata 合并到··的区块里。

区块的对齐值

区块的大小是需要对齐的,有两种对齐值,一种用于磁盘文件,一种用于内存中,PE文件头中FileAlignment与SectionAlignment分别声明了磁盘区块与内存区块的对齐值

如果区块没这么多,就用0填充

文件偏移与虚拟地址的转换

RVA 和文件偏移的转换

RVA 是相对虚拟地址(RelativeVirtual Address)的缩写,顾名思义,它是一个“相对地址”是相对于基地址的相对地址

举个例子,如果 Windows 装载器将一个PE 文件装入到 00400000h 处的内存中,而某个区块中的某个数据被装入 0040··xh 处,那么这个数据的 RVA 就是(0040··xh - 00400000h )= ··xh,反过来说,将 RVA 的值加上文件被装载的基地址,就可以找到数据在内存中的实际地址。

换算 RVA 和文件偏移

因为对齐值的不同,所以文件映射在内存中相对偏移地址会不同,也就出现了文件偏移地址-相对虚拟地址的转换

从图中可以看到 File Offset=RVA+k

 File Offset=VA-k-ImageBase

后面不同块的转换计算又要涉及到,区块间隙的不同,可以按照表自己去算,也可以用工具

输入表

小tips,这里之前可以先看看c语言补习班中的联合体概念

一个程序,使用来自其他DLL的代码或数据的动作称为输入。当PE文件被载入时,Windows加载器的工作之一就是定位所有被输入的函数和数据

输入函数的调用

输入函数就是这个程序要调用的函数,但是这个函数不在程序所在文件中,他可能在别的DLL文件中,在运行之前,PE加载器是不知道使用的函数在其文件中的地址等信息,直到使用了这个函数才会有信息

当应用程序调用一个DLL文件的代码或者数据的时候,它正在被隐式的地链接到DLL,这个过程由Windows加载器自动完成,在运行到相关API时,这意味着其所在的DLL文件已经被加载

这些过程几乎都是由Loadlibrary与GetProcAddress函数完成的,并且当他们在隐式链接的时候,这些工作都是由Windows加载器完成的,如果调用的DLL文件里的函数里还有别的DLL文件里的函数,也将由Windows加载器来完成相关加载调用

在PE文件里有一组数据结构,它们分别对应于被输入的DLL。每一个这样的结构都给出了被输入的DLL的名称并指向一组函数指针,这组函数指针被称为输入地址表(IAT),每一个被引入的API在IAT里都有保留位置,在那里它将被Windows加载器写入输入函数的地址

一旦模块被载入,IAT中将包含所要调用输入函数的地址

如果要调用一个输入函数,那么就会有如下两种方式调用

我们先假设402010h处有我们需要调用的函数地址,且402010h位于IAT中

一.低效调用

:::success
Call 00401164

……

:00401164

Jmp dword ptr [00402010]

:::

可以看到是先跳转到一个jmp指令在实现跳转,这是因为**CALL**指令中后面紧跟着的其实是实际地址,而并不是函数指针(dword ptr [00402010]),所以需要配合jmp指令

二.优化

那么想要让call指令后变成函数指针类型,就需要一个给编译器的提示形式,指令变成

:::success
CALL DWORD PTR [XXXXXXXX]

:::

可以发现变成跳转函数指针了,但是这样的跳转就需要目标函数定义的时候加上前缀_declspec,示例如下:

:::success
_declsped(dllimport) void Foo(void);

:::

输入表的结构

在PE文件头的可选映像头IMAGE_OPTIONAL_HEADER,数据目录表的第2个成员指向输入表。输入表以一个IMAGE_IMPORT_DESCRIPTOR(IID)数组开始。每个被PE文件隐式链接的DLL都有一个IID。

在这个结构中并没有指出该结构数组的项数,但是他最后一个单元是NULL,例如,某个PE文件从两个DLL文件中引入函数,因此存在两个IID结构来描述这些DLL,并在两个IID结构的最后一个内容全为0的IID结构作为结束。IID的结构如下

:::success
typedef struct _IMAGE_IMPORT_DESCRIPTOR {

union {

    DWORD   Characteristics;

    DWORD   OriginalFirstThunk;             //导入名称表(INT)的地址RVA

} DUMMYUNIONNAME;

DWORD   TimeDateStamp;                      //时间戳多数情况可忽略.如果是0xFFFFFFFF为绑定导入

DWORD   ForwarderChain;                     //链表的前一个结构

DWORD   Name;                               //导入DLL文件名的地址RVA

DWORD   FirstThunk;                         //导入地址表(IAT)的地址RVA

} IMAGE_IMPORT_DESCRIPTOR;

typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

:::

**OriginalFirstThunk**:包含指向**导入名称表(INT)**的RVA。INT是一个**IMAGE_THUNK_DATA**结构的数组,数组中的每个**IMAGE_THUNK_DATA**结构都指向**IMAGE_IMPORT_BY_NAME**,数组以一个内容为0的IMAGE_THUNK_DATA结构结束

IMAGE_THUNK_DATA是双字的结构如下:

:::success
//成员OriginalFirstThunk与FirstThunk都指向此结构:

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;

:::

IMAGE_THUNK_DATA 是一个联合体,在任何时刻,它只会使用其中一个成员。而我们通常关心的是 AddressOfData,它存储指向 IMAGE_IMPORT_BY_NAME 结构的指针。

IMAGE_THUNK_DATA值的最高位为1时,表示函数以序号方式输入,这时低31位或63位被看成一个函数序号

IMAGE_THUNK_DATA值得最高位为0时,表示函数以字符串类型的函数名方式输入,这时双字的值是一个RVA。指向IMAGE_IMPORT_BY_NAME结构

IMAGE_IMPORT_BY_NAME仅有一字大小,结构如下:

:::success
//如果结构IMAGE_THUNK_DATA32成员最高有效位(MSB)为1时低31位为导出序号.否则指向此结构.

typedef struct _IMAGE_IMPORT_BY_NAME {

WORD    Hint;   //导出序号(有些编译器不会填充此值)

CHAR   Name[1]; //该值长度不准确,以0结尾的字符串.导出函数名.

} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

:::

 **Hint**:本函数在其所驻留的DLL的输出表的序号。这个值不是必须的,一些链接器将它设为0。他的作用是让PE装载器快速从DLL的输出表中快速查询函数

Name:含有输入函数的函数名。函数名是一个ASCII字符串,以“NULL”结尾

:::color5
举例:如果程序需要调用外部函数 MessageBoxAOriginalFirstThunk 可能会包含如下信息:

  1. 它首先指向 IMAGE_THUNK_DATA 数组中的第一个元素,该元素指向 IMAGE_IMPORT_BY_NAME,而 IMAGE_IMPORT_BY_NAME 则存储 MessageBoxA 的名称。
  2. 该数组以一个 IMAGE_THUNK_DATA 结构结束,内容为 0,表示数组的终结

:::

也就是本来这里存的是地址,而地址指向的是一个联合体,联合体中的第四个存储的是IMAGE_IMPORT_BY_NAME 的地址,最后IMAGE_IMPORT_BY_NAME 中有关于导入函数的信息

这里要是还不明白先看下面的图

TimeDateStamp:一个32位的时间标志,可以忽略

ForwarderChain:这是第一个被转向的API的索引,一般为0,在程序引用一个DLL中的API而这个API又在引用其他DLL的API时使用(很少出现)

NameDLL名字的指针。他是一个以”00”结尾的ASCII字符的RVA地址,该字符串包含输入的DLL名,例如“KERNEKL32.DLL””USER32.DLL”

FirstThunk:包含指向输入地址表(IAT)的RVA,IAT也是IMAGE_THUNK_DATA结构的数组

是不是发现,欸?FirstThunkOriginalFirstThunk感觉一样啊,因为他们本质上都是指向了IMAGE_THUNK_DATA结构,如图

他们的结束都是由一个值为0的IMAGE_THUNK_DATA元素表示的。

输入地址表

那么为什么会有两个看上去一样的结构一样的表呢

第一个由OriginalFirstThunk指向的IMAGE_THUNK_DATA是不可改写的,单独的一项,而第二个由FirstThunk指向的IMAGE_THUNK_DATA是由PE装载器,通过第一个OriginalFirstThunk指向的IMAGE_THUNK_DATA找到的IMAGE_IMPORT_BY_NAME中的地址,写到第二个表中的,如下图好理解

输入表实例分析

在这里进行这节的汇总,首先输入表在数据目录表的第2个成员指向输入表(**DataDirectory[16]**这个东东)

文件里看到是80h,再根据头文件起始是B0h,输入表地址就是在整个文件的130h处,看到是40 20 00 00,那么就是002040h,不过这里是RVA值,如果在磁盘文件中查看的话就得转换成文件偏移地址

通过stu_PE可以看到,2040h属于.rdata段,且.rdata段文件偏移对应的是600h,相对虚拟地址是2000h,所以k=2000h-600h=1A00h(注意这里是16进制算法),所以输入表**IMAGE_IMPORT_DESCRIPTOR(IID)**在2040h-1A00h=640h处

如图便是IID处,IID包含五个双字,因为IID结束的标志是下一个IID都是0,起到NULL的效果,所以上图一共是有两个IID,每个IID的第四个字段是指向DLL名称的指针,那我们来看第一个,是00 00 21 74,根据上一张图我们知道,还是属于.rdata段,所以计算结果是2174h-1A00h=774h

原来是USER.dll,再通过IID开始的数组元素一步步去找函数名,00 00 20 8C->06 8C,是00002110->710,所以INT是如下图

但是发现,如›LoadIconA函数,前面有两个不可见字符,这个便是作为函数名(Hint)引用的,可以为0

并且还可以观察到,IID里面的IAT与INT指向的地方刚开始是一样的。系统在程序初始化的时候会根据OrginalFirstThunk的值找到函数名,然后用GetProcAddress函数(后功能类似的系统代码)并根据函数名取得函数的入口地址,最后用函数入口地址取代FirstThunk指向的地址串中对应的值

这是开始前的图

接下来是一个从内存中抓取的实例(刚刚程序在内存中启动后保存成磁盘文件),所以他的结构就是映射到内存中的状态,由于内存中的对齐值与内存页相同,所以此时文件偏移地址与相对虚拟地址相等,先看原本的IID(2040),也就是不用转换的值

发现,OrginalFirstThunk没变,但是FirstThunk变了,跟着去看2010->会得到一个地址表(IAT)如下

这个地址装的就是函数地址了,最后上一个图

绑定输入

这个的存在就是为了节省一个程序启动的准备时间,如果一个程序每开一次都要一个个去映射DLL文件然后找地址再赋值,是很麻烦的

那么就可以写好一个程序使用绑定程序将其绑定,此时IAT在磁盘中也存放的是与DLL输出函数相关的实际地址,如VS中的Bind.exe就是一个绑定程序

在进程执行中就需要两个假设

- 当程序初始化时,需要的DLL实际上加载到了他们的首选基地址中
- 自从绑定操作执行以来,DLL输出表中引用的符号位置一直没有改变

数据目录表的第12个成员指向绑定输入,绑定输入以一个IMAGE_BOUND_IMPORT_DESCRIPTOR结构的数组开始

:::success
typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR {

     DWORD TimeDateStamp; 被输入DLL的时间,以便加载器快速判断绑定是不是最新的 WORD  			OffsetModuleName; 指向被输入DLL的名称的偏移,与第一个IBID结构之间的偏移 WORD 			NumberOfModuleForwarderRefs; 其后ModuleForwarderRefs的数目 

// Array of zero or more IMAGE_BOUND_FORWARDER_REF follows

} IMAGE_BOUND_IMPORT_DESCRIPTOR,

*PIMAGE_BOUND_IMPORT_DESCRIPTOR;

:::

输出表

输出表的结构

输出表的主要内容是一个表格,其中包含函数名称、输出序数等

输出表是数据目录表的第一个成员,指向IMAGE_EXPORT_DIRECTORY(IED)

:::success
typedef struct _IMAGE_EXPORT_DIRECTORY {

DWORD Characteristics;

DWORD TimeDateStamp;

WORD MajorVersion;

WORD MinorVersion;

DWORD Name; //DLL的名称

DWORD Base; //起始序数值,正常为1,查询输出函数时,将该值减去,得到进入EAT的索引 DWORD NumberOfFunctions; //EAT中的条目数量

DWORD NumberOfNames; //ENT表中的条目数量

DWORD AddressOfFunctions; // EAT的RVA

DWORD AddressOfNames; // ENT的RVA

DWORD AddressOfNameOrdinals; // 输出序数表的RVA

} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

:::

这里加两个名称,输出地址表(EAT),输出函数名称表(ENT)

输出表实例分析

先根据数据目录表的第一个元素,78h知道偏移位置,再根据PE文件头起始位置是100h,所以去看178h

这里是00 40 00 00,倒过来就是00 00 40 00->0C00h

这个就是输出表的内容了

Name:在00004032h->C32h处,可以看到是DLLDemo.DLL

AddressOfNames:在0000402Ch->C2Ch处,可以看到是0000403E,是个指针,再跟着去看C3Eh,MsgBox函数名

AddressOfNameOrdinals:在4030h处也就是C30h,00 00代表输出序号数组

PE装载器调用GetProcAddress来查找DLLDemo.DLL里的API函数MsgBox,系统通过这个结构(IMAGE_EXPORT_DIRECTORY)来获得ENT的起始地址,然后通过二进制查找,知道发现目标函数的字符串,最后用输出序数,进入EAT找到目标函数的RVA最后得到地址

基址重定位

基址重定义的概念

这个好理解,就是把可能需要改变的地址提前存在一个数组里,等真的要改变基地址的时候(比如程序的基地址在00400000h,DLL载入一般在00870000h)去计算新的

那么有可能改变的是什么呢?就是如下这种

:::success
jmp 00401020

call 00402210

:::

这样子的地址,如果他的基地址是00400000h的话,这个可以成立,但是如果是00870000h呢?所以到时候就会被标注改成对应的跳转,如下图

基址重定位表的结构

基址重定位表位于**.reloc**块中

数据目录表的IMAGE_DIRECTORY_ENTRY_BASERELOC条目记录了他的位置

基址重定位数据采用类似按页分割的方法组织,是由许多重定位块串接成的

IMAGE_BASE_RELOCATION结构开始

:::success
IMAGE_BASE_RELOCATION STRUCT

DWORD VirtualAddress; //这一组重定位数据的开始RVA,各重定向的地址必须加上这个RVA才是重 定向的完整RVA

DWORD SizeOfBlock; //当前重定位结构的大小

IMAGE_BASE_RELOCATION ENDS

WORD TypeOffset[1]; //数组,分高4位和低12位,高4位代表重定位类型,低12位代表重定位地址

//该地址与VirtualAddress相加,就得到需要修改数据的地址的指针

:::

基址重定位表实例分析

以DllDemo.DLL为例分析,数据目录表指向重定位表的指针是00005000h->00000E00h

VirtualAddress SizeOfBlock TypeOffset[](两字节大小)

以第一个为例试试,300F,高四位是3,所以是类型3,低十二位是00F,也就是说第一个需要改动的是00001000h+000000Fh=100Fh->60Fh

资源

资源结构

资源目录结构 IMAGE_RESOURCE_DIRECTORY

数据目录表中的IMAGE_DIRECTORY_ENTRY_RESOUCE包含资源的RVA和大小

资源目录结构中的每一个节点都是由IMAGE_RESOURCE_DIRECTORY结构和紧随其后的数个IMAGE_RESOURCE_DICETORY_ENTRY结构组成的,这两种结构组成了一个数据块

IMAGE_RESOURCE_DIRECTORY一般16字节,定义如下

:::success
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;

:::

NumberOfNamedEntriesNumberOfIdEntries加起来是本目录的目录项总和

资源目录入口结构 IMAGE_RESOURCE_DICETORY_ENTRY

紧跟资源目录结构的是资源目录入口结构

IMAGE_RESOURCE_DICETORY_ENTRY,长度8字节,包含两个字段

:::success
IMAGE_RESOURCE_DIRECTORY_ENTRY STRUCT

DWORD Name //目录项的名称字符串指针或ID

DWORD OffsetToData //资源数据偏移地址或子目录偏移地址

IMAGE_RESOURCE_DIRECTORY_ENTRY ENDS

:::

Name

    * 当结构用于第1层目录时,定义的是资源类型

    * 当结构用于第2层目录时,定义的是资源的名称
    * 当结构用于第3层目录时,定义的是代码页编号
    * 当最高值为0时,当ID用
    * 当最高值为1时,低位为指针,资源名称字符串使用**Unicode**编码,指针指向的是**IMAGE_RESOUCE_DIR_STRING_U**结构定义如下

:::success
IMAGE_RESOUCE_DIR_STRING_U STRUCT

WORD Length //字符串的长度

WCHAR NameString //Unicode字符串,按字对齐长度可变

//由Length指明Unicode字符串的长度

IMAGE_RESOUCE_DIR_STRING_U ENDS

:::

OffsetToData

    * 当最高位为1时,低位数据指向下一层目录块的起始位置
    * 当最高位为0时,指针指向**IMAGE_RESOURCE_DATA_ENTRY**结构

注意:Name与OffsetData作为指针的时候,偏移量从资源区块开始算起,而不是RVA(根目录的起始位置)开始

资源数据入口 IMAGE_RESOURCE_DATA_ENTRY

经过三层IMAGE_RESOURCE_DICETORY_ENTRY(一般是),第三层目录结构中OffsetToData一般指向IMAGE_RESOURCE_DATA_ENTRY,该结构描述了资源数据的位置和大小,其定义如下

:::success
IMAGE_RESOURCE_DATA_ENTRY STRUCT

DOWRD OffsetToData //资源数据的RVA

DOWRD Size //资源数据的长度

DOWRD CodePage //代码页,一般为0

DOWRD Reserved //保留字段

IMAGE_RESOURCE_DATA_ENTRYENDS

:::

经过多层结构,这里的IMAGE_RESOURCE_DATA_ENTRY结构就是真正的资源数据了,OffsetToData指向资源数据的指针

资源结构实例分析

实例:pediy.exe

数据目录表第三个成员指向资源结构,该指针具体位置在PE文件头的88h处

该文件PE文件头起始位置为0C0h,所以去看0C0h+88h=148h

是“00 40 00 00”->00004000h,因为本文件对齐值为1000h,所以直接看4000h即可

这里再捋一遍,IMAGE_DIRECTORY_ENTRY_RESOUCE(资源目录结构)后面会跟着很多IMAGE_RESOURCE_DICETORY_ENTRY(资源目录入口结构),在资源目录入口结构中的OffsetToData会指向下一层目录,如此往返

根目录

根据结构IMAGE_DIRECTORY_ENTRY_RESOUCE

第一层目录中,第二个IMAGE_RESOURCE_DICETORY_ENTRY结构中Name定义为类型,ID为04h,所以是菜单,在根据OffsetToData找到下一级目录,首先OffsetToData字段80000040第一个字节80h的二进制码

这里后面的内容是重复的,只需要注意每层目录的IMAGE_RESOURCE_DICETORY_ENTRY里的Name定义即可

资源编辑工具

资源数据一般存储在PE文件的.rsrc区块中,而且不能通过由程序源代码定义的变量直接访问

资源类型主要有一下三种

- VC类标准资源:包括菜单、对话框、串表等资源
- Delphi类标准资源:Rcdata资源
- 非标准的Unicode字符:主要是一些VB编译程序等

对这些资源可以进行定制和修改,例如更改字体,对话框,增加按钮、菜单等

TIS

TLS是各线程的独立的数据存储空间,使用TLS技术可在线程内部独立使用

TLS回调函数

每当创建/终止进程的线程时会自动调用执行的函数