在 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) 的位址空間,以**遠程執行緒**形式執行**。任務管理器僅顯示宿主進程,我們的邏輯隱匿其中。
OpenProcess(PROCESS_ALL_ACCESS, ...):取得目標進程控制代碼 (需權限)。VirtualAllocEx(hProc, NULL, size, MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE):在目標進程配置可執行記憶體 (RWX)。WriteProcessMemory(hProc, pRemote, &localCode, size, &written):將本地機器碼 (Shellcode/執行緒函式) 複製過去。CreateRemoteThread(hProc, NULL, 0, (LPTHREAD_START_ROUTINE)pRemote, pParam, 0, &tid):在目標進程建立執行緒開始執行。__imp_ 導入表)、**堆疊框架**。直接複製機器碼到他程序會因位址不同而崩潰。
struct TRemoteParams {
// API 位址 (本地透過 GetProcAddress 取得,假設目標進程同模組基址 — ASLR 下不保證!)
FARPROC pMessageBoxA;
FARPROC pLoadLibraryA;
FARPROC pGetProcAddress;
FARPROC pExitThread;
// 字串資料
char szTitle[64];
char szText[256];
char szDllPath[MAX_PATH]; // 若需載入 DLL
};
// 使用 #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;
}
(BYTE*)RemoteThreadProc + size 不可靠。StartMark()/EndMark(),計算位址差 (但鏈接器可能重排)。__declspec(naked) 手寫組語殼 + 內嵌機器碼陣列。LoadLibrary。遠程執行緒起始位址 = LoadLibraryA (或 LoadLibraryW) 位址,參數 = 遠程記憶體中 DLL 完整路徑字串。
FreeLibrary)。Injected.dll),DllMain 處理 DLL_PROCESS_ATTACH 啟動業務執行緒/Hook/服務。VirtualAllocEx 寫入 DLL 路徑字串 (含結束 NULL)。GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA") 取得位址 (假設 kernel32 基址相同 — **ASLR 下需修正**,見下)。CreateRemoteThread 呼叫 LoadLibraryA。GetExitCodeThread 取得 HMODULE (DLL 基址)。Vista+ 預設啟用 ASLR,kernel32.dll、user32.dll 在各進程基址**不同**。本地取得的 LoadLibraryA 位址**不能**直接用於遠程進程。
KASLR 但 **kernel32 基址在 32 位元進程間通常相同** (早期版本)。**不可靠**。LoadLibrary 位址。GetModuleHandle("kernel32.dll") + GetProcAddress("LoadLibraryA") 邏輯 (需解析 kernel32 基址,可透過 PEB Ldr 列舉)。CreateRemoteThread 呼叫 LoadLibrary 時,傳遞 **DLL 完整路徑**,但起始位址需為遠程進程的 LoadLibrary。NtCreateThreadEx (未文件化) + RtlCreateUserThread 配合 LdrLoadDll,或直接用 **DLL 注入工具庫** (如 Microsoft Detours, EasyHook, MinHook) 封裝細節。PsCreateSystemThread / KeStackAttachProcess 在核心層注入,繞過用戶態 ASLR/權限限制。注入系統進程 (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) 行為。
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/子系統、合法軟體擴充 |
Microsoft-Windows-Kernel-Process/Thread 記錄 CreateRemoteThread, VirtualAllocEx(RWX), WriteProcessMemory。ntdll!NtCreateThreadEx, kernel32!CreateRemoteThread, kernel32!VirtualAllocEx 參數異常 (RWX、跨進程)。MEM_PRIVATE + PAGE_EXECUTE_READWRITE 區域、無對應模組的執行緒起始位址。PsSetLoadImageNotifyRoutine (核心) / AppInit_DLLs / KnownDLLs 監控非預期 DLL 載入。Arbitrary Code Guard (ACG), Control Flow Guard (CFG), Export Address Filtering (EAF), Strict Handle Validation (Windows 10 1809+ / 11)。Windows 進程隱藏的本質是**代碼注入與執行上下文劫持**。純遠程執行緒 (Shellcode) 提供極致隱蔽但開發維護極難;DLL 注入則是工程化、可維護、功能完整的主流選擇。現代環境下 ASLR、CFG、ACG、EDR、HVCI 疊加防禦,使得用戶態注入愈發困難。作為開發者/研究員,應專注於**合法授權場景下的架構設計**,並深入理解防禦機制以構建更安全的系統。