Beacon在接收执行obj前,Cobalt Strike会先对这个obj文件进行一些处理,比如解析obj文件中一些需要的段.text,.data,在处理一些表比如IMAGE_RELOCATION,IMAGE_SYMBOL等等,然后在经过一系列的处理后,会把需要的部分按照一定格式打包起来随后在发送给Beacon,这时Beacon接收到的是Cobalt Strike已经解析处理过的obj文件数据,并非是原本的obj文件,所以Beacon主要做的是必须是在进程内才能确定并完成的事情比如处理重定位,填充函数指针等等,最后去执行go入口点
COFF解读
微软官方pe coff文档:https://learn.microsoft.com/zh-cn/windows/win32/debug/pe-format
COFF 文件基本结构
COFF 文件主要由以下几部分组成:
-
文件头 (File Header)
-
可选头 (Optional Header) - 可能不存在
-
节头表 (Section Headers)
-
节数据 (Section Data)
-
符号表 (Symbol Table)
-
字符串表 (String Table)
文件头 (File Header)
typedef struct {
uint16_t Machine; // 目标机器类型
uint16_t NumberOfSections; // 节的数量
uint32_t TimeDateStamp; // 文件创建时间戳
uint32_t PointerToSymbolTable; // 符号表偏移量
uint32_t NumberOfSymbols; // 符号表条目数
uint16_t SizeOfOptionalHeader; // 可选头大小
uint16_t Characteristics; // 文件属性标志
} COFF_FileHeader;
可选头 (Optional Header)
typedef struct {
uint16_t Magic; // 标识类型
uint8_t MajorLinkerVersion; // 链接器主版本
uint8_t MinorLinkerVersion; // 链接器次版本
uint32_t SizeOfCode; // 代码段大小
uint32_t SizeOfInitializedData; // 初始化数据大小
uint32_t SizeOfUninitializedData; // 未初始化数据大小
uint32_t AddressOfEntryPoint; // 入口点地址
uint32_t BaseOfCode; // 代码段基址
uint32_t BaseOfData; // 数据段基址
// PE特有字段会继续扩展...
} COFF_OptionalHeader;
节头表 (Section Headers)
typedef struct {
char Name[8]; // 节名称(8字节或以0填充)
uint32_t VirtualSize; // 节在内存中的大小
uint32_t VirtualAddress; // 节在内存中的虚拟地址
uint32_t SizeOfRawData; // 节在文件中的大小
uint32_t PointerToRawData; // 节数据在文件中的偏移
uint32_t PointerToRelocations;// 重定位信息偏移
uint32_t PointerToLinenumbers;// 行号信息偏移
uint16_t NumberOfRelocations; // 重定位条目数
uint16_t NumberOfLinenumbers; // 行号条目数
uint32_t Characteristics; // 节属性标志
} COFF_SectionHeader;
符号表 (Symbol Table)
typedef struct {
union {
char ShortName[8]; // 短名称(8字节)
struct {
uint32_t Zeroes; // 0表示使用长名称
uint32_t Offset; // 字符串表中的偏移
} LongName;
} Name;
uint32_t Value; // 符号值(地址或其他)
int16_t SectionNumber; // 节号(正数表示节索引)
uint16_t Type; // 符号类型
uint8_t StorageClass; // 存储类别
uint8_t NumberOfAuxSymbols; // 附加符号数
} COFF_Symbol;
重定位信息 (Relocations)
typedef struct {
uint32_t VirtualAddress; // 需要重定位的位置地址
uint32_t SymbolTableIndex; // 符号表索引
uint16_t Type; // 重定位类型
} COFF_Relocation;
COFF 文件解析流程
-
读取文件头,获取基本信息
-
如果有可选头,读取可选头
-
读取节头表,了解各节信息
-
根据节头表中的信息读取各节数据
-
读取符号表和字符串表(如果有)
-
处理重定位信息(如果有)
常见节类型
-
.text
: 代码节 -
.data
: 已初始化数据 -
.bss
: 未初始化数据 -
.rdata
: 只读数据 -
.reloc
: 重定位信息 -
.debug
: 调试信息
编写解析器
#include <stdio.h>
#include <Windows.h>
void parse_coff() {
HANDLE hFile = CreateFile("C:\Users\liuhua\Desktop\Demon.obj", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("CreateFile error.n");
return;
}
int file_size = 0;
file_size = GetFileSize(hFile, NULL);
char* data;
data = (char*)malloc(file_size);
DWORD dwRead;
if (!ReadFile(hFile, data, file_size, &dwRead, NULL))
{
printf("ReadFile error.n");
return;
}
//COFF文件头
PIMAGE_FILE_HEADER PECOFF_FileHeader = (PIMAGE_FILE_HEADER)data;
printf("COFF File Header:n");
printf("Machine: %x n", PECOFF_FileHeader->Machine);
printf("NumberOfSections %d n", PECOFF_FileHeader->NumberOfSections);
printf("TimeDateStamp %d n", PECOFF_FileHeader->TimeDateStamp);
printf("PointerToSymbolTable %d n", PECOFF_FileHeader->PointerToSymbolTable);
printf("NumberOfSymbols %d n", PECOFF_FileHeader->NumberOfSymbols);
printf("SizeOfOptionalHeader %d n", PECOFF_FileHeader->SizeOfOptionalHeader);
printf("Characteristics %x n", PECOFF_FileHeader->Characteristics);
// 跳过可选头(如果有)
if (PECOFF_FileHeader->SizeOfOptionalHeader > 0) {
}
// 计算节表起始位置
char* section_table = data + sizeof(IMAGE_FILE_HEADER) + PECOFF_FileHeader->SizeOfOptionalHeader;
// 读取节表
printf("nSection Headers:n");
PIMAGE_SECTION_HEADER section_headers = (PIMAGE_SECTION_HEADER)section_table;
for (int i = 0; i < PECOFF_FileHeader->NumberOfSections; i++) {
printf(" Section %d: %-8sn", i + 1, section_headers[i].Name);
printf(" Virtual Size: 0x%08Xn", section_headers[i].Misc.VirtualSize);
printf(" Virtual Address: 0x%08Xn", section_headers[i].VirtualAddress);
printf(" Raw Data Size: 0x%08Xn", section_headers[i].SizeOfRawData);
printf(" Raw Data Pointer: 0x%08Xn", section_headers[i].PointerToRawData);
printf(" Characteristics: 0x%08Xn", section_headers[i].Characteristics);
}
// 解析符号表(如果有)
if (PECOFF_FileHeader->PointerToSymbolTable != 0 && PECOFF_FileHeader->NumberOfSymbols > 0) {
printf("nSymbol Table:n");
PIMAGE_SYMBOL symbol_table = (PIMAGE_SYMBOL)(data + PECOFF_FileHeader->PointerToSymbolTable);
// 字符串表紧跟在符号表后面
DWORD string_table_size = *(DWORD*)(symbol_table + PECOFF_FileHeader->NumberOfSymbols);
char* string_table = (char*)(symbol_table + PECOFF_FileHeader->NumberOfSymbols);
for (int i = 0; i < PECOFF_FileHeader->NumberOfSymbols; i++) {
char name_buffer[9] = { 0 };
const char* name = NULL;
// 检查是短名称还是长名称
if (symbol_table[i].N.Name.Short != 0) {
// 短名称(最多8个字符)
memcpy(name_buffer, symbol_table[i].N.ShortName, 8);
name_buffer[8] = ' ';
name = name_buffer;
}
else {
// 长名称(在字符串表中)
DWORD offset = symbol_table[i].N.Name.Long;
if (offset < string_table_size) {
name = string_table + offset;
}
else {
name = "(invalid string table offset)";
}
}
printf(" Symbol %d: %sn", i + 1, name);
printf(" Value: 0x%08Xn", symbol_table[i].Value);
printf(" Section: %dn", symbol_table[i].SectionNumber);
printf(" Type: 0x%04Xn", symbol_table[i].Type);
printf(" Storage Class: 0x%02Xn", symbol_table[i].StorageClass);
// 跳过辅助符号
i += symbol_table[i].NumberOfAuxSymbols;
}
}
free(data);
}
int main(int argc, char* argv[]) {
parse_coff();
return 0;
}
武器化Demon代码
参考wbglil的思路:
-
加载读取obj文件
-
解析IMAGE_FILE_HEADER头
-
解析IMAGE_SECTION_HEADER节表
-
解析IMAGE_SYMBOL符号表
-
处理数据重定位和填充函数指针
-
根据符号表寻找go函数的地址然后call过去
修改重定位表
修复重定位表(Relocation Table)的目的是 在运行时动态修正代码/数据中的内存地址引用,确保程序能正确执行。
.obj
文件本身没有标准入口点
-
目标文件(
.obj
) 是编译器生成的中间文件,尚未经过链接器处理。 -
它可能包含多个函数(如
main
、go
、func1
等),但没有默认的EntryPoint
字段(不像 PE 文件的AddressOfEntryPoint
)。 -
必须通过符号表(Symbol Table)手动查找入口函数(如
main
或go
)
demo.c
以下代码使用 cl /c /GS- /Fo Demon.obj go_main.c编译为obj(注:请使用cl并且编译为x64位)
#include <stdio.h>
#include <windows.h>
DECLSPEC_IMPORT void vPrintf(char * fmt, ...);
DECLSPEC_IMPORT DWORD WINAPI User32$MessageBoxA(HWND,LPCTSTR,LPCTSTR,UINT);
void go() {
vPrintf("Hello World");
User32$MessageBoxA(NULL,"hello",NULL,NULL);
}
//这里的vPrintf函数其实就相当于Beacon里的Beaconxxxx函数
//至于其他的win api使用方式我还是按照CS的来采用DllName$FunName 这种格式
loader.c
以下代码使用 cl.exe /GS- loader.c /Fe loader.exe 或者Vs直接贴入运行
//仅支持cl编译的x64 obj
#include<Windows.h>
#include<stdio.h>
void vPrintf(char* fmt) {
printf(fmt);
}
int main()
{
HANDLE hFile = CreateFile("C:\Users\***\Desktop\Demon.obj", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("CreateFile error.n");
return 0;
}
int file_size = 0;
file_size = GetFileSize(hFile, NULL);
char* buff;
buff = (char*)malloc(file_size);
DWORD dwRead;
if (!ReadFile(hFile, buff, file_size, &dwRead, NULL))
{
printf("ReadFile error.n");
return 0;
}
//COFF文件头
PIMAGE_FILE_HEADER PECOFF_FileHeader = (PIMAGE_FILE_HEADER)buff;
printf("Machine: %x n", PECOFF_FileHeader->Machine);
printf("NumberOfSections %d n", PECOFF_FileHeader->NumberOfSections);
printf("TimeDateStamp %d n", PECOFF_FileHeader->TimeDateStamp);
printf("PointerToSymbolTable %d n", PECOFF_FileHeader->PointerToSymbolTable);
printf("NumberOfSymbols %d n", PECOFF_FileHeader->NumberOfSymbols);
printf("SizeOfOptionalHeader %d n", PECOFF_FileHeader->SizeOfOptionalHeader);
printf("Characteristics %x n", PECOFF_FileHeader->Characteristics);
//SizeOfOptionalHeader no
//COFF节表处理
PIMAGE_SECTION_HEADER* PECOFF_SectionHeader_arr = (PIMAGE_SECTION_HEADER*)malloc(PECOFF_FileHeader->NumberOfSections * sizeof(PIMAGE_SECTION_HEADER));
memset(PECOFF_SectionHeader_arr, 0, PECOFF_FileHeader->NumberOfSections * sizeof(PIMAGE_SECTION_HEADER));
PIMAGE_SECTION_HEADER PECOFF_SectionHeader = (PIMAGE_SECTION_HEADER)(buff + sizeof(IMAGE_FILE_HEADER));
for (size_t i = 0; i <= PECOFF_FileHeader->NumberOfSections - 1; i++)
{
PECOFF_SectionHeader_arr[i] = PECOFF_SectionHeader;
printf("段名称 %s n", PECOFF_SectionHeader->Name);
printf("段大小 %d n", PECOFF_SectionHeader->SizeOfRawData);
PECOFF_SectionHeader++;
}
//重定位表
int Relocation_len = 0;
for (int i = 0; i <= PECOFF_FileHeader->NumberOfSections - 1; i++)
{
Relocation_len += PECOFF_SectionHeader_arr[i]->NumberOfRelocations;
}
int x = 0;
PIMAGE_RELOCATION* PECOFF_Relocation_arr = (PIMAGE_RELOCATION*)malloc(Relocation_len * sizeof(PIMAGE_RELOCATION));
memset(PECOFF_Relocation_arr, 0, Relocation_len * sizeof(PIMAGE_RELOCATION));
for (int i = 0; i <= PECOFF_FileHeader->NumberOfSections - 1; i++)
{
if (PECOFF_SectionHeader_arr[i]->NumberOfRelocations)
{
PIMAGE_RELOCATION PECOFF_Relocation = (PIMAGE_RELOCATION)(buff + PECOFF_SectionHeader_arr[i]->PointerToRelocations);
for (int y = 0; y < PECOFF_SectionHeader_arr[i]->NumberOfRelocations; y++)
{
PECOFF_Relocation_arr[x] = PECOFF_Relocation;
PECOFF_Relocation++;
x++;
}
}
}
//打印输出
//符号表
PIMAGE_SYMBOL PECOFF_SYMBOL = (PIMAGE_SYMBOL)(buff + PECOFF_FileHeader->PointerToSymbolTable);
PIMAGE_SYMBOL* PECOFF_SYMBOL_arr = (PIMAGE_SYMBOL*)malloc(PECOFF_FileHeader->NumberOfSymbols * sizeof(PIMAGE_SYMBOL));
memset(PECOFF_SYMBOL_arr, 0, PECOFF_FileHeader->NumberOfSymbols * sizeof(PIMAGE_SYMBOL));
for (int i = 0; i <= PECOFF_FileHeader->NumberOfSymbols - 1; i++)
{
PECOFF_SYMBOL_arr[i] = PECOFF_SYMBOL;
PECOFF_SYMBOL++;
}
//无需处理NumberOfAuxSymbols
//处理重定位和函数指针
char* Fun_ptr = buff + PECOFF_SectionHeader_arr[0]->PointerToRawData;
for (int i = 0; i <= PECOFF_FileHeader->NumberOfSections - 1; i++)
{
if (PECOFF_SectionHeader_arr[i]->NumberOfRelocations)
{
PIMAGE_RELOCATION PECOFF_Relocation = (PIMAGE_RELOCATION)(buff + PECOFF_SectionHeader_arr[i]->PointerToRelocations);
for (int y = 0; y < PECOFF_SectionHeader_arr[i]->NumberOfRelocations; y++)
{
int sys_index = PECOFF_Relocation->SymbolTableIndex;
if (PECOFF_SYMBOL_arr[sys_index]->StorageClass == 3)
{
char* patch_data = buff + (PECOFF_Relocation->VirtualAddress + PECOFF_SectionHeader_arr[i]->PointerToRawData);
*(DWORD*)patch_data = ((DWORD64)(buff + ((PECOFF_SYMBOL_arr[sys_index]->Value) + (PECOFF_SectionHeader_arr[PECOFF_SYMBOL_arr[sys_index]->SectionNumber - 1]->PointerToRawData))) - (DWORD64)(patch_data + 4));
}
else
{
if (!(PECOFF_SYMBOL_arr[sys_index]->N.Name.Short))
{
char* pstr = (buff + PECOFF_FileHeader->PointerToSymbolTable) + (PECOFF_FileHeader->NumberOfSymbols * sizeof(IMAGE_SYMBOL));
pstr += (DWORD)(PECOFF_SYMBOL_arr[sys_index]->N.Name.Long);
if (!strcmp(pstr, "__imp_vPrintf"))
{
char* patch_data = buff + (PECOFF_Relocation->VirtualAddress + PECOFF_SectionHeader_arr[i]->PointerToRawData);
*(DWORD64*)Fun_ptr = (DWORD64)vPrintf;
*(DWORD*)patch_data = ((DWORD64)Fun_ptr - (DWORD64)(patch_data + 4));
DWORD64* ptr = (DWORD64*)Fun_ptr;
ptr++;
Fun_ptr = (char*)ptr;
}
else
{
pstr += 6;
char* dllname;
char* funname;
dllname = strtok(pstr, "$");
funname = strtok(NULL, "$");
DWORD64 fun_add = (DWORD64)GetProcAddress(LoadLibraryA(dllname), funname);
char* patch_data = buff + (PECOFF_Relocation->VirtualAddress + PECOFF_SectionHeader_arr[i]->PointerToRawData);
*(DWORD64*)Fun_ptr = (DWORD64)fun_add;
*(DWORD*)patch_data = ((DWORD64)Fun_ptr - (DWORD64)(patch_data + 4));
DWORD64* ptr = (DWORD64*)Fun_ptr;
ptr++;
Fun_ptr = (char*)ptr;
}
}
}
PECOFF_Relocation++;
}
}
}
//寻找go函数作为入口点
DWORD oep;
for (int i = 0; i < PECOFF_FileHeader->NumberOfSymbols - 1; i++)
{
if (!strncmp((char*)(PECOFF_SYMBOL_arr[i]->N.ShortName), "go", 2))
{
oep = PECOFF_SYMBOL_arr[i]->Value;
}
}
char* jmp;
for (int i = 0; i < PECOFF_FileHeader->NumberOfSections - 1; i++)
{
if (!strncmp((char*)PECOFF_SectionHeader_arr[i]->Name, ".text", 5))
{
jmp = (buff + PECOFF_SectionHeader_arr[i]->PointerToRawData + oep);
}
}
printf("0x%016I64x n", jmp);
DWORD Protect;
if (VirtualProtect(buff, file_size, PAGE_EXECUTE_READWRITE, &Protect) != 0)
{
((void(*)(void))jmp)();
};
//printf("%x",GetLastError());
return 0;
}
这里是参考的wbglil的代码,我自己写的代码在修复重定位表时有点问题,也希望大佬能斧正斧正我的代码,如下
#include <stdio.h>
#include <Windows.h>
void parse_coff() {
HANDLE hFile = CreateFile("C:\Users\***\Desktop\Demon.obj", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
printf("CreateFile error.n");
return;
}
int file_size = GetFileSize(hFile, NULL);
char* data = (char*)malloc(file_size);
DWORD dwRead;
if (!ReadFile(hFile, data, file_size, &dwRead, NULL)) {
printf("ReadFile error.n");
return;
}
// COFF 文件头
PIMAGE_FILE_HEADER PECOFF_FileHeader = (PIMAGE_FILE_HEADER)data;
printf("COFF File Header:n");
printf("Machine: %x n", PECOFF_FileHeader->Machine);
printf("NumberOfSections %d n", PECOFF_FileHeader->NumberOfSections);
printf("TimeDateStamp %d n", PECOFF_FileHeader->TimeDateStamp);
printf("PointerToSymbolTable %d n", PECOFF_FileHeader->PointerToSymbolTable);
printf("NumberOfSymbols %d n", PECOFF_FileHeader->NumberOfSymbols);
printf("SizeOfOptionalHeader %d n", PECOFF_FileHeader->SizeOfOptionalHeader);
printf("Characteristics %x n", PECOFF_FileHeader->Characteristics);
// 跳过可选头(如果有)
if (PECOFF_FileHeader->SizeOfOptionalHeader > 0) {
// 可选头处理代码
}
// 计算节表起始位置
char* section_table = data + sizeof(IMAGE_FILE_HEADER) + PECOFF_FileHeader->SizeOfOptionalHeader;
// 读取节表
printf("nSection Headers:n");
PIMAGE_SECTION_HEADER section_headers = (PIMAGE_SECTION_HEADER)section_table;
for (int i = 0; i < PECOFF_FileHeader->NumberOfSections; i++) {
printf(" Section %d: %-8sn", i + 1, section_headers[i].Name);
printf(" Virtual Size: 0x%08Xn", section_headers[i].Misc.VirtualSize);
printf(" Virtual Address: 0x%08Xn", section_headers[i].VirtualAddress);
printf(" Raw Data Size: 0x%08Xn", section_headers[i].SizeOfRawData);
printf(" Raw Data Pointer: 0x%08Xn", section_headers[i].PointerToRawData);
printf(" Characteristics: 0x%08Xn", section_headers[i].Characteristics);
// 读取重定位表
if (section_headers[i].PointerToRelocations != 0) {
PIMAGE_RELOCATION relocation_table = (PIMAGE_RELOCATION)(data + section_headers[i].PointerToRelocations);
printf(" Relocation Table:n");
for (int j = 0; j < section_headers[i].NumberOfRelocations; j++) {
printf(" Relocation: Type: 0x%04X, Offset: 0x%08Xn",
relocation_table[j].Type, relocation_table[j].VirtualAddress);
// 处理重定位:根据类型修改地址
// 通常是符号的地址需要调整
// 你可以在这里处理重定位表的类型和具体的调整逻辑
}
}
}
// 解析符号表(如果有)
if (PECOFF_FileHeader->PointerToSymbolTable != 0 && PECOFF_FileHeader->NumberOfSymbols > 0) {
printf("nSymbol Table:n");
PIMAGE_SYMBOL symbol_table = (PIMAGE_SYMBOL)(data + PECOFF_FileHeader->PointerToSymbolTable);
// 字符串表紧跟在符号表后面
DWORD string_table_size = *(DWORD*)(symbol_table + PECOFF_FileHeader->NumberOfSymbols);
char* string_table = (char*)(symbol_table + PECOFF_FileHeader->NumberOfSymbols);
//寻找go函数作为入口点
DWORD oep = 0;
for (int i = 0; i < PECOFF_FileHeader->NumberOfSymbols; i++) {
char name_buffer[9] = { 0 };
const char* name = NULL;
// 检查是短名称还是长名称
if (symbol_table[i].N.Name.Short != 0) {
// 短名称(最多8个字符)
memcpy(name_buffer, symbol_table[i].N.ShortName, 8);
name_buffer[8] = ' ';
name = name_buffer;
}
else {
// 长名称(在字符串表中)
DWORD offset = symbol_table[i].N.Name.Long;
if (offset < string_table_size) {
name = string_table + offset;
}
else {
name = "(invalid string table offset)";
}
}
printf(" Symbol %d: %sn", i + 1, name);
printf(" Value: 0x%08Xn", symbol_table[i].Value);
printf(" Section: %dn", symbol_table[i].SectionNumber);
printf(" Type: 0x%04Xn", symbol_table[i].Type);
printf(" Storage Class: 0x%02Xn", symbol_table[i].StorageClass);
if (strcmp(name, "go") == 0) {
printf("发现_GO函数n");
oep = symbol_table[i].Value;
}
// 跳过辅助符号
i += symbol_table[i].NumberOfAuxSymbols;
}
if (oep != 0) {
printf("Go 入口点: 0x%08Xn", oep);
}
else {
printf("未找到 Go 函数n");
}
}
free(data);
}
int main(int argc, char* argv[]) {
parse_coff();
return 0;
}
运行效果
最后进行一些总结这次的文章,COFF像极了PE格式,一个是链接后的,一个是链接前,而PE是有entiry入口点,而COFF是无的,它的所有函数都放在符号表中,较长的函数名还会存在字符串表。
而加载呢,却和PEload很像,修复解析格式,重定位表,最后一步两者就有区别了,PE是指定入口,而我们的COFF就要指定函数名(通过遍历符号表寻找)这也就是为啥CS的BOF是指定GO函数为编写函数了,因为官方解析时候只找GO函数的符号
END
参考文章:
https://wbglil.gitbook.io/cobalt-strike/cobalt-strike-yuan-li-jie-shao/untitled-3#obj-mu-biao-wen-jian
原文始发于微信公众号(T3Ysec):Cobalt Strike BOF实现的原理
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论