1.前言
Brute Ratel C4 1.2.2版本被破解并公开泄露到互联网上,Brute Ratel C4的开发者NinjaParanoid在Twitter上发文称是MdSec将Brc4上传到VT(VirusTotal)然后被俄罗斯的Molecules组织破解导致Brc4 1.2.2 Scandinavian Defense在互联网上流传,并且Brc4 1.2.5泄露版在少部分人中流传。

Brute Ratel C4的开发者NinjaParanoid在discord群组中追踪了Brc4泄露的经过。

MdSec是一家来自于英国的安全公司,主要业务是渗透测试对手模拟,MdSec开发了Nighthawk C2用于售卖,毫无疑问MdSec的Nighthawk和Brute Ratel C4属于竞品关系。

可以看到介绍新的Nighthawk Licensing费用为每位用户每年为7500英镑或者10000美元,而且必须至少购买3个用户许可证。

2.样本IOCs
名称: bruteratel-1.2.2-pwn3rzs-cyberarsenal.7z大小: 82818265 字节 (78.9 MiB)MD5:756bf6d0e21d9e8247a08352cd38dffdSHA1: 10ae61b605a51a71dc87abb9c28d1e2567d9b32eSHA256: d5b0c42ef9642dce715b252a07fc07ad9917bfdc13bd699d517b78210cc6ec60
3.恶意代码分析
泄露的Brc4 1.2.2版本目录结构如下:

bruteratel团队服务器端有x64和arm64两个版本,我们使用file命令查看服务端通过Go BuildId可以判断是使用golang编写

commander-runme是客户端图形化界面,commander-runme是指向/lib64/commander的快捷方式。

commander是使用Qt编写的图形界面客户端。

启动团队服务端-a指定用户名,-p指定密码,-h指定服务端口,-sc指定cert.pem证书,-sk指定key.pem私钥

之后我们启动客户端连接,Brc4只有HTTP Listener和DOH Listener两种Listener

这里我们创建一个HTTP Listener,这里我本地测试直接使用了ip地址,端口和User-Agent使用默认的配置,添加了3个URl,Sleep Obfuscation使用了默认的APC方式,睡眠时间默认,Common Auth认证密码我这里选择了随机生成,如果选择One Time Auth认证密码上线一次过后当前Auth认证密码将会被移除也就是只支持上线一次,如果勾选Die if C2 is inaccessible选项如果连接C2失败将会自动退出。

我们打开Payload Profiler选项Add Payload Profile,还可以添加TCP和SMB的Payload,这两种Payload并不可以直接单独上线和bruteratel服务器端通信而是需要通过已上线的badger进行转发到bruteratel服务器,使用这两种Payload场景一般在同一个域中进行内网横向移动时使用。

要使用TCP或者SMB的Payload需要域内有一个已经通过HTTP或者DOH上线的badger主机,然后使用pivot_tcp命令在上线的badger主机上创建tcp端口监听配合TCP的Payload连接,使用pivot_smb命令通过命名管道连接到SMB的Payload,在TCP或者SMB的badger主机执行任何命令都会发送至已上线的HTTP或者DOH的badger主机转发到bruteratel团队服务器,我们可以看到b-21是SMB的badger而b-22是TCP的badger,这2种badger都是通过b-20的HTTP badger进行转发的。

接下来我们生成payload,这里我将首先分析x64架构的Default下的Shellcode RtlExitUserThread

