​Cobalt Strike BOF原理与自实现

BOF功能增加了Cobalt Strike快速扩展的能力

0x00 官方说明

根据官方说明:

对于 Beacon 来说,BOF 只是一个与位置无关的代码块,它接收指向一些 Beacon 内部 API 的指针。
对 Cobalt Strike 而言,BOF 是由 C 编译器生成的对象文件。Cobalt Strike 会解析该文件,并充当其内容的链接器和加载器。这种方法允许你编写与位置无关的代码,以便在 Beacon 中使用,而无需繁琐地管理字符串和动态调用 Win32 API。

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个字节为字符串偏移.

​Cobalt Strike BOF原理与自实现

可以看到以上.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开始:

​Cobalt Strike BOF原理与自实现

查看0x138处:

​Cobalt Strike BOF原理与自实现

比如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;
}

具体逻辑就是:

  1. 从文件中读取COFF数据

  2. 将.text和其他需要的段的数据写入到可读可写可执行的内存中

  3. 遍历重定位表, 根据相应的类型, 进行处理, 比如如果符号表名字是'__imp__USER32$MessageBoxA', 则加载usr32.dll并获取MessageBoxA的函数地址, 再把函数地址写到需要重定位的地址处

  4. 找到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文件

执行:

​Cobalt Strike BOF原理与自实现

可以看到操作数已经被修改为了我们把.rdata段数据放入到的内存中的地址, 且WIN API地址也被修改, 然后

​Cobalt Strike BOF原理与自实现

完美运行。

原文始发于微信公众号(山石网科安全技术研究院):​Cobalt Strike BOF原理与自实现

版权声明:admin 发表于 2024年5月13日 下午2:08。
转载请注明:​Cobalt Strike BOF原理与自实现 | CTF导航

相关文章