恶意软件开发系列(八):COFF注入与内存执行

admin 2025-12-22 04:19:39 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 文章介绍了COFF对象文件格式及其在恶意软件开发中的应用,特别是如何实现一个COFF对象文件加载器来在内存中注入和执行代码。作者详细解释了COFF文件的结构(头部、节、符号表)和重定位机制,并提供了一个算法来解析和执行COFF文件。这种技术类似于CobaltStrike的BOF功能,可以用于绕过安全检测,对恶意代码检测能力构成了挑战。 综合评分: 91 文章分类: 恶意软件,代码审计,漏洞分析,二进制安全,免杀


cover_image

恶意软件开发系列(八):COFF注入与内存执行

aeverj

红队工坊

2025年12月16日 06:00 北京

翻译自 Malware development part 8 – COFF injection and in-memory execution

这是恶意软件开发系列的第八篇文章。在这个系列中,我们将探索并尝试实现恶意应用程序用来执行代码、躲避防御和持久化的多种技术。

这次我们将实现一个 COFF 对象文件加载器,它类似于 [Cobalt Strike 的 BOF(Beacon 对象文件)功能]https://www.cobaltstrike.com/help-beacon-object-files。[TrustedSec]https://www.trustedsec.com/blog/coffloader-building-your-own-in-memory-loader-or-how-to-run-bofs/ 也研究过这个话题。

代码在 [我的 GitHub 仓库]https://github.com/0xpat/COFFInjector 中。

注意:我们这里处理的是 64 位代码。

C 代码编译过程