3.1 badger_x64_rtl.bin
3.1.1 badger shellcode load
接下来我们使用如下的shellcode load代码加载shellcode进行分析
DWORD dwOldProtect = 0;
OVERLAPPED ol = { 0 };
HANDLE hFile = CreateFileW(L"shellcode.bin", GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
int fileSize = GetFileSize(hFile, NULL);
LPVOID lpShellCode = VirtualAlloc(NULL, fileSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
ReadFileEx(hFile, lpShellCode, fileSize, &ol, NULL);
CloseHandle(hFile);
VirtualProtect(lpShellCode, fileSize, PAGE_EXECUTE_READ, &dwOldProtect);
((void(*)())lpShellCode)();
WaitForSingleObject((HANDLE)-1, -1);
使用ida分析,这是shellcode起始的特征,在保存了寄存器环境后,通过大量的mov reg, imm,push reg组合在栈上初始化了大量数据。

首先在栈上初始化了0x12C字节的base64编码的Brc4配置文件,配置文件大小根据Listeners的配置而变化

接着初始化在栈上的0x39410字节的加密数据

接下来会将栈上的加密数据和base64配置文件数据都复制到申请的堆空间上

MemMoveAllocHeap函数的主要逻辑获取ntdll基址后通过ror13hash获取RtlAllocateHeap函数地址,申请堆空间将数据复制到堆空间。

函数GetNdllBaseAddress,获取_PEB_LDR_DATA结构地址后*力暴**搜索到MZSignature标志,然后对比如果AddressOfNewExeHeader-0x40小于0x3BF并且NtSignature标志为PE就找到ntdll.dll基址了。

通过ror13hash获取RtlAllocateHeap函数地址并申请堆空间

将0x39410字节的加密数据复制到堆空间

配置文件数据也同样复制到堆空间

接下来进入主函数

通过PEB+0xBC地址的NtGlobalFlang字段进行反调试,如果进程是由调试器创建的话将会设置(FLG_HEAP_ENABLE_TAIL_CHECK (0x10) | FLG_HEAP_ENABLE_FREE_CHECK (0x20) |FLG_HEAP_VALIDATE_PARAMETERS (0x40))标志也就是0x70,调式器附加并不会设置此标志。

获取到ntdll基址后通过ror13hash获取RtlFreeHeap函数地址,并且获取了ZwProtectVirtualMemory函数syscall id和syscall地址。

CheckinlineHookAndGetSyscallId函数检测会检测函数头部字节是否为0xCC(int3软件断点),还会检测函数第1个和第4个字节是否为0xE9(jmp)检测函数是否被inlineHook,如果当前函数检测到inlineHook还会继续检测上一个Ntxxxx函数。

如果检测通过会通过对比opcode特征获取syscall id,Windows的syscall id需要使用一个WORD类型(2字节)存储所以这里要将第5第6字节合为一个WORD类型(2字节)。

我们查看NT10 (Windows 10/11)的syscall id table,可以看到同一个函数在不同的系统版本号下syscall id可能会发生变化,并且不同函数的syscall id占用空间大小不同所以需要一个WORD(2字节)进行存储。

搜索Ntxxxx函数的opcode特征0x0F05(syscall)以及0xC3(ret)特征获取获取syscall地址

接着获取了LdrGetDllHandleEx和LdrGetProcedureAddress函数地址

GetDllAddrOrGetDllFullPathByHash函数根据参数3传入的标志判断获取dll基址还是获取UNICODE_STRING FullDllName字段返回

根据参数3来判断,如果参数3为0则返回dll基址,如果参数3为1则返回UNICODE_STRING FullDllName字段

接着调用LdrGetDllHandleEx函数加载Kernel32.dll并获取基址,然后获取ZwFlushInstructionCache函数地址syscall id和syscall地址

接下来会调用Rc4Decrypt函数使用rc4算法解密0x39410字节大小的加密数据

Rc4Decrypt函数首先通过ror13hash获取了Exit Method函数RtlExitUserThread,GetProcAddress、LoadLibraryA的函数地址

rc4的密钥在加密数据的尾部的8字节也就是*@$/}lu},此rc4密钥为随机生成

我们打开CyberChef使用rc4密钥*@$/}lu}解密从内存中dump下来的加密数据后发现为一个被抹掉MZSignature标志的的PE文件

将dump出来的PE文件使用Exeinfo查看是一个使用Mingw编译的x64 dll

我们查看x64dbg中解密后内存中的数据和我们使用CyberChef解密的相同

在rc4解密后PE文件尾部的8字节密钥被加密,此时尾部16字节的头8字节为被解密的Brc4配置文件的rc4密钥

我们使用rc4密钥f?zi\)*<解密Base64编码的Brc4的配置文件后,可以看到解密的Brc4配置文件内容

接下来调用ReflectiveDLLInjection函数通过反射式注入调用解密的badger core dll

将badger shellcode load的虚拟内存首地址0x4D0000内存权限改为PAGE_READWRITE

使用processhacker工具查看内存我们看到0x4D0000虚拟内存权限已经被改为RW权限了

将堆中的badger core dll前0x400字节复制到0x4D0000地址的RW权限内存中,这里开始复用了我们shellcode load申请的虚拟内存用于反射式注入badger core dll

