标签 CONTEXT结构 下的文章
- 作者: 纯情
- 时间:
- 分类: 开源
- 评论
0x1.前言
Brute Ratel C4(以下简称BRC4)是一位印度老哥开发的C2,用于对抗EDR,也是一款极其优秀的C2了。本文使用的BRC4版本为1.2.2(早已经有更新的破解版本了,但载荷都大差不差)。生成马被称为Badger,类似CobaltStrike的Beacon。如下图是它支持的一些规避方法。在第一篇文章后分析其实现方式,第二篇文章实现此睡眠混淆方式,以及如何在自己开发的马中实现Sleeping Masking。
什么是睡眠混淆,简单的来说就是你的马运行后,为了opsec,需要Sleep,CobaltStrike等C2就是默认Sleep 60s,这就是睡眠的概念。混淆就是指为了规避AV/EDR的内存扫描,需要在内存中对我们的马进行加密,这样就绕过了内存扫描。
在本文中分析BRC4的睡眠混淆方法中的利用APC来达到Sleeping Masking。什么是APC参考MSDN: https://learn.microsoft.com/en-us/windows/win32/sync/asynchronous-procedure-calls 。
0x02.利用APC进行Sleeping Masking分析
搭建BRC4这些就不说了,创建的监听器的时候,设置Sleep Mask为APC。
然后生成马子进行分析,Stageless即可。
写个简单的加载器,直接开始调试就行。开头有大量push操作,猜测是核心载荷被压入栈,直接跳过这些。
需要先绕过NtGlobalFlag的反调试,把ZF标志位改了就行,接着往下。
在通过Hash的方式获取到一些API的地址后,RC4解密出了一个被抹掉MZ头的PE文件,带解密的内容很显然是前面push压入的数据。得到的是核心载荷,后面可能被反射式注入执行。
我们将这个PE文件从内存中Dump出来。
然后很显然接下来的这个call,开始反射式注入此解密出PE文件了。
然后call rax,跳到修复后DLL执行DllMain开始执行。这大概就是Badger的执行上线流程。
上线后,我们关注点是Sleeping Masking。直接定位到睡眠混淆的地方。
可以根据动态函数地址以及前面Dump出的PE,在IDA中修改函数名(其中调用很多函数都是通过Syscall),进行分析。首先会在堆上分配很多CONTEXT结构,这里的0x4D0代表CONTEXT结构体大小。
往下判断是否开启CFG,开启的话,使用SetProcessValidCallTargets扩展CFG允许集合。这是为了后续构造ROP链执行做准备。
为什么这里判断是否开启CFG?在实际中,一般shellcode都是注入到别的系统进程中,而不是自己写的Loader,系统进程例如RunTimerBorker.exe基本都是开启CF Guard的。所以这里的判断很合理也必要。
继续往下看。第一个if块这里不太明白是在干嘛,像是获取当前PE的VA,调试发现不会进入这一块,先跳过。第二个if块是最关键的位置,显然能够进入此if块执行,第一个条件是创建了一个Event对象,第二个是创建了一个挂起的线程,入口点是TpReleaseCleanupGroupMembers + 0x450的地方。这个线程主要是为了后续调用ZwGetThreadContext,为分配的CONTEXT结构体,提供一个正常的CONTEXT和执行APC回调的地方。
往下进入第二个if块内容。首先给这几个CONTEXT结构体赋值,表明对什么寄存器感兴趣。然后通过ZwDuplicateObject赋值了当前线程的句柄。然后调用ZwGetThreadContext获取前面TpReleaseCleanupGroupMembers + 0x450入口线程的CONTEXT。并依次赋值到所有的CONTEXT中去。
在sub_10019490函数中,之所以没有rename是因为找不到合适的名字,这个函数获取了一个CONTEXT。这个CONTEXT是从当前进程但不是当前执行线程的CONTEXT,在ZwGetThreadContext后,在对这个CONTEXT的Rsp值和Rip值进行处理。
奇怪的是该函数返回后并不会进入下面的if分支。不过这里像是在做堆栈欺骗类似的动作?先不管吧,继续往下。
开始构造ROP链,对前面分配的每个CONTEXT进行赋值。Rcx、Rdx、R8、R9分别是前四个参数,Rip是要执行的API,返回地址为NtTestAlert,作用就是为了触发APC队列。ROP链执行函数的顺序为ZwWaitForSingleObject、NtProtectVirtualMemory、SystemFunction032、ZwGetContextThread、ZwSetContextThread、WaitForSingleObjectEx、SystemFunction032、NtProtectVirtualMemory、ZwSetContextThread、RtlExitUserThread。
其中SystemFunction032可能会让我们感到疑惑,这是微软未公开的函数,其实就是一个RC4加密算法,ReactOS可以看到定义,在advapi32.dll模块中实现。前后各调用一次,即是对在内存的载荷加解密。
然后到了最关键的地方,通过NtQueueApcThread插入到指定线程的APC队列,回调函数为ZwContinue,其作用是恢复CONTEXT下文,参数即为前面分配赋值的CONTEXT。插入完成后,NtAlertResumeThread恢复ThreadHandle,这里的Thread就是前面的TpReleaseCleanupGroupMembers + 0x450,然后设置Alerted状态,开始执行APC队列。
首先执行ZwWaiteForSingleObject,这里是为了等待NtSignalAndWaitForSingleObject Signal EventHandle,然后Waite ThreadHandle。
执行NtProtectVirtualMemory将内存属性修改为RW,这样极大减少了被扫描的可能性,很多AV内存扫描的目标仅仅是带有X属性的内存。
SystemFunction032 RC4加密相关内存。
然后调用ZwGetThreadContext,获取前面通过ZwDuplicateObject复制的TargetHandle句柄(badger执行的主线程)的CONTEXT结构,调用ZwSetThreadContext设置TargetHandle的CONTEXT为sub_10019490获取的CONTEXT,这里像是在做调用堆栈的伪装,但是前面没进入那个IF块,又不像堆栈欺骗。这里可能是在睡眠期间,将调用堆栈也做了混淆,目的是看不出在进行了APC等睡眠混淆的操作。
调用WaitForSingleObjectEx模拟正常睡眠,这个时间就是Listener或Profile中设置的Sleep时长(0x4E20 == 20000)。
睡眠完成后开始恢复,首先是再次调用SystemFunction032 RC4解密内存。
然后NtProtectVirtualMemory恢复内存属性。
调用ZwSetThreadContext还原TargetHandle的CONTEXT上下文。最后RtlExitUserThread退出TpReleaseCleanupGroupMembers + 0x450线程。
至此一个完成睡眠混淆流程结束。对于一个存活时间长的马,睡眠时间栈大多数,执行任务的时间很短。
可能有人要问,为什么我不能直接对马改内存属性和加密呢?还要写APC回调?这样写其实解决了加密代码不能加密自己的问题,以及修改了内存属性如何改回来的问题,万一有些AV就专门盯着你写的那块加密代码,这样马就被杀了。
下一篇文章中,根据分析结果实现APC Sleeping Masking