从 C/C++ 源代码生成可执行文件需要三个步骤:

  1. 预处理 – 解释预编译器指令(合并 #include 的文件,替换 #define 定义的标识符)。预编译器本质上是在源代码中进行文本替换,生成一个翻译单元。
  2. 编译(我们在第 6 部分详细讨论过)。编译器从源代码生成汇编代码,并创建对象文件。
  3. 链接 – 将对象文件和所需的库组合成最终的可执行文件(也可能是 DLL)。

可执行文件既可以由操作系统加载器直接执行,也可以注入到内存中(例如通过进程挖空或其他适用的技术)。

但如果我们能直接执行对象文件呢?实际上这是可行的,因为这些文件包含了我们真正感兴趣的机器码。

COFF 对象文件

通用对象文件格式(Common Object File Format,COFF)是一种源自 Unix 的可执行代码格式。微软创建了自己的 COFF 变体,并在此基础上建立了 PE 格式。[微软文档]https://docs.microsoft.com/en-us/windows/win32/debug/pe-format 包含了关于 COFF 和 PE 文件格式的混合信息。

Visual Studio 编译器生成的对象文件使用 COFF 格式。这样的对象文件(扩展名为 .obj)包含:

  • 头部(含架构信息、时间戳、节(section)数量和符号数量等)
  • (含汇编代码、调试信息、链接器指令、异常信息、静态数据等)
  • 符号表(如函数和变量)及其位置信息

节可能包含重定位信息,它指定链接器应该如何修改节数据,以及在加载到内存时如何处理。例如,包含汇编代码的 .text 节会有信息指定代码的哪些部分应该被替换,以及它们应该引用内存中的什么内容。稍后会详细说明。

我们需要浏览 COFF 文件的内容,提取汇编代码和重定位数据,并执行重定位。应用重定位后的最终代码可以通过将其作为函数调用来执行(((void(*)())(code))()),或者使用 CreateThread 等方式执行。

示例对象文件

让我们看一个非常简单的控制台应用程序:

int main()
{
 MessageBoxA(NULL, "Content", "Title", NULL);
 return 0;
}

MessageBoxA 函数位于 user32.dll 中 – 我们需要告诉链接器这一点。

通常,.lib 文件是包含代码(实际上是对象文件)的静态库,可以静态链接到可执行文件。然而,在动态链接中,链接器使用特殊的 .lib 文件,这些文件指向相关的动态库 – 链接器使用这些信息来构建可执行文件的导入地址表(Import Address Table)。

这可以通过在 Visual Studio 中更改项目选项来实现,或者使用以下指令:

#pragma comment(lib, "user32.lib")

我为这段代码禁用了编译器优化(/Od)。启用优化会导致对象文件中的数据排列不同,并给我的概念验证 COFF 加载器带来问题。需要进一步测试。

使用 MSVC 编译器(cl.exe)编译会生成一个对象文件(扩展名为 .obj)。我们可以使用 MSVC 附带的 dumpbin 工具来分析其内容。让我们看看该工具的部分输出。

指令(.drectve 节)

这里是链接器指令,最重要的是关于应该在哪些库中查找外部函数的信息。

只读数据(.rdata 节)

这是静态初始化的数据,例如字符串字面量。

可执行代码(.text 节)

这是实际的汇编代码。在我的示例中,MSVC 编译器将这个节命名为 .text$mn

这里事情变得更有趣了,让我们反汇编这段代码:

我们在这里看到 MessageBoxA 的参数(int 0、char* “Content”、char* “Title”、int 0)是如何根据 [x64 调用约定]https://docs.microsoft.com/en-us/cpp/build/x64-calling-convention 传递的。

让我们看看第三条指令(位于偏移 0x07 处)。当使用例如 [ShellNoob]https://github.com/reyammer/shellnoob 反汇编时,该指令使用 0x00000000 偏移:

那么 dumpbin 反汇编器是如何知道函数应该引用一个符号的呢?这就是重定位发挥作用的地方。在 .text 节原始数据下方,我们可以看到重定位信息。例如,第一个重定位表条目说,偏移 0x0A 处的 0x00000000 字节(也就是 lea 指令的第二个操作数)应该被替换为符号 8 的实际地址(相对于 RIP)。

call 指令(位于偏移 0x17 处)及其相应的重定位条目和符号也是如此。但是这里的重定位涉及相对函数地址。没错 – 相对地址(call 操作数)会被解引用,存储在那里的值(实际的 MessageBoxA 地址)会被调用。

当代码加载到系统内存时,加载器会解析重定位数据,并将函数和数据地址放在正确的位置。然而,这发生在 PE 可执行文件加载期间。我们想要加载 COFF 对象文件,所以我们需要分析它并在内存中执行重定位。

符号表

此表包含符号,如静态变量或外部函数。

符号表 [有点难以阅读和理解]https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#coff-symbol-table。然而,通常 Value 字段表示符号在节内的偏移量(由 SectionNumber 字段描述)。

此外,要知道一个符号数据在哪里结束,我们需要检查同一节中下一个符号的偏移量或节的总大小。

对象文件加载器

要注入并执行一个简单的 COFF 文件,我们需要读取 .text 节,并用外部函数和静态数据的相对地址填充所有零(即重定位此节中引用的符号)。当然,我们还需要将这些符号放在内存的某个位置,例如在汇编代码之后。

要查找外部函数,我们需要浏览链接器指令中指示的库。我们可以使用 LoadLibrary/GetModuleHandle/GetProcAddress 函数,或者浏览 PEB 和 InMemoryOrderModuleList(参见第 4 部分)。

下图说明了这个概念:

我使用了 [COFFI]https://github.com/serge1/COFFI 库来解析 COFF 文件。这是一个很棒的纯头文件 C++ 库,包含了我从对象文件中读取内容所需的所有函数。COFFI 使用了一些 C++ 标准库数据结构,如字符串、向量等,我的代码也是如此。

我的算法如下:

  1. 获取指向 .text 节和重定位、指令、静态数据以及符号表的指针。
  2. 计算汇编代码 + 静态数据 + 外部函数指针所需的内存(通过迭代所有 .text 重定位)。
  3. 将汇编代码复制到 RW(X) 内存。
  4. 将静态符号复制到代码之后(每个符号的大小通过检查给定节中下一个符号的偏移量来计算)。
  5. 在复制静态符号的同时执行重定位(用相对地址替换汇编代码中的零)。
  6. 通过查找链接器指令中引用的库(LoadLibrary,但是 dll 而不是 lib 文件)来解析所有静态函数,将地址放在内存中(紧随静态数据之后;使用 GetProcAddress)并执行重定位。COFF 符号表中的 WinAPI 函数名以 __imp_ 为前缀。
  7. 调用汇编代码的起始位置(确保内存可执行 – 如有必要使用 VirtualProtect)。

定义额外的 API

Cobalt Strike 的 BOF 实现了一组可以从注入的对象文件代码中调用的函数(又名 Beacon API)。我们也可以这样做。

加载的对象文件可以定义一个内部函数,例如:

void COFF_API_Print(char* string)
{
 printf(string);
}

并将其作为导入添加到对象文件代码中:

__declspec(dllimport) void COFF_API_Print(char* string);

然后在加载期间像处理 WinAPI 导入一样处理它。

返回值

当从对象文件调用注入的 main 函数时,我们可以从调用者访问返回的值:

int returnedValue = ((int(*)())code)();

注意事项

  • 这个概念验证代码假设对象文件只包含单个函数(main),如果有其他子例程将无法工作。
  • 对象文件在没有 C 运行时的情况下编译,没有运行时初始化函数 – main 是入口点。此外,编译器的代码优化被禁用。

总结

我们已经了解了 MSVC 编译器生成的 COFF 对象文件格式。由于这些文件包含执行代码所需的所有信息,它们也可以在内存中注入和执行,例如通过 C&C 通道传递。这是一种强大的技术,无疑对恶意代码检测能力构成了挑战。

代码在 [我的 GitHub 仓库]https://github.com/0xpat/COFFInjector 中。

如果你对网络安全、红队攻防技术充满热情,渴望学习更多实战技巧,例如渗透测试、自动化脚本编写、免杀技术等, 欢迎关注我的公众号

在这里,我会持续分享更多高质量的技术文章,与你一同探索网络安全的奥秘,提升实战技能! 让我们一起在队攻防的道路上,不断精进,突破边界!

免责声明: 本文仅供安全技术研究与学习交流之用。 严禁将本文所提及的技术用于任何非法用途,包括但不限于未经授权的渗透测试、网络攻击、恶意代码传播等。


查看原文:《恶意软件开发系列(八):COFF注入与内存执行》

评论:0   参与:  2