将badger core dll所有section复制到RW内存

填充导入表到RW内存

修复重定位数据

将Base64编码的配置文件的rc4密钥复制到申请的堆空间

修改各个区段的内存权限

释放用于存放解密后的badger core dll的堆内存

调用ZwFlushInstructionCache刷新代码缓存

然后通过call rax调用badger core dll main

3.1.2 badger core dll
badger core dll的main函数处,调用FreeConsole函数隐藏控制台窗口,初始化了一些用于存储dll基址的全局变量

GetSomeDllFunAddress函数获取了ntdll.dll、kernel32.dll、kernelbase.dll、advapi32.dll、crypt32.dll、ws2_32.dll中需要用到的函数地址。

通过ror13hash获取ntdll.dll中的函数地址

GetWs2_32DllFunAddress函数中使用rc4密钥bYXJm/3#M?:XyMBF解密ws2_32.dll字符串通过GetDllBaseAddress函数获取dll基址并通过ror13hash获取函数地址。

GetDllBaseAddress函数会先调用GetDllAddrOrFullPathByHash函数获取dll基址,如果获取失败将会调用RtlRegisterWait函数通过线程池的工作线程执行LoadLibraryA函数,将参数3的CALLBACKFUNC设置为LoadLibraryA函数地址,参数4设置为要加载的对应dll字符串,之后通过WaitForSingleObject函数等待事件对象执行,当LoadLibraryA函数将对应dll加载到当前进程内存再调用GetDllAddrOrFullPathByHash函数获取加载到内存中的对应dll基址。

使用RtlRegisterWait函数加载dll是通过线程池的工作线程执行LoadLibraryA函数所以可以达到隐藏调用堆栈的效果,可以防止EDR/AV通过回溯LoadLibraryA调用栈的方式来判断是否为恶意软件调用。

之后通过SyscallZwCreateThreadEx函数创建线程,SyscallNtGetContextThread函数获取线程CONTEXT,SyscallNtSetContextThread函数设置线程执行函数为ThreadMain函数地址,SyscallNtResumeThread函数恢复线程执行。

接下来我们看ThreadMain函数函数

首先调用了initKeyAndFormatInfo函数

initBrc4EncryptAlgorithmArray函数初始化了Brc4的自定义加密算法使用到的9个数组,每个数组共有256个元素,初始化方式就是将数组内所有元素减1,关于Brc4的自定义加密算法可以去我的github查看我逆向还原的算法。

我们查看未初始化的ArrayBox1

初始化完毕的ArrayBox1每位元素减1

接下来初始化了一些格式化字符用于在commander界面中显示上线的badger基本信息

解码Base64后使用rc4密钥f?zi\)*<解密Brc4配置文件

配置文件是通过|符号(0x7C)进行分隔,所以会通0x7C判断并逐字段解析配置文件

解析完之后个别字段会调用AsciiToHexadecimal函数将解析的配置文件字段从Ascii转为十六进制

接下来GetSystemInfo函数获取当前系统基本信息供上线包使用

WSAStartup函数初始化WinSock版本2.2

接着查询了0x511000内存地址的属性

我们查看现在的内存,0x511000这块虚拟内存权限为RX,这块内存是之前用于运行反射式注入badger core dll的代码。

将0x511000内存属性更改为PAGE_READWRITE并将内存清零

我们查看下现在的内存结构0x511000内存属性被更改为PAGE_READWRITE并且内存被清零

接下来会先发送上线包到C2

首先通过ror13 hash方式获取Wininet.dll的导出函数地址

格式化上线包

使用Brc4自定义的加密算法加密上线包

加密完后使用Base64进行编码上线包

接着会将加密后的上线包发送给C2

首先设置User-Agent

设置域名以及端口

设置请求方式和请求路径

发送POST请求

查询C2任务队列是否有任务

任务队列存在任务则读取C2服务器返回数据

base64解码

使用Brc4自定义加密算法解密,这里返回的数据用于心跳包,b-xx为当前Listeners上线的badger数量从b-0开始累计,\\后面的为b-cookie,这两个数据用于确定当前上线badger的唯一性

之后调用initCallbackFun函数初始化Brc4的内置后利用命令函数调用表,使用2个数组按分别按照顺序一一对应存储函数调用地址和函数调用ID

