BOF功能增加了Cobalt Strike快速扩展的能力
0x00 官方说明
根据官方说明:
Beacon具有动态解析的功能,在编写BOF时,只需要把用到的API声明为LIBRARY$Function即可,例如:
#include <windows.h>
#include <stdio.h>
#include "beacon.h"
DECLSPEC_IMPORT int USER32$MessageBoxA(HWND, LPCSTR,LPCSTR,UINT);
void go(char * args, int alen) {
DWORD dwRet;
dwRet = USER32$MessageBoxA(NULL, "BOF TEST", "BOF", MB_OK);
BeaconPrintf(CALLBACK_OUTPUT, "Ret: %d", dwRet);
}
然后编译为.o文件:
To compile this with Visual Studio:cl.exe /c /GS- hello.c /Fohello.o
To compile this with x86 MinGW:gcc -c hello.c -o hello.o -m32
To compile this with x64 MinGW:gcc -c hello.c -o hello.o
The commands above produce a hello.o file. Use inline-execute in Beacon to run the BOF.
beacon> inline-execute /path/to/hello.o these are arguments
那么可以看到BOF文件的本质就是.o/.obj文件,下面将探究.o文件格式以及如何自己实现一个.o文件加载器.
0x01 .o文件格式
以windows平台为例, 使用COFF文件格式, 具体逻辑可查看微软文档
COFF和PE结构很相似,比如COFF文件标头:
offset | 大小 | 字段 | 说明 |
---|---|---|---|
0 | 2 | Machine | 标识目标机类型的数字 |
2 | 2 | NumberOfSections | 节区数量 |
4 | 4 | TimeDateStamp | 时间戳, 表明文件创建时间 |
8 | 4 | PointerToSymbolTable | COFF 符号表的文件偏移量;如果没有 COFF 符号表,则为零 |
12 | 4 | NumberOfSymbols | 符号表中的项数。此数据可用于查找紧跟在符号表后面的字符串表 |
16 | 2 | SizeOfOptionalHeader | 可选标头的大小 |
18 | 2 | Characteristics | 指示文件属性的标志 |
其中有一个SymbolTable记录了所有符号:
offset | 大小 | 字段 | 说明 |
---|---|---|---|
0 | 8 | Name(*) | 使用union并集三个字段 |
8 | 4 | Value | 与符号关联的值。此字段的解释取决于 SectionNumber 和 StorageClass |
12 | 2 | SectionNumber | 使用节表中从一开始的索引标识节的带符号整数 |
14 | 2 | Type | 一个表示类型的数字。Microsoft 将此字段设置为 0x20(函数)或 0x0(不是函数) |
16 | 1 | StorageClass | 一个表示存储类的枚举值 |
17 | 1 | NumberOfAuxSymbols | 此记录后面的辅助符号表条目的数量 |
Name是一个Union, 如果名字小于8个字符则填充至此, 如果大于8个字符则前4个字节为0, 后4个字节为字符串偏移.
可以看到以上.o文件中的符号的名字就是'__imp__USER32$MessageBoxA', 其实这里也可以猜出来Beacon就是在符号表里遍历这些符号, 当发现LIBRARY$Function格式的名字的时候, 就动态加载LIBRARY并填充真正的Function的地址, Beacon充当了链接器和PE加载器
根据以上微软的官方文档, 可以写出COFF文件的格式定义:
typedef struct COFF_FILE_HEADER {
uint16_t Machine;
uint16_t NumberOfSections;
uint32_t TimeDateStamp;
uint32_t PointerToSymbolTable;
uint32_t NumberOfSymbols;
uint16_t SizeOfOptionalHeader;
uint16_t Characteristics;
} COFF_FILE_HEADER_T;
typedef struct COFF_SYMBOL {
union {
char ShortName[8]; //An array of 8 bytes. This array is padded with nulls on the right if the name is less than 8 bytes long.
struct {
uint32_t Zeroes; //A field that is set to all zeros if the name is longer than 8 bytes.
uint32_t Offset; //An offset into the string table.
};
}Name;
uint32_t Value;
uint16_t SectionNumber;
uint16_t Type;
uint8_t StorageClass;
uint8_t NumberOfAuxSymbols;
} COFF_SYMBOL_T;
//等等...
0x02 自实现加载器
显然要想运行一个.o中间文件, 至少需要实现重定位+解析符号表
首先需要知道的是, SectionHeader->PointerToRelocations指向了重定位表, COFF重定位表的定义如下:
offset | size | 字段 | 说明 |
---|---|---|---|
0 | 4 | VirtualAddress | 节区开头地址+此值 |
4 | 4 | SymbolTableIndex | 对应的符号表的index |
8 | 2 | Type | 类型 |
比如.text段的PointerToRelocations指向了0x138处, 则重定位表的第一项就是从0x138开始:
查看0x138处:
比如Reloc[2]的VirtualAddress是0x26, 那么.text地址+0x26就是需要重定位修改的操作数, SymbolTableIndex指明了该重定位项对应的符号表
下面简单实现一个(使用C++20标准 MSVC编译):
-
main.h文件:
#pragma once
#include <Windows.h>
#include <cstdint>
#pragma pack(push, 1) //取消内存对齐
typedef struct COFF_FILE_HEADER {
uint16_t Machine;
uint16_t NumberOfSections;
uint32_t TimeDateStamp;
uint32_t PointerToSymbolTable;
uint32_t NumberOfSymbols;
uint16_t SizeOfOptionalHeader;
uint16_t Characteristics;
} COFF_FILE_HEADER_T;
typedef struct COFF_SYMBOL {
union {
char ShortName[8]; //An array of 8 bytes. This array is padded with nulls on the right if the name is less than 8 bytes long.
struct {
uint32_t Zeroes; //A field that is set to all zeros if the name is longer than 8 bytes.
uint32_t Offset; //An offset into the string table.
};
}Name;
uint32_t Value;
uint16_t SectionNumber;
uint16_t Type;
uint8_t StorageClass;
uint8_t NumberOfAuxSymbols;
} COFF_SYMBOL_T;
typedef struct COFF_STRING_TABLE {
uint32_t Size;
const char* String[0];
}COFF_STRING_TABLE_T;
typedef struct COFF_RELOCATION {
uint32_t VirtualAddress; //The address of the item to which relocation is applied. This is the offset from the beginning of the section, plus the value of the section's RVA/Offset field.
uint32_t SymbolTableIndex; //A zero-based index into the symbol table. This symbol gives the address that is to be used for the relocation.
uint16_t Type; //A value that indicates the kind of relocation that should be performed.
} COFF_RELOCATION_T;
typedef struct COFF_SECTION {
char Name[8];
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_SECTION_T;
#pragma pack(pop) // 恢复内存对齐
在main.h中定义了所有用到的COFF文件解析格式声明.
-
main.cpp文件:
#include <iostream>
#include <map>
#include <string>
#include <fstream>
#include <format>
#include <filesystem>
#include "main.h"
namespace fs = std::filesystem;
void SelfPrint(const char* str)
{
puts(str);
}
//全局变量 用于存放(内部)函数的地址
static std::map<std::string, LPVOID> g_func_map;
int main(int argc, const char* argv[])
{
if (argc != 2)
{
std::cout << "Usage: " << argv[0] << " *.obj" << std::endl;
return 0;
}
// 检查文件是否存在
fs::path obj_path = argv[1];
if (!fs::exists(obj_path)) {
std::cerr << "[!] File not exist: " << obj_path << std::endl;
return 1;
}
//初始化函数map
g_func_map["SelfPrint"] = (LPVOID)SelfPrint;
//...其他内部函数...
try {
// 打开文件
std::ifstream file_stream(obj_path, std::ios::in | std::ios::binary);
// 检查文件是否成功打开
if (!file_stream.is_open()) {
std::cerr << "[!] Can't open file: " << obj_path << std::endl;
return 1;
}
// 将文件内容读入字符串
std::string file_contents;
file_stream.seekg(0, std::ios::end); // 定位到文件末尾
file_contents.reserve(file_stream.tellg()); // 分配足够的空间
file_stream.seekg(0, std::ios::beg); // 定位到文件开头
file_contents.assign((std::istreambuf_iterator<char>(file_stream)),
std::istreambuf_iterator<char>());
// 关闭文件
file_stream.close();
const char* file_data = file_contents.data();
COFF_FILE_HEADER_T* p_coff_file_header = (COFF_FILE_HEADER_T*)file_data;// File Header
std::cout << std::format("[*] COFF->Machine:tt0x{:08X}n", p_coff_file_header->Machine);
std::cout << std::format("[*] COFF->PointerToSymbolTable:t0x{:08X}n", p_coff_file_header->PointerToSymbolTable);
std::cout << std::format("[*] COFF->NumberOfSymbols:t0x{:08X}n", p_coff_file_header->NumberOfSymbols);
COFF_SECTION_T* p_coff_section_header = (COFF_SECTION_T*)(file_data + sizeof(COFF_FILE_HEADER_T));//section header
COFF_SYMBOL_T* p_coff_symbol = (COFF_SYMBOL_T*)(file_data + p_coff_file_header->PointerToSymbolTable); //符号表
COFF_STRING_TABLE_T* p_coff_string = (COFF_STRING_TABLE_T*)(file_data + p_coff_file_header->PointerToSymbolTable + (p_coff_file_header->NumberOfSymbols) * sizeof(COFF_SYMBOL_T)); //字符串表
for (size_t i = 0; i < p_coff_file_header->NumberOfSymbols; i++)
{
COFF_SYMBOL_T* p_cur_symbol = (p_coff_symbol + i);
if ((p_cur_symbol->Name.Zeroes == 0) && (p_cur_symbol->Name.Offset > 3))
std::cout << std::format("[{:02}] Symbol->Name:t{}n", i, (char*)((char*)p_coff_string + p_cur_symbol->Name.Offset));
else
std::cout << std::format("[{:02}] Symbol->Name:t{}n", i, p_cur_symbol->Name.ShortName);
}
//简单处理 只加载.text和.rdata段
COFF_SECTION_T* p_coff_text_header = NULL, *p_coff_rdata_header = NULL;
for (size_t i = 0; i < p_coff_file_header->NumberOfSections; i++)
{
COFF_SECTION_T* p_coff_cur_header = p_coff_section_header + i;
std::string tmp_ = p_coff_cur_header->Name;
if (tmp_ == ".text") p_coff_text_header = p_coff_cur_header;
if (tmp_ == ".rdata") p_coff_rdata_header = p_coff_cur_header;
if ((p_coff_text_header != NULL) && (p_coff_rdata_header != NULL)) break;
}
if (p_coff_text_header == NULL)
{
std::cerr << "[!] Can't find .text section!" << std::endl;
return 1;
}
//申请一段可读可写可执行的内存
int mem_size = p_coff_text_header->SizeOfRawData + p_coff_rdata_header->SizeOfRawData;
LPVOID mem = VirtualAlloc(NULL, mem_size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
//写入数据到内存 先写.rdata再写.text
LPBYTE lp_rdata_in_file = (LPBYTE)(p_coff_rdata_header->PointerToRawData + file_data), lp_text_in_file = (LPBYTE)(p_coff_text_header->PointerToRawData + file_data);
LPBYTE lp_rdata_addr = (LPBYTE)mem, lp_text_addr = lp_rdata_addr + p_coff_rdata_header->SizeOfRawData;
std::memcpy(lp_rdata_addr, lp_rdata_in_file, p_coff_rdata_header->SizeOfRawData);
std::memcpy(lp_text_addr, lp_text_in_file, p_coff_text_header->SizeOfRawData);
//处理重定位
COFF_RELOCATION_T* p_coff_relocation = (COFF_RELOCATION_T*)(file_data + p_coff_text_header->PointerToRelocations);
for (size_t i = 0; i < p_coff_text_header->NumberOfRelocations; i++)
{
COFF_RELOCATION_T* p_cur_relocation = p_coff_relocation + i; //第i项重定位表
COFF_SYMBOL_T* p_cur_symbol = p_coff_symbol + p_cur_relocation->SymbolTableIndex; //第i项重定位表对应的符号表
char* cur_name = NULL; //获取当前对应的符号表项的名字
if (p_cur_symbol->Name.Zeroes == 0)
cur_name = (char*)((char*)p_coff_string + p_cur_symbol->Name.Offset);
else
cur_name = p_cur_symbol->Name.ShortName;
std::string tmp_name = cur_name;
//获取要写入重定位后数据的地址
LPBYTE lp_rel_addr = p_cur_relocation->VirtualAddress + lp_text_addr;
//以下其实还应该根据COFF_RELOCATION->Type来处理不同类型的重定位行为, 但简单处理了
//如果对应的符号项在.rdata 简单处理 直接加上地址
if (tmp_name == ".rdata")
{
uint32_t cur_operand = *((uint32_t*)lp_rel_addr);
uint32_t operand_rel = (uint32_t)(cur_operand + lp_rdata_addr);
*((uint32_t*)lp_rel_addr) = operand_rel;//修改数据
continue;
}
//判断是否是'__imp__LIB$FUNC', 如果是则加载DLL并填充地址
std::string prefix = "__imp__";//如果以__imp__打头才继续解析
if (tmp_name.compare(0, prefix.length(), prefix) == 0)
{
std::string library, function;
std::string remaining = tmp_name.substr(prefix.length());
size_t pos = remaining.find("$");
if (pos != std::string::npos) {
library = remaining.substr(0, pos);
function = remaining.substr(pos + 1);
}
if (!library.empty())
{
std::string dll_name = library + ".dll";
HMODULE hDll = LoadLibraryA(dll_name.c_str());
if (hDll == NULL)
{
std::cerr << std::format("[!] Load {} Error: [{}]n", dll_name, GetLastError());
VirtualFree(mem, 0, MEM_RELEASE);
return 1;
}
FARPROC func_addr = GetProcAddress(hDll, function.c_str());
if (func_addr == NULL)
{
std::cerr << std::format("[!] GetFuncAddr {} Error: [{}]n", function, GetLastError());
VirtualFree(mem, 0, MEM_RELEASE);
return 1;
}
g_func_map[function.c_str()] = func_addr;
//将真实地址填充到操作数处
*((DWORD_PTR*)lp_rel_addr) = (DWORD_PTR)(&(g_func_map[function.c_str()]));//修改数据
continue;
}
}
//如果是_SelfPrint之类的内部函数 解析...
//...
}
// 找到self_go的地址, 并执行
typedef void(*FunctionPtr)();
bool find_go_entry = false;
LPBYTE go_addr = lp_text_addr;
for (size_t i = 0; i < p_coff_file_header->NumberOfSymbols; i++)
{
COFF_SYMBOL_T* p_cur_symbol = (p_coff_symbol + i);
char* cur_name = NULL;
if (p_cur_symbol->Name.Zeroes == 0)
cur_name = (char*)((char*)p_coff_string + p_cur_symbol->Name.Offset);
else
cur_name = p_cur_symbol->Name.ShortName;
std::string tmp_name = cur_name;
if (tmp_name == "_self_go")
{
go_addr += p_cur_symbol->Value;
find_go_entry = true;
break;
}
}
if (find_go_entry)
{
FunctionPtr go_func = reinterpret_cast<FunctionPtr>(go_addr);
go_func();
std::cout << "[+] go run over!" << std::endl;
}
else
{
std::cerr << "[!] Can't find entry to run!" << std::endl;
VirtualFree(mem, 0, MEM_RELEASE);
return 1;
}
// 释放内存
VirtualFree(mem, 0, MEM_RELEASE);
}
catch (const std::exception& e) {
std::cerr << "[!] err: " << e.what() << std::endl;
return 1;
}
return 0;
}
具体逻辑就是:
-
从文件中读取COFF数据
-
将.text和其他需要的段的数据写入到可读可写可执行的内存中
-
遍历重定位表, 根据相应的类型, 进行处理, 比如如果符号表名字是'__imp__USER32$MessageBoxA', 则加载usr32.dll并获取MessageBoxA的函数地址, 再把函数地址写到需要重定位的地址处
-
找到self_go地址, call它就行了
当然很多细节没有处理, 只是简单验证而已.
然后测试一下:
先写个bof.c, 这里我们规定入口名是'self_go', 因为上面编写的加载器找的就是self_go这个符号去执行
#include <windows.h>
#include <stdio.h>
DECLSPEC_IMPORT int USER32$MessageBoxA(HWND, LPCSTR,LPCSTR,UINT);
void self_go() {
DWORD dwRet;
dwRet = USER32$MessageBoxA(NULL, "BOF TEST", "BOF", MB_OK);
}
然后 gcc -c bof.c -o bof.o -m32 (windows下可以使用tdm-gcc, 使用MSVC也是一样的)生成COFF文件
执行:
可以看到操作数已经被修改为了我们把.rdata段数据放入到的内存中的地址, 且WIN API地址也被修改, 然后
完美运行。
原文始发于微信公众号(山石网科安全技术研究院):Cobalt Strike BOF原理与自实现
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论