你真的了解CobaltStrike Bof吗?

文摘   2024-09-24 13:00   陕西  
简介

前面其实我们已经讲过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格式,所以可以使用相似的数据结构来描述其结构。

  1. 文件头(IMAGE_FILE_HEADER)

    这是目标文件的文件头,包含了目标文件的基本信息,比如目标文件的类型、机器类型、节的数量等。它是每个目标文件的开头部分。

    主要字段保护如下:

    Machine:表示目标文件生成的平台(例如 x86x64)。

    NumberOfSections:文件中节的数量。

    TimeDateStamp:文件的时间戳。

    PointerToSymbolTable:符号表的指针。

    NumberOfSymbols:符号表中符号的数量。

    SizeOfOptionalHeader:PE 文件中通常有可选头,但在目标文件中通常是 0。

    Characteristics:文件的属性标志。

  2. 节头(IMAGE_SECTION_HEADER)

    节头描述目标文件中的每一个节,例如代码节,数据节,它包含了每一个节的大小,起始位置以及虚拟地址等信息。

    主要字段包括:

    Name:节的名称,通常是 .text.data.bss 等。

    VirtualSize:节的实际大小。

    VirtualAddress:节的加载地址。

    SizeOfRawData:节在文件中的大小。

    PointerToRawData:节的原始数据在文件中的偏移。

    Characteristics:节的属性标志,如可执行、只读等。

  3. 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; }}

完整代码:

#include <Windows.h>#include <stdio.h>
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;}

Relay学安全
这是一个纯分享技术的公众号,只想做安全圈的一股清流,不会发任何广告,不会接受任何广告,只会分享纯技术文章,欢迎各行各业的小伙伴关注。让我们一起提升技术。
 最新文章