这是Brc4内置的后利用命令和对应的函数调用ID,除了help、cls、title这3个用于C2 Commander图形化界面的命令外,一共有131个命令用于和Badger交互,有2个命令的函数调用ID是相同的
|
调用ID |
命令 |
|
0x3C9F |
pwd |
|
0xD53F |
arp |
|
0x4FFE |
userinfo |
|
0x391 |
lockws |
|
0x609 |
lsdr |
|
0xA01 |
uptime |
|
0xB06 |
idletime |
|
0x703 |
exit_process |
|
0x605 |
revtoken |
|
0x105 |
dumpclip |
|
0xC144 |
drivers |
|
0x2919 |
list_downloads |
|
0xA217 |
get_parent |
|
0x1719 |
set_debug |
|
0x4318 |
tasks |
|
0x5921 |
get_child |
|
0x6154 |
psclean |
|
0x9C41 |
screenshot |
|
0xBA9D |
list_tcppivot |
|
0xA63C |
clear_parent |
|
0x3BA8 |
clear_child |
|
0x71C6 |
get_argument |
|
0x93D6 |
clear_argument |
|
0xE3CB |
dcenum |
|
0x8289 |
get_malloc |
|
0x8146 |
get_threadex |
|
0xF616 |
ipstats |
|
0x4A9E |
dll_block |
|
0xB3E4 |
dll_unblock |
|
0x3793 |
get_wmiconfig |
|
0x2698 |
reset_wmiconfig |
|
0x44B7 |
token_vault |
|
0xED33 |
vault_clear |
|
0x803 |
exit_thread |
|
0x4395 |
get_killdate |
|
0x4934 |
shadowcloak |
|
0xB339 |
netstat |
|
0xD41A |
routes |
|
0xBE9A |
local_sessions |
|
0x38B7 |
dnscache |
|
0xB6A3 |
getenv |
|
0x5248 |
sysinfo |
|
0x6135 |
windowlist |
|
0x73E8 |
applist |
|
0xD9A3 |
crisis_monitor |
|
0xD359 |
socks_start、socks_profile_start |
|
0xD959 |
socks_stop |
|
0x2DA1 |
keylogger |
|
0x2129 |
sleep |
|
0x1139 |
cd |
|
0xA905 |
cp |
|
0x9B84 |
mv |
|
0xE993 |
rm |
|
0x3F61 |
mkdir |
|
0x8F40 |
rmdir |
|
0xA32 |
ls |
|
0xA959 |
net |
|
0xF584 |
runas |
|
0xF999 |
make_token |
|
0xE9B0 |
run |
|
0xEBC0 |
kill |
|
0xBED0 |
shellspawn |
|
0x9DE0 |
ps |
|
0x8AFA |
set_parent |
|
0x6BAE |
get_system |
|
0x6F39 |
system_exec |
|
0xF3D9 |
psreflect |
|
0x3FD4 |
loadr、mimikatz |
|
0x2C74 |
download |
|
0x6C36 |
reg |
|
0xC929 |
set_child |
|
0xB458 |
scquery |
|
0xE2EA |
psimport |
|
0x13A1 |
upload |
|
0x699A |
pivot_tcp |
|
0x73E6 |
set_argument |
|
0x3C4D |
pivot_smb |
|
0xFE37 |
psexec |
|
0x97E9 |
sccreate |
|
0xFA73 |
scdelete |
|
0x3B3E |
scdivert |
|
0x5962 |
set_malloc |
|
0x5761 |
set_threadex |
|
0xC662 |
psgrep |
|
0xE591 |
portscan |
|
0x9881 |
dcsync |
|
0x4953 |
netshares |
|
0x4355 |
set_wmiconfig |
|
0x5213 |
wmiquery |
|
0x81E7 |
grab_token |
|
0xF856 |
impersonate |
|
0xCB46 |
vault_remove |
|
0x4932 |
coffexec |
|
0x6492 |
list_modules |
|
0x2133 |
memhunt |
|
0x7348 |
suspended_run |
|
0x8491 |
set_killdate |
|
0x8044 |
sharpinline |
|
0x3456 |
scstart |
|
0xB98E |
query_session |
|
0x7579 |
sentinel |
|
0xB99A |
passpol |
|
0xB69A |
schtquery |
|
0x29B3 |
sharescan |
|
0xE4A9 |
shinject_ex |
|
0xD8F3 |
ps_ex |
|
0xB6BF |
switch_profile |
|
0xB3A9 |
timeloop |
|
0xE19A |
preview |
|
0xA657 |
lookup |
|
0xA5F1 |
memdump |
|
0xD163 |
addpriv |
|
0xE53A |
fileinfo |
|
0xB1D3 |
wmiexec |
|
0xF83E |
lstree |
|
0xE4B9 |
kerberoast |
|
0xB93A |
icmp_ping |
|
0xDA9C |
phish_creds |
|
0x34AE |
start_address |
|
0xCDE4 |
threads |
|
0xE1BA |
phantom_thread |
|
0xF2ED |
stop_task |
|
0xD2E5 |
obfsleep |
|
0x4A83 |
socks_profile |
|
0x3BD8 |
memhook |
|
0xA23B |
samdump |
|
0xE3D2 |
sharpreflect |
|
0xA7D9 |
set_coffargs |
|
0xD8A9 |
clear_coffargs |
在C2 Commander图形化界面输入help命令可以获得所有内置命令,但是并没有0xF83E对应的命令,通过逆向brute-ratel-linx64也就是C2的TeamServer端发现对应的命令为lstree,此命令可能还在开发中并不能使用

