暂无简介
红队开发基础-基础免杀(三)
引言
本文是《红队开发基础-基础免杀》系列的第三篇文章,主要介绍了“删除ntdll中的钩子”、“伪造线程调用栈”、“beacon的内存加密这几种手段”,达到了bypass edr的效果。
删除ntdll.dll中的钩子
实现原理
此技术在《红队队开发基础-基础免杀(二)》中提到的syscall工具ParallelSyscalls中有提及。
该技术最初于《EDR Parallel-asis through Analysis》这篇文章中被提出。
Windows10之后的版本增加了windows parallel加载的特性,简单点说就是win10之前的系统dll加载都是同步加载的,windows10以后引入异步加载。
在加载所有dll之前系统会做一些列判断,判断是采用同步还是异步加载。
在这过程种,windows会保存NtOpenFile(), NtCreateSection(), ZwQueryAttributeFile(), ZwOpenSection(), ZwMapViewOfFile()这几个函数的存根,保存位置在ntdll的.text节中。
这样就是说,这几个函数就算被hook,我们也可以获取到syscall number。并且有了这个函数,我们可以重新把内存种的ntdll换成干净的ntdll,实现了unhook的操作。
其中获取到纯净的ntdll有两种方式,如图:
具体实现
参考工具RefleXXion
该工具有exe和dll的形式可以直接编译dll进行使用。利用RefleXXion的dll可以解除对ntdll.32的hook。
下面的例子是对Sleep函数进行了hook,因为sleep函数在Kernel32.dll中,要对dll源码进行改动。
首先调用InitSyscallsFromLdrpThunkSignature函数,函数名顾名思义就说获取到syscall存根。
这段代码似曾相识,和《红队队开发基础-基础免杀(二)》中的从dll搜索是从ntdll.dll中搜索出syscall的系统调用号几乎一样:
使用BuildSyscallStub工厂函数生成不同函数的syscall内联汇编代码:
强制转换成函数指针以备调用
接下来要替换掉内存中ntdll.dll的函数,该工具使用两种技术,说的两种技术其实主要是ntdll.dll获取的位置不同,技术一从\??\C:\Windows\System32\ntdll.dll读取,技术二从\KnownDlls\ntdll.dll读取。
技术一
使用NtCreateSection和NtMapViewOfSection api:
首先通过本地文件创建内存session:
ntStatus = RxNtCreateSection(&hSection, STANDARD_RIGHTS_REQUIRED | SECTION_MAP_READ | SECTION_QUERY,
NULL, NULL, PAGE_READONLY, SEC_IMAGE, hFile);
之后映射到当前进程的内存中:
ntStatus = RxNtMapViewOfSection(hSection, NtCurrentProcess(), &pCleanNtdll, NULL, NULL, NULL, &sztViewSize, 1, 0, PAGE_READONLY);
可以看到dll被载入了内存,明显是MZ头为PE文件。之后搜索当前进程中已经加载的ntdll.dll:
解析已有的dll的pe结构,找到.text段:
进行替换:
这样就解除了对ntdll.dll中函数的hook。
技术二
主要是用NtOpenSection和NtMapViewOfSection实现,dll获取方式不同对应使用的api就不同,这里技术二和技术一原理差不多,这里不做过多分析。
解决问题
编译dll直接调用,发现没有成功。
找调试dll的方式,只要将dll项目的debug选项调成加载该dll的exe就可以实现dll的远程调试:
发现RtlInitUnicodeString调用返回了false,这个函数是ntdll.dll里的,这里改动有问题:
这里返回的hHookedNtdll变量有两个作用,一是获取到RtlInitUnicodeString函数,二是要作为下面等待被替换的dll名称。
这里作用一应该是ntdll.dll,而作用二应该是kernel32.dll。进行一系列修改:
又报错,一样的问题,这里的pCleanNtll应该是ntdll的副本,这里是kernel32.dll的副本了:
改为从ntdll获取这个api。
没有被hook之前,sleep函数的内存为:
hook后产生变化:
加载dll后恢复:
可以看到hook已经被解除,sleep函数被正常调用:
伪造线程调用堆栈
下面介绍的两种技术都是配套cs进行使用的:
基础知识
Cobalt Strike默认对命令有60s的等待时间,我们可以通过sleep x命令修改这个时间。通过sleep实现了beacon的通讯间隔控制。beacon中调用系统sleep进行休眠,teamserver实现一种消息队列,将命令存储在消息队列中。当beacon连接teamserver时读取命令并执行。
常规的cs在sleep休眠时,线程返回地址会指向驻留在内存中的shellcode。通过检查可疑进程中线程的返回地址,我们的implant shellcode很容易被发现。
实现原理
在ThreadStackSpoofer项目的readme中有这样一张图:
笔者理解是EDR/工具获取调用栈是通过某一时刻的栈的状态获生成一个链状的图,在某个时间损坏中间的某个环节可以导致链状图不完整伪造调用图。
笔者没找到ThreadStackSpoofer作者的效果图是哪个工具生成的,这里直接贴ThreadStackSpoofer README中的图,经过hook的调用栈应该像下面的图:
没有经过hook的调用栈:
代码实现
在主线程中HOOK SLEEP函数,跳转到Mysleep函数。
通过创建进程的方式启动beacon,将Mysleep函数原本返回值的位置改为0:
这样就可以简单的扰乱程序的调用栈了。
beacon的内存加密
基本原理
主要是根据ShellcodeFluctuation
该项目是基于threadstackspoofer项目的加强版,在sleep函数执行的时候在对shellcode内存的修改属性且解密。可以一定程度上绕过edr的内存扫描。原理就是beacon线程在执行sleep函数的时候,会自动将自己的内存加密并修改属性为不可执行,再执行正常的sleep函数。执行成功后恢复shellcode并使之可以执行,等待下一次连接重复上述操作。在sleep函数真正执行的过程中,shellcode为不可执行属性可以绕过edr的检查。
一个疑问
这里存在一个问题,加密shellcode的话会不会影响Mysleep函数的执行?
推测Mysleep位于主进程的地址空间,始终可以被访问。跳出Mysleep的代码是在beacon线程的地址空间,被加上了不可执行属性。
我们通过对hookSleep函数的调试,可以看到一些东西:
addressToHook是原本Sleep函数所在的位置,jumpAddress为Mysleep函数所在的位置:
alloc为shellcode所在地址,可以看到和前面两个sleep函数所在地址相差非常多,可以印证前面的猜想。
代码实现
该工具主要有两种实现方式,依靠判断第二个命令行参数实现。该参数类型为int,对应枚举类,如下图:
0表示不对内存操作
1表示将内存标识为RW,
2表示将内存标识为NO_ACCESS,通过异常处理机制注册VEX实现修改代码执行逻辑。
两种技术前面的过程都差不多,进行参数解析后,hook sleep函数
hook依旧依靠fastTrampoline函数:
接着beacon线程进入mysleep函数,sleep函数一共做了几件事情:
- initializeShellcodeFluctuation
- shellcodeEncryptDecrypt
- unhook sleep
- true sleep
- shellcodeEncryptDecrypt(set memery to RW)
- rehook sleep
首先进入initializeShellcodeFluctuation函数,这个函数主要从mysleep的返回地址的内存进行搜索,找到shellcode的位置:
搜索的方式还是挺有意思的,memoryMap是存储内存块的一个容器:
看看实现,VirtualQueryEx返回一个MEMORY_BASIC_INFORMATION对象,其RegionSize表示这块内存的大小。
通过不停的遍历,将所有存储内存块信息的对象mbi的首地址放入容器。后续判断sleep的返回地址是否在这块内存中定位到shellcode的内存段,随后完成对g_fluctuationData对象的初始化赋值:
g_fluctuationData主要包括shellcode内存块的位置,大小,是否加密,加密key等属性。
之后对shellcode进行xor加密,并将内存设为RW属性,没加密之前的内存:
主要通过shellcodeEncryptDecrypt函数加密
加密后的内存:
密钥为
使用Python验证,加密结果和预期的一致
之后取消掉hook并执行常规的sleep:
等待cs默认的一分钟后,解密shellcode并设置内存属性为RX,并且重新hook sleep函数,以便下次执行:
内存已经被重置
除了通过set RW属性外,还可以set NO_ACCESS属性,对应就是工具的命令行参数2,和参数一不同的是在注入shellcode之前注册了一个VEX:
接着触发到sleep和前面差不多,只是加密shellcode后标识为NO_ACCESS
后续访问到这块内存的时候进入异常处理函数,将内存属性重新设为RX。恢复代码的执行。
源码
本文实现的例子相关代码均进行了开源:EDR-Bypass-demo
参考文章
https://tttang.com/archive/1464/
https://github.com/mgeeky/ThreadStackSpoofer
https://github.com/hlldz/RefleXXion
https://www.mdsec.co.uk/2022/01/edr-parallel-asis-through-analysis/
https://github.com/mdsecactivebreach/ParallelSyscalls
https://github.com/mgeeky/ShellcodeFluctuation
- 本文作者: 7bits安全团队
- 本文来源: 先知社区
- 原文链接: https://xz.aliyun.com/t/11532
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!