编写具有自我重定位功能的64位代码并注入其他进程
进程注入的一种实现方法是将恶意代码直接复制到目标进程的内存空间,并通过CreateRemoteThread在目标进程中执行这段恶意代码。这个方法的一个难点在于,恶意代码复制到目标进程的内存空间后,它的基地址可能会发生变化。假如说恶意代码需要对自身某个特定地址的数据进行访问,就会访问不到这个数据,因为数据的地址已经改变了。为了解决这一问题,需要进行重定位的操作。
一种重定位的方法是:在注入恶意代码之前,对代码内容进行预处理,根据实际申请到的目标进程内存首地址,修正恶意代码中实际地址与预期地址的差异。这一过程可以借助重定位表来完成,重定位表中包含一个数组,记录了代码中需要重定位的数据的相对虚拟地址RVA。
另一种方式就是恶意代码自身进行重定位,比如下面这段代码:
1 | call reloc |
这段代码执行完毕后,rbx寄存器中就保存了真实地址和预期地址的差值。接下来,假设想要获得变量Variable
的真实地址,则可以执行这段代码:
1 | mov rax, offset Variable |
执行完毕后,rbx中就包含变量Variable
的真实地址了。
编写具有自我重定位功能的64位汇编代码
获得64位汇编代码
首先选择一个64位汇编的编译器。本文选用的是Visual Studio提供的ml64.exe。MASM for x64 (ml64.exe)
如果对ml64.exe不太熟悉,可以先编写C语言代码,然后再使用Visual Studio的命令行工具将C语言文件转换成汇编语言文件。
要想使用Visual Studio的命令行工具,可以先运行VsDevCmd.bat批处理文件,然后就能直接使用各种命令行工具,而不用输入路径名了。VsDevCmd.bat通常位于Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\Common7\Tools
文件夹下。
首先编写一个无限弹窗C语言代码:
1 |
|
保存到shellcode.c文件中。接下来,使用Visual Studio的命令行工具cl.exe编译:
1 | > vsdevcmd -arch=amd64 |
选项/FA就可以让其产生汇编代码文件shellcode.asm。不过这个时候shellcode.asm内容比较复杂,编译得到的shellcode.exe也比较大(大约90KB)。如果可以简化shellcode.asm,并适当缩小shellcode.exe的体积将会更有利于后续分析。
对shellcode.asm进行修改,得到:
1 | INCLUDELIB user32.lib |
删掉了一些不必要的INCLUDE语句,仅保留一个INCLUDELIB,并删去一些不需要的节,如pdata和xdata。此外,还将数据节中的数据转移到代码节,并将数据节给删掉了。
其实我还改了一个地方,就是将第14行和第15行的lea指令修改成mov指令。如果是lea指令,则这里使用的是相对寻址,不需要进行重定位的操作。为了说明重定位的原理,将其修改为mov指令,从而让这里使用绝对寻址。
使用ml64.exe将修改后的shellcode.asm编译链接成可执行文件shellcode.exe:
1 | > ml64 shellcode.asm /link /ENTRY:main |
shellcode.exe的体积缩小到了仅有大约3KB。
添加自我重定位功能
在shellcode.asm中,调用MessageBoxA前会进行一系列传参操作。其中,传入Caption和Text的地址使用的是绝对地址。代码注入其他进程后,这个地址可能会发生改变,因此需要进行重定位操作。
1 | .text:000000014000101A 41 B9 10 00 00 00 mov r9d, 10h ; uType |
添加重定位操作后的代码为:
1 | INCLUDELIB user32.lib |
rbx寄存器保存的就是Caption的真实地址,Caption的长度为11,因此Text的地址就为rbx+11。
用LoadLibraryA和GetProcAddress代替MessageBoxA
MessageBoxA是user32.dll提供的一个函数。但是,并不是所有进程都会加载user32.dll,对于那些没有加载user32.dll的进程,注入的代码是无法正常运行的。不过,几乎所有进程都会加载kernel32.dll,而kernel32.dll中又包含LoadLibraryA和GetProcAddress,可以利用它们加载user32.dll并获取MessageBoxA的地址。
1 | INCLUDELIB kernel32.lib |
处理调用系统API的语句
现在的代码中有两个调用系统API的语句:
1 | call QWORD PTR __imp_LoadLibraryA |
将代码注入目标进程后,这两条语句就有可能不奏效了,因此把这两条语句处理一下。首先找到LoadLibraryA和GetProcAddress的地址,可以写一个很简单的C语言程序来找:
1 |
|
在我当前的电脑上,运行结果为:
1 | LoadLibraryA Address: 00007FFFC185EBB0 |
因此,对这两条语句作如下修改:
1 | mov rax, 00007FFFC185EBB0H ;LoadLibraryA入口地址 |
这里用到了一个性质,就是对于一台电脑上运行的多个进程,系统API的虚拟地址通常是相等的。不过我希望这个代码还能在其他机器上运行,所以这里的00007FFFC185EBB0H
和00007FFFC185A360H
只是临时的,在注入其他进程之前还会被修改。
最终得到的代码
1 | _TEXT |
使用ml64.exe生成可执行文件:
1 | > ml64 shellcode.asm /link /ENTRY:main |
然后,再将shellcode.exe拖入IDA Pro中,并将代码部分拖黑,再选择Edit->Export Data,将其导出为C语言数组形式:
1 | unsigned char ida_chars[] = |
编写进程注入程序
获得了C语言数组形式的代码后,接下来要做的就是将代码注入到目标进程的内存空间中,然后在目标进程中执行这段代码了。不过在这之前,先要处理一下LoadLibraryA和GetProcAddress的地址。
之前的代码假定LoadLibraryA和GetProcAddress的地址分别为00007FFFC185EBB0H
和00007FFFC185A360H
,而这并不总是成立的。所以,先将这两个数值替换为LoadLibraryA和GetProcAddress的实际地址:
1 | //获取LoadLibraryA和GetProcAddress的真实地址 |
然后,就可以将数组ida_chars的内容写到目标进程的内存空间,并CreateRemoteThread开启远程线程执行这段代码了:
1 | //将ida_chars[]写入目标进程内存空间 |
完整代码
1 |
|