Cobalt Strike BOF原理分析
2022-10-25 11:33:3 Author: 跳跳糖社区(查看原文) 阅读量:14 收藏

点击 / 关注我们

0x01 Beacon Object File

BOF(Beacon 对象文件)是C/C++编译,但未链接产生的Obj文件,BOF运行在Beacon进程中,并执行内部的Beacon API和Win32 API函数。BOF本质是COFF Obj文件,其符合COFF文件格式规范,结构类似于windows PE文件格式。在被Cobalt Strike加载和使用过程中,BOF是一段地址无关的Shellcode,BOF本身体积比较小,在传输过程中,适用于那些传输带宽小的模式,然后其本身运行在beacon进程内部,不会重新创建进程,也可以有效规避EDR。

0x02 如何开发BOF

下面是官方提供的一个demo

#include <windows.h>
#include "beacon.h"
void go(char * args, int alen) {
    BeaconPrintf(CALLBACK_OUTPUT, "Hello World: %s", args);
}

可以使用Visual Studio或者MinGW进行编译,最后生成.obj文件。

cl.exe //GS- hello./Fo hello.obj
i686-w64-mingw32-gcc -c hello.-o hello.o
x86_64-w64-mingw32-gcc -c hello.-o hello.o

在cl.exe生成obj文件的时候,可能遇到fatal error C1034: stdio.h: 不包括路径集问题,产生这个的原因是没有设置对应的INCLUDE和LIB环境变量。而且不能仅仅设置Vs的Include的路径,还要设置SDK的路径。具体如下:https://blog.csdn.net/weixin_41115751/article/details/89817123

在生成.obj之后,使用inline-execute + obj_path 执行obj文件

BOF内部自带4种API,数据解析API,主要解析Aggressor Script 使用bof_pack函数打包的参数。打印输出API,主要起到打印输出的作用。格式化API,以及内部API。内部API主要包含一些令牌句柄的使用,以及进程注入相关的API。具体细节可以参考官方的Bof文档

数据解析API主要包含: * char * BeaconDataExtract (datap * parser, int * size) * int BeaconDataInt (datap * parser) * int BeaconDataLength (datap * parser) * void BeaconDataParse (datap * parser, char * buffer, int size) * short BeaconDataShort (datap * parser)

在beacon.h中可以看到这些API原型。

/* Token Functions */
DECLSPEC_IMPORT BOOL   BeaconUseToken(HANDLE token);
DECLSPEC_IMPORT void   BeaconRevertToken();
DECLSPEC_IMPORT BOOL   BeaconIsAdmin();

/* Spawn+Inject Functions */
DECLSPEC_IMPORT void   BeaconGetSpawnTo(BOOL x86, char * buffer, int length);
DECLSPEC_IMPORT void   BeaconInjectProcess(HANDLE hProc, int pid, char * payload, int p_len, int p_offset, char * arg, int a_len);
DECLSPEC_IMPORT void   BeaconInjectTemporaryProcess(PROCESS_INFORMATION * pInfo, char * payload, int p_len, int p_offset, char * arg, int a_len);
DECLSPEC_IMPORT BOOL   BeaconSpawnTemporaryProcess(BOOL x86, BOOL ignoreToken, STARTUPINFO * si, PROCESS_INFORMATION * pInfo);
DECLSPEC_IMPORT void   BeaconCleanupProcess(PROCESS_INFORMATION * pInfo);

/* Utility Functions */
DECLSPEC_IMPORT BOOL   toWideChar(char * src, wchar_t * dst, int max);

0x03 动态函数解析(DFR)

动态函数解析,即Dynamic Function Resolution (DFR)

以下demo的功能是查找当前域,需要使用两个API函数DsGetDcNameA,NetApiBufferFree都是由NETAPI32模块进行导出。

  • • DECLSPEC_IMPORT:导入函数的关键字

  • • WINAPI:函数调用约定,一般API函数都是这个

  • • NETAPI32:函数所在的模块名

  • • DsGetDcNameA/NetApiBufferFree:函数名称

