【翻译】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 在内存中映射并执行。
|
|
|
---|---|---|
大小 |
|
|
原因 |
|
|
额外的 100 KB 几乎都是 C 运行时代码。即使使用 MSVC 2022 编译的几行代码,如果静态链接 CRT,也会达到 139 KB:
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
(不建议用于生产)。
-
创建 .def 文件,列出你需要的 msvcrt.dll
导出项,然后构建一个 stub 库:lib /def:msvcrt.def /out:msvcrt.lib /machine:x64
-
添加最小 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); }
-
编译: 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?
|
|
---|---|
|
|
printf , argc/argv , …) |
|
|
|
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 的二进制文件:
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
的东西,唯一的建议是使用 WriteConsoleA
、WriteFile
,或者直接使用 C。
你有四个潜在的入口点,取决于子系统和字符编码:
|
|
---|---|
|
|
|
|
|
|
|
|
在反汇编了几个应用程序后,我注意到在调用上述函数之前,会调用 msvcrt!__getmainargs
或 msvcrt!__wgetmainargs
。虽然当时未被文档化,但使用原型和库时,使用 TASM 或 MASM 工作起来非常简单。
由于 msvcrt.dll
自 1997 年以来一直随 Windows 一起发货,msvcp60.dll
自 2000 年以来发货,一个选项是获取一个较旧版本的 MinGW,并定义 MSVCRT_VERSION
低于 140。另一个选项是创建一个 .def
文件,列出 msvcrt
符号,使用它构建一个 stub.lib
,并在链接时传递 /nodefaultlib
。
模块定义文件
一个 .def
文件指定了 DLL 导出的哪些函数、变量或符号。
|
|
---|---|
|
|
|
|
|
|
|
|
dll2def.py
可以从 DLL 的导出生成一个 .def
。然后,使用 MSVC LIB 工具创建一个 .lib
:
lib /def:msvcrt.def /out:msvcrt.lib /machine:x64
C 运行时 Stub
以下是解析命令行、调用用户入口点并在返回后退出所需的最少代码量。我不保证它能与 C++ 兼容,因为缺少一些函数。
intmain(int, char **, char **);
intwmain(int, wchar_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);
voidmainCRTStartup(void)
void wmainCRTStartup(void)
{
_startupinfo si = {0};
int argc, ret;
__set_app_type(__CONSOLE_APP);
char **argv = NULL, **envp = NULL;
__getmainargs(&argc, &argv, &envp, 0, &si);
ret = main(argc, argv, envp);
wchar_t **wargv = NULL, **wenvp = NULL;
__wgetmainargs(&argc, &wargv, &wenvp, 0, &si);
ret = wmain(argc, wargv, wenvp);
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 更容易。如果你担心代码被特征识别,可以考虑使用一些加密或基本的混淆技术。
参考资料
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 文件对比
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论