红队开发基础-基础免杀(三)
2022-7-18 16:5:32 Author: xz.aliyun.com(查看原文) 阅读量:78 收藏

引言

本文是《红队开发基础-基础免杀》系列的第三篇文章,主要介绍了“删除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函数一共做了几件事情:

  1. initializeShellcodeFluctuation
  2. shellcodeEncryptDecrypt
  3. unhook sleep
  4. true sleep
  5. shellcodeEncryptDecrypt(set memery to RW)
  6. 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


文章来源: https://xz.aliyun.com/t/11532
如有侵权请联系:admin#unsafe.sh