首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C++ >> 關於C++

Windows NT/2000 進程隱藏技術深度解析:遠程執行緒注入 vs DLL 注入完整實作對比 Windows NT/2000 進程隱藏技術深度解析:遠程執行緒注入 vs DLL 注入完整實作對比

     編輯:關於C++

前言:什麼是「真正的進程隱藏」?

在 Windows NT/2000/XP/2003/Vista/7/8/10/11 上,任務管理器 (taskmgr)、pslist、Process Explorer 等工具透過系統 API (NtQuerySystemInformation, CreateToolhelp32Snapshot, EnumProcesses) 列舉**進程物件 (EPROCESS)**。若我們的程式碼以**獨立進程**形式執行 (擁有 PID、EPROCESS),**絕對無法**從這些工具隱藏——除非修補核心資料結構 (DKOM, Direct Kernel Object Manipulation) 或載入核心驅動程式 (Rootkit),屬於極高風險且現代 PatchGuard/虛擬化安全 (VBS/HVCI) 環境下極難實現。

本文探討的「進程隱藏」實為:**不建立獨立進程,而是將程式碼注入現有合法進程 (如 explorer.exe, svchost.exe, winlogon.exe) 的位址空間,以**遠程執行緒**形式執行**。任務管理器僅顯示宿主進程,我們的邏輯隱匿其中。

核心技術棧:VirtualAllocEx + WriteProcessMemory + CreateRemoteThread

  1. OpenProcess(PROCESS_ALL_ACCESS, ...):取得目標進程控制代碼 (需權限)。
  2. VirtualAllocEx(hProc, NULL, size, MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE):在目標進程配置可執行記憶體 (RWX)。
  3. WriteProcessMemory(hProc, pRemote, &localCode, size, &written):將本地機器碼 (Shellcode/執行緒函式) 複製過去。
  4. CreateRemoteThread(hProc, NULL, 0, (LPTHREAD_START_ROUTINE)pRemote, pParam, 0, &tid):在目標進程建立執行緒開始執行。

方案一:純遠程執行緒 (Shellcode 風格) — 免 DLL 檔案

挑戰:位址獨立程式碼 (PIC) 與參數傳遞

  • 編譯器產生的函式包含**相對呼叫**、**全域資料存取** (字串常數、__imp_ 導入表)、**堆疊框架**。直接複製機器碼到他程序會因位址不同而崩潰。
  • 解決:**手工編寫位址獨立 Shellcode** (組語),或**將所有外部相依 (API 位址、字串、結構) 打包成參數結構體**,傳遞指標給遠程執行緒函式。

參數結構體設計


struct TRemoteParams {
  // API 位址 (本地透過 GetProcAddress 取得,假設目標進程同模組基址 — ASLR 下不保證!)
  FARPROC pMessageBoxA; 
  FARPROC pLoadLibraryA;
  FARPROC pGetProcAddress;
  FARPROC pExitThread;
  // 字串資料
  char szTitle[64];
  char szText[256];
  char szDllPath[MAX_PATH]; // 若需載入 DLL
};

遠程執行緒函式 (必須為 __stdcall, 無堆疊框架優化)


// 使用 #pragma optimize("", off) 避免編譯器優化破壞堆疊布局
#pragma optimize("", off)
DWORD __stdcall RemoteThreadProc(TRemoteParams* p) {
  // 直接呼叫傳入的 API 位址
  typedef int (WINAPI *PFN_MessageBoxA)(HWND, LPCSTR, LPCSTR, UINT);
  ((PFN_MessageBoxA)p->pMessageBoxA)(NULL, p->szText, p->szTitle, MB_OK);
  return 0;
}
#pragma optimize("", on)

主程式注入流程