#include <windows.h> 
#include <stdio.h> 
#include <dsgetdc.h> 
#include "beacon.h" 
DECLSPEC_IMPORT DWORD WINAPI NETAPI32$DsGetDcNameA(LPVOID, LPVOID, LPVOID, LPVOID, ULONG, LPVOID); 
DECLSPEC_IMPORT DWORD WINAPI NETAPI32$NetApiBufferFree(LPVOID); 
void go(char * args, int alen) { 
    DWORD dwRet; 
    PDOMAIN_CONTROLLER_INFO pdcInfo
    dwRet = NETAPI32$DsGetDcNameA(NULL, NULL, NULL, NULL, 0, &pdcInfo); 
    if (ERROR_SUCCESS == dwRet) { 
        BeaconPrintf(CALLBACK_OUTPUT, "%s", pdcInfo->DomainName); 
    } 
    NETAPI32$NetApiBufferFree(pdcInfo); 
}

可以使用bof_help这个工具自动修改符合BOF格式的函数原型。但目前来说可能不是很好用了。参考自https://idiotc4t.com/weaponization/bof-weaponization

0x04 Obj文件解析

OBj文件的文件类型是COFF Object,使用dumpbin /all obj_path解析Obj文件格式。Obj文件首先是_IMAGE_FILE_HEADER,保存着整个文件基本信息,然后依次保存着每个节区的SECTION HEADER和节区内容。然后接着就是重定位表,符号表。

文件头格式如下:

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;
    WORD    NumberOfSections;
    DWORD   TimeDateStamp;
    DWORD   PointerToSymbolTable;
    DWORD   NumberOfSymbols;
    WORD    SizeOfOptionalHeader;
    WORD    Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
  • • Machine为0x14c,表示这是一个x86的Obj

  • • NumberOfSections为4,说明有4个Section

  • • TimeDateStamp是时间戳

  • • PointerToSymbolTable;指向符号表

  • • NumberOfSymbols:符号个数

FILE HEADER VALUES
             14C machine (x86)
               4 number of sections
        63*E60*D time date stamp Thu ***  6 13:*:29 20**
             1E6 file pointer to symbol table
               D number of symbols
               0 size of optional header
               0 characteristics

节区头的结构体如下:

typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];
    union {
            DWORD   PhysicalAddress;
            DWORD   VirtualSize;
    } Misc;
    DWORD   VirtualAddress;
    DWORD   SizeOfRawData;
    DWORD   PointerToRawData;
    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
  • • Name:表示节区的名字

  • • SizeOfRawData:表示节区数据的大小

  • • PointerToRawData:表示节区数据的偏移或者指针。

    mark

text段

mark

重定位表

mark

0x05 服务端BOF实现原理

首先是如何定位入口点,如果熟悉Cobalt Strike伪源码的,应该知道Cobalt Strike的命令分发执行位于BeaconConsole.java的public void actionPerformed(ActionEvent var1)函数,如果不熟悉Cobalt Strike伪源码呢,使用notepad++的文件夹搜索功能,搜索inline-execute也可以定位到public void actionPerformed(ActionEvent var1)函数。

mark

显然,可以看见,当执行inline-execute命令时,首先会将命令中的objectfile的路径解析出来,然后作为参数传入InlineExecuteObject函数。

mark
InlineExecuteObject函数中,首先,调用DataUtils.getBeacon获取Beacon的各种信息,这里使用到的是CPU架构。然后传入this.InlineExecuteObject函数中。最终调用go()这个函数。
mark
在go这个函数中,依次获取架构,是x64还是x86,并判断Obj的架构和beacon的架构是否一致,一致才可以继续
mark
然后读取Object文件,分别解析Code段,RData段,Data段,和Relocations段,复制这些段的数据,复制原理如下,首先,通过解析Header中的数据,可以获取各个段的起始地址和大小,这样就可以获取各个段的范围,然后就可以获取指定段的内容。
mark
mark
getRelocations()函数中,会根据不同的段,插入不同的Magic Number。例如,如果是.rdata,则会插入1024,如果是.data,则会插入1025,如果是.text,则会插入1026,如果是DynamicFunction,则会插入1027,最后以插入1028结尾。
mark
同时,可以看到插入数据的结构,首先是插入的Type(类型),然后插入一个Magic Number,第三是插入偏移,最后插入在段中的偏移。有个例外,针对DynamicFunction这块的处理可能需要插入其他的数据。
mark
接着是构造命令,依次添加命令号,添加obj的入口点,添加code,添加Rdata,添加data,添加Relocations,和Arguments,这个Arguments没理解是什么东西。
mark
mark

