反射型DLL注入及sRDI
Reflective Loader
最初的目的是为了实现从内存中加载dll文件,而不需要从磁盘上读取dll文件
反射型dll是结构特殊的dll文件,包含自己的PE loader,这个loader
作为一个导出函数ReflectiveLoader()
,当这个方法被调用时,会开始解析其所需要的导入函数(kernel32.dll
等),然后申请在进程空间一段内存,然后把文件头和段数据拷贝到内存中,然后遍历Attack.dll
的IAT表,完成函数地址的重定位,然后调用dll文件的入口函数DllMain()
,也就是在ReflectiveLoader()
函数中手动触发DllMain()
反射型dll注入与其他dll注入不同的是,其不需要使用LoadLibrary
这一函数,而是自己来实现整个装载过程。我们可以为待注入的DLL添加一个导出函数,ReflectiveLoader
,这个函数的功能就是装载它自身。
由于是自己实现,因此不会利用系统自身Loadlibrary
,不会“注册”到系统,不会被系统记录。也不会被ProcessExplorerer
发现。
要实现反射型DLL注入需要两个部分,注射器和被注入的DLL。
注射器的执行流程:
1 | 1.将待注入DLL读入自身内存(避免落地) |
反射dll注入思路:
1 | 1.根据需要注入的进程,向服务器申请dll下发; |
Source Code
参考https://github.com/stephenfewer/ReflectiveDLLInjection/blob/master/dll/src/ReflectiveLoader.c
1 | //===============================================================================================// |
获取文件加载基址
首先看最开始,获取当前内存文件的加载基址
1 | uiLibraryAddress = caller(); |
这个caller的定义在
1 | __declspec(noinline) ULONG_PTR caller( VOID ) { return (ULONG_PTR)_ReturnAddress(); } |
这个函数_ReturnAddress
返回当前调用函数返回的地址,即函数下一条指令地址
caller()
函数调用了 ReturnAddress()
函数,返回地址为call caller()
汇编代码的下一条指令的地址
因为我们获取的这个地址uiLibraryAddress
肯定是在.text
段,按照PE文件的结构,如果反向往低地址方向找,肯定能找到DOS Header
1 | while( TRUE ) |
定位所需的DLL文件的导出函数
这里使用的是PEB,主要函数也就是kernel32.dll
和ntdll.dll
1 | // STEP 1: process the kernels exports for the functions our loader needs... |
将恶意dll文件加载到新的数据段
1 | // STEP 2: load our image into a new permanent location in memory... |
拷贝恶意dll中的所有的节数据
1 | // STEP 3: load in all of our sections... |
这里为什么要用PointerToRawData
字段,因为反射型dll注入的过程中,dll文件不是正常由操作系统加载进目标进程的,而是一种类似于机械的拷贝的动作,它在内存中的布局和在磁盘文件中的布局保持一致
PS:也包括导入表和导出表的数据
修复导入函数表
1 | // STEP 4: process our images import table... |
1 | uiAddressArray += ( ( IMAGE_ORDINAL( ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal ) - ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->Base ) * sizeof(DWORD) ); |
这行代码的目的是根据 导入的序号来从目标 DLL 的导出表中找到对应的函数地址。它通过序号来计算并定位目标函数的地址,然后将其写入导入地址表中。
((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal
,uiValueD
是指向导入表中某个条目的指针。每个条目要么是按名称导入的函数,要么是按序号 导入的函数。在这个条目中,u1.Ordinal
表示导入的函数序号
IMAGE_ORDINAL(((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal)
,IMAGE_ORDINAL
是一个宏,它的作用是从 Ordinal
中提取出一个有效的函数序号
IMAGE_ORDINAL(((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal) - ((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->Base
,计算出导入的函数序号和导出表中的基础序号(Base)之间的偏移量。也就是说,它计算出目标 DLL 中的导出函数序号相对于导出表中基础序号的偏移。
修复重定位表
1 | // STEP 5: process all of our images relocations... |
Demo
implant.dll
1 |
|
很好理解,在dll被加载的时候,调用Go()
函数,Go()
函数就是解密shellcode,弹出计算器
ReflectiveDLLInjection.h
1 | //===============================================================================================// |
ReflectiveLoader.c,就是GitHub
ReflectiveLoader.h
1 | //===============================================================================================// |
compile.bat
1 | @ECHO OFF |
先运行上面的compile.bat,编译成dll
implant.cpp
1 |
|
compile.bat
1 | @ECHO OFF |
在上面的implant.cpp中,这个cpp会被编译成.exe文件,这一段就是加密之后的dll文件的密钥
1 | // reflective DLL payload |
当exe运行的时候,会解密这个payload成dll文件,然后payload变量就是指向了dll文件在内存中的起始地址(这个时候还是磁盘文件的布局),然后调用GetReflectiveLoaderOffset()
函数,去获取这个dll文件的导出函数的地址,然后调用ReflectiveLoader()
函数,将恶意dll文件在内存中展开,最后触发DllMain()
函数
加密dll的代码
1 | # Red Team Operator course code template |
运行demo
加密dll
1 | python aes.py implant.dll |
会输出密钥和加密之后的payload,然后把密钥和payload丢到implant.cpp中,生成exe,成功运行
调试分析的话,可以在代码的一些位置打上断点
1 | if ( rv != 0 ) { |
__debugbreak();
,会让CPU进入int 3
的断点,如果此条件下生成的exe程序不在debug中运行就抛出异常,然而没有异常处理代码,所以程序会直接崩溃
为了防止ReflectiveLoader这个字符串在内存中被杀软捕获,可以修改这里的宏定义
1 | #define REFLDR_NAME "ReflectiveLoader" |
Shellcode RDI
如果没有dll的源码,能做什么?只有编译好的dll,能怎么运用呢?
参考
作者提出了Improved Reflective DLL Injection
,旨在允许在DLLMain
之后调用附加函数,并支持将用户参数传递到该附加函数中,可以加载 DLL,调用其入口点,然后将数据传递给另一个导出函数。
用法如下:
implant.cpp,可以看到在dll main中并没有调用Go()
函数,只是把Go()
函数定义为导出函数
1 |
|
然后正常编译成dll文件
然后进入sRDI目录的python目录下
1 | C:\Users\Jack\Documents\malware\RTO-MDI\MDI\03.ReflectiveCode\02.Binary>python .\sRDI\Python\ConvertToShellcode.py --help |
-f
参数指定需要调用的函数名,-u
可以为函数传递参数
1 | python .\sRDI\Python\ConvertToShellcode.py -f Go implant.dll |
执行完毕之后会生成一个.bin文件,aes加密这个.bin文件,然后把payload和key放到代码里去
1 |
|
可以看到这个代码并没有定位ReflectiveLoader
函数的位置,而是直接执行payload
一个断点调试的技巧
比如上面的代码,我们要断到payload里
1 | int main(void) { |
然后编译成exe,运行,然后打开Process Hacker,找到进程,双击,进入Memory,然后找到对应的段,可以看到payload已经被加载了
如果想在xdbg里断到这里面,在Memory的地方右键,change protection,修改为0x40
然后把payload里的第一字节修改为cc
然后点击Write(不是save)
在x64dbg里attach这个进程,终端里回车,再多按几次回车,xdbg里运行几次,成功断在payload里
然后把cc这里修改为e8也就是原来的payload,右键binary->edit
现在要断在执行th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE) exec_mem, 0, 0, 0);
这一句的位置,
xdbg里进入symbols,进入kernel32.dll
找到createthread并打上断点
执行,观察call stack里的调用栈信息,可以看到断在了kernel32.dll里,是0000006D2FAFFCB0
处调用的,双击进入0000006D2FAFFCB0
这里,成功定位到CreateThread
的代码
此外,执行到CreateThread的时候,r8寄存器存储的是第三个参数,也就是shellcode的地址(64位下的传参约定),右键r8,follow in dump,可以看到就是解密之后的shellcode