对《逆向工程核心原理》 PE 相关的拓展知识。
Windows 系列:
Windows 1.0 ,Windows 2.0 ,Windows 3.0 ,Windows 3.1 ,Windows 95 , Windows 98 ,Windows 98 SE , Windows ME , Windows NT , Windows 2000 , Windows XP , Windows Vista , Windows 7
Win 16 平台,比如 Windows 3.x ,可执行文件格式是 NE 。
PE 文件(Windows 95 以后)衍生于 COFF 文件格式(也是一种跨平台的通用格式),这使得文件格式统一,exe 和 dll 文件的格式唯一的区别就是用一个字段标识出这个文件是 DLL 还是 Exe。OCX 控件和控制面板程序(.CPL文件)都是 DLL ,拥有一样的实体。
PE 载入内存后,映射文件的起始地址称为模块句柄(hModule),也称为基地址(ImageBase),Windows CE 除外。并不是所有的数据都会映射,不能映射的数据放在文件的尾部。
Instance Handle: NT、95 将基地址称为 Hinstance(实例句柄),每个执行实例都有自己的数据段并以此相互区分;32 位 Windows 后每个应用程序都拥有一个虚拟地址空间,不再需要区分,也不再需要使用这个名字了。
页表(Page Table)在当中扮演了地址转换的角色,系统为每一个进程维护了一份独立的页表。虚拟内存本身是逻辑意义上的,一个程序若占有4GB的虚拟内存,并非4GB全部映射到物理内存上,而是存有程序数据的地方才会映射到物理内存上。
A4/B3 (共享内存)如共享动态库一样,被加载到物理内存中,程序使用时直接将其映射到虚拟内存空间。驻留内存是指位于物理内存中的内存块。
DOS 头
e_lfarlc 是指 relocation,指向 Dos Stub 代码起始处,e_ip,e_cs 都是 0
FileHeader 文件头
PointerToSymbolTable: COFF 符号表的文件偏移位置。
NumberOfSymbols: 如果有 COFF 符号表,它代表其中的符号数目。现在采用新的 debug 格式,因此上述两个域一般都为 0
SizeOfOptionalHeader :指定大小,32位通常是 0E0h,64 位通常是 0F0h,但这只是要求的最小值。
OptionalHeader 可选头
64位与32位的差别就在这个结构中,其中只有几个域长度变长了:SizeOfStackReserve, ImageBase, SizeOfStackCommit, SizeOfHeapReserve, SizeOfHeapCommit 都变为了 ULONG,8字节。
SizeOfCode:有 IMAGE_SCN_CNT_CODE 属性的区块的总大小(只入不舍),这个值是向上对齐某一个值的整数倍。例如,本例是200h,即对齐的是一个磁盘扇区字节数(200h)的整数倍。在通常情况下,多数文件只有1个 Code 块,所以这个字段和 .text 块的大小匹配。
SizeOfInitializedData:已初始化数据块的大小,即在编译时所构成的块的大小(不包括代码段)。但这个数据不太准确。而未初始化数据放在 .bss 块中。
BaseOfCode:代码段的起始RVA。在内存中,代码段通常在PE文件头之后,数据块之前。在Microsoft链接器生成的可执行文件中,RVA的值通常是 1000h。Borland 的 Tlink32 用ImageBase 加第1个 Code Section 的 RVA,并将结果存入该字段。
AddressOfEntryPoint:程序执行入口 RVA。对于 DLL,这个入口点在进程初始化和关闭时及线程创建和毁灭时被调用。在大多数可执行文件中,这个地址不直接指向 Main、WinMain 或 DlIMain 函数,而指向运行时的库代码(ntdll_RtlUserThreadStart )并由它来调用上述函数。在 DLL 中,这个域能被设置为0,此时前面提到的通知消息都无法收到。链接器的 /NOENTRY 开关可以设置这个域为0。
ImageBase:文件在内存中的首选载入地址。如果有可能(也就是说,如果目前没有其他文件占据这块地址,它就是正确对齐的并且是一个合法的地址),加载器会试图在这个地址载入PE 文件。如果 PE 文件是在这个地址载入的,那么加载器将跳过应用基址重定位的步骤。
SectionAlignment:如果它小于 CPU 页尺寸,则必须与 FileAlignment 对齐。
后面的 major 和 minor 指主要的和次要的的意思
CheckSum:映像的校验和。IMACEHLP.DLL 中的CheckSumMappedFile函数可以计算该值。一般的EXE 文件该值可以是0,但一些内核模式的驱动程序和系统DLL必须有一个校验和。当链接器的/RELEASE开关被使用时,校验和被置于文件中。
Subsystem:一个标明可执行文件所期望的子系统(用户界面类型)的枚举值。这个值只对EXE 重要。
LoaderFlags:与调试有关
区块表
Name:前面的 . 不是必须的,名字长度超过8字节,没有终止标志的 NULL 字符。前面带有“$”字符的同名区块会被合并。这些区块是按“$”后面的字符的字母顺序合并的。
VirtualSize、VirtualAddress:在 obj 文件中该字段设置为 0
NumberOfRelocations:在 EXE 中无意义,在 obj 文件中有意义。
Characteristics:(可丢弃应该是指不载入内存)
常见区块与区块合并
可以通过声明,将数据插入自定义的区块,而不是默认的区块。
链接器的工作是合并所有 OBJ 和库中所有的块,使其最终成为一个合适的区块。OBJ 文件中还可以存在一个放置链接信息的区块,链接完即删除掉。
区块合并没有什么硬性规定,把 .rdata 合并到 .text 不会有什么问题。但不应将 .rsrc、.reloc 或 .pdata合并到其他区块里。
输入表
逆向工程核心原理那里写的思路实在是有点混乱,这里稍作整理。
程序在编译时即预留 IAT 位置,运行时 Windows 加载器将输入函数的地址写入。下面有两种调用函数的方式:
① Call 00401164
:00401164
Jmp dword ptr ds:[00402010]
② Call DWORD PTR [00402010]
因为编译器无法确定函数调用的地址,后面会由链接器填充实际的地址。因为编译器不能区分到底是DLL库中的输入函数还是程序中自定义的函数,默认情况下,会使用如 ① 中的 Call 方式(这种方式是自定义函数的 Call 方式);当链接器找到该函数来自于另一个 DLL,而输入函数的一般调用方式是通过间接调用即方式 ②(间接调用的好处在于:不需要修改原始代码,程序加载时是直接将地址写入 IAT 的),因此会单独拿出一块地址,专门用作 JMP stub 。
在输出函数前面加上修饰符 “__declspec(dllimport)” 可以告诉编译器这个函数来自另一个 DLL,编译器就会给函数加上 “__imp_ ” 前缀,使用方式 ② 调用函数。此时,在编译阶段就能定位到要使用的函数(猜测该修饰符实现了符号共享,函数名可以被程序看到),并在 IAT 表中留出一个位置给该函数,否则会在链接阶段找匹配的函数 。系统文件中的很多导出 API 都是这样加了修饰符的。
输入表结构
ForwarderChain:第一个被转向的 API 的索引。程序中引用的 DLL 的 API 引用了另一个 DLL 的 API 时使用。
INT、IAT(对前面混乱表述的整理)
1#. 之前查看的程序是 Win xp sp3 中的 notepad.exe。在 IAT 中看到的都是硬编码后的地址,与 Windows 装载器装载之后的地址值相同,因为这个程序是绑定输入的。
现在重新来看看由书作者自行编写的程序。得到 User32.dll 的 INT 和 IAT 的文件偏移分别是 68c,610
INT
IAT
可以看到 INT 和 IAT 中存储的值都是一样的。注意该大小变为了 64 bits 。
这两个结构同属于一个结构体:IMAGE_THUNK_DATA
未载入时,该双字的最高为1时,低位全部用来表示 Ordinal。最高位为 0 时,是一个指向 IMAGE_IMPORT_BY_NAME 的 RVA 地址。载入后,意义就不大了,只用 IAT 就可以正常运行了。
Hint:若有值,则被用来在 DLL 的输出表里快速查询函数,非必需。
Name:大小可变,ASCII 字符串,以 NULL 结尾
绑定输入表(32/64 结构体大小无变化)
因为程序载入时会检查输入表并将相关 DLL 映射到进程地址空间,用真实的函数地址逐个替换 IAT 。如果程序早就放好了地址,那么 PE 装载器就不需要替换地址。Visual Studio 中的 Bind.exe 可以实现绑定功能。
当下面任意一种情况发生时,IAT 中所有的地址都被判定无效。
1#.进程初始化时,DLL 加载到了它们的首先基地址中。
2#.自从绑定操作执行以来,DLL 输出表中引用的符号位置一直没有改变。
若 IAT 无效,则通过 INT 来加载。若没有 INT 则可执行文件不能被绑定(MicroSoft 链接器)。Borland 链接器生成的文件不能被绑定。当程序安装时,Windows 安装器的 BindImage 会执行绑定操作。
DataDirectory 的第 12 个成员 IMAGE_BOUND_IMPORT_DESCRIPTOR :
TimeDateStamp:一个双字,包含一个被输入 DLL 的时间/日期戳。它允许加载器快速判断绑定是否是新的。
OffsetModuleName:一个字,包含一个指向被输入 DLL 的名称的偏移。这个字段是与第1个 IBID 结构之间的偏移(不是 RVA ).
NumberOfModuleForwarderRefs:一个字,包含紧跟该结构的 IMAGE_BOUND_FORWARDER_REF 结构的数目。除了最后一个字(NumberOfModuleForwarderRefs)被保留外,其结构和 IBID 相同。
当绑定的一个 API 被转向另一个 DLL 时,转向的 DLL 的有效性也要被检查。这样,IMAGE_BOUND_FORWARDER_REF 和 IMAGE_BOUND_IMPORT_DESCRIPTOR 结构就是交叉存取的了。例如,链接到HeapAlloc,它被转向 NTDLL 中的 RtlAllocateHeap,然后对可执行文件运行 BIND。在 EXE 里,已经有一个针对 KERNEL32.DLL 的 IBID,它的后面跟着一个针对 NTDLLDLL 的 IMAGE_BOUND_FORWARDER_REF。跟在后面的可能是另外绑定的针对其他 DLL 的 IBID。当然,若 NumberOfModuleForwarderRefs 为 0,则不会后面不会跟着 IMAGE_BOUND_FORWARDER_REF 。
基址重定位表(32/64 结构体大小无变化)
在文件中找到重定位结构体中的相关数据,该数据就是 PE 文件映射在要求的 ImageBase 上时正确的数据。发生重定位时,将其修改为 该数据值-ImageBase+实际映射的地址。
资源表
比较复杂。资源修改工具:Resource Hacker 和 eXeScope 等。https://www.cnblogs.com/qintangtao/archive/2013/01/11/2857193.html
TLS 表
使用线程本地存储器(TLS)可以将数据与执行的特定线程联系起来。当使用 _declspec(thread) 声明的 TLS 变量时,编译器将它们放入一个 .tls 区块。当应用程序加载到内存中时,系统要寻找可执行文件中的 .tls区块,并且动态地分配一个足够大的内存块,以便存放所有的 TLS 变量。系统也将一个指向已分配的内存的指针放到 TLS 数组里,这个数组由 FS:[2Ch] 指向(在 x86 架构上)。
TLS 结构体第一个和第二个元素指向 .TLS 块。IMAGE_TLS_DIRECTORY 本身不在 .tls 区块中,而在 .rdata 区块中。
调试目录表
debug 数据多半指向外部 PDB 文件的路径。在 Visual Studio 6.0中,debug,头部以 NB10 标识开始。在 VisualStudio .NET中,debug 头部以 RSDS 开始。
延迟载入表
延迟载入不是操作系统的特征,它完全通过向链接器和运行库加入额外的代码和数据来实现。数据目录表中的 IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 条目指向延迟载入的数据,这是一个指向 ImgDelayDescr 结构数组的 RVA。
第一个字段被设为1,则成员视为 RVA 。
程序异常表
当一个异常发生时,系统通过遍历这个表来定位合适的人口并处理它。异常表是一个 IMACE_RUNTIME_FUNCTION_ENTRY 结构数组,数组是由数据目录表中的IMAGE_DIRECTORY_ENTRY_EXCEPTION 条目指向的。IMAGE_RUNTIME_FUNCTION_ENTRY 结构的格式随体系结构的不同而不同。对 IA-64,其布局示例如下。
.NET 头
.NET 文件是 Microsoft .NET 环境生成的可执行文件。.NET 环境由公共语言运行环境(CLR)和 .NET 框架类库组成。可以把CLR看成一台虚拟机,.NET 应用程序就在这台机器中运行。.NET 可执行文件的主要目的是获得 .NET 特定的载入内存的信息。例如元数据(Metadata)和中间语言( Intermediate Language,IL)。 .NET 可执行文件依靠MSCOREE.DLL 进行链接,这个 DLL 对一个 .NET 进程而言是起始点。当一个 .NET 可执行文件被载入时,它的入口通常是一小块残余代码,这块代码只是跳到 MSCOREE.DLL 中的一个输出函数(_CorExeMain或_CorDlIMain)而已。从那里开始, MSCOREE 接管并使用来自可执行文件的元数据和中间语言。这种运行方式类似于 Visual Basic 程序使用MSVBVM60.DLL 的方式。.NET 环境下的 PE 文件,在整体结构上与传统 PE 文件一致。不同的是,.NET 环境下的 PE 文件利用数据目录表中的 IMACE_DIRECTORY_ENTRY_COM_DESCRIPTOR 条目扩充了其结构。这个条目原本是用于 COM 的,但一直没有被使用,现在用于保存 .NET 的信息结构,指向 IMAGE_COR20_HEADER。第 24 章有详细的介绍。