brute ratel C4 badger全分析文档
Brute Ratel C4是一款类似于Cobalt Strike的商业红队武器框架,每年License收费为2500美元,客户需要提供企业电子邮件地址并在颁发许可证之前进行验证,首个版本Brute Ratel C4 v0.2于2021年2月9日发布,它是由Mandiant和CrowdStrike的前红队队员Chetan Nayak创建的,该工具独特之处在于它专门设计防止端点检测和响应(EDR)和防病毒(AV)软件的检测,是一款新型的红队商业对抗性攻击模拟武器。
**AV(Antivirus):主要依赖于特征库来检测计算机病毒,杀毒的能力完全取决于其拥有的特征库的更新程度。
EDR(Endpoint Detection and Response):是一种网络安全解决方案,专注于监控、检测和响应计算机端点(如桌面计算机、笔记本电脑、服务器等)上的潜在威胁和恶意活动。EDR系统通过收集和分析端点数据来发现异常行为、潜在攻击和漏洞利用。
与Cobalt Strike的Beacon后门类似,Brute Ratel C4允许红队在远程终端主机上部署Badger后门程序,Badger连接回攻击者的命令和控制服务器,接收服务器端的命令执行相关的恶意行为。
样本信息:
名称: Roshan_CV.iso
大小: 4839424 字节 (4726 KiB)
MD5: a7df3462a6dce565064cfe408557c4df
SHA1: 6b91bfc761fe958c8ac04dd403db284ccc3a530e
SHA256: 1fc7b0e1054d54ce8f1de0cc95976081c7a85c7926c03172a3ddaa672690042c
上线流程
阶段一:钓鱼过程分析
恶意样本是个iso文件,双击挂载iso,一共有5个文件,除了Roshan-Bandara_CV_Dialog
这个快捷方式之外其他的文件都是隐藏文件
恶意样本是个iso文件,双击挂载iso,一共有5个文件,除了Roshan-Bandara_CV_Dialog
这个快捷方式之外其他的文件都是隐藏文件
在箭头处打开隐藏文件显示
我们查看Roshan-Bandara_CV_Dialog快捷方式指向的目标%windir%/system32/cmd.exe /c start OneDriveUpdater.exe
,主要就是调用cmd命令执行了隐藏文件OneDriveUpdater.exe,cmd.exe /c
命令指定执行完后会自动关闭控制台窗口
OneDriveUpdater.exe是Microsoft的数字签名二进制文件,用于将数据从本地计算机同步到云。它不是恶意的,而是被滥用来加载参与者的DLL。执行OneDriveUpdater.exe后,将执行以下操作:
1.由于Version.dll是OneDriveUpdater.exe的依赖DLL,并且与OneDriveUpdater.exe存在于同一目录中,因此将加载它。
2.Version.dll已被Actor修改,以加载加密的有效负载文件OneDrive.update。修改解密文件并在内存中加载shellcode的第一阶段。为了维护代码功能,参与者使用DLL API转发请求到名为vresion.dll的合法版本. dll。Vresion.dll是执行元的version.dll的依赖文件,将与执行元的version.dll一起加载。
使用 Dependencies 查看dll调用情况
劫持DLL的攻击原理是在DLL搜索顺序的较高优先级位置放置一个恶意DLL,这个恶意DLL具有与目标DLL相同的文件名。当应用程序尝试加载目标DLL时,Windows首先找到并加载恶意DLL,从而在受害进程中执行恶意代码.可以发现,黑dll在白dll前。
vresion.dll和OneDriveUpdater.exe为白文件都有Microsoft的有效数字签名
3.内存中的代码,即Brute Ratel C4,在RuntimeBroker.exe进程空间中作为Windows线程执行
流程图示:
阶段二:version.dll分析
Version.dll是用C++编写的合法Microsoft文件的修改版本。植入的代码用于加载和解密加密的有效载荷文件。解密后的有效载荷是shellcode(x64汇编)的有效载荷,该shellcode进一步用于在主机上执行Brute Ratel C4。
为了使Version.dll保持其OneDriveUpdater.exe的代码功能,参与者包括合法的数字签名Microsoft version.dll并将其命名为vresion.dll。任何时候OneDriveUpdater.exe调用Actor的Version.dll,该调用都会被代理到vresion.dll。因此,执行元的version.dll将作为依赖文件加载vresion.dll。
version.dll和vresion.dll的导出函数完全相同
任务一:搜寻Runtimebroker.exe进程
唯一调用了一个函数
枚举所有进程并循环查找找到Runtimebroker.exe的进程ID(PID)。
附加进程到exe上进行动调
从当前工作目录读取负载文件OneDrive.Update。
打印字符串"Please wait..."
使用步骤1中的进程ID调用Windows API ntdll ZwOpenProcess。进程以完全控制访问打开。
任务二:解密shellcode
使用XOR加密算法和28字节密钥对有效负载文件shellcode进行解密:
jikoewarfkmzsdlhfnuiwaejrpaw
解密前:
解密之后,如下所示:
00007FFFF8142326地址在内存中看,即可看到shellcode
任务三:进程注入
调用了NtCreateSection
函数创建Section
接着调用NtMapViewOfSection
函数将section映射到本进程虚拟内存
两次调用Windows API NtMapViewOfSection。第一个调用将解密的有效负载的内容映射到当前进程内存空间,第二个调用将内容映射到Runtimebroker.exe内存空间。
1 | ZwMapViewOfSection 例程将节的视图映射到主题进程的虚拟地址空间中 |
调用Windows API NtDelayExecution并休眠(暂停执行)
1 | **KeDelayExecutionThread** 例程将当前线程置于指定间隔内可发出警报或不可更改的等待状态。 |
调用Windows API NtcreatThreadEx。此API启动一个新线程,并将内存的起始地址复制到Runtimebroker.exe。 在RuntimeBroker.exe进程中创建远程线程执行注入的shellcode 这是关键的一步骤。
调用Windows API NtDelayExecution并休眠(暂停执行)
随后程序结束
接下来的逻辑还是熟悉的通过PEB-Ldr->InMemoryOrderModuleList
获取ntdll.dll的基址
version.dll中的加载方法也是如此
任务四:将shellcode dump出来
将这段shellcode执行 使用process hacker 工具此时可以在内存中查看到runtimebroker
使用其他方法也能查看到
在内存布局中转到该地址,然后将这块内存转存为文件
复现:
1 |
|
阶段三:分析shellcode
这个文件的作用是释放Brute Ratel C4 Badger的payload
任务一:手动加载shllcode
将dump下来的shellcode.bin文件使用编写的shellcode loader运行
1 |
|
任务二:shellcode分析
shellcode开头有巨量的mov
和push
执行的组合,通过压入栈中的数据可以判断大致是在初始化数据和函数调用要用到的参数等等
通过访问0x3C位置AddressOfNewExeHeader开始解析此PE文件
查看rsp寄存器内存,可以发现压入栈中的众多数据包含了一个PE文件并且此PE文件的Dos头的MZSignature被抹除防止EDR进行内存扫描查杀,
最终获取导出函数的RVA:0x92E0
通过PEB-Ldr->InMemoryOrderModuleList
获取ntdll.dll的基址
值得一提的是:
需要调用api都是通过hash算法获取API的名称然后进行调用的 (加密与解密P557),这样可以提升逆向分析难度,也可以缩小shellcode的体积
调用searchByHash函数(上述函数重命名)通过Hash获取6个对应函数的地址
调用NtAllocateVirtualMemory分配指定的内存空间,然后再把payload的ShellCode代码拷贝到该内存空间当中,如下所示:
使用Windows API调用NtProtectVirtualMemory更改新分配的内存块的保护。
调用NtProtectVirtualMemory修改该内存的属性,如下所示:
调用NtCreateThreadEx启动线程代码,如下所示调用ZwWaitForSingleObject函数等待线程执行完毕
在线程中释放Brute Ratel C4 Badger的攻击载荷,badger与cobalt strike十分相似。在此略去。
下面这一apt就是相似的攻击链加载cobalt strike载荷
响尾蛇组织使用DLL劫持加载Cobalt Strike攻击巴基斯坦政府 - FreeBuf网络安全行业门户
BRC 4最终有效载荷
Unregister DLL load callbacks 取消注册DLL加载回调
某些EDR产品可能会在内核级别注册其函数回调,以接收来自DLL加载或卸载事件的遥测。
为了绕过这一点,使用了一种技术,其中注册了一个空的回调函数,该回调函数被添加到已注册回调函数链的末尾。回调列表存储在循环双向链表中,其中最后一个元素指向链中的第一个元素。
通过找到指向链开始的指针,该技术可以遍历列表并取消链接所有已注册的回调,如果EDR已经在内核级别注册了自己的回调函数,则可以有效地抑制EDR接收DLL加载和卸载遥测的尝试
1 | // 检索注册回调函数列表的末尾 |
这段代码片段遍历了PLDR_DLL_NOTIFICATION_ENTRY
结构体的双向链表,这些结构体代表注册的DLL加载和卸载事件的回调函数。它从列表中取消每个节点的链接,有效地移除了所有注册的回调。
1 | ``` |
typedef struct _LDR_DLL_NOTIFICATION_ENTRY {
LIST_ENTRY List;
PLDR_DLL_NOTIFICATION_FUNCTION Callback;
PVOID Context;
} LDR_DLL_NOTIFICATION_ENTRY, * PLDR_DLL_NOTIFICATION_ENTRY;
1 | ``` |
这里,代码使用LdrRegisterDllNotification
注册了一个空(或虚拟)回调函数。这个函数被添加到现有回调链的末尾。
1 | // ... |
这段代码寻找DLL(ntdll.dll)的PE头中的特定节,并反向遍历注册回调的列表,直到找到它注册的那个回调(使用our_callback_cookie
)。一旦找到,它就使用LdrUnregisterDllNotification
注销回调。
检索回调链尾指针:
1
end_of_list = (PLDR_DLL_NOTIFICATION_ENTRY)ret_head_of_reg_callback_funcs();
这行代码调用一个函数以获取DLL通知回调链的尾指针。
遍历并取消链接回调:
如果end_of_list
非空,代码将遍历回调链,并取消每个节点的链接:1
2
3for (head = ...; head != end_of_list; head = flink) {
...
}这里,
List.Flink
和List.Blink
分别是指向链表中下一个和上一个节点的指针。通过修改这些指针,当前节点从链表中被移除。注册空回调函数:
1
LdrRegisterDllNotification(0LL, EmptynotificationFunc, 0LL, &our_callback_cookie);
这行代码使用
LdrRegisterDllNotification
函数注册一个空的回调函数EmptynotificationFunc
。这个空回调被添加到回调链的末尾,用于后续操作。寻找特定DLL的节信息:
代码通过解析PE头(Portable Executable header)来寻找特定DLL(如ntdll.dll)的节信息,特别是.data
节:1
2
3
4
5
6while (ntHdrs->FileHeader.NumberOfSections > (int)i) {
...
if (!(unsigned int)getNullByteAtEndOfName(secHdrs, ".data")) {
...
}
}这可能用于确定特定数据区域的位置。
反向遍历回调链:
代码使用our_callback_cookie
作为起始点,反向遍历回调链:1
2
3while (our_callback_cookie != entry) {
...
}目的是找到特定的回调函数或执行某些操作。
注销回调:
当找到特定的回调时,使用LdrUnregisterDllNotification
函数将其注销:1
LdrUnregisterDllNotification(our_callback_cookie);
整个过程的目的是为了清理或修改现有的DLL加载和卸载通知回调,防止EDR产品接收到这些事件的通知。通过这种方式,可以有效地抑制EDR产品对DLL加载和卸载事件的监控,从而绕过某些安全检测机制。
伪代码:
1 |
|
dll callback injection:
https://www.cnblogs.com/zha0gongz1/p/17633377.html
https://shorsec.io/blog/dll-notification-injection/
项目
https://github.com/ShorSec/DllNotificationInjection
代理DLL加载以隐藏ETWTI堆栈跟踪 Proxying DLL Loads To Hide From ETWTI Stack Tracing
https://github.com/paranoidninja/Proxy-DLL-Loads
https://0xdarkvortex.dev/proxying-dll-loads-for-hiding-etwti-stack-tracing/
这里的检测技术非常聪明。一些EDR使用用户态钩子,而另一些则使用ETW来捕获堆栈遥测。例如,假设你想在没有模块踩踏的情况下执行你的shellcode。因此,您通过VirtualAlloc或相对的NTAPI NtAllocateVirtualMemory分配一些内存,然后复制您的shellcode并执行它。现在您的shellcode可能有自己的依赖项,它可能会调用
LoadLibraryA
或LdrLoadDll
将dll从磁盘加载到内存中。如果你的EDR使用userland钩子,它们可能已经钩住了LoadLibrary
和LdrLoadDll
,在这种情况下,它们可以检查由RX shellcode区域压入堆栈的返回地址。这是特定于一些EDR像哨兵一号,Crowdstrike等,这将立即杀死你的有效载荷。 其他EDR,如Microsoft Defender ATP(MDATP),Elastic,FortiEDR将使用ETW或内核回调来检查LoadLibrary
调用的来源。堆栈跟踪将提供返回地址的完整堆栈帧以及从那里开始调用LoadLibrary
的所有函数。 简而言之,如果你执行一个DLL Sideload,它执行你的shellcode,调用LoadLibrary
,它看起来像这样:
这意味着任何在用户模式下或通过内核回调/ETW挂接LoadLibrary的
EDR都可以检查最后一个返回地址区域或调用的来源。在BRc 4的v1.1版本中,我开始使用RtlRegisterWait
API,它可以请求线程池中的工作线程在单独的线程中执行LoadLibraryA
来加载库。一旦库被加载,我们可以通过简单地遍历PEB(进程环境块)来提取其基址。Nighthawk后来将此技术应用于RtlWorkItem
API,它是WorkUserWorkItem
背后的主要NTAPI,也可以将请求排队到工作线程以加载具有干净堆栈的库。然而,Proofpoint在去年的某个时候在他们的博客中对此进行了研究,最近Elastic的Joe Desimone也发布了一条关于BRc 4使用的RtlRegisterWait
API的推文。 这意味着迟早会被检测到,并且需要更多这样的API来进一步规避。因此,我决定花一些时间从ntdll中反转一些未文档化的API,并发现至少有27个不同的回调
,只要稍加调整和黑客攻击,就可以利用它们来加载我们的DLL。
简而言之:就是在一个单独的,“干净的线程”中使用loadlibraryA
下面是项目源代码:TpPostWork
调用WorkCallback
,但WorkCallback
不调用LoadLibraryA
,而是跳转到它的指针。WorkCallback
简单地将RDX
寄存器中的库名称移动到RCX
,擦除RDX
,从adhoc函数获取LoadLibraryA
的地址,然后跳转到LoadLibraryA
,这最终重新排列整个堆栈帧,而不添加我们的返回地址。
1 |
|
**** 通过操作堆栈帧将WorkCallback重新路由到LoadLibrary的ASM代码
1 | section .text |
可以看到堆栈非常干净
规避ETW事件日志记录
他们在ETW调用的两个常见函数“NtTraceEvent“和“NtTraceControl”上设置硬件断点,以记录进程事件。然后,它们注册自己的VEH来捕获EXCEPTION_SINGLE_STEP异常,这在命中硬件断点时发生。
过修补负责生成或跟踪系统事件的已知API来规避Windows事件跟踪(ETW)和AMSI Windows机制。它使用“0xC3”操作码修补“EtwEventWrite”API,该操作码是一个“返回指令”,用于规避ETW事件跟踪日志记录。
当断点被命中并且处理程序捕获时,执行被重定向到一个除了返回零之外什么也不做的伪函数。这会阻止实际的函数逻辑执行,因此不会记录事件。
内核是否在记录
伪代码:
1 | if (ExceptionInfo->ExceptionRecord->ExceptionCode == (unsigned int)EXCEPTION_SINGLE_STEP) // check the code that triggered the handler (it triggers when a |
1 | hHandler = RtlAddVectoredExceptionHandler(1LL, VectoredExceptionHandler); // register a vector exception handler to be called first whenever a new exception occur |
1 | ``` |