通过C2返回的唯一ID数据格式化心跳包

使用Brc4自定义加密算法加密心跳包

使用base64编码

发送心跳包到C2

之后进入睡眠混淆函数首先使用SystemFunction036函数随机生成16字节伪随机数用于加密堆空间数据的rc4密钥

Rc4CryptDecryptHeapData函数使用随机生成的rc4密钥将所有使用的堆空间数据加密

根据配置文件选择的睡眠混淆方式选择对应的睡眠混淆函数,我们配置的是APC方式

创建纤程执行睡眠混淆函数

切换到纤程执行

首先为ROP链分配CONTEXT结构体(0x4D0)大小的堆空间,并且使用SystemFunction036生成16字节的伪随机数用于加密睡眠中badger core dll内存的rc4密钥

接着调用SetProcessValidCallTargets函数关闭以下几个ROP链用到的函数的CFG(Control Flow Guard)保护,CFG保护在Windows 8.1 UPDATE (KB3000850)补丁后开始包含,可以防止在程序中间接执行任意代码,在VS编译器中开启/guard:cf标志可以启用

在自己编写的load程序中一般在编译中是不会开启CFG保护的,但是考虑到一般要把shellcode注入到系统进程空间运行,而系统进程编译时基本都是开启了CFG保护的,所以为了兼容性考虑所以要把ROP链中用到的函数的CFG保护关闭。

创建一个Event对象,创建了一个挂起的线程并将函数TpReleaseCleanupGroupMembers+0x450地址设置为线程起始地址,填充ROP链CONTEXT结构的ContextFlags为CONTEXT_FULL(0x10000B)

获取创建的挂起线程的CONTEXT并复制到ROP链的CONTEXT

然后使用NtWaitForWorkViaWorkerFactory、BaseThreadInitThunk、RtlUserThreadStart函数构造了一个虚假线程调用堆栈上下文

接着开始填充ROP链函数ZwWaitForSingleObject、ZwProtectVirtualMemory、SystemFunction032、ZwGetContextThread的CONTEXT结构上下文,ZwTestAlert函数用于立即执行线程APC队列中挂起的APC回调

填充ROP链函数ZwSetContextThread、WaitForSingleObjectEx、SystemFunction032、ZwGetContextThread的CONTEXT结构上下文

使用NtQueueApcThread函数创建APC队列,将ZwContinue函数作为回调函数用于执行插入APC队列中的ROP链CONTEXT,使用ZwAlertResumeThread函数恢复线程执行,然后用rc4算法加密了最后一块堆内存,NtSignalAndWaitForSingleObject函数通过通知事件在保持不可警报的同时等待当前进程

ZwContinue函数的原型如下,参数一为CONTEXT结构体指针

ROP链开始执行此时RIP为ZwContinue函数,RCX为ZwWaitForSingleObject函数CONTEXT结构,ZwWaitForSingleObject等待当前进程对象作为不可警报。

