Process Hollowing在64位进程中的简单实现
首先,创建一个挂起状态的合法进程(比如notepad进程),然后再使用ZwUnmapViewOfSection
或NtUnmapViewOfSection
将合法的notepad模块占据的内存空间给unmap掉。接下来,向notepad的内存空间中写入恶意的PE文件,并通过修改进程的context,将入口点改为恶意PE文件的入口点。最后,使用ResumeThread
使notepad恢复执行,从而达到在notepad进程空间中运行恶意PE文件的效果。这种方法就是Process Hollowing。
本文大量参考Leitch, J. (n.d.). Process Hollowing.这篇文章。
获取一个符合要求的PE文件
Process Hollowing需要将一个PE文件手动加载到其他进程的内存空间中。因此,首先要获取一个PE文件。根据Leitch, J. (n.d.). Process Hollowing.这篇文章的说法,PE文件需要满足以下要求:
To successfully perform process hollowing the source image must meet a few requirements:
- To maximize compatibility, the subsystem of the source image should be set to windows.
- The compiler should use the static version of the run-time library to remove dependence to the Visual C++ runtime DLL. This can be achieved by using the /MT or /MTd compiler options.
- Either the preferred base address (assuming it has one) of the source image must match that of the destination image, or the source must contain a relocation table and the image needs to be rebased to the address of the destination. For compatibility reasons the rebasing route is preferred. The /DYNAMICBASE or /FIXED:NO linker options can be used to generate a relocation table.
首先,为了增强PE文件的兼容性,subsystem需要设为windows。在实际操作中,我发现如果subsystem设置成了console,则PE文件无法注入Windows窗口程序(如notepad,calc),只能注入控制台程序(如cmd)。
另外,PE文件不应该依赖Visual C++ runtime DLL,这可以通过在编译时使用/MT或/MTd选项解决。
最后,如果PE文件无法加载到预定的基地址,还需进行重定位操作。不过本文只考虑PE文件可以加载到预定基地址的情况,因此不会进行重定位的操作。
根据上述三个要求,编写如下代码:
1 |
|
然后使用Visual Studio的命令行工具编译:
1 | > vsdevcmd -arch=amd64 |
根据PE文件内容将其加载到内存
假设已经将PE文件全部读入内存,并且保存到了一个buf
数组当中,现在要获取其装入内存后的情况,并且保存在另一个数组mem
中。
首先,找到e_lfanew,并据此定位到其NT映像头:
1 | //读取DOS文件头,获取e_lfanew |
NT映像头中包含一些信息,比如可执行文件默认装入的地址ImageBase,装入内存后映像的总尺寸SizeOfImage,程序入口点AddressOfEntryPoint等。根据SizeOfImage就可以知道需要多大的内存来保存PE文件装入内存的状态,而ImageBase和AddressOfEntryPoint则在后面的操作需要用到:
1 | //根据可选头的SizeOfImage分配内存 |
NT映像头中还包含一个SizeOfHeaders,根据这一数据将PE文件的头部复制到内存中:
1 | //将文件头复制到内存中 |
最后,根据节表包含的信息将每一节依次装载到内存中的特定位置:
1 | //获取节表起始位置 |
将上述过程写成load_pe64
函数,其完整内容如下:
1 | unsigned char *load_pe64(unsigned char *buf, int *pSizeOfImage, ULONGLONG *pImageBase, DWORD *pEntryPoint) { |
编写程序实现Process Hollowing
创建挂起的notepad进程
创建挂起状态的子进程只需要在CreateProcessA的时候加一句CREATE_SUSPENDED就行了。
1 | //创建挂起的notepad进程 |
获取notepad加载的基地址
进程真实加载的地址需要从PEB中找到,所以先要找到PEB的基地址。一种方法是通过NtQueryInformationProcess找到PEB基地址。
1 | //获取notepad进程的PEB地址 |
如果查看微软关于PEB的官方文档,会发现PEB中很多项都是Reserved。要想知道PEB中每一项真实的含义是什么,可以在别的网站上看:
PEB (Process Enviroment Block)
然后就可以发现,官方文档中的Reserved3[1]
这一项实际上就是ImageBaseAddress,据此就可以找到notepad模块的基地址:
1 | //PEB的Reserved3[1]就是notepad进程加载的基地址 |
知道了notepad的基地址后,就可以通过NtUnmapViewOfSection将notepad占据的内存给unmap掉了:
1 | //unmap合法内存的代码 |
将PE文件加载到notepad内存空间中
将想要加载的PE文件读入内存:
1 | //读取source.exe文件,将PE文件内容全部放入内存 |
然后,调用之前写的load_pe64
函数,将PE文件按照文件结构载入内存中:
1 | //根据PE文件内容生成PE文件载入内存的状态 |
最后,使用WriteProcessMemory,将这块内存写到notepad的内存空间中:
1 | //VirtualAllocEx申请一块内存加载source.exe |
由于没有进行重定位操作,这里申请的内存起始地址必须为可执行文件默认装入的内存地址,即PE文件中的ImageBase。
PEB中的ImageBaseAddress这一项也得进行相应的修改:
1 | //修改PEB中的ImageBase |
修改进程的context
CONTEXT结构体中存储了一些寄存器的值,可以通过设置notepad进程的context设置它的寄存器的值。
当进程以挂起状态被创建时,它的入口点被存储在了寄存器当中。在32位进程中,存储入口点的寄存器为EAX,所以Leitch, J. (n.d.). Process Hollowing.这篇文章会设置CONTEXT结构体中的Eax这一项。
在64位进程中,存储入口点的寄存器变成了RCX。可以在创建了挂起的notepad进程后,使用x64dbg附加到这个进程上,看一下各个寄存器的值:
其中RCX的值就是<notepad.EntryPoint>。
由于PE文件已经加载到了notepad内存空间中,EntryPoint也发生了相应的变化,故需要对RCX寄存器的值进行修改:
1 | //修改notepad进程的context,将入口点(rcx寄存器)设置为source.exe的入口点 |
让挂起的notepad恢复运行
最后,使用ResumeThread恢复notepad的运行:
1 | ResumeThread(pi.hThread); |
然后就能发现,运行的并不是notepad程序,而是一个弹窗程序,这就说明成功进行了进程的替换。
完整代码
1 |
|
编译运行:
1 | > cl hollow.c |