「议题文档」高级恶意软件开发之RDI的进化

Abstract

从RDI(Reflective Dll Injection)这项技术最初出现,发展到现在的恶意代码开发领域所使用的RDI,其一直在不断地进化并有非常多的改进。如今RDI作为一种关键技术,频繁应用于在现代的C2开发中,其仍然是在植入体初始化以及post-ex期间最常用的技术之一,本议题将通过以下几个方面深入探讨RDI的演变与应用,并且介绍一种后渗透人物的内存清理方式:

  1. RDI技术原理:介绍RDI的基本原理及其在恶意代码注入中的工作机制。
  2. OPSEC优化:深入探讨在 RDI技术中如何进行操作安全(OPSEC)优化,提供一些关键的OPSEC优化技巧和最佳实践。
  3. RDI的进化:分析RDI技术自初次出现以来的演进历程,探讨其在不同阶段中出现的技术改进和优化。
  4. 内存自清理方法(Phantom Execution):介绍一种在后渗透阶段所使用的,依赖RDI自身构造ROP,实现的新型执行模块后的内存自清理方法,该方法保证了模块在内存中无任何残留痕迹,达到完全 OPSEC 的效果。

通过以上内容,本议题将为与会者提供对RDI技术全面而深入的理解,以及OPSEC的改进点,并且深入探讨高阶的RDI内存清理技术,并发布新的自清理技术,帮助他们掌握最新的恶意代码开发和防御技术。

Presentation Outline

0x00 Background

RDI(Reflective Dll Injection)做为现代C2的基础技术,从十几年前至今,应用非常广泛,并且在不断地进化,可以说是非常值得学习研究的一项技术,目前不论是公开的C2或是自研工具,都大量运用到了此技术;并且其仍然是在beacon启动以及post-ex期间最常用的技术之一。本议题在介绍其进化过程以及技术细节的同时,也研究了他在执行功能模块时的清理工作,实现了一个OPSECRDI引导和清理技术。

0x01 Reflective Dll Injection

此项技术最早是由12年前的@stephenfewer提出,我想还是有必要简单介绍一下原始Reflective Dll Injection技术的一些关键点,最早此项技术是一种从内存中将DLL注入到进程的技术,他为现在大家所经常使用的在内存中执行PE技术提供了一个很好的基础。

「议题文档」高级恶意软件开发之RDI的进化
rdi流程图

工作原理:

  1. 通过CreateRemoteThread() 或通过bootstrap shellcode,执行DLL的名为ReflectiveLoader的导出函数。
  2. 然后ReflectiveLoader函数会在内存中定位到自身模块的位置,找到PE头,为了后续的PE展开做准备。
  3. ReflectiveLoader函数继续解析kernel32.dll,动态寻找几个API的地址,即LoadLibraryAGetProcAddressVirtualAlloc
  4. ReflectiveLoader函数继续分配一个新的内存,为了存放之后被解析展开的PE。
  5. 接着将DLL的PE头和Section拷贝到新的内存中
  6. 开始在新申请的内存中进行PE的展开,进行处理导入表和重定位表的操作。
  7. 最后使用DLL_PROCESS_ATTACH调用入口点,执行DLLMain
「议题文档」高级恶意软件开发之RDI的进化
RDI流程

Reflective Dll Injection可以实现从内存中加载DLL文件执行,有不会被记录在PEB的模块装载链表、是一种不落地执行的早期技术,在CobaltStrikeMetasploit中也有很多运用,比如说反射DLL修补,用作第一次加载和后渗透任务的执行。

0x02 OPSEC in RDI

反射DLL原始RAW内存的清理

ReflectiveDllInjection执行完毕后,内存中会出现2块内存区域,其中一块是初始RDI代码区域,另一块为RDI执行后创建的展开DLL文件的RWX区域,这是十分不安全的,会被EDR/AV关注到。

「议题文档」高级恶意软件开发之RDI的进化

当新内存展开后,包含ReflectiveLoader函数的内存区域已经完成使命,可以清理掉,有两种方式:

a. 在执行后的DLLMain中获取老内存地址,进行Free,如beacon.dll的处理:

「议题文档」高级恶意软件开发之RDI的进化

BRC4中的处理:

「议题文档」高级恶意软件开发之RDI的进化

b. 在RDI中进行自身的释放,如KaynStrike

「议题文档」高级恶意软件开发之RDI的进化

这种方式比较巧妙,以ROP的思想,通过捕获寄存器状态,更改寄存器的值设置特定的上下文,使用NtContinue执行,构造了可以自己清理自身所在内存区域的操作,我们后面要实现的自清理也来源于这个思路。

PE头的处理

原始RDIDOS Header也拷贝到了新内存,这样在内存中会存在非常明显的PE头

「议题文档」高级恶意软件开发之RDI的进化

因为PE头指示的一些信息,在RDI函数展开时使用即可,完全没必要拷贝过去,所以说可以清理掉,或者完全不拷贝过去。

再更进一步思考一下,我们既然是自己控制DLL在内存中的装载过程,并且只需要PE头中的部分信息,那么我们其实可以在加载前对DLL进行预处理,将需要的信息重新组合一个自定义结构,这样完全杜绝了PE头的特征。

避免申请RWX内存

大多数EDR都会HOOK内存申请类的API,以识别在进程中具有PAGE_EXECUTE_READWRITE权限的新内存块,从而使我们的DLL有被识别到的风险。

那么我们是否可以像我们常规shellcode执行的时候直接给这块新内存赋予RX权限呢,这是不行的,因为PE 的不同Section需要不同类型的权限。

所以我们可以把PE的不同section都赋予不同的权限,通过这样做,每个页面都将拥有自己的权限,并且我们永远不需要完整的 RWX 区域,这样分开后,每一个内存区域都像是一些垃圾内存,我们在执行DLLMain前将每一个Section的权限赋予即可:

「议题文档」高级恶意软件开发之RDI的进化
ReflectiveLoader这个展开后没有用的函数也被复制过去了

这个导出函数已经完成使命了,但是他还存在于DLL的.text段内,被复制到了新的内存中,这也是一个潜在的IOC,那么有没有什么方法,可以不让RDI这块代码区域被复制过去?

修复IAT的时候会有潜在的堆栈回溯检测

解决思路:

  1. 添加LoadLibrary时候的堆栈欺骗,比如ProxyDllLoad(#PIG 检测规则)
  2. 使用module stomping,但是UDRL执行解析功能之前要把自身也都放到模块里面去也可以配合提前内存对其的方式,实现起来比较复杂
  3. 修改beacon,wininet、winhttp相关的API动态导入
  4. 如果只是检测winhttp这类比较敏感的dll,那是不是加载一些会导入winhttp的但又不在检测规则里的就可以了

0x03 Evolution in RDI

反射DLL修补

修改现有的PE头,Patch汇编指令,去调用当前DLL导出的ReflectiveLoader函数地址,CS内实现:

「议题文档」高级恶意软件开发之RDI的进化
prepended RDI

前置式RDI,不再需要把ReflectiveLoader函数写在DLL内,而是把它放到DLL的前面,RDI向后去取DLL的基地址,展开后也不需要考虑ReflectiveLoader函数也被拷贝到新内存的问题,另外也不用制作包含ReflectiveLoader函数的DLL。

a. Double Pulsar

「议题文档」高级恶意软件开发之RDI的进化
double pulsar

b.sRDI

他可以把DLL文件转换为shellcode,具体是由两部分实现的,一部分是一个PIC化的RDI,他将RDI解析函数编译为shellcode拼接在原始DLL之前

Bootstrap shellcode + RDI shellcode + DLL Bytes + User data

另一部分是将RDI、DLL拼接在一起的代码。

c. CobaltStrike UDRL

前置式的RDI,需要确保RDI函数在shellcode最开始,并且要定位DLL文件基地址,可能有多种方法取寻,比如在DLL前面加一些TAG去遍历等。

这里使用了code_seg pragma 指令,code_seg可用于指定哪个部分用于存储特定函数。然后可以使用字母值对这些部分进行排序,例如.text$a.text$b

我们可以定义ReflectiveLoader函数在.text段的最开始,

「议题文档」高级恶意软件开发之RDI的进化

然后也可以定义一个LdrEnd()函数到z,使其在.text段的最尾部

「议题文档」高级恶意软件开发之RDI的进化

最终效果如下:

「议题文档」高级恶意软件开发之RDI的进化
前置rdi

我们可以灵活控制代码的位置还可以对核心部分进行压缩加密。

HOOK
「议题文档」高级恶意软件开发之RDI的进化

因为RDI中有对要加载的DLL做IAT修复的操作,并且由于目前CobaltStrike的beacon内的内存特征、以及一些API的调用堆栈问题,所以实际上在RDI中做HOOK一般是针对于beacon来进行的。

可以在UDRL(User-Defined Reflective Loader)里Hook Sleep函数去对内存实现加密;还可以Hook一些Wininet的API进行堆栈欺骗的操作等

0x04 Phantom Execution,Self Cleanup in Post-ex Job

「议题文档」高级恶意软件开发之RDI的进化

由于远程进程注入的研究/武器化成本过高,常规的远程进程注入基本无法绕过多数防护软件,所以很多人在使用CobaltStrike的时候,在执行功能模块时会让beacon自己注入自己,也就是让RDI函数在自己进程进行执行,从而规避被检测查杀。

当然这样会避免产生进程注入,牺牲了一定的稳定性,最终也可以同样完成执行功能模块的效果,但是当功能模块执行完毕,退出线程后,在内存中会是这样的:

「议题文档」高级恶意软件开发之RDI的进化

可以看到一块是包含RDI函数的shellcode:

「议题文档」高级恶意软件开发之RDI的进化

另一块是DLL在Beacon进程内展开的内存:

「议题文档」高级恶意软件开发之RDI的进化

所以功能模块rdi所展开的内存并没有被清理,上面是执行了一次,假如说我们执行五次截图操作:

「议题文档」高级恶意软件开发之RDI的进化

所以RDI展开后,除了已经清理掉的RDI代码,当插件执行完毕的时候内存中被展开的DLL,也应该被清理掉,确保内存干净不被扫描。

所以如何清理RDI过程后的残留内存?

那么怎么清理?当然是执行完后清理,那么如何知道插件执行完毕,所以要注意清理的时机。

如何知道插件执行完毕了,以及如何拿到RDI内部分配的内存地址?这需要干预插件DLLMain的代码,以及beacon内需要执行清理操作,不是很通用。

RDI本身是可以拿到分配的内存地址,所以,能不能让RDI来释放两块内存?

但是目前还差一个条件,需要寻找一个方法让RDI也可以知道DLL执行完毕的方法,RDI本身的清理可以参考KaynStrike的方式。

指针执行后,因为一些插件内部还有工作没有进行完毕,比如说screenshot,通过管道回传的结果还没有传回来,我们直接Free内存的话轻则拿不到结果,重则导致内存冲突问题。

那么是否可以考虑创建一个线程并等待,从而安全的执行后续的清理操作呢,我们在DLL中执行完毕后手动加入ExitThread(),在RDI以创建线程的方式去执行如下图,是可以成功清理掉新分配的内存的。

「议题文档」高级恶意软件开发之RDI的进化

清理完新的内存之后,我们可以再用构造ROP链的方式来清理掉RDI自身:

「议题文档」高级恶意软件开发之RDI的进化

那么这样我们就可以完美的在RDI中清理掉新旧内存,实现了干干净净的内存执行插件。

但是,这就结束了吗?

别忘了我们还需要在要执行的DLL中加入ExitThread(),还是不够完美,那么可不可以有一种不需要更改beacon,以及dll的方法?

那现在我们创建线程是可以,但是DLL内默认执行并不会出现退出线程的操作,所以我们又想到了ROP链,我们可以修改寄存器的值,构造一个退出线程的操作来,我们只需要将RSP同样设置为退出线程的操作即可,这意味着当 DllMain 返回时,会调用 RtlExitUserThread,从而安全地退出线程。

「议题文档」高级恶意软件开发之RDI的进化

这种实现实际上属于一种构造ROP(Return-Oriented Programming)链的形式。在这段代码中,虽然没有直接使用传统的ROP gadgets但它通过修改线程的上下文来控制程序的执行流,这与ROP的基本原理一致。我们通过创建新线程、设置特定的上下文、以及精心安排的堆栈布局来确保执行 DLL 的 DllMain 函数,同时避免内存访问冲突并实现线程的安全退出,实现了我们想要的特定功能。

x86下需要修改传参方式:

「议题文档」高级恶意软件开发之RDI的进化
那这次,真的结束了吗?还没有。

还有一个问题,万一某些DLL插件的执行时间过长?RDI中一直在等待,那么原始的RDI和原始DLL所在的内存就一直在停留,增加了被扫描的风险,这个风险点该如何解决呢?

我们可不可以再解决一个问题,

解决思路:

  1. 写一段shellcode,将RDI和DLL进行加密,这样就能一定程度杜绝扫描特征的发生。
  2. 当RDI的使命完成了,先把RDI的内存清理掉,然后接着执行新内存的DLLMain。

但是目前来看这当然有问题了,我们本身就靠RDI来清理后续的新内存,那么假如说清理掉RDI我们还怎么清理,并且清理掉RDI谁来执行DLLMain,这里也会产生冲突问题。

那我们可不可以再开发一段shellcode,去避免在RDI本身去执行这些引发的冲突问题。

「议题文档」高级恶意软件开发之RDI的进化

这段shellcode可以放到RDI的最尾部,供RDI最终去调用,去接收DLLMain的地址然后先清理掉RDI和原始DLL区域,接着按上面的方式执行DLLMain并清理新内存,最后清理shellcode自身。

「议题文档」高级恶意软件开发之RDI的进化

但是这其中涉及很多定位的问题,比较麻烦以及容易出错。

进一步优化后,我们发现可以将这段shellcode,在RDI申请新内存的时候,把他先拷贝到新内存,然后再拷贝DLL过去展开,这样RDI后面用DLLMain的地址、RDI的起始地址为参数,直接调用这个shellcode,shellcode先把RDI的地址清理掉后,接着只需要创建线程执行DLLMain然后等待线程结束,清理自身所在内存即可。

CleanJob的代码我们可以使用pragma 指令的特点,可以控制函数在text段的位置,所以不在需要额外编写shellcode在外部拼接,我们将其放到排序$y,也就是倒数第二个函数,CleanJob的功能就是接收DLLMain的地址以及RDI的起始地址,然后先清理原始RDI地址,接着执行DLLMain,等待结束后,清理当前新内存包含自身的内存空间:

「议题文档」高级恶意软件开发之RDI的进化

接着还需要定义一个函数,用来获取CleanJob函数的大小以及地址:

「议题文档」高级恶意软件开发之RDI的进化

然后就是在RDI申请空间的时候,将Clean函数先放进去:

「议题文档」高级恶意软件开发之RDI的进化

在新的内存空间就是:

「议题文档」高级恶意软件开发之RDI的进化

这样就解决了一些长时间插件的情况下,无用内存一直存在的问题,最终的流程如下图:

「议题文档」高级恶意软件开发之RDI的进化
「议题文档」高级恶意软件开发之RDI的进化

所以我们实现了一个新的,从RDI本身来执行并清理两块内存,而不用更改DLL本身的通用DLL加载以及清理方法:

「议题文档」高级恶意软件开发之RDI的进化

0x05 Conclusion

    本议题从背景知识,技术原理、OPSEC优化、历史发展等视角探讨了Reflective Dll Injection这项优秀的不落地执行技术,最后介绍了RDI技术在后渗透阶段的高级利用,以及技术实现细节和原理,实现在内存中无影无踪的执行后利用模块,展示了当今的高级恶意代码研究开发技术。

休假后,我会将完整代码以及PPT再做更新。


原文始发于微信公众号(黑客在思考):「议题文档」高级恶意软件开发之RDI的进化

版权声明:admin 发表于 2024年8月25日 下午8:53。
转载请注明:「议题文档」高级恶意软件开发之RDI的进化 | CTF导航

相关文章