本文将结合Visual Studio动态调试过程,重点从内存映射、符号解析和重定位三个方面对COffLoader的工作流程进行详细阐述,这篇文章是对《COffLoader For Windows(BOFLoader)》的补充。
一、COFF文件结构

COFF_FILE_HEADER
1 | #pragma pack(push,1) |
COFF_SECTION
1 | #pragma pack(push,1) |
COFF_RELOC
1 | #pragma pack(push,1) |
COFFEE
1 | #pragma pack(push,1) |
1 | #pragma pack(push,1) |
表示结构体按1字节对齐,这个可是有大用处的,对于自定义文件结构,按1字节对齐不易出错。
不按1字节对齐,则可能出现下面的错误

本文会使用下面的BOF
1 | #include <windows.h> |
其汇编代码如下
1 | .text$mn:0000000000000000 |
⚠注意:只分析x64的情况!
二、内存映射

- 首先找到节的地址,此时节还是以磁盘文件形式存在。
1 | pCoffee->Section = (PVOID)(((ULONG_PTR)pCoffee->Data) + sizeof(COFF_FILE_HEADER) + (ULONG_PTR)(sizeof(COFF_SECTION) * dwSecCnt)); |
- 将映射后的节的大小和地址存放到
SecMap里,Size表示这个节当前按原始数据拷贝的大小。Ptr表示这个节被映射到内存后的起始地址
后面所有重定位,都会靠 SecMap把“节号 + 节内偏移”换算成“真实运行时地址”。
1 | pCoffee->SecMap[dwSecCnt].Size = pCoffee->Section->SizeOfRawData; |
- pNextBase指针移动
pCoffee->Section->SizeOfRawData字节的大小,随后pNextBase按页对齐,页的大小为4096B,即0x1000B。
1 | ((ULONG_PTR)pNextBase) += pCoffee->Section->SizeOfRawData; |
- 将磁盘文件形式的节数据复制到新的运行时地址。
1 | _Memcpy(pCoffee->SecMap[dwSecCnt].Ptr, (PVOID)((ULONG_PTR)pCoffee->Data + pCoffee->Section->PointerToRawData), pCoffee->Section->SizeOfRawData); |
- 所有节数据复制完后,紧接着放
GOT和BSS
1 | pCoffee->GOT = pNextBase; |
前面的文章我说过,GOT 的大小是所有函数符号在汇编中出现的次数,而 BSS 的大小则是bss变量符号在汇编中出现的次数。随然会冗余统计大小,但这无伤大雅。
loader没有单独给.bss建section,而是自己在尾部划了一块匿名内存出来当 .bss。
下图是mingw编译的obj文件

下图是msvc编译的obj文件,可以明显看出没有.bss节

下图是Cen4enCen师傅的解释

这里请先记住GOT和BSS的地址,下文将正式开启VS调试,请各位坐好。
三、符号解析
- 首先在
CoffeeProcessSection调式出pCoffSymbol,因为每一个重定位项都对应一个符号。


- 将符号名提取到pSymbolName。如果符号名是短名,则从
pCoffSymbol->First.Name获取;如果是长名,则去符号字符串表里拿。

提取出的符号名为 num1。

- 判断符号是外部符号还是本地符号。只有外部符号才需要通过
CoffeeProcessSymbol解析出函数地址或者在bss中的偏移地址。
本地符号:
SectionNumber > 0:表示当前目标文件中的某个节给出定义。其值表示在节映射表SecMap的第几个节。请注意SecMap是以0为起始索引。

外部符号需要同时满足:
StorageClass == IMAGE_SYM_CLASS_EXTERNAL:表示这个符号是“外部符号”。但“外部符号”不一定意味着“未定义”,因为有些外部符号也可能已经在当前obj的某个节里给出定义。SectionNumber == 0x0:这个符号没有定义在当前目标文件的任何节中

- 对于函数符号,给出最终函数地址;对于bss变量,则返回它在BSS段中的偏移
终于到CoffeeProcessSymbol了,请注意我的CoffeeProcessSymbol和Cen4enCen师傅的代码是不一样的,毕竟要适配x86。

因为 num1 是bss变量,故 dwImportPrefixSize 和 dwBeaconPrefixSize 都为0。所以会来到下面的这个分支。

因为 pCoffSymbol 是第一次出现,没有被记录,故BssEntry记录这个符号在bss节的偏移和 pvSysmbolAddr。

最后返回 num1 这个bss变量在bss节中的偏移,为4,跳过前面的“哨兵”。

下面看第二次出现 num1 符号时,代码会不再记录已经记录过的 pCoffSymbol。

接下来分析函数符号如何解析。函数符号有两类,一类是Beacon API,另一类是Windows API。
首先分析Beacon API,dwImportPrefixSize = 6,即 __imp_ 的长度,表示这是需要导入的外部函数符号。

dwImportPrefixSize = 0xc ,即 __imp_Beacon 的长度,表示这是需要导入的Beacon API。

因 dwBeaconPrefixSize != 0,进入到这个条件分支。

把BOF中引用的Beacon API名字,经过去前缀和哈希匹配后,映射到当前 Loader自己实现的Beacon API函数地址。你可以把它理解成一个“Beacon 运行时导入表解析器”。

另一种就是 __imp_libraryName$functionName , __imp_functionName
- 先判断是不是标准BOF导入格式。即存在
$。 - 去掉
__imp_前缀,准备拆库名和函数名。 - 规范化DLL名
- 先算DLL名哈希
- 如果是标准格式,就先找模块,再找导出函数
- 处理函数名
- 构造 ANSI_STRING,调LdrGetProcedureAddress解析真实函数地址
- 不是标准模式,则从LdrApi里找。

代码太多了,略过一些步骤,看最终的返回结果。

非标准模式

四、重定位


对于 num1 这个bss变量,重定位的时候,会来到这段代码

BSS节的地址是 0x00007ffcebb77010,通过 CoffeeProcessSection 得出 num1 在BSS节内偏移0x4的位置。
则 ulBssAddr = 0x00007ffcebb77010 + 4 = 0x00007ffcebb77014 是 num1 这个bss变量的地址。
修正的目的是得出重定位字段地址与bss变量地址之间的差值。
公式:
1 | Offset = Target - NextIP - extra |
对于 num1 这个变量
Target = ulBssAddr = 0x00007ffcebb77014
NextIP = pvRelocAddr + 4。pvRelocAddr 是重定位字段的地址,具体看下图

重定位字段的地址 pvRelocAddr = 0x00007ffcebb7200f

这个汇编指令看着是不是很眼熟呢?其实就是BOF文件的text节里的指令。

Type = IMAGE_REL_AMD64_REL32_4,extra = 8 - 4 = 4

我们手工计算一下偏移:
1 | Offset = Target - NextIP - extra |
代码计算出来的偏移

最后看重定位字段有没有被修正

外部函数符号的重定位和BSS变量的重定位是一样的分析过程,在此略过,本篇文章结束。
本篇文章是对上一篇文章的补充扩展,如果有不懂或者本文哪里有解释错误地方,可以和我探讨!