NtProtectVirtualMemory将当前的badger core dll内存权限修改为PAGE_READWRITE

SystemFunction032通过rc4算法使用之前随机生成的16个字节伪随机数加密badger core dll内存

ZwGetContextThread获取当前执行ROP链线程的CONTEXT结构上下文

ZwSetContextThread设置虚假调用堆栈到ROP链线程

WaitForSingleObjectEx通过配置文件设置的睡眠时间进行睡眠等待

查看睡眠中的badger core dll,内存权限被改为RW并且全部被加密

查看欺骗线程调用堆栈都有一个固定的函数偏移,RtlUserThreadStart+0x21、BaseThreadInitThunk+0x14、TpReleaseCleanupGroupMembers+0x747、ZwWaitForWorkViaWorkerFactory+0x14

睡眠完成后之后调用了SystemFunction032函数通过rc4算法解密badger core dll内存

NtProtectVirtualMemory函数将badger core dll的内存权限改为PAGE_EXECUTE_READ

ZwSetContextThread恢复ROP链线程的CONTEXT结构上下文

RtlExitUserThread退出当前线程结束ROP链

结束后释放ROP链的堆空间并关闭句柄然后使用SwitchToFiber函数切换Fiber

Brc4支持3种睡眠混淆方式,我们使用obfsleep命令可以切换睡眠混淆方式,obfsleep 0就是刚刚分析的APC方式,obfslep 1和obfsleep 2对应Poling-0和Poling-1,接下来我们分析Poling方式的睡眠混淆,首先还是为ROP链分配CONTEXT结构体(0x4D0)字节大小的堆空间,并且使用SystemFunction036函数生成16字节的伪随机数用于加密睡眠中badger core dll内存的rc4密钥。

然后关闭ROP链用到的函数的CFG保护,并且创建一个Event对象,这里通过IsPoling标志判断了如果我们使用obfslep 1则会使用RtlCreateTimer函数,如果obfsleep 2则使用RtlRegisterWait函数,回调函数RtlCaptureContext用于获取RtlCreateTimer或RtlRegisterWait函数CONTEXT上下文结构。

然后将RtlCaptureContext回调获取的CONTEXT结构上下文复制到ROP链的CONTEXT

通过NtWaitForWorkViaWorkerFactory、BaseThreadInitThunk、RtlUserThreadStart函数构造了一个虚假线程调用堆栈CONTEXT结构上下文

然后填充ROP链函数VirtualProtect、SystemFunction032、ZwGetContextThread、ZwSetContextThread、WaitForSingleObject的CONTEXT结构的上下文结构,RSP-8用于调整RtlCaptureContext回调函数调用时造成的的偏移量。

填充ROP链函数ZwSetContextThread、SystemFunction032、VirtualProtect、NtSetEvent的CONTEXT结构上下文。

通过IsPooling标志判断选择RtlCreateTimer函数创建计时器队列或RtlRegisterWait函数注册等待句柄以创建ROP链,ZwContinue作为回调函数执行ROP链的CONTEXT,参数5用于调整ROP链调用之间的时间间隔,然后rc4算法加密最后一块堆空间数据,通过WaitForSingleObject等待Event对象。

VirtualProtect函数将badger core dll内存权限改为PAGE_READWRITE权限

SystemFunction032通过rc4算法使用之前随机生成的16个字节伪随机数加密badger core dll内存

ZwGetContextThread获取当前执行ROP链线程的CONTEXT结构上下文

ZwSetContextThread设置虚假调用线程堆栈到ROP链线程

通过WaitForSingleObject通过配置文件设置的睡眠时间进行睡眠等待

ZwSetContextThread恢复执行当前ROP链线程的CONTEXT结构上下文

SystemFunction032函数使用rc4算法解密badger core dll内存

VirtualProtect函数将badger core dll的内存权限改为PAGE_EXECUTE_READ

ZwSetEvent函数设置Event对象状态为Signaled

结束后释放ROP链内存,关闭句柄,切换回Fiber

接下来分析Brc4的后利用命令调用,我们使用mkdir testdir命令测试,在发送完心跳包到C2后会查询任务队列是否有任务,如果有则任务则解析,首先第一层为base64编码,第二层然后经过Brc4自定义加密算法解密,第三层还是base64编码解码后就为实际的命令调用数据

