简介
前面其实我们已经讲过Bof这个东西了,并教大家如何去编写,接下来我们需要去了解一下这个Bof文件以及如何去解析这个Bof文件。
这样可以让我们更好的去理解Beacon Object File。
目标文件
首先我们需要去了解一下目标文件,.c文件或.cpp文件一般要经过如下几个阶段才可以变成可执行文件。
源代码 (.c / .cpp)
↓ 预处理 (Preprocessing)
预处理后的代码 (.i 文件)
↓ 编译 (Compilation)
汇编代码 (.asm 文件)
↓ 汇编 (Assembly)
目标文件 (.obj 或 .o 文件)
↓ 链接 (Linking)
可执行文件 (.exe 或 二进制文件)
如下图:
原始的relaysec.c文件通过编译会变成relaysec.o或relaysec.obj文件,这取决于你是windows还是linux,如果是windows则编译出来为.obj文件,如果是linux则编译出来为.o文件,最后通过链接的方式生成relaysec.exe可执行文件。
那么也就是说Bof文件,只是将.c文件编译为了.obj或.o文件,并没有链接。所以Bof文件实际就是未链接的目标文件。
我们可以使用cl工具尝试对其进行编译。
那么我们在想CobaltStrike Bof文件为什么不进行链接?
Bof文件的目标是为CobaltStrike的Beacon 代理提供额外的功能,Beacon负责加载并执行这些未链接的目标文件,所以不需要将他们链接成一个完整的可执行程序。
Cobalt Strike Beacon 使用它自己的运行时环境,它会在需要时加载并执行这些 .obj
文件中的代码,而不需要生成一个独立的可执行文件。
Bof文件的工作原理
Beacon 会将 .obj
文件中的机器代码加载到内存中执行。这种方式与传统的加载可执行文件或 DLL 的方式不同,更加轻量且适合在内存中操作,具有较强的隐蔽性。
链接步骤并不需要,因为 Beacon 本身充当了链接器的角色,负责将 BOF 文件中的函数和数据整合到当前的 Beacon 进程中。
目标文件和PE文件
目标文件和PE文件的结构有很多相似之处,他们都遵循了COFF格式,所以可以使用相似的数据结构来描述其结构。
文件头(IMAGE_FILE_HEADER)
这是目标文件的文件头,包含了目标文件的基本信息,比如目标文件的类型、机器类型、节的数量等。它是每个目标文件的开头部分。
主要字段保护如下:
Machine:表示目标文件生成的平台(例如
x86
或x64
)。NumberOfSections:文件中节的数量。
TimeDateStamp:文件的时间戳。
PointerToSymbolTable:符号表的指针。
NumberOfSymbols:符号表中符号的数量。
SizeOfOptionalHeader:PE 文件中通常有可选头,但在目标文件中通常是 0。
Characteristics:文件的属性标志。
节头(IMAGE_SECTION_HEADER)
节头描述目标文件中的每一个节,例如代码节,数据节,它包含了每一个节的大小,起始位置以及虚拟地址等信息。
主要字段包括:
Name:节的名称,通常是
.text
、.data
、.bss
等。VirtualSize:节的实际大小。
VirtualAddress:节的加载地址。
SizeOfRawData:节在文件中的大小。
PointerToRawData:节的原始数据在文件中的偏移。
Characteristics:节的属性标志,如可执行、只读等。
IMAGE_SYMBOL
(符号表)符号表是目标文件中特有的,用于存储符号信息。符号表中的每个符号可以是全局变量、函数名称、未解析的外部符号等。
主要字段包括:
Name:符号的名称。
Value:符号的值,通常是符号在目标文件中的地址或偏移量。
SectionNumber:该符号所在的节编号。
Type:符号的类型(例如函数、数据)。
StorageClass:符号的存储类别(例如全局、局部)。
NumberOfAuxSymbols:符号的辅助条目数量。
解析目标文件
目标文件和PE文件的解析非常相似,可以使用想通的解析逻辑。
比如说从IMAGE_FILE_HEADER开始,获取文件的基本信息,然后通过IMAGE_SECTION_HEADER解析每一个节的内容。
最后通过IMAGE_SYMBOL解析符号表中的符号信息。
在解析目标文件之前,我们需要去将其从磁盘上读取到内存中。
BOOL ReadFileFromDiskA(IN LPCSTR cFileName, OUT PBYTE* ppFileBuffer, OUT PDWORD pdwFileSize) {
HANDLE hFile = INVALID_HANDLE_VALUE;
DWORD dwFileSize = NULL,
dwNumberOfBytesRead = NULL;
PBYTE pBaseAddress = NULL;
if (!cFileName || !pdwFileSize || !ppFileBuffer)
goto _END_OF_FUNC;
//打开目标文件
if ((hFile = CreateFileA(cFileName, GENERIC_READ, 0x00, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)) == INVALID_HANDLE_VALUE) {
printf("[!] CreateFileA Failed With Error: %d \n", GetLastError());
goto _END_OF_FUNC;
}
//获取目标文件的大小
if ((dwFileSize = GetFileSize(hFile, NULL)) == INVALID_FILE_SIZE) {
printf("[!] GetFileSize Failed With Error: %d \n", GetLastError());
goto _END_OF_FUNC;
}
//申请一块堆栈内存 大小就是上面获取到目标文件的大小
if (!(pBaseAddress = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwFileSize))) {
printf("[!] HeapAlloc Failed With Error: %d \n", GetLastError());
goto _END_OF_FUNC;
}
//将目标文件读入到内存缓冲区中
if (!ReadFile(hFile, pBaseAddress, dwFileSize, &dwNumberOfBytesRead, NULL) || dwFileSize != dwNumberOfBytesRead) {
printf("[!] ReadFile Failed With Error: %d \n[i] Read %d Of %d Bytes \n", GetLastError(), dwNumberOfBytesRead, dwFileSize);
goto _END_OF_FUNC;
}
*ppFileBuffer = pBaseAddress;
*pdwFileSize = dwFileSize;
_END_OF_FUNC:
if (hFile != INVALID_HANDLE_VALUE)
CloseHandle(hFile);
if (pBaseAddress && !*ppFileBuffer)
HeapFree(GetProcessHeap(), 0x00, pBaseAddress);
return (*ppFileBuffer && *pdwFileSize) ? TRUE : FALSE;
}
如下图:
我们首先需要去解析文件头,也就是IMAGE_FILE_HEADER。
我们先来看一下这个结构:
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; // 目标计算机的架构类型
WORD NumberOfSections; // 节的数量
DWORD TimeDateStamp; // 文件的时间戳,表示文件的创建时间
DWORD PointerToSymbolTable; // 符号表的指针(对于目标文件)
DWORD NumberOfSymbols; // 符号表中的符号数量(对于目标文件)
WORD SizeOfOptionalHeader; // 可选头的大小(IMAGE_OPTIONAL_HEADER)
WORD Characteristics; // 文件的特征标志(如是否为可执行文件)
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
这里着重介绍两个字段,分别是PointerToSymbolTable和NumberOfSymbols。
PointerToSymbolTable字段表示符号表的指针,它指向符号表的文件偏移量,通常仅在目标文件(如OBJ文件)中有效。对于可执行文件,一般为 0
。
NumberOfSymbols字段表示符号表中的符号数量,通常在目标文件中有效,对于可执行文件来说通常为0。
如下代码:
//pImageFileHdr指向一个PIMAGE_FILE_HEADER的指针 用于保存目标文件的文件头
PIMAGE_FILE_HEADER pImgFileHdr = { 0 };
//pImgSymbol是一个指向IMAGE_SYMBOL结构的指针 用于保存符号表的指针
PIMAGE_SYMBOL pImgSymbol = { 0 };
//pObject就是我们上面将目标文件读取到内存中 这里转换为PIMAGE_FILE_HEADER类型 pImgFileHdr 就指向了目标文件中的文件头(COFF 文件头)。
pImgFileHdr = (PIMAGE_FILE_HEADER)((PVOID)pObject);
//pImgFileHdr->PointerToSymbolTable 是 IMAGE_FILE_HEADER 结构体中的一个字段,表示符号表在文件中的偏移量。
//pObject + (ULONG_PTR)pImgFileHdr->PointerToSymbolTable 的意思是从文件开始位置 (pObject) 加上符号表的偏移量,计算出符号表的实际位置。
pImgSymbol = (PIMAGE_SYMBOL)(pObject + (ULONG_PTR)pImgFileHdr->PointerToSymbolTable);
puts("");
printf("==== Object Header ====\n"
" - 节的数量: %d \n"
" - 符号的数量: %ld\n"
" - 符号表的位置 : %p \n",
//打印目标文件中节的数量
pImgFileHdr->NumberOfSections,
pImgFileHdr->NumberOfSymbols,
pImgSymbol
);
我们从文件头中解析出节数,符号数,符号表之后,我们就可以解析目标文件的节了。解析目标文件中的节可以使用与PE文件类似的方式,也就是使用IMAGE_SECTION_HEADER结构体来提取节的相关信息,如RVA偏移量,原始数据的地址以及节的大小。
IMAGE_SECTION_HEADER结构如下:
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 节的名称
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
Misc;
DWORD VirtualAddress; // RVA(节的相对虚拟地址)
DWORD SizeOfRawData; // 节的大小
DWORD PointerToRawData; // 节的原始数据在文件中的偏移量
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics; // 节的属性标志
IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
如下代码:
//由于节表紧跟在 IMAGE_FILE_HEADER 之后,因此可以通过 (pImgFileHdr + 1) 来获取节表的起始位置。
PIMAGE_SECTION_HEADER pSectionHdr = (PIMAGE_SECTION_HEADER)((PBYTE)(pImgFileHdr + 1)); // 文件头后的第一节
printf("\n==== Section Table ====\n");
//遍历节表 这里的NumberOfSections是节的数量
for (int i = 0; i < pImgFileHdr->NumberOfSections; i++) {
printf("\n节 %d\n", i + 1);
printf(" - 名称 : %.8s\n", pSectionHdr[i].Name);
printf(" - 虚拟地址 : 0x%08X\n", pSectionHdr[i].VirtualAddress);
printf(" - 原始数据大小 : %d\n", pSectionHdr[i].SizeOfRawData);
printf(" - 原始数据偏移量 : 0x%08X\n", pSectionHdr[i].PointerToRawData);
printf(" - 特性标志 : 0x%08X\n", pSectionHdr[i].Characteristics);
}
那么我们解析完文件头和节之后,符号表中的每个符号通过 IMAGE_SYMBOL
结构体表示,它包含关键信息,例如变量、函数的定义,以及从外部库导入的函数等。我们可以遍历符号表并解析每个符号的相关信息。
IMAGE_SYMBOL结构如下:
typedef struct _IMAGE_SYMBOL {
//名称存储 如果如果符号名称长度小于 8 个字节,它将直接存储在 ShortName[8] 中。我们可以从此字段直接读取符号名称。
//如果符号名称长度超过 8 个字节,那么它不会直接存储在 IMAGE_SYMBOL 结构中。相反,符号表只存储符号名在字符串表中的偏移量。
//该偏移量存储在 N.Name.Long(或 N.LongName[1])中。解析时,需要将该偏移量加上字符串表的基地址,来读取符号的实际名称。
union {
BYTE ShortName[8];
struct {
DWORD Short; // 如果名称长度小于8字节
DWORD Long; // 如果名称在字符串表中
} Name;
DWORD LongName[2]; // 用于名称字符串偏移
} N;
DWORD Value; // 符号的值(地址、偏移等)
SHORT SectionNumber; // 符号所属的节
WORD Type; // 符号类型
BYTE StorageClass; // 存储类别
BYTE NumberOfAuxSymbols; // 辅助符号数目
} IMAGE_SYMBOL;
如下代码:
void ParseSymbolTable(PIMAGE_SYMBOL pImgSymbol, DWORD NumberOfSymbols, PBYTE pStringTable) {
printf("\n==== 符号表 ====\n");
for (DWORD i = 0; i < NumberOfSymbols; i++) {
PIMAGE_SYMBOL pSymbol = &pImgSymbol[i];
char* SymbolName = NULL;
// 获取符号名称
if (pSymbol->N.Name.Short != 0) {
SymbolName = (char*)pSymbol->N.ShortName;
}
else {
SymbolName = (char*)(pStringTable + pSymbol->N.Name.Long);
}
// 确定符号是否为函数
BOOL isFunction = (pSymbol->Type & IMAGE_SYM_DTYPE_FUNCTION) != 0;
printf("\n符号 %d\n", i + 1);
printf(" - 名称: %s\n", SymbolName);
printf(" - 值 : 0x%08X\n", pSymbol->Value);
printf(" - 节号: %d\n", pSymbol->SectionNumber);
printf(" - 类型: 0x%04X %s\n", pSymbol->Type, isFunction ? "(函数)" : "(非函数)");
printf(" - 存储类别: 0x%02X\n", pSymbol->StorageClass);
printf(" - 辅助符号数量: %d\n", pSymbol->NumberOfAuxSymbols);
// 如果有辅助符号,跳过
i += pSymbol->NumberOfAuxSymbols;
}
}
完整代码:
BOOL ReadFileFromDiskA(IN LPCSTR cFileName, OUT PBYTE* ppFileBuffer, OUT PDWORD pdwFileSize) {
HANDLE hFile = INVALID_HANDLE_VALUE;
DWORD dwFileSize = NULL,
dwNumberOfBytesRead = NULL;
PBYTE pBaseAddress = NULL;
if (!cFileName || !pdwFileSize || !ppFileBuffer)
goto _END_OF_FUNC;
// 打开目标文件
if ((hFile = CreateFileA(cFileName, GENERIC_READ, 0x00, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)) == INVALID_HANDLE_VALUE) {
printf("[!] 打开文件失败, 错误代码: %d \n", GetLastError());
goto _END_OF_FUNC;
}
// 获取文件大小
if ((dwFileSize = GetFileSize(hFile, NULL)) == INVALID_FILE_SIZE) {
printf("[!] 获取文件大小失败, 错误代码: %d \n", GetLastError());
goto _END_OF_FUNC;
}
// 申请内存
if (!(pBaseAddress = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwFileSize))) {
printf("[!] 分配内存失败, 错误代码: %d \n", GetLastError());
goto _END_OF_FUNC;
}
// 读取文件内容到缓冲区
if (!ReadFile(hFile, pBaseAddress, dwFileSize, &dwNumberOfBytesRead, NULL) || dwFileSize != dwNumberOfBytesRead) {
printf("[!] 读取文件失败, 错误代码: %d \n[i] 已读取 %d / %d 字节 \n", GetLastError(), dwNumberOfBytesRead, dwFileSize);
goto _END_OF_FUNC;
}
*ppFileBuffer = pBaseAddress;
*pdwFileSize = dwFileSize;
_END_OF_FUNC:
if (hFile != INVALID_HANDLE_VALUE)
CloseHandle(hFile);
if (pBaseAddress && !*ppFileBuffer)
HeapFree(GetProcessHeap(), 0x00, pBaseAddress);
return (*ppFileBuffer && *pdwFileSize) ? TRUE : FALSE;
}
void ParseSectionHeaders(PIMAGE_SECTION_HEADER pSectionHeader, int NumberOfSections) {
printf("\n==== 节表 ====\n");
for (int i = 0; i < NumberOfSections; i++) {
PIMAGE_SECTION_HEADER pSecHdr = &pSectionHeader[i];
printf("节 %d: 名称: %.8s\n", i + 1, pSecHdr->Name);
printf(" - 虚拟地址: 0x%08X\n", pSecHdr->VirtualAddress);
printf(" - 原始数据大小: 0x%08X\n", pSecHdr->SizeOfRawData);
printf(" - 数据指针: 0x%08X\n", pSecHdr->PointerToRawData);
}
}
void ParseSymbolTable(PIMAGE_SYMBOL pImgSymbol, DWORD NumberOfSymbols, PBYTE pStringTable) {
printf("\n==== 符号表 ====\n");
for (DWORD i = 0; i < NumberOfSymbols; i++) {
PIMAGE_SYMBOL pSymbol = &pImgSymbol[i];
char* SymbolName = NULL;
// 获取符号名称
if (pSymbol->N.Name.Short != 0) {
SymbolName = (char*)pSymbol->N.ShortName;
}
else {
SymbolName = (char*)(pStringTable + pSymbol->N.Name.Long);
}
// 确定符号是否为函数
BOOL isFunction = (pSymbol->Type & IMAGE_SYM_DTYPE_FUNCTION) != 0;
printf("\n符号 %d\n", i + 1);
printf(" - 名称: %s\n", SymbolName);
printf(" - 值 : 0x%08X\n", pSymbol->Value);
printf(" - 节号: %d\n", pSymbol->SectionNumber);
printf(" - 类型: 0x%04X %s\n", pSymbol->Type, isFunction ? "(函数)" : "(非函数)");
printf(" - 存储类别: 0x%02X\n", pSymbol->StorageClass);
printf(" - 辅助符号数量: %d\n", pSymbol->NumberOfAuxSymbols);
// 如果有辅助符号,跳过
i += pSymbol->NumberOfAuxSymbols;
}
}
int main()
{
LPSTR sPath = { 0 };
ULONG_PTR pObject = { 0 };
ULONG uLength = { 0 };
sPath = "C:\\Users\\Administrator\\source\\repos\\TokenTest\\x64\\Debug\\whoami.x64.o";
if (!ReadFileFromDiskA(sPath, (PBYTE*)&pObject, &uLength)) {
printf("[!] 读取文件失败: %s\n", sPath);
return -1;
}
printf("[*] 目标文件加载到 %p [%d 字节]\n", pObject, uLength);
// 解析文件头
PIMAGE_FILE_HEADER pImgFileHdr = { 0 };
pImgFileHdr = (PIMAGE_FILE_HEADER)((PVOID)pObject);
printf("\n==== 文件头 ====\n"
" - 节数: %d \n"
" - 符号数: %ld\n"
" - 符号表指针: %p \n",
pImgFileHdr->NumberOfSections,
pImgFileHdr->NumberOfSymbols,
(PVOID)(pObject + pImgFileHdr->PointerToSymbolTable));
// 解析节表
PIMAGE_SECTION_HEADER pSectionHeader = (PIMAGE_SECTION_HEADER)((PBYTE)pImgFileHdr + sizeof(IMAGE_FILE_HEADER));
ParseSectionHeaders(pSectionHeader, pImgFileHdr->NumberOfSections);
// 解析符号表
PIMAGE_SYMBOL pImgSymbol = (PIMAGE_SYMBOL)(pObject + pImgFileHdr->PointerToSymbolTable);
PBYTE pStringTable = (PBYTE)(pObject + pImgFileHdr->PointerToSymbolTable + pImgFileHdr->NumberOfSymbols * sizeof(IMAGE_SYMBOL));
ParseSymbolTable(pImgSymbol, pImgFileHdr->NumberOfSymbols, pStringTable);
return 0;
}