bool InjectRemoteThread(DWORD targetPid) {
  // 1. 開啟目標進程
  HANDLE hProc = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION |
                             PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ,
                             FALSE, targetPid);
  if (!hProc) return false;

  // 2. 準備參數 (本地填充)
  TRemoteParams localParams = {0};
  HMODULE hUser32 = LoadLibrary("user32.dll");
  HMODULE hKernel32 = GetModuleHandle("kernel32.dll");
  localParams.pMessageBoxA = GetProcAddress(hUser32, "MessageBoxA");
  strcpy_s(localParams.szTitle, "Injected!");
  strcpy_s(localParams.szText, "Hello from Remote Thread!");

  // 3. 配置遠程記憶體 (程式碼 + 參數)
  SIZE_T codeSize = 4096; // 頁對齊,足夠大
  LPVOID pRemoteCode = VirtualAllocEx(hProc, NULL, codeSize, MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE);
  LPVOID pRemoteParams = VirtualAllocEx(hProc, NULL, sizeof(TRemoteParams), MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
  if (!pRemoteCode || !pRemoteParams) { /* cleanup */ return false; }

  // 4. 寫入程式碼 (需確知確切大小 — 見下文難點)
  // 此處簡化:假設 RemoteThreadProc 大小 < codeSize
  if (!WriteProcessMemory(hProc, pRemoteCode, (LPCVOID)RemoteThreadProc, codeSize, NULL)) { /* cleanup */ return false; }
  if (!WriteProcessMemory(hProc, pRemoteParams, &localParams, sizeof(TRemoteParams), NULL)) { /* cleanup */ return false; }

  // 5. 建立遠程執行緒
  HANDLE hThread = CreateRemoteThread(hProc, NULL, 0, (LPTHREAD_START_ROUTINE)pRemoteCode, pRemoteParams, 0, NULL);
  if (!hThread) { /* cleanup */ return false; }

  // 6. 可選:等待執行緒結束、釋放記憶體
  WaitForSingleObject(hThread, 5000);
  VirtualFreeEx(hProc, pRemoteCode, 0, MEM_RELEASE);
  VirtualFreeEx(hProc, pRemoteParams, 0, MEM_RELEASE);
  CloseHandle(hThread);
  CloseHandle(hProc);
  return true;
}

核心難點:如何精確取得 RemoteThreadProc 機器碼大小?

  • 編譯器不保證函式連續、無內聯、無尾部填充。(BYTE*)RemoteThreadProc + size 不可靠。
  • 常見 Hack:
    1. 在函式前後放入標記函式 StartMark()/EndMark(),計算位址差 (但鏈接器可能重排)。
    2. 使用 __declspec(naked) 手寫組語殼 + 內嵌機器碼陣列。
    3. **最穩健**:改用方案二 (DLL 注入),或將邏輯編譯為 **獨立 DLL**,遠程執行緒僅呼叫 LoadLibrary。

方案二:DLL 注入 (LoadLibrary 法) — 工程化、可除錯、穩健

遠程執行緒起始位址 = LoadLibraryA (或 LoadLibraryW) 位址,參數 = 遠程記憶體中 DLL 完整路徑字串。

優勢

  • DLL 正常編譯、連結、除錯 (附加至宿主進程)。
  • 支援 C++ 類別、RTTI、例外、TLS、靜態變數初始化。
  • 無需手工計算 Shellcode 大小、處理位址重定位。
  • DLL 卸載時自動清理 (或呼叫 FreeLibrary)。

實作步驟

  1. 建立 DLL 專案 (Injected.dll),DllMain 處理 DLL_PROCESS_ATTACH 啟動業務執行緒/Hook/服務。
  2. 主程式:
    a. VirtualAllocEx 寫入 DLL 路徑字串 (含結束 NULL)。
    b. GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA") 取得位址 (假設 kernel32 基址相同 — **ASLR 下需修正**,見下)。
    c. CreateRemoteThread 呼叫 LoadLibraryA。
    d. 等待執行緒結束,GetExitCodeThread 取得 HMODULE (DLL 基址)。

ASLR (Address Space Layout Randomization) 影響與對策

Vista+ 預設啟用 ASLR,kernel32.dll、user32.dll 在各進程基址**不同**。本地取得的 LoadLibraryA 位址**不能**直接用於遠程進程。

  • 對策 1 (簡單): 目標進程為 **32 位元** 且系統啟用 KASLR 但 **kernel32 基址在 32 位元進程間通常相同** (早期版本)。**不可靠**。
  • 對策 2 (標準):** 在遠程進程中解析 LoadLibrary 位址。
    - 手法:注入一小段 Shellcode,內含 GetModuleHandle("kernel32.dll") + GetProcAddress("LoadLibraryA") 邏輯 (需解析 kernel32 基址,可透過 PEB Ldr 列舉)。
    - 或使用 CreateRemoteThread 呼叫 LoadLibrary 時,傳遞 **DLL 完整路徑**,但起始位址需為遠程進程的 LoadLibrary。
    - **最簡通用**:使用 NtCreateThreadEx (未文件化) + RtlCreateUserThread 配合 LdrLoadDll,或直接用 **DLL 注入工具庫** (如 Microsoft Detours, EasyHook, MinHook) 封裝細節。
  • 對策 3 (企業級):** 簽署驅動程式,透過 PsCreateSystemThread / KeStackAttachProcess 在核心層注入,繞過用戶態 ASLR/權限限制。