我们查看解密后的数据,0x3f61为mkdir命令的函数调用ID,0x20为分隔符,testdir是我们mkdir命令的参数

接下来将函数调用ID和参数分别复制到堆空间中

创建任务线程执行命令

进入TastThread将调用CheckCurrentCallbackFunid函数检查当前函数调用ID是否在当前函数调用ID表下标

函数调用ID占2字节,所以分别比对当前函数调用ID和当前下标的函数调用表ID如果2个字节都相同则函数返回TRUE对比成功

如果当前函数调用ID不在当前函数调用ID表下标则会将下标加1,并将函数调用ID表数组地址加3,循环比对函数调用ID表中下一个函数调用ID

如果ID比对成功则进行调用,通过call qword ptr [rax+rsi*8]进行调用,rax为函数调用地址表数组中的首地址,rsi为我们当前函数调用ID在函数调用ID表中的下标

进入mkdir函数,使用CreateDirectoryA函数创建testdir文件

如果文件夹创建成功则格式化返回信息字符串

将格式化完成的返回消息进行base64编码然后加密等待将数据发送给C2

我们查看COMMAND界面返回的信息

3.2 badger_x64_stealth_rtl.bin
上小节我们分析了Default下的Exit Method:RtlExitUserThread的shellcode以及badger core dll payload,接下来我们分析Stealth下的Exit Method:RtlExitUserThread的shellcode,我们主要分析Stealth的不同点和Default相同的代码就不再分析,Stealth相比于Default的shellcode主要增加了FixDllMemoryHook函数

获取kernelbase.dll的UNICODE_STRING FullDllName

获取ntdll.dll的UNICODE_STRING FullDllName

之后调用ReadDiskDllFixMemoryHook函数,首先将\??\和FullDllName拼接

NtOpenFile函数打开dll文件句柄

打开文件成功则调用NtReadFile函数读取dll到申请的堆中

之后通过解析PE文件获得disk dll和memory dll的.text section地址,并将memory dll .text section内存权限改为PAGE_EXECUTE_READWRITE权限

将disk dll的.text section覆盖memory dll的.text section

将memory dll的.text section内存权限改为PAGE_EXECUTE_READ后调用GetDiskDllRdataSectionInfo函数

函数GetDiskDllRdataSectionInfo通过解析PE文件获取disk dll和memory dll的.rdata section地址

将memory dll .rdata section内存权限改为PAGE_READWRITE

将disk dll .rdata section覆盖memory dll .rdata section

之后分别对kernel32.dll和kernelbase.dll调用ReadDiskDllFixMemoryHook进行UnHook

之后调用ZwFlushInstructionCache刷新代码缓存

Stealth相比于Defualt的shellcode主要就是多了一个FixDllMemoryHook函数,主要就是读取ntdll.dll、kernel32.dll、kernelbase.dll在硬盘中的.text section和.rdata section覆盖内存中可能被AV或EDR等安全软件hook的.text section和.rdata section

3.3 stage_x64_rtl.bin
使用ida分析stage shellcode起始的特征和badger shellcode的特征相同

在栈上初始化了0xC4大小的加密数据,尾部的8字节为rc4密钥

使用CyberChef进行解密发现为配置文件

stage和stealth的payload一样都有FixDllMemoryHook函数用于对ntdll.dll、kernel32.dll、kernelbase.dll进行Unhook

使用rc4算法密钥bYXJm/3#M?:XyMBF解密wininet.dll字符串然后调用GetDllBaseAddressRtlRegisterWait函数加载dll获取基址

首先创建Event对象

RtlRegisterWait函数通过线程池的工作线程调用LoadLibraryA函数,函数参数3为LoadLibraryA函数地址,参数4为wininet.dll字符串

然后WaitForSingleObject等待Event对象

调用GetDllBaseAddressOrGetDllFullPath函数获取加载到内存中的winnet.dll基址

之后通过ror13hash获取winnet.dll中需要用到的函数地址

使用rc4算法密钥bYXJm/3#M?:XyMBF解密crypt.dll字符串然后并加载获取dll基址

之后通过ror13hash获取CryptBinaryToStringA函数地址

使用rc4算法密钥>s?un&>)解密配置文件和我们刚刚使用CyberChef解密的相同,此rc4密钥为随机生成

之后调用ParsingStageConfiguration函数逐个解析配置文件字段

解析完配置文件后调用HttpGetBadgerShellcode函数获取下一阶段的Badger Shellcode

通过配置文件auth密钥格式化认证包

使用rc4算法密钥>s?un&>)加密认证包

使用base64编码加密后的认证包

调用InternetOpenA函数设置User-Agent

InternetConnectA设置域名和端口

HttpOpenRequestA设置请求方式和path

HttpAddRequestHeadersA将rc4算法加密密钥附加到HTTP请求头

HttpSendRequestA函数将指定的请求发送到C2服务器

InternetQueryDataAvailable函数查询C2服务器返回的数据大小

InternetReadFile读取C2服务器返回的数据

使用rc4算法密钥>s?un&>)解密C2服务器返回的数据

使用ZwAllocateVirtualMemory函数申请PAGE_READWRITE权限的虚拟内存

将Badger Shellcode复制到新申请的虚拟内存中

调用ZwProtectVirtualMemory将Badger Shellcode内存权限改为PAGE_EXECUTE_READ

调用Badger Shellcode,由于此前已经分析过Badger Shellcode所以这里不在继续分析

使用BinDiff对比发现stage获取的shellcode为badger_x64_stealth_ret

3.4 badger_x64.dll
使用Exeinfo查看badger_x64.dll可以看到是用MinGW-w64编译的x64 dll,并且有一个大的.data section

接下来分析dll main,主要就是通过ror13hash获取了ntdll基址然后获取NtProtectVirtualMemory、ZwAllocateVirtualMemroy、ZwWaitForSingleObject、ZwCreateThreadEx函数的syscall id和syacall地址

ZwAllocateVirtualMemory给shellcode申请PAGE_READWRITE权限的虚拟内存,将.data section的shellcode复制到申请的虚拟内存,然后将.data section的shellcode清零,然后将shellcode虚拟内存权限改为PAGE_EXECUTEREAD

我们可以看到.data section的Badger Shellcode没有进行任何加密

之后ZwCreateThreadEx创建线程执行Badger Shellcode,分析到这里可得出结论badger_x64.dll其实就是一个用于加载Badger Shellcode的加载器

使用BinDiff对比badger_x64.dll使用的shellcode为badger_x64_ret

3.5 badger_x64_svc*ex.e**
使用Exeinfo查看badger_x64_svc*ex.e**也使用MinGW-w64编译的64位程序并且也有一个大的.data section

这里获取ntdll.dll基址不是通过ror13hash而是直接通过*力暴**搜索的方式

接着通过ror13hash获取NtProtectVirtualMemory、ZwAllocateVirtualMemroy、ZwWaitForSingleObject、ZwCreateThreadEx的函数syscall id和syscall address

接下来还是相同的操作将.data section的Badger Shellcode复制到申请的虚拟内存,然后清空.data section的shellcode,将虚拟内存权限改为PAGE_EXECUTEREAD然后调用ZwCreateThreadEx创建线程执行

使用BinDiff对比发现badger_x64_svc*ex.e**使用的shellcode也是badger_x64_ret

3.6 badger_x64_stealth_svc*ex.e**
使用Exeinfo查看badger_x64_stealth_svc*ex.e**是使用MinGW-w64编译的64位程序同样有大的.data section,根据此特征可知主要逻辑也是加载.data section的shellcode执行

badger_x64_stealth_svc的主要逻辑和badger_x64_svc相同不在阐述。

经过BinDiff对比badger_x64_stealth_svc使用的shellcode为badger_x64_stealth_ret

4.总结
本篇文章详细分析了Brute Ratel C4 Scandinavian Defense 1.2.2泄露版HTTP Listener的x64架构的所有Badger Payload,截至发稿前Brc4已经更新到了1.4 Resurgence版本开发者对bruteratel团队服务器和shellcode核心进行了改进,比如删除了一直使用的ror13哈希算法,将配置文件的存储方式从base64编码改为二进制,删除了badger core dll中用于解密dll字符串的rc4密钥bYXJm/3#M?:XyMBF,将badger core dll中使用的所有命令输出字符串删除将由bruteratel服务器直接进行格式化输出等等,由于篇幅原因下一篇文章将带来检测运行和睡眠中的badger core dll和bruteratel团队服务器。
from https://www.freebuf.com/articles/system/356597.html