0x06 beacon端调用原理分析

beacon分为loader和payload,loader可以自行开发,payload采用反射注入的方式进行加载,默认情况下,导出表有两个函数,一个是ReflectiveLoader另外一个是DllEntryPoint。在执行payload的时候,优先执行ReflectiveLoader,在处理完PE数据后,跳转到DllEntryPoint函数,然后根据dll加载的原因选择进行数据的初始化,还是进行工作。

mark

在4.1的Cobalt Strike生成的beacon中,大概在这个地方(Sub_336560_CommandDisPatch)进行命令操作。

mark
这是服务端传来的原始数据,显然,前四个字节正好是100,为命令号,和cobaltstrike发送命令数据的结构一致。
mark
mark
在函数Sub_336560_CommandDisPatch中,显然可以看到,首先解析出命令号,然后将除了命令号以外的数据作为第二个参数传入,将结果作为第三个参数传入,用以获取执行的结果。
mark
根据命令号,选择不同需要执行的函数,此处将该函数命名为Sub_32D020_inline_execute,在Sub_32D020_inline_execute中,首先依次解析Code段,RData段,Data段,Relocations段,和Arguments。
mark
随后,便开始解析Relocations段,cobaltstrike通过不同的硬编码数据将不同的数据类型进行分割,0x400表示.rdata段,0x401表示.data段,0x402表示.text段,0x403表示DynamicFunction,0x404则表示结束。
mark
 Sub_32D4F4_WriteOffset函数的目的是修改代码段中的一些常量或者DynamicFunction的地址,因为在汇编层级,这些地址都是偏移量,所以需要计算偏移量并写入代码中,才能实现调用。
mark
mark
最后执行shellcode
mark
以下是传入的数据。显然,第一行是命令号和EntryPoint,然后下面是code段和rdata段,再下面是Relocations段。
mark
mark
然后在对比一下原始的code段,和需要执行的shellcode的区别,很显然,关于常量的偏移地址是不同的。也就是说此处做了重定位。
mark

0x07 检测思路

常规的工具(BeaconEye)可能没有什么好的检测思路,我之前设想过,通过beaconEye有没有可能检测BOF,但是后来仔细想了一下发现不行,因为BeaconEye通过检测内存中的特征码实现的,但是BOF在调用执行完shellcode的时候就被释放了,可能无法检测。

从流量角度看,传入的流量数据,起始的命令号(100)以及getRelocations中的Magic Number是否可以作为检测依据?

0x08 execuate-assembly

在CobaltStrike3中,新增了名为execuate-assembly命令,该命令本质是实现了在内存中加载.Net程序集。

执行execute-assembly命令之后,判断是否存在参数,如果存在参数,得到CSharp程序路径和参数,分别传入ExecuteAssembly,如果不存在参数,只需要传入CSharp程序路径。

mark

ExecuteAssembly函数中,首先读取Charp程序,并判断其是否是一个.NET程序,然后根据Beacon判断是否是64位系统,如果是X64的话,则会加载X64的装载程序初始化CRL环境并加载.NET程序

mark
读取resources/invokeassembly.dll文件,该程序的作用是初始化CLR以及加载.Net程序集,然后将invokeassembly.dll,CSharp程序,以及一些配置信息一起发送给beacon。
mark
beacon.exe,第70号命令即为execte-assembly,其逻辑也很简单,在解析玩配置信息之后,拉起一个Rundll32进程,然后将invokeassembly.dll注入进去,invokeassembly.dll就会在rundll32中初始化环境并加载程序集了。
mark

转储了invokeassembly.dll,拖到IDA中,发现其和beacon一样采用了反射注入的方式。直接定位到关键函数。

__int64 __fastcall sub_180001470(__int64 a1, const void *a2, unsigned int a3)
{
[.....]
  v6 = GetStdHandle(0xFFFFFFF5);
  SetStdHandle(0xFFFFFFF4, v6);
  if ( !sub_180001294(&v16) )
  {
    result = Sub_1800022B8_Output("[-] Failed to create the runtime host\n", v7);
    goto LABEL_27;
  }
  v9 = (*(*v16 + 80i64))(v16);
  if ( v9 < 0 )
  {
    v10 = "[-] CLR failed to start w/hr 0x%08lx\n";
LABEL_5:
    result = Sub_1800022B8_Output(v10, v9);
    goto LABEL_27;
  }
  if ( v18 )
    ((*v18)[2])(v18);
  v18 = 0i64;
  v9 = (*(*v16 + 104i64))(v16, &v18);
  if ( v9 < 0 )
  {
    v10 = "[-] ICorRuntimeHost::GetDefaultDomain failed w/hr 0x%08lx\n";
    goto LABEL_5;
  }
  v11 = v18;
  if ( !v18 )
  {
    sub_180001DA0(0x80004003i64);
    __debugbreak();
  }
  if ( v22 )
    (*(*v22 + 16i64))(v22);
  v22 = 0i64;
  v12 = *v11;
  v13 = sub_180001000(&v22);
  v9 = (*v12)(v11, &unk_180010510, v13);
  if ( v9 < 0 )
  {
    v10 = "[-] Failed to get default AppDomain w/hr 0x%08lx\n";
    goto LABEL_5;
  }
  rgsabound.cElements = v3;
  rgsabound.lLbound = 0;
  v14 = SafeArrayCreate(0x11u, 1u, &rgsabound);
  SafeArrayLock(v14);
  memmove(v14->pvData, a2, v3);
  SafeArrayUnlock(v14);
[.....]
  if ( v17 )
    (*(*v17 + 16i64))(v17);
  v17 = 0i64;
  v9 = (*(*v15 + 360i64))(v15, v14, &v17);
  if ( v9 < 0 )
  {
    v10 = "[-] Failed to load the assembly w/hr 0x%08lx\n";
    goto LABEL_5;
  }
  v20 = v17;
  if ( v17 )
    (*(*v17 + 8i64))(v17);
[.....]
  if ( v17 )
    result = (*(*v17 + 16i64))(v17);
  if ( v22 )
    result = (*(*v22 + 16i64))(v22);
  if ( v18 )
    result = ((*v18)[2])(v18);
  return result;
}

首先初始化CLR环境,根据不同的版本采用不同的函数初始化CLR环境,

mark

启动CLR环境

mark
获取默认的程序域
mark
加载assembly
mark
获取入口点,并执行。
mark
idiotc4t在Execute-Assembly实现中仔细描述了如何编写一段内存加载.Net程序集。首先初始化CLR环境,CLR全称为公共语言运行库,即Common Language Runtime。CLR托管在进程中,是加载和运行.Net程序集的地方,关于CLR的概述可以参考微软关于CLR的描述。常见的windows进程并不会加载CLR环境,可以使用ProcessExplorer或者ProcessHacker等工具查看是否加载CLR环境。加载CLR环境主要分四步:
mark

  • • \1) 调用CLRCreateInstance函数以实例化ICLRMetaHostICLRMetaHostPolicy接口,CLRCreateInstance函数原型如下,第一个参数为clsid,第二个参数是 riid,第三个参数是返回的接口。

HRESULT CLRCreateInstance(  
    [in]  REFCLSID  clsid,  
    [in]  REFIID     riid,  
    [out] LPVOID  * ppInterface  
);  
  • • \2) 调用ICLRMetaHost::EnumerateInstalledRuntimesICLRMetaHost::GetRuntime或者ICLRMetaHostPolicy::GetRequestedRuntime方法以获取有效的ICLRRuntimeInfo指针。以ICLRMetaHost::GetRuntime为例,第一个参数为pwzVersion,表示 .NET Framework 的版本,riid为标识符,此参数的唯一有效值是 IID_ICLRRuntimeInfo。第三个参数是返回的ICLRRuntimeInfo接口的指针。

HRESULT GetRuntime (  
    [in] LPCWSTR pwzVersion,  
    [in] REFIID riid,  
    [out,iid_is(riid), retval] LPVOID *ppRuntime  
);  
  • • \3) 调用GetInterface获取ICorRuntimeHost或者ICLRRuntimeHost。rclsid为接口的CLSID,riid是接口的iid,ppUnk返回的接口的指针。加载.Net程序集可以使用两种接口ICorRuntimeHost或者ICLRRuntimeHost。使用ICorRuntimeHost的有点是可以兼容V1.0的程序集,但是ICLRRuntimeHost在代码实现上会比较容易。

HRESULT GetInterface(  
[in]  REFCLSID rclsid,  
[in]  REFIID   riid,  
[out, iid_is(riid), retval] LPVOID *ppUnk);  
  • • 4)启动CLR环境集

CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (VOID**)&iMetaHost);
iMetaHost->GetRuntime(L"v4.0.30319", IID_ICLRRuntimeInfo, (VOID**)&iRuntimeInfo);
iRuntimeInfo->GetInterface(CLSID_CorRuntimeHost, IID_ICorRuntimeHost, (VOID**)&iRuntimeHost);
iRuntimeHost->Start();

程序域为安全性、可靠性和版本控制以及卸载程序集提供了隔离边界,需要将程序集加载到对应的程序域中,才能执行其中包含的代码,这也就是为啥要获取程序集的原因了。程序集的加载方式决定了它的即时 (JIT) 编译代码是否可以由进程中的多个应用程序域共享,以及程序集是否可以从进程中卸载。具体关于程序域和程序集可以参考微软关于程序域和程序集的概述,通过调用GetDefaultDomain获取默认的程序集,并通过调用QueryInterface检索该程序集的接口。

iRuntimeHost->GetDefaultDomain(&pAppDomain);
pAppDomain->QueryInterface(__uuidof(_AppDomain), (VOID**)&pDefaultAppDomain); 

在拥有运行时环境CLR,已经可以被托管的容器程序域之后,可以加载程序集了。调用Load_3函数加载程序集安全数组,并获取入口点。

HRESULT Load_3 (
    SAFEARRAY* rawAssembly,
    Assembly **pRetVal )

pDefaultAppDomain->Load_3(pSafeArray, &pAssembly);
pAssembly->get_EntryPoint(&pMethodInfo);

最后,调用Invoke_3执行程序集的入口点。

HRESULT hr = pMethodInfo->Invoke_3(vObj, args, &vRet);

0x09 参考

  • • [官方文档-Beacon Object Files]https://hstechdocs.helpsystems.com/manuals/cobaltstrike/current/userguide/content/topics/beacon-object-files_main.htm

  • • [A DEVELOPER’S INTRODUCTION TO BEACON OBJECT FILES]https://www.trustedsec.com/blog/a-developers-introduction-to-beacon-object-files/

  • • [CobaltStirke BOF技术剖析(一)|BOF实现源码级分析]https://www.freebuf.com/articles/network/282744.html

  • • https://idiotc4t.com/weaponization/bof-weaponization

  • • [Execute-Assembly实现]https://idiotc4t.com/defense-evasion/cobaltstrike-executeassembly-realization

  • • [从内存加载.NET程序集的利用分析]https://3gstudent.github.io/从内存加载.NET程序集(execute-assembly)的利用分析

  • • [微软关于CLR的描述]https://learn.microsoft.com/en-us/dotnet/standard/clr

  • • [微软关于程序域和程序集的概述]https://learn.microsoft.com/en-us/dotnet/framework/app-domains/application-domains

推荐阅读:
Edge浏览器-通过XSS获取高权限从而RCE
The End of AFR?
java免杀合集
ATT&CK中的攻与防——T1059
若依(RuoYi)管理系统后台sql注入漏洞分析

跳跳糖是一个安全社区,旨在为安全人员提供一个能让思维跳跃起来的交流平台。

跳跳糖持续向广大安全从业者征集高质量技术文章,可以是漏洞分析,事件分析,渗透技巧,安全工具等等。
通过审核且发布将予以500RMB-1000RMB不等的奖励,具体文章要求可以查看“投稿须知”。
阅读更多原创技术文章,戳“阅读全文

文章来源: http://mp.weixin.qq.com/s?__biz=MzkxNDMxMTQyMg==&mid=2247495725&idx=1&sn=8d5a192eaea6bed710fc9609ef0634cf&chksm=c172e1bcf60568aa595e11f2591ef2ca8e26a6361fbf2844130d16b9d116d511eb1b1266e4ee#rd
如有侵权请联系:admin#unsafe.sh