Beacon Object Files (BOF) 与微型 EXE 文件对比

admin 2025年5月8日14:05:31评论2 views字数 6208阅读20分41秒阅读模式

【翻译】Beacon Object Files vs Tiny EXE Files 

TL;DR

EXE 文件中的许多冗余代码只是静态链接的 C 运行时。通过动态链接到 msvcrt.dll(或 Win 10+ 上的 ucrtbase.dll)加上 40 行的 stub 代码,根据程序的大小,你可以将整个 EXE 文件缩小到约 3 KB。这样你既保留了正常 C 代码的便捷性,又能达到 BOF 级别的大小,非常适合内存执行和低带宽信标。

Demo here.[1]

1. 背景

  • 原始想法: Z0MBiE 的 PGN2 插件框架(2001)和 29a#6 电子杂志首先展示了它。
  • 演变: Stephen Fewer 的 反射 DLLs(2008)将加载器捆绑到 DLL 中。
  • 现代工具:sRDI 和 Donut 添加了支持;现在存在更完整的项目,如 No-Consolation

2. Beacon Object Files (BOFs)

C 或 C++ 编译到通用对象文件格式(COFF)。使用 COFF Loader 在内存中映射并执行。

典型 EXE
与 BOF 相同的 exploit
大小
> 100 KB
< 3 KB
原因
与静态 C 运行时链接。
COFF 格式,无需庞大的 CRT

额外的 100 KB 几乎都是 C 运行时代码。即使使用 MSVC 2022 编译的几行代码,如果静态链接 CRT,也会达到 139 KB

#include<stdio.h>intmain(int argc, char *argv[]){    for (int i = 0; i < argc; ++i)        printf("argv[%d]: %sn", i, argv[i]);    return 0;}

3. 分步骤构建小型 PE 文件

  • msvcrt.dll(C)和 msvcp60.dll(C++)自 1997 年和 2000 年起分别预安装。
  • 开发者使用静态库链接以避免“缺少 DLL”错误。
  • Windows 10/11预装了通用 CRT,因此你可以动态链接代替。

如果你只针对现代 Windows,链接到 ucrtbase.dll。对于可以追溯到 Win 98 的 PoC,你仍然可以使用 msvcrt.dll(不建议用于生产)。

  1. 创建 .def 文件,列出你需要的 msvcrt.dll 导出项,然后构建一个 stub 库:lib /def:msvcrt.def /out:msvcrt.lib /machine:x64
  2. 添加最小 CRT stub(摘录):void mainCRTStartup(void) { // 或wmainCRTStartup用于UNICODE _startupinfo si = {0}; int argc, ret; char **argv, **envp; __set_app_type(1); __getmainargs(&argc, &argv, &envp, 0, &si); ret = main(argc, argv, envp); ExitProcess(ret); }
  3. 编译:cl /D_NO_CRT_STDIO_INLINE /O1 /GS- hello.c crt.c /link /nodefaultlib /subsystem:console msvcrt.lib kernel32.lib 结果是一个 ~3 KB 的 PE – 几乎与 BOF 一样小,但仍然是一个正常的 EXE。

4. 为什么选择 PE 而不是 BOF?

PE 加载到内存中
BOF
使用普通工具调试
需要 Cobalt Strike 加载器
正常的 C 工作流(printfargc/argv, …)
手动实现 I/O 或导入
可以被剥离/加密以逃避 YARA
已经很小,但更难调整

PE 仍然携带着 DOS.stub、NT 头和节表,这些可能会被防御者标记。如果这让你担忧,简单地:

  • 擦除加载后的 stub 和头,并/或
  • 加密映像并在运行时解密。

引言

在内存中执行 PE 文件的做法可以追溯到 **Z0MBiE 的 PGN2“插件”**项目,发表于 2001 年,以及 29a e-zine 第#6 期的一篇文章,名为 In-Memory PE EXE Execution[2]。我在 2019 年写了一篇关于它的文章([这里](https://modexp.wordpress.com/2019/06/24/inmem-exec-dll/))。唯一的区别是 Z0MBiE 描述的和 RDLLs 在 2008 年由 Stephen Fewer[3] 开发的只是后者内置了 PE 加载器。进一步发展,代码支持执行控制台应用程序被添加到 Donut Shellcode Generator[4]。自那以来,我没有开发任何相关的东西,而更好的工具已经出现,如 No-Consolation[5] 项目,由 S4ntiagoP[6] 开发。

我们这里讨论的不是新的。它甚至可能会引起一些老派黑客的批评,他们认为这些东西是显而易见的,不值得写一篇博客文章。EthicalChaos[7] 在 this post[8] 中也讨论了使用 PE 文件而不是 BOF 的优点,但我想从 Cobalt Strike 发布 BOF 加载器开始就讨论这个话题。

EXE 示例在这里。[9]

Beacon Object Files

当 BOFs 与 Cobalt Strike 4.1 的发布于 2020 年 6 月 25 日一起宣布时,我记得我曾经想知道为什么开发者选择使用 COFF 而不是 PE 文件。虽然我从未问过,但其中一个最有可能的解释是 PE 文件静态链接到 C 运行时库,这会膨胀文件的大小。有一个关于这方面的暗示这里[10]

BOFs 也非常小。一个 UAC 绕过特权提升的反射 DLL 实现可能达到 100 KB+。同样的 exploit(利用),作为 BOF 构建的,< 3 KB。这在使用带宽受限的信道,如 DNS 时,可以产生很大的差异。

文章没有提到的是,PE 文件额外的 100 KB 是 C 运行时代码。即使是最基本的 C 程序也可以超过 100KB,并且如果你的应用程序链接到 C++ STL,情况会变得更糟。使用 MSVC 2022 编译以下代码,你将得到一个 139 KB 的二进制文件:

#include<stdio.h>intmain(int argc, char *argv[]){    for (int i = 0; i < argc; i++) {        printf("argv[%i]: %sn", i, argv[i]);    }    return 0;}

预安装的 C 运行时

通用 CRT(Universal CRT)是从 Windows 10 开始的核心操作系统组件,但在那之前,除了 msvcrt.dll 和 msvcp60.dll 之外,许多 C 和 C++ 运行时并不是默认预安装的;不同的 Visual Studio 版本可能会抛出缺少 DLL 的错误。一些人可能会记得尝试运行用 Visual Basic 编写的工具,这些工具也遭受了同样的问题。为了避免移植问题,大多数用 C/C++ 编写的 PE 文件都是静态链接到 CRT 的,产生了 100-250 KB 的二进制文件。

使用像 SizeBench[11] 这样的工具可以发现,大多数的大小都是由 CRT 代码占用的,而其中很多并不是真正需要的。为了减小大小,我们可以动态链接到系统预安装的 DLL。msvcrt.dll 自 1997 年以来一直可用于所有 Windows,但 Raymond Chen 建议不要使用它[12]。如果你知道你的应用程序只会在 Windows 10 或 11 上运行,那么动态链接到 ucrtbase.dll 可能是一个更安全的替代方案?使用 msvcrt.dll 只是为了展示可以做什么。

只有汇编语言?

早期的 32 位 x86 汇编语言编程对于控制台应用程序来说是痛苦的:唯一推荐的帮助解析命令行的方法是 GetCommandLineA。它返回完整的命令行(包括可执行文件名),并且开发者需要自己解析参数–处理空格、制表符、引号…这些乐趣。至少对于 UNICODE,有 CommandLineToArgvW。如果你想要类似于 printf 的东西,唯一的建议是使用 WriteConsoleAWriteFile,或者直接使用 C。

你有四个潜在的入口点,取决于子系统和字符编码:

函数
用户入口点
mainCRTStartup
main
wmainCRTStartup
wmain
WinMainCRTStartup
WinMain
wWinMainCRTStartup
wWinMain

在反汇编了几个应用程序后,我注意到在调用上述函数之前,会调用 msvcrt!__getmainargs 或 msvcrt!__wgetmainargs。虽然当时未被文档化,但使用原型和库时,使用 TASM 或 MASM 工作起来非常简单。

由于 msvcrt.dll 自 1997 年以来一直随 Windows 一起发货,msvcp60.dll 自 2000 年以来发货,一个选项是获取一个较旧版本的 MinGW,并定义 MSVCRT_VERSION 低于 140。另一个选项是创建一个 .def 文件,列出 msvcrt 符号,使用它构建一个 stub.lib,并在链接时传递 /nodefaultlib

模块定义文件

一个 .def 文件指定了 DLL 导出的哪些函数、变量或符号。

指令
描述
LIBRARY
指定 DLL 的名称。
EXPORTS
列出导出的符号(可以有可选的序号或别名)。
PRIVATE
标记一个导出为模块内部的。
DESCRIPTION
可选的人类可读的元数据。

dll2def.py 可以从 DLL 的导出生成一个 .def。然后,使用 MSVC LIB 工具创建一个 .lib

lib /def:msvcrt.def /out:msvcrt.lib /machine:x64

C 运行时 Stub

以下是解析命令行、调用用户入口点并在返回后退出所需的最少代码量。我不保证它能与 C++ 兼容,因为缺少一些函数。

#include<stdio.h>#include<windows.h>#define __UNKNOWN_APP  0#define __CONSOLE_APP  1#define __GUI_APP      2intmain(intchar **, char **);intwmain(intwchar_t **, wchar_t **);typedefvoid(__cdecl * new_handler)();typedef struct _startupinfo_t {    int newmode;    new_handler newh;} _startupinfo;int  __getmainargs (int *argc, char ***argv,  char ***penv, int glob, _startupinfo *info);int  __wgetmainargs(int *argc, wchar_t ***argv, wchar_t ***penv, int glob, _startupinfo *info);#ifndef _UNICODEvoidmainCRTStartup(void)#elsevoid wmainCRTStartup(void)#endif{    _startupinfo si = {0};    int argc, ret;    __set_app_type(__CONSOLE_APP);#ifndef _UNICODE    char **argv = NULL, **envp = NULL;    __getmainargs(&argc, &argv, &envp, 0, &si);    ret = main(argc, argv, envp);#else    wchar_t **wargv = NULL, **wenvp = NULL;    __wgetmainargs(&argc, &wargv, &wenvp, 0, &si);    ret = wmain(argc, wargv, wenvp);#endif    ExitProcess(ret);}

使用 stub 编译 hello.c 并动态链接到 msvcrt.dll,生成了一个 3 KB 的 PE 文件:

cl /D_NO_CRT_STDIO_INLINE /Os /O1 /GS- hello.c crt.c ^/link /nodefaultlib /subsystem:console msvcrt.lib kernel32.lib

总结

这就是一个在运行时链接到 msvcrt.dll 的 PE 文件。在我看来,这种方法优于 COFF/BOF 方案:它更易于编写、调试更简单,而且同样小巧。NTDLL 也包含许多 C 运行时函数,但不如 MSVCRT 多。

有人可能会说 PE 文件仍然包含 DOS 存根、NT 头文件和节表,这些在内存中执行时容易被 EDR 和 YARA 规则识别。如果这是个问题,你可以剥离这些头文件,重写节表,这样仍然比使用 BOF 更容易。如果你担心代码被特征识别,可以考虑使用一些加密或基本的混淆技术。

参考资料

[1] 

Demo here.: https://gist.github.com/odzhan/8e3801f4dfb1a1851d79cc8a5bca96c3

[2] 

In-Memory PE EXE Execution: https://github.com/SPTHvx/ezines/blob/e105e848165d5d1c0634103bfbeace509b9aec15/29a6/29A-6.210.txt

[3] 

这里: https://x.com/stephenfewer

[4] 

Donut Shellcode Generator: https://github.com/TheWover/donut/blob/master/loader/inmem_pe.c

[5] 

No-Consolation: https://github.com/fortra/No-Consolation

[6] 

S4ntiagoP: https://x.com/s4ntiago_p

[7] 

EthicalChaos: https://x.com/EthicalChaos

[8] 

this post: https://www.netspi.com/blog/technical-blog/adversary-simulation/the-future-of-beacon-object-files/

[9] 

EXE 示例在这里。: https://gist.github.com/odzhan/8e3801f4dfb1a1851d79cc8a5bca96c3

[10] 

这里: https://hstechdocs.helpsystems.com/manuals/cobaltstrike/current/userguide/content/topics/beacon-object-files_main.htm

[11] 

SizeBench: https://devblogs.microsoft.com/performance-diagnostics/sizebench-a-new-tool-for-analyzing-windows-binary-size/

[12] 

Raymond Chen 建议不要使用它: https://devblogs.microsoft.com/oldnewthing/20140411-00/?p=1273

原文始发于微信公众号(securitainment):Beacon Object Files (BOF) 与微型 EXE 文件对比

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年5月8日14:05:31
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Beacon Object Files (BOF) 与微型 EXE 文件对比https://cn-sec.com/archives/4041834.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息