Writing Beacon Object Files Without DFR
介绍
Beacon 对象文件已经在红队中变得非常流行,因为它们可以在不需要包含反射 DLL 或.NET 程序集的情况下,动态添加额外的功能。这种优势的代价是 Beacon 对象文件的开发有些尴尬。一个开发上的怪癖是需要在导入的符号前加上符号所在的相关库名称。这个概念被称为动态函数解析(或 DFR),它是 BOF 告诉 BOF 加载器在哪里找到外部符号的方法。
如果我告诉你,在开发 BOF 时,你不需要在代码中编写这些 DFR 原型呢?
简而言之
objcopy支持使用--redefine-sym
或--redefine-syms
标志重新定义对象文件中的符号名称。这可以用于在后处理期间将导入的符号从正常的系统定义转换为 DFR 格式。
DFR 的必要性
关于 Beacon 对象文件需要注意的一点是,BOF 加载器加载的结果文件是一个通用的COFF文件。这对于以前编写过 BOF 的人来说可能显而易见,但需要注意的是,这有助于识别 BOF 加载器究竟可以处理什么以及缺少什么数据。
COFF 包含的一条信息是一个符号表。这是一个结构数组,列出了 COFF 定义的符号和 COFF 需要的外部符号。每个符号定义都包括在源文件中声明的符号名称。符号表缺少的是可以找到符号的外部库的名称。链接器的工作是从符号表中获取这些符号,并在预定义的库集中搜索它们。由于 BOF 不经过链接阶段,这项工作被传递给 BOF 加载器。链接器将执行一种暴力搜索来定位这些符号,这对于 BOF 加载器来说不是最理想的方法。
这就是 DFR 概念的用武之地。与其每次加载 BOF 时都进行这种暴力搜索,不如将库名称直接集成到符号名称中。这样做的方法是用库名称作为前缀,并用美元符号($
)分隔。例如,如果 BOF 需要符号CreateFileA
,而该符号位于kernel32.dll
中,BOF 将定义符号KERNEL32$CreateFileA
,BOF 加载器将知道在kernel32
中搜索CreateFileA
符号。
DFR 问题
这种在符号名称前加上库名称的方法有效,并且已经成为在 BOF 中定义导入的相当标准的方法,但这种方法可能会有一些问题。
不匹配的原型
由于已经声明的符号需要使用 DFR 格式重新声明,这种手动重新声明可能由于打字错误而无法完全匹配原始声明。对于基本函数,这种错误可能很容易发现,但对于更复杂的函数,识别起来可能会有些困难。
Windows API 中的一些函数已知包含相当多的参数。以CreateProcessWithLogonW为例。以下是为了在 BOF 中使用此函数的 DFR 声明。
DECLSPEC_IMPORT BOOL WINAPI ADVAPI32$CreateProcessWithLogonW(
LPCWSTR lpUsername,
LPCWSTR lpDomain,
LPCWSTR lpPassword,
DWORD dwLogonFlags,
LPCWSTR lpApplicationName,
LPWSTR lpCommandLine,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCWSTR lpCurrentDirectory,
LPSTARTUPINFOW lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation);
如果在声明中参数被交换、遗漏或类型错误,函数可能不会按预期运行。具体出现的问题取决于声明中哪个部分不正确。在某些情况下,函数可能在一次运行中正常工作,但在另一次运行中失败。就像许多与 C 语言开发相关的事情一样,这取决于计算机在那个时刻的“心情”是否会正常工作¯_(ツ)_/¯。
无论在这种问题发生时返回什么错误,这种错误只能通过手动审查代码并仔细检查 DFR 原型是否与原始声明完全匹配来发现。
C++,救世主
在 Fortra 的“简化 BOF 开发:调试、测试和保存你的 B(e)acon”博客文章中,Henri Nurmi提到使用 C++11 的decltype说明符来帮助解决这个问题。
这种模式更不容易出错,因为新的 DFR 符号声明是直接从原始声明构建的。
使用 decltype,CreateProcessWithLogonW
的原型看起来会是这样的。
DECLSPEC_IMPORT decltype(CreateProcessWithLogonW) ADVAPI32$CreateProcessWithLogonW;
原始函数原型不需要手动重写,因为它由 decltype 提供。
这里的警告是你需要一个 C++ 编译器来编译 BOF,因此需要处理一些 C++ 特有的细微差别,比如名称修饰,但它确实使声明 DFR 原型变得更加容易。
可执行文件生成
在开发 BOF 的测试阶段,将代码编译成可执行文件或将其集成到测试框架中是有帮助的。将 DFR 规范添加到导入的符号意味着它们将无法被系统链接器解析。系统链接器不了解 DFR 格式,并将<library>$<symbol>
值解释为符号名称本身。
对此有一些解决方法。
Filename:examplebof.c
#include <windows.h>
#include <lmcons.h>
#include "beacon.h"
DECLSPEC_IMPORT DWORD WINAPI KERNEL32$GetCurrentProcessId(void);
DECLSPEC_IMPORT BOOL WINAPI ADVAPI32$GetUserNameA(LPSTR, LPDWORD);
void go(void) {
DWORD pid = KERNEL32$GetCurrentProcessId();
BeaconPrintf(CALLBACK_OUTPUT, "Current process id: %lu", pid);
char username[UNLEN + 1] = {0};
if (ADVAPI32$GetUserNameA(username, &(DWORD){ sizeof(username) }) == TRUE) {
BeaconPrintf(CALLBACK_OUTPUT, "Username: %s", username);
}
}
// Main function for building as an executable
int main() {
go();
return 0;
}
尝试将此示例 BOF 编译为可执行文件时,在链接时会抛出“未定义引用”错误。
matt@laptop :: ~/Documents/dev/writing-bofs-without-dfr-example >> x86_64-w64-mingw32-gcc -o examplebof.exe examplebof.c
/usr/lib/gcc/x86_64-w64-mingw32/14.1.1/../../../../x86_64-w64-mingw32/bin/ld: /tmp/ccQizFTo.o:examplebof.c:(.text+0x14): undefined reference to `__imp_KERNEL32$GetCurrentProcessId'
/usr/lib/gcc/x86_64-w64-mingw32/14.1.1/../../../../x86_64-w64-mingw32/bin/ld: /tmp/ccQizFTo.o:examplebof.c:(.text+0x3b): undefined reference to `__imp_BeaconPrintf'
/usr/lib/gcc/x86_64-w64-mingw32/14.1.1/../../../../x86_64-w64-mingw32/bin/ld: /tmp/ccQizFTo.o:examplebof.c:(.text+0x73): undefined reference to `__imp_ADVAPI32$GetUserNameA'
/usr/lib/gcc/x86_64-w64-mingw32/14.1.1/../../../../x86_64-w64-mingw32/bin/ld: /tmp/ccQizFTo.o:examplebof.c:(.text+0x97): undefined reference to `__imp_BeaconPrintf'
collect2: error: ld returned 1 exit status
matt@laptop :: ~/Documents/dev/writing-bofs-without-dfr-example 1 >>
解决此问题的一种常见方法是将所有的 DFR 原型包装在一个#ifdef
宏中,并根据该宏是否定义来选择使用哪个版本的符号。DFR 符号和系统符号可以别名为一个通用名称,以便根据配置引用不同的变体。
Filename:examplebof.c
#include <windows.h>
#include <lmcons.h>
#include "beacon.h"
#ifdef BOF
// Use DFR if `BOF` is defined
DECLSPEC_IMPORT DWORD WINAPI KERNEL32$GetCurrentProcessId(void);
#define kernel32_GetCurrentProcessId KERNEL32$GetCurrentProcessId
DECLSPEC_IMPORT BOOL WINAPI ADVAPI32$GetUserNameA(LPSTR, LPDWORD);
#define advapi32_GetUserNameA ADVAPI32$GetUserNameA
#else // BOF
// Do not use DFR if `BOF` is not defined
#define kernel32_GetCurrentProcessId GetCurrentProcessId
#define advapi32_GetUserNameA GetUserNameA
#endif // BOF
void go(void) {
DWORD pid = kernel32_GetCurrentProcessId();
BeaconPrintf(CALLBACK_OUTPUT, "Current process id: %lu", pid);
char username[UNLEN + 1] = {0};
if (advapi32_GetUserNameA(username, &(DWORD){ sizeof(username) }) == TRUE) {
BeaconPrintf(CALLBACK_OUTPUT, "Username: %s", username);
}
}
#ifndef BOF
// Create a main function to wrap the BOF entrypoint
int main() {
go();
return 0;
}
#endif // BOF
在编译过程中传入-DBOF
标志将使用 DFR 导入声明,而省略该标志将使用正常的系统声明。两个符号都使用<libname>_<symbol>
进行别名处理,以便在引用它们时使用正确的符号。
# For compiling with the BOF DFR imports
x86_64-w64-mingw32-gcc -DBOF -c -o examplebof.o examplebof.c
# For compiling the executable with the normal system imports
x86_64-w64-mingw32-gcc -o examplebof.exe examplebof.c
使用 Makefile 进行编译
Filename:Makefile
CC = x86_64-w64-mingw32-gcc
RM = rm -vf
sources := examplebof.c
objs := $(sources:.c=.o)
.PHONY: all clean
all : examplebof.bof.o examplebof.exe
clean:
$(RM) examplebof.bof.o examplebof.exe $(objs)
examplebof.o : examplebof.c beacon.h
examplebof.bof.o : examplebof.c beacon.h
%.bof.o : %.c
$(CC) -DBOF $(CFLAGS) $(TARGET_ARCH) -c $(OUTPUT_OPTION) $<
% : %.o
%.exe : %.o
$(LINK.o) $^ $(LDLIBS) -o $@
matt@laptop :: ~/Documents/dev/writing-bofs-without-dfr-example >> make
x86_64-w64-mingw32-gcc -DBOF -c -o examplebof.bof.o examplebof.c
x86_64-w64-mingw32-gcc -c -o examplebof.o examplebof.c
x86_64-w64-mingw32-gcc examplebof.o -o examplebof.exe
/usr/lib/gcc/x86_64-w64-mingw32/14.1.1/../../../../x86_64-w64-mingw32/bin/ld: examplebof.o:examplebof.c:(.text+0x3b): undefined reference to `__imp_BeaconPrintf'
/usr/lib/gcc/x86_64-w64-mingw32/14.1.1/../../../../x86_64-w64-mingw32/bin/ld: examplebof.o:examplebof.c:(.text+0x97): undefined reference to `__imp_BeaconPrintf'
collect2: error: ld returned 1 exit status
make: *** [Makefile:20: examplebof.exe] Error 1
matt@laptop :: ~/Documents/dev/writing-bofs-without-dfr-example 2 >>
BOF 版本编译成功;然而,可执行版本仍然出现了一些“未定义引用”的链接错误。
链接器无法找到 Beacon API(BeaconPrintf)符号。这些符号通常存在于 BOF 加载器中,并在加载过程中解析,因此它们不在系统的任何地方供链接器查找。
解决此问题的一个方法是创建 Beacon API 的模拟实现,并在编译可执行版本时将其链接进去。提供的 Visual Studio BOF 模板在“简化 BOF 开发:调试、测试和保存你的 B(e)acon”博客文章中提到使用了这种策略(Cobalt-Strike/bof-vs/BOF-template/base/mock.cpp#L391)。
可以将模拟的 Beacon API 实现移植到适用于此 BOF 的方式。理想情况下,这些将被实现为一个静态库,可以在不同项目中重用,但由于这个项目只需要BeaconPrintf
函数,因此将其与 BOF 一起包含就可以了。
Filename:beacon_mock.c
#include "beacon_mock.h"
#include <assert.h>
#include <stdarg.h>
#include <stdio.h>
/// BeaconPrintf implementation for printing to stdout.
/// Based off of https://github.com/Cobalt-Strike/bof-vs/blob/855a33afacd6efad3eaceebe42c5ece4a435d91d/BOF-Template/base/mock.cpp#L391 but ported to C
void BeaconPrintf(int type, const char *fmt, ...) {
printf("[Output Callback: (0x%02x)]: ", type);
va_list args;
va_start(args, fmt);
vprintf(fmt, args);
puts("");
assert(fflush(stdout) == 0);
va_end(args);
}
Filename:beacon_mock.h
#ifndef BEACON_MOCK_H
#define BEACON_MOCK_H
#define CALLBACK_OUTPUT 0x0
#define CALLBACK_OUTPUT_OEM 0x1e
#define CALLBACK_OUTPUT_UTF8 0x20
#define CALLBACK_ERROR 0x0d
void BeaconPrintf(int type, const char *fmt, ...);
#endif // BEACON_MOCK_H
Filename:examplebof.c
#include <windows.h>
#include <lmcons.h>
#ifdef BOF
// Include the regular beacon.h definitions when compiling as a BOF
#include "beacon.h"
DECLSPEC_IMPORT DWORD WINAPI KERNEL32$GetCurrentProcessId(void);
#define kernel32_GetCurrentProcessId KERNEL32$GetCurrentProcessId
DECLSPEC_IMPORT BOOL WINAPI ADVAPI32$GetUserNameA(LPSTR, LPDWORD);
#define advapi32_GetUserNameA ADVAPI32$GetUserNameA
#else // BOF
// Include the mock Beacon API definitions when compiling as a standalone executable
#include "beacon_mock.h"
#define kernel32_GetCurrentProcessId GetCurrentProcessId
#define advapi32_GetUserNameA GetUserNameA
#endif // BOF
void go(void) {
DWORD pid = kernel32_GetCurrentProcessId();
BeaconPrintf(CALLBACK_OUTPUT, "Current process id: %lu", pid);
char username[UNLEN + 1] = {0};
if (advapi32_GetUserNameA(username, &(DWORD){ sizeof(username) }) == TRUE) {
BeaconPrintf(CALLBACK_OUTPUT, "Username: %s", username);
}
}
#ifndef BOF
// Create a main function to wrap the BOF entrypoint
int main() {
go();
return 0;
}
#endif // BOF
并调整上面的 Makefile 以包含新的模拟 Beacon API 实现
Filename:Makefile
CC = x86_64-w64-mingw32-gcc
RM = rm -vf
sources := examplebof.c beacon_mock.c
objs := $(sources:.c=.o)
.PHONY: all clean
all : examplebof.bof.o examplebof.exe
clean:
$(RM) examplebof.bof.o examplebof.exe $(objs)
examplebof.exe : examplebof.o beacon_mock.o
examplebof.bof.o : examplebof.c beacon.h
examplebof.o : examplebof.c beacon_mock.h
beacon_mock.o : beacon_mock.c beacon_mock.h
%.bof.o : %.c
$(CC) -DBOF $(CFLAGS) $(TARGET_ARCH) -c $(OUTPUT_OPTION) $<
% : %.o
%.exe : %.o
$(LINK.o) $^ $(LDLIBS) -o $@
现在将其编译为 BOF 和独立可执行文件应该可以正常工作,因为存在链接器可以找到的BeaconPrintf
实现。
matt@laptop :: ~/Documents/dev/writing-bofs-without-dfr-example >> ls
beacon.h beacon_mock.c beacon_mock.h examplebof.c Makefile
matt@laptop :: ~/Documents/dev/writing-bofs-without-dfr-example >> make
x86_64-w64-mingw32-gcc -DBOF -c -o examplebof.bof.o examplebof.c
x86_64-w64-mingw32-gcc -c -o examplebof.o examplebof.c
x86_64-w64-mingw32-gcc -c -o beacon_mock.o beacon_mock.c
x86_64-w64-mingw32-gcc examplebof.o beacon_mock.o -o examplebof.exe
matt@laptop :: ~/Documents/dev/writing-bofs-without-dfr-example >> ls
beacon.h beacon_mock.c beacon_mock.h beacon_mock.o examplebof.bof.o examplebof.c examplebof.exe examplebof.o Makefile
matt@laptop :: ~/Documents/dev/writing-bofs-without-dfr-example >> wine ./examplebof.exe
002c:fixme:winediag:loader_init wine-staging 9.15 is a testing version containing experimental patches.
002c:fixme:winediag:loader_init Please mention your exact version when filing bug reports on winehq.org.
[Output Callback: (0x00)]: Current process id: 32
[Output Callback: (0x00)]: Username: matt
matt@laptop :: ~/Documents/dev/writing-bofs-without-dfr-example >>
使用 Objcopy 重新定义符号
如上所述,这种管理 DFR 导入的方法可能有点麻烦。它需要在源代码中使用各种宏和其他辅助工具,导致代码变得杂乱无章。如果有一种方法可以像编写普通 C 程序一样编写 BOF,但将 DFR 方面的管理与源代码分开,那就太好了。
这可以通过使用类似objcopy的工具来实现。
Objcopy 是GNU Binutils中的一个工具,旨在操作目标文件。Binutils 已经存在了二十多年,并且在大多数 Linux 系统上都很常见。Objcopy 支持多种不同的目标文件格式,包括 COFFs。
Objcopy 的一个功能是能够使用--redefine-sym
标志重命名符号。这允许更改现有目标文件中符号的名称。
以下是如何将其与 Beacon Object Files 一起使用。
上面的示例 BOF 代码可以重写以去除所有 DFR 细节。
Filename:examplebof.c
#include <windows.h>
#include <lmcons.h>
#include "beacon.h"
void go(void) {
DWORD pid = GetCurrentProcessId();
BeaconPrintf(CALLBACK_OUTPUT, "Current process id: %lu", pid);
char username[UNLEN + 1] = {0};
if (GetUserNameA(username, &(DWORD){ sizeof(username) }) == TRUE) {
BeaconPrintf(CALLBACK_OUTPUT, "Username: %s", username);
}
}
编译这个版本的示例 BOF 并将其与TrustedSec 的 COFFLoader一起使用将会失败。
PS C:UsersUserDocumentswriting-bofs-without-dfr-example> .COFFLoader.exe go .examplebof.bof.o
Got contents of COFF file
Running/Parsing the COFF file
Machine 0x8664
Number of sections: 7
TimeDateStamp : 0
PointerToSymbolTable : 0x2C2
NumberOfSymbols: 21
OptionalHeaderSize: 0
Characteristics: 4
...
Doing Relocations of section: 0
VirtualAddress: 0x14
SymbolTableIndex: 0x12
Type: 0x4
SymPtr: 0x1A
SymVal: __imp_GetCurrentProcessId
SectionNumber: 0x0
Failed to resolve symbol
Returning
Failed to run/parse the COFF file
PS C:UsersUserDocumentswriting-bofs-without-dfr-example>
COFFLoader 在处理__imp_GetCurrentProcessId
符号时抛出错误,因为它无法解析出库名称。
使用 objcopy,可以将示例 BOF 中的导入符号重写为符合 DFR 格式。这是 BOF 加载器在解析这些导入符号时期望看到的格式。
objcopy
--redefine-sym '__imp_GetCurrentProcessId=__imp_KERNEL32$GetCurrentProcessId'
--redefine-sym '__imp_GetUserNameA=__imp_ADVAPI32$GetUserNameA'
examplebof.bof.o examplebof-redefined.bof.o
Rabin2可以用来验证符号是否被正确重命名。
File:examplebof.bof.o
matt@laptop :: ~/Documents/dev/writing-bofs-without-dfr-example >> rabin2 -s examplebof.bof.o
[Symbols]
nth paddr vaddr bind type size lib name demangled
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
0 0x00000000 0x00000000 LOCAL FILE 4 .file
0 0x0000012c 0x00000000 GLOBAL FUNC 4 go
0 0x0000012c 0x00000000 LOCAL SECT 4 .text
0 0x00000000 0x000000b0 LOCAL SECT 4 .data
0 0x00000000 0x000000c0 LOCAL SECT 4 .bss
0 0x000001dc 0x000000d0 LOCAL SECT 4 .rdata
0 0x0000020c 0x00000100 LOCAL SECT 4 .xdata
0 0x0000021c 0x00000110 LOCAL SECT 4 .pdata
0 0x00000228 0x00000120 LOCAL UNK 4 .rdata$zzz
0 ---------- ---------- NONE UNK 4 imp.__imp_GetCurrentProcessId
0 ---------- ---------- NONE UNK 4 imp.__imp_BeaconPrintf
0 ---------- ---------- NONE UNK 4 imp.__imp_GetUserNameA
File:examplebof-redefined.bof.o
matt@laptop :: ~/Documents/dev/writing-bofs-without-dfr-example >> rabin2 -s example-redefined.bof.o
[Symbols]
nth paddr vaddr bind type size lib name demangled
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
0 0x00000000 0x00000000 LOCAL FILE 4 .file
0 0x0000012c 0x00000000 GLOBAL FUNC 4 go
0 0x0000012c 0x00000000 LOCAL SECT 4 .text
0 0x00000000 0x000000b0 LOCAL SECT 4 .data
0 0x00000000 0x000000c0 LOCAL SECT 4 .bss
0 0x000001dc 0x000000d0 LOCAL SECT 4 .rdata
0 0x0000020c 0x00000100 LOCAL SECT 4 .xdata
0 0x0000021c 0x00000110 LOCAL SECT 4 .pdata
0 0x00000228 0x00000120 LOCAL UNK 4 .rdata$zzz
0 ---------- ---------- NONE UNK 4 imp.__imp_KERNEL32$GetCurrentProcessId
0 ---------- ---------- NONE UNK 4 imp.__imp_BeaconPrintf
0 ---------- ---------- NONE UNK 4 imp.__imp_ADVAPI32$GetUserNameA
使用重新定义符号生成的新 BOF 现在可以与 COFFLoader 一起工作
PS C:UsersUserDocumentswriting-bofs-without-dfr-example> .COFFLoader.exe go .example-redefined.bof.o
Got contents of COFF file
Running/Parsing the COFF file
Machine 0x8664
Number of sections: 7
TimeDateStamp : 0
PointerToSymbolTable : 0x2C2
NumberOfSymbols: 21
OptionalHeaderSize: 0
Characteristics: 4
...
: Section: 0, Value: 0x0
: Section: 7, Value: 0x0
8: Section: 0, Value: 0x0
: Section: 0, Value: 0x0
: Section: 0, Value: 0x0
: Section: 0, Value: 0x0
Back
Returning
Ran/parsed the coff
Outdata Below:
Current process id: 9028
Username: User
PS C:UsersUserDocumentswriting-bofs-without-dfr-example>
这个命令行标志是让这个 BOF 工作的唯一需要。源代码可以保持原样,不需要 DFR 声明。
每次在命令行上指定导入重命名可能会变得难以管理,尤其是当添加更多导入时。这可以通过将名称映射存储在一个专用文件中来简化。
添加一个 Imports.txt
Objcopy 有一个--redefine-syms
标志,它允许指定一个包含符号重新定义列表的文件,而不需要每次在命令行上传递它们。这个文件的格式是:当前符号名称,一个空格,以及符号的新名称。这些重新定义可以在文件中多次列出,每行一个。
以下是如何为上述示例 BOF 创建一个 imports.txt 文件。
Filename:imports.txt
__imp_GetCurrentProcessId __imp_KERNEL32$GetCurrentProcessId
__imp_GetUserNameA __imp_ADVAPI32$GetUserNameA
使用imports.txt
文件中指定的符号重新运行 objcopy 应该重新定义所有需要的 BOF 导入。
objcopy --redefine-syms=imports.txt example.bof.o example-redefined.bof.o
使用 rabin2 检查符号表确认新符号名称已正确应用。
matt@laptop :: ~/Documents/dev/writing-bofs-without-dfr-example >> rabin2 -s example-redefined.bof.o
[Symbols]
nth paddr vaddr bind type size lib name demangled
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
0 0x00000000 0x00000000 LOCAL FILE 4 .file
0 0x0000012c 0x00000000 GLOBAL FUNC 4 go
0 0x0000012c 0x00000000 LOCAL SECT 4 .text
0 0x00000000 0x000000b0 LOCAL SECT 4 .data
0 0x00000000 0x000000c0 LOCAL SECT 4 .bss
0 0x000001dc 0x000000d0 LOCAL SECT 4 .rdata
0 0x0000020c 0x00000100 LOCAL SECT 4 .xdata
0 0x0000021c 0x00000110 LOCAL SECT 4 .pdata
0 0x00000228 0x00000120 LOCAL UNK 4 .rdata$zzz
0 ---------- ---------- NONE UNK 4 imp.__imp_KERNEL32$GetCurrentProcessId
0 ---------- ---------- NONE UNK 4 imp.__imp_BeaconPrintf
0 ---------- ---------- NONE UNK 4 imp.__imp_ADVAPI32$GetUserNameA
这提供了一种在源代码之外的单独文件中指定 BOF 导入的方法。BOF 可以像其他普通 C 程序一样编写,而无需进行特殊的 BOF 特定调整来管理导入。
那么这是如何工作的?
这将深入探讨 objcopy 在底层究竟做了什么来实现这一点。Objcopy 来自GNUBinutils,这意味着它可能在非自由平台上效果不佳(咳咳 Windows)。LLVM 有他们自己的 objcopy 版本(llvm-objcopy),它可以在 Windows 上运行;然而,拥有一个专门用于此的独立工具可能会更好。自定义工具可以增加很多灵活性或其他更适合 BOF 开发的功能。
在深入研究之前,最好先熟悉 COFF 的原始文件结构。不仅要了解每个组件(文件头、节头、重定位等)是什么,还要知道它们在原始文件中的位置。对 COFF 进行十六进制转储并标记每个组件看起来像这样。
Hexdump output forexamplebof.bof.o
───────────────────────────────────────────────────────────────────
00000000: 6486 0700 0000 0000 c202 0000 1500 0000 d............... COFF File Header
┌───────────────────────────────────────────────
00000010: 0000 0400│2e74 6578 7400 0000 0000 0000 .....text....... COFF Section
───────────────────┘ Headers
00000020: 0000 0000 b000 0000 2c01 0000 6802 0000 ........,...h...
00000030: 0000 0000 0600 0000 2000 5060 2e64 6174 ........ .P`.dat
00000040: 6100 0000 0000 0000 0000 0000 0000 0000 a...............
00000050: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000060: 4000 50c0 2e62 7373 0000 0000 0000 0000 @.P..bss........
00000070: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000080: 0000 0000 0000 0000 8000 50c0 2e72 6461 ..........P..rda
00000090: 7461 0000 0000 0000 0000 0000 3000 0000 ta..........0...
000000a0: dc01 0000 0000 0000 0000 0000 0000 0000 ................
000000b0: 4000 5040 2e78 6461 7461 0000 0000 0000 @[email protected]......
000000c0: 0000 0000 1000 0000 0c02 0000 0000 0000 ................
000000d0: 0000 0000 0000 0000 4000 3040 2e70 6461 [email protected]@.pda
000000e0: 7461 0000 0000 0000 0000 0000 0c00 0000 ta..............
000000f0: 1c02 0000 a402 0000 0000 0000 0300 0000 ................
00000100: 4000 3040 2f34 0000 0000 0000 0000 0000 @.0@/4..........
00000110: 0000 0000 4000 0000 2802 0000 0000 0000 ....@...(.......
┌─────────────────────────────────────
00000120: 0000 0000 0000 0000│4000 5040 5557 4881 [email protected]@UWH. Raw data for
─────────────────────────────┘ each section
00000130: ec48 0100 0048 8dac 2480 0000 0048 8b05 .H...H..$....H..
00000140: 0000 0000 ffd0 8985 bc00 0000 8b85 bc00 ................
00000150: 0000 4189 c048 8d05 0000 0000 4889 c2b9 ..A..H......H...
00000160: 0000 0000 488b 0500 0000 00ff d048 8d55 ....H........H.U
00000170: b0b8 0000 0000 b920 0000 0048 89d7 f348 ....... ...H...H
00000180: ab48 89fa 8802 4883 c201 c745 ac01 0100 .H....H....E....
00000190: 0048 8d55 ac48 8d45 b048 89c1 488b 0500 .H.U.H.E.H..H...
000001a0: 0000 00ff d083 f801 751f 488d 45b0 4989 ........u.H.E.I.
000001b0: c048 8d05 1800 0000 4889 c2b9 0000 0000 .H......H.......
000001c0: 488b 0500 0000 00ff d090 4881 c448 0100 H.........H..H..
000001d0: 005f 5dc3 9090 9090 9090 9090 4375 7272 ._].........Curr
000001e0: 656e 7420 7072 6f63 6573 7320 6964 3a20 ent process id:
000001f0: 256c 7500 5573 6572 6e61 6d65 3a20 2573 %lu.Username: %s
00000200: 0000 0000 0000 0000 0000 0000 0111 0585 ................
00000210: 1103 0901 2900 0270 0150 0000 0000 0000 ....)..p.P......
00000220: a800 0000 0000 0000 4743 433a 2028 474e ........GCC: (GN
00000230: 5529 2031 342e 312e 3120 3230 3234 3036 U) 14.1.1 202406
00000240: 3037 2028 4665 646f 7261 204d 696e 4757 07 (Fedora MinGW
00000250: 2031 342e 312e 312d 332e 6663 3430 2900 14.1.1-3.fc40).
┌─────────────────────────────────────
00000260: 0000 0000 0000 0000│1400 0000 1200 0000 ................ Relocation data
─────────────────────────────┘
00000270: 0400 2c00 0000 0a00 0000 0400 3b00 0000 ..,.........;...
00000280: 1300 0000 0400 7300 0000 1400 0000 0400 ......s.........
00000290: 8800 0000 0a00 0000 0400 9700 0000 1300 ................
000002a0: 0000 0400 0000 0000 0400 0000 0300 0400 ................
000002b0: 0000 0400 0000 0300 0800 0000 0c00 0000 ................
┌────────────────────────────────────────────────────
000002c0: 0300│2e66 696c 6500 0000 0000 0000 feff ...file......... Symbol Table
──────────────┘
000002d0: 0000 6701 6578 616d 706c 6562 6f66 2e63 ..g.examplebof.c
000002e0: 0000 0000 0000 676f 0000 0000 0000 0000 ......go........
000002f0: 0000 0100 2000 0201 0000 0000 0000 0000 .... ...........
00000300: 0000 0000 0000 0000 0000 2e74 6578 7400 ...........text.
00000310: 0000 0000 0000 0100 0000 0301 a800 0000 ................
00000320: 0600 0000 0000 0000 0000 0000 0000 2e64 ...............d
00000330: 6174 6100 0000 0000 0000 0200 0000 0301 ata.............
00000340: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000350: 0000 2e62 7373 0000 0000 0000 0000 0300 ...bss..........
00000360: 0000 0301 0000 0000 0000 0000 0000 0000 ................
00000370: 0000 0000 0000 2e72 6461 7461 0000 0000 .......rdata....
00000380: 0000 0400 0000 0301 2500 0000 0000 0000 ........%.......
00000390: 0000 0000 0000 0000 0000 2e78 6461 7461 ...........xdata
000003a0: 0000 0000 0000 0500 0000 0301 1000 0000 ................
000003b0: 0000 0000 0000 0000 0000 0000 0000 2e70 ...............p
000003c0: 6461 7461 0000 0000 0000 0600 0000 0301 data............
000003d0: 0c00 0000 0300 0000 0000 0000 0000 0000 ................
000003e0: 0000 0000 0000 0f00 0000 0000 0000 0700 ................
000003f0: 0000 0301 3800 0000 0000 0000 0000 0000 ....8...........
00000400: 0000 0000 0000 0000 0000 1a00 0000 0000 ................
00000410: 0000 0000 0000 0200 0000 0000 3400 0000 ............4...
00000420: 0000 0000 0000 0000 0200 0000 0000 4700 ..............G.
┌───────────────────────────
00000430: 0000 0000 0000 0000 0000 0200│5a00 0000 ............Z... String Table
───────────────────────────────────────┘
00000440: 2e72 6461 7461 247a 7a7a 002e 7264 6174 .rdata$zzz..rdat
00000450: 6124 7a7a 7a00 5f5f 696d 705f 4765 7443 a$zzz.__imp_GetC
00000460: 7572 7265 6e74 5072 6f63 6573 7349 6400 urrentProcessId.
00000470: 5f5f 696d 705f 4265 6163 6f6e 5072 696e __imp_BeaconPrin
00000480: 7466 005f 5f69 6d70 5f47 6574 5573 6572 tf.__imp_GetUser
00000490: 4e61 6d65 4100 NameA.
───────────────────────────────────────────────────────────────────
重要的部分是符号表、节头和字符串表。方便的是,字符串表位于文件的末尾。
这些部分是如何相互关联的?符号表和节头包含的字符串数据可能会引用字符串表。每个结构将在名称字段中预留 8 个字节来保存这些字符串数据。
名称字段的格式在节表(节头)和COFF 符号表PE 格式文档中有详细说明。
可以通过查看上面十六进制转储中的.text
节的条目来检查这一点。
Name (8 bytes)
│
╭───────────────────┬─────────┴────┬─────╮
00000010: │2e74 6578 7400 0000│0000 0000 │.text│......
╰───────────────────╯ ╰─────╯
00000020: 0000 0000 b000 0000 2c01 0000 6802 0000 ........,...h...
00000030: 0000 0000 0600 0000 2000 5060 ........ .P`
由于该节的名称可以适应为名称字段预留的 8 个字节,因此它嵌入在节元数据中。
如果节名称超过 8 个字节会发生什么?
这时字符串表就派上用场了。名称字段不会包含节的名称,而是以斜杠(/)开头,然后是一个 ASCII 数字,表示节名称在字符串表中的索引。
在上面的十六进制转储示例中,最后一个节头就是这种情况。
The name field in this instance is "/4" meaning that the name
of the section is really in the string table starting at the
fourth index.
Name (8 bytes)
│
╭───────────────────┬─────────┴────┬────────╮
00000100: │2f34 0000 0000 0000│0000 0000 │/4......│....
╰───────────────────╯ ╰────────╯
00000110: 0000 0000 4000 0000 2802 0000 0000 0000 ....@...(.......
00000120: 0000 0000 0000 0000 ........
字符串表中该索引处的值是“.rdata$zzz”。
Real name of the section
│
00000430: │ 5a00 0000 Z...
╭───────┴───────────────────┬─────────────┬──────────╮
00000440:│2e72 6461 7461 247a 7a7a 00│2e 7264 6174 │.rdata$zzz│.rdat
╰───────────────────────────╯ ╰──────────╯
00000450: 6124 7a7a 7a00 5f5f 696d 705f 4765 7443 a$zzz.__imp_GetC
00000460: 7572 7265 6e74 5072 6f63 6573 7349 6400 urrentProcessId.
00000470: 5f5f 696d 705f 4265 6163 6f6e 5072 696e __imp_BeaconPrin
00000480: 7466 005f 5f69 6d70 5f47 6574 5573 6572 tf.__imp_GetUser
00000490: 4e61 6d65 4100 NameA.
需要注意的一点是,这个索引在字符串表中的起始位置。字符串表的前四个字节(0x43c – 0x43f)是大小字段。这是一个小端整数(0x5a),包含字符串表的大小,包括大小字段本身。字符串表的数据从 0x440 开始,其数据大小是大小字段的值减去该字段的大小(0x5a - 4 = 0x56)。
节头中定义的索引不是基于字符串表数据的起始位置,而是基于包括大小字段在内的字符串表的起始位置。
符号名称遵循相同的模式,但有一个区别。这个区别在符号名称表示PE 格式文档中有说明。
符号名称字段的前四个字节将被设置为 NULL,接下来的四个字节是字符串表索引。这个索引是一个常规的 4 字节小端整数,而不是像节头中那样的 ASCII 十进制数。符号表中的名称索引遵循与节头名称相同的规则,即该索引是基于从字符串表的起始位置开始的,包括大小字段,而不是从字符串数据开始的位置。
在示例的十六进制转储中,符号表中的最后一个符号包含一个超过 8 字节的符号名称。
First four bytes are all 0
│ The symbol name is in the string
│ table at index 0x47
│ │
╭┴────────┬───┴╮
00000420: │0000 0000│4700│ ....G.
╰─────────┤ │
╭──────────────────────────────────╯ │
│ ╭──────────────────────────────────╯
00000430:│0000│0000 0000 0000 0000 0200 ............
╰────╯
字符串表
00000430: 5a00 0000 Z...
00000440: 2e72 6461 7461 247a 7a7a 002e 7264 6174 .rdata$zzz..rdat
00000450: 6124 7a7a 7a00 5f5f 696d 705f 4765 7443 a$zzz.__imp_GetC
00000460: 7572 7265 6e74 5072 6f63 6573 7349 6400 urrentProcessId.
00000470: 5f5f 696d 705f 4265 6163 6f6e 5072 696e __imp_BeaconPrin
╭────────────────────────────────╮ ┌─────────────╮
00000480: 7466 00│5f 5f69 6d70 5f47 6574 5573 6572│ tf│__imp_GetUser│
╭───────┘ ╭─────────┬───────────────┴┬──┘ ┌──────────┘
00000490:│4e61 6d65 4100│ │ │NameA│
└──────────────┘ │ └─────┘
│
The name of this symbol is "__imp_GetUserNameA"
这展示了字符串表如何与节头和符号表连接。现在,来看看符号重命名是如何工作的。
自己动手符号重命名
由于节头和符号表与字符串表的交互特性,重命名符号的操作相当简单。
首先要做的是找到需要重命名的符号的符号表条目。名称字段要么保存符号名称作为字节字符串,要么是字符串表中的索引,这取决于名称是否超过 8 个字节。如果当前符号名称和需要重命名的新名称都不超过 8 个字节,则可以直接将新名称插入名称字段中替换旧值。COFF 中的其他部分无需修改。
符号名称超过 8 个字节的情况更为常见,这需要多做一些工作。符号可以通过几种方式重命名,但这里将介绍“简单”方法和另一种通过在字符串表中直接替换旧字符串为新字符串的方法。
简单方法
还记得在这篇博客文章中解释 COFF 文件布局的部分提到字符串表“方便地”位于文件末尾吗?这之所以方便,是因为可以在不需要更新 COFF 其他部分的情况下向其附加任意字符串!
COFF 元数据有时会包含对文件其他部分的引用。在文件中间插入或删除任何数据会导致各种组件发生位移。这需要回过头来检查 COFF 以确保任何旧文件引用都已更新以适应这种位移。附加数据不会导致这种位移,因此文件引用将保持有效。
以简单方式重命名符号,新符号名称可以简单地附加到字符串表的末尾,同时更新字符串表大小字段以考虑添加的字符串。剩下要做的就是更改 COFF 符号表条目中的名称字段以引用新添加的字符串。COFF 的其余部分可以保持不变,因为这种方法不会导致文件的其他部分发生位移。旧名称字符串可以保留在原处,它将仅作为一些未使用数据的额外字节存在。
当需要重命名大量符号时,这种方法可能不是最理想的,因为旧符号名称会占用空间。
用新条目替换当前字符串表条目
重命名符号的另一种选择是用新名称替换字符串表中的当前符号名称。这里唯一的区别是,节头和符号表中的其他字符串表引用需要调整以适应任何位移。如果需要用比原始名称长 9 个字节的新名称重命名符号,则任何引用该字符串之后的字符串的节头或符号表条目都需要将其索引增加 9 以适应大小变化。
总结
这篇博客文章的目的是展示一个来自 GNU Binutils 的较不为人知的工具,很多人可能不太熟悉。它在对象文件中重新定义符号的能力可以在 Beacon 对象文件开发中得到利用。BOF 不需要在源代码中以 DFR 格式手动声明其导入。它们可以单独管理,而代码则更专注于 BOF 正在开发的功能。保持源代码更符合传统 C 代码的编写方式,使其更好地与现有开发工具集成。
另一个要点是“解耦”设计用于在非典型环境中运行的软件的约束与软件本身的概念。保持软件更通用,使其在需要测试或调试时更易于使用。通过库或其他工具找到在代码之外处理这些约束的替代方法,意味着代码可以专注于其设计目的,而各种环境因素则在其他地方处理。
使用专用工具来管理 Beacon 对象文件中的导入函数是应用这种解耦概念的一个例子。
原文始发于微信公众号(securitainment):BOF 开发 - 无需 DFR 编写 Beacon 对象文件
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论