權限提升:SeDebugPrivilege (EnableDebugPriv)

注入系統進程 (lsass.exe, csrss.exe, winlogon.exe, services.exe) 需 SeDebugPrivilege。


BOOL EnableDebugPriv() {
  HANDLE hToken; TOKEN_PRIVILEGES tp; LUID luid;
  if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) return FALSE;
  if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid)) { CloseHandle(hToken); return FALSE; }
  tp.PrivilegeCount = 1; tp.Privileges[0].Luid = luid; tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
  BOOL ok = AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
  CloseHandle(hToken);
  return ok && GetLastError() != ERROR_NOT_ALL_ASSIGNED;
}

**注意**:啟用此特權本身需管理員權限 (UAC 提權)。現代 EDR 會監控 AdjustTokenPrivileges(SeDebugPrivilege) 行為。

目標進程 PID 取得方式

  • CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS) + Process32First/Next 比對 szExeFile。
  • EnumProcesses (PSAPI) + OpenProcess + GetModuleFileNameEx。
  • WMI Win32_Process 查詢。
  • NtQuerySystemInformation(SystemProcessInformation) 核心級最完整。

兩大方案終極對比

維度純遠程執行緒 (Shellcode)DLL 注入 (LoadLibrary)
檔案落地**無** (純記憶體),極難取證需 DLL 檔案 (可加密、資源內嵌、記憶體載入 LdrLoadDll 避免落地)
開發難度極高 (組語/PIC、大小計算、API 解析、ASLR、例外處理)低 (標準 C/C++、完整 CRT、可除錯)
功能豐富度受限 (無 CRT、無 C++ 特性、棧/堆管理困難)完整 (支援所有語言特性、第三方庫)
穩定性低 (微小錯誤即崩潰宿主)高 (載入器處理重定位、TLS、靜態初始化)
反偵測優 (無模組載入事件、無磁碟檔案、記憶體掃描難發現)較弱 (載入 DLL 觸發 LoadImage 通知、磁碟檔案可掃描)
適用場景高隱蔽需求、無檔案攻擊/防禦測試、Shellcode 執行器功能複雜、長期駐留、Hook/子系統、合法軟體擴充

防禦與偵測視角 (藍隊必讀)

  • ETW (Event Tracing for Windows): Microsoft-Windows-Kernel-Process/Thread 記錄 CreateRemoteThread, VirtualAllocEx(RWX), WriteProcessMemory。
  • EDR 用戶態 Hook: 監控 ntdll!NtCreateThreadEx, kernel32!CreateRemoteThread, kernel32!VirtualAllocEx 參數異常 (RWX、跨進程)。
  • 掃描可疑記憶體區域: 掃描進程 MEM_PRIVATE + PAGE_EXECUTE_READWRITE 區域、無對應模組的執行緒起始位址。
  • DLL 載入監控: PsSetLoadImageNotifyRoutine (核心) / AppInit_DLLs / KnownDLLs 監控非預期 DLL 載入。
  • 緩解措施: 啟用 Arbitrary Code Guard (ACG), Control Flow Guard (CFG), Export Address Filtering (EAF), Strict Handle Validation (Windows 10 1809+ / 11)。

法律與道德紅線

  1. **未經授權**向他人電腦注入程式碼、隱藏進程、竊取資料、繞過安全產品,均屬**違法** (電腦犯罪、資安法、個資法)。
  2. 本技術僅供:**授權滲透測試**、安全產品研發 (EDR/AV)、惡意軟體分析 (沙箱注入樣本)、合法軟體架構設計 (外掛系統、熱更新、進程間協作)。
  3. 實作時**必須**加入:授權檢查、操作審計日誌、可移除機制、資料加密、最小權限原則。

結語

Windows 進程隱藏的本質是**代碼注入與執行上下文劫持**。純遠程執行緒 (Shellcode) 提供極致隱蔽但開發維護極難;DLL 注入則是工程化、可維護、功能完整的主流選擇。現代環境下 ASLR、CFG、ACG、EDR、HVCI 疊加防禦,使得用戶態注入愈發困難。作為開發者/研究員,應專注於**合法授權場景下的架構設計**,並深入理解防禦機制以構建更安全的系統。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved