在內核里操作進程
在內核里操作進程,相信是很多對 WINDOWS 內核編程感興趣的朋友第一個學習的知識點。但在這里,我要讓大家失望了,在內核里操作進程沒什么特別的,就標准方法而言,還是調用那幾個和進程相關的 NATIVE API 而已(當然了,本文所說的進程操作,還包括對線程和 DLL 模塊的操作)。本文包括 10 個部分:分別是:枚舉進程、暫停進程、恢復進程、結束進程、枚舉線程、暫停線程、恢復線程、結束線程、枚舉 DLL 模塊、卸載 DLL 模塊。
1.枚舉進程。進程就是活動起來的程序。每一個進程在內核里,都有一個名為 EPROCESS 的巨大結構體記錄它的詳細信息,包括它的名字,編號(PID),出生地點(進程路徑),老爹是誰(PPID 或父進程 ID)等。在 RING3 枚舉進程,通常只要列出所有進程的編號即可。不過在 RING0 里,我們還要把它的身份證(EPROCESS)地址給列舉出來。順帶說一句, 現實中男人最怕的事情 就是“ 喜當爹” , 這種事情在內核里更加容易發生。因為 EPROCESS 里 有 且只有 一個 成員 是記錄父進程 ID 的,稍微改一下,就可以認任意進程為爹了。枚舉進程的方法很多,標准方法是使用 ZwQuerySystemInformation 的 SystemProcessInformation 功能號,不過如果在內核里也這么用的話,那就真是脫了褲子放屁——多此一舉。因為在內核里使用這個函數照樣是得不到進程的 EPROCESS 地址,而且一旦內存出錯,還會藍屏,更加逃不過任何隱藏進程的手法。 所以在 內核里 穩定 又不失 強度 的 枚舉 進程方法舉 是枚舉 PspCidTable , 它能最大的好處是能得到進程的 EPROCESS 地址 , 而且 能 檢查出 使用“ 斷鏈 ” 這種低級 手法 的隱藏進程。不過話也說回來,枚舉 PspCidTable 並不是一件很爽的事情,因為 PspCidTable 是一個 不公開的變量,要 獲得它地址 的話,必然 要 使用硬編碼或者符號。所以 我的 方法是:變相枚舉 PspCidTable。內核里有個函數叫做 PsLookupProcessByProcessId,它能通過進程 PID 查到進程的 EPROCESS,它的內部實現正是枚舉了 PspCidTable。PID 的范圍是從 4 開始,到MAX_INT(2^31-1)結束,步進為 4。但實際上,大家見到的 PID 基本都是小於 10000 的,而上 10000 的 PID 相信很多人都沒有見過。所以我們實際的枚舉范圍是 4~2^18,如果PsLookupProcessByProcessId 返回失敗,則證明此進程不存在,如果返回成功,則把 EPROCESS、PID、PPID、進程名打印出來。
1.枚舉進程 //聲明 API NTKERNELAPI UCHAR* PsGetProcessImageFileName(IN PEPROCESS Process); NTKERNELAPI HANDLE PsGetProcessInheritedFromUniqueProcessId(IN PEPROCESS Process); //根據進程 ID 返回進程 EPROCESS,失敗返回 NULL PEPROCESS LookupProcess(HANDLE Pid) { PEPROCESS eprocess = NULL; if (NT_SUCCESS(PsLookupProcessByProcessId(Pid, &eprocess))) return eprocess; else return NULL; } //枚舉進程 VOID EnumProcess() { ULONG i = 0; PEPROCESS eproc = NULL; for (i = 4; i<262144; i = i + 4) { eproc = LookupProcess((HANDLE)i); if (eproc != NULL) { DbgPrint("EPROCESS = %p, PID = %ld, PPID = %ld, Name = %s\n", eproc, (DWORD)PsGetProcessId(eproc), (DWORD)PsGetProcessInheritedFromUniqueProcessId(eproc), PsGetProcessImageFileName(eproc)); ObDereferenceObject(eproc); } } } 2.暫停進程。暫停進程就是暫停進程的活動,但是不將其殺死。暫停進程在 VISTA 之后有導 出的函數:PsSuspendProcess。它的函數原型很簡單: NTKERNELAPI //聲明要使用此函數 NTSTATUS //返回類型 PsSuspendProcess(PEPROCESS Process); //唯一的參數是 EPROCESS 3.恢復進程。恢復進程就是讓被暫停進程的恢復活動,是上一個操作的反操作。恢復進程在 VISTA 之后有導出的函數:PsResumeProcess。它的函數原型很簡單: NTKERNELAPI //聲明要使用此函數 NTSTATUS //返回類型 PsResumeProcess(PEPROCESS Process); //唯一的參數是 EPROCESS 4.結束進程。結束進程的標准方法就是使用 ZwOpenProcess 打開進程獲得句柄,然后使用 ZwTerminateProcess 結束,最后使用 ZwClose 關閉句柄。除了這種方法之外,還能用使用內 存清零的方式結束進程,后者使用有一定的危險性,可能在特殊情況下發生藍屏,但強度比 前者大得多。在 WIN64 不可以搞內核 HOOK 的大前提下,后者可以結束任何被保護的進程。 //正規方法結束進程 void ZwKillProcess() { HANDLE hProcess = NULL; CLIENT_ID ClientId; OBJECT_ATTRIBUTES oa; //填充 CID ClientId.UniqueProcess = (HANDLE)2908; //這里修改為你要的 PID ClientId.UniqueThread = 0; //填充 OA oa.Length = sizeof(oa); oa.RootDirectory = 0; oa.ObjectName = 0; oa.Attributes = 0; oa.SecurityDescriptor = 0; oa.SecurityQualityOfService = 0; //打開進程,如果句柄有效,則結束進程 ZwOpenProcess(&hProcess, 1, &oa, &ClientId); if (hProcess) { ZwTerminateProcess(hProcess, 0); ZwClose(hProcess); }; } 內存清0方式結束進程 NTKERNELAPI VOID NTAPI KeAttachProcess(PEPROCESS Process); NTKERNELAPI VOID NTAPI KeDetachProcess(); //內存清零法結束進程 void PVASE() { SIZE_T i = 0; //依附進程 KeAttachProcess((PEPROCESS)0xFFFFFA8003ABDB30); //這里改為指定進程的 EPROCESS for (i = 0x10000; i<0x20000000; i += PAGE_SIZE) { __try { memset((PVOID)i, 0, PAGE_SIZE); //把進程內存全部置零 } _except(1) { ; } } //退出依附進程 KeDetachProcess(); } 5.枚舉線程。線程跟進程類似,也有一個身份證一樣的結構體 ETHREAD 存放在內核里,而它 所有的 ETHREAD 也是放在 PspCidTable 里的。於是有了類似枚舉進程的代碼: //根據線程 ID 返回線程 ETHREAD,失敗返回 NULL PETHREAD LookupThread(HANDLE Tid) { PETHREAD ethread; if (NT_SUCCESS(PsLookupThreadByThreadId(Tid, ðread))) return ethread; else return NULL; } //枚舉指定進程的線程 VOID EnumThread(PEPROCESS Process) { ULONG i = 0, c = 0; PETHREAD ethrd = NULL; PEPROCESS eproc = NULL; for (i = 4; i<262144; i = i + 4) { ethrd = LookupThread((HANDLE)i); if (ethrd != NULL) { //獲得線程所屬進程 eproc = IoThreadToProcess(ethrd); if (eproc == Process) { //打印出 ETHREAD 和 TID DbgPrint("ETHREAD=%p, TID=%ld\n", ethrd, (ULONG)PsGetThreadId(ethrd)); } ObDereferenceObject(ethrd); } } } 6.掛起線程。類似於“掛起進程”,唯一的差別是沒有導出函數可用了。可以自行定位 PsSuspendThread,它的原型如下: NTSTATUS PsSuspendThread (IN PETHREAD Thread, //線程 ETHREAD OUT PULONG PreviousSuspendCount OPTIONAL) //掛起的次數,每掛起一次此值增 1 7.恢復線程。類似於“恢復進程”, 唯一的差別是沒有導出函數可用了。可以自行定位 PsResumeThread,它的原型如下: NTSTATUS PsResumeThread (PETHREAD Thread, //線程 ETHREAD OUT PULONG PreviousCount); //恢復的次數,每恢復一次此值減 1,為 0 時線程才正常 8.結束線程。結束線程的標准方法是 ZwOpenThread+ZwTerminateThread+ZwClose,暴力方法 是直接調用 PspTerminateThreadByPointer。暴力方法在后面的課程里講,這里先講標准方法。 由於 ZwTerminateThread 沒有導出,所以只能先硬編碼了(在 WINDBG 里使用 x 命令獲得地 址:x nt!ZwTerminateThread): typedef NTSTATUS(__fastcall *ZWTERMINATETHREAD)(HANDLE hThread, ULONG uExitCode); ZWTERMINATETHREAD ZwTerminateThread = 0Xfffff80012345678; //要修改這個值 //正規方法結束線程 void ZwKillThread() { HANDLE hThread = NULL; CLIENT_ID ClientId; OBJECT_ATTRIBUTES oa; //填充 CID ClientId.UniqueProcess = 0; ClientId.UniqueThread = (HANDLE)1234; //這里修改為你要的 TID //填充 OA oa.Length = sizeof(oa); oa.RootDirectory = 0; oa.ObjectName = 0; oa.Attributes = 0; oa.SecurityDescriptor = 0; oa.SecurityQualityOfService = 0; //打開進程,如果句柄有效,則結束進程 ZwOpenProcess(&hThread, 1, &oa, &ClientId); if (hThread) { ZwTerminateThread(hThread, 0); ZwClose(hThread); };} 9.枚舉 DLL 模塊。DLL 模塊記錄在 PEB 的 LDR 鏈表里,LDR 是一個雙向鏈表,枚舉它即可。 另外,DLL 模塊列表包含 EXE 的相關信息。換句話說, 枚舉 DLL 模塊 即可 實現 枚舉 進程 路徑。 // 聲明偏移 ULONG64 LdrInPebOffset = 0x018; //peb.ldr ULONG64 ModListInPebOffset = 0x010; //peb.ldr.InLoadOrderModuleList //聲明 API NTKERNELAPI PPEB PsGetProcessPeb(PEPROCESS Process); //聲明結構體 typedef struct _LDR_DATA_TABLE_ENTRY { LIST_ENTRY64 InLoadOrderLinks; LIST_ENTRY64 InMemoryOrderLinks; LIST_ENTRY64 InInitializationOrderLinks; PVOID DllBase; PVOID EntryPoint; ULONG SizeOfImage; UNICODE_STRING FullDllName; UNICODE_STRING BaseDllName; ULONG Flags; USHORT LoadCount; USHORT TlsIndex; PVOID SectionPointer; ULONG CheckSum; PVOID LoadedImports; PVOID EntryPointActivationContext; PVOID PatchInformation; LIST_ENTRY64 ForwarderLinks; LIST_ENTRY64 ServiceTagLinks; LIST_ENTRY64 StaticLinks; PVOID ContextInformation; ULONG64 OriginalBase; LARGE_INTEGER LoadTime; } LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY; //根據進程枚舉模塊 VOID EnumModule(PEPROCESS Process) { ULONG64 Peb = 0; ULONG64 Ldr = 0; PLIST_ENTRY ModListHead = 0; PLIST_ENTRY Module = 0; ANSI_STRING AnsiString; KAPC_STATE ks; //EPROCESS 地址無效則退出 if (!MmIsAddressValid(Process)) return; //獲取 PEB 地址 Peb = PsGetProcessPeb(Process); //PEB 地址無效則退出 if (!Peb) return; //依附進程 KeStackAttachProcess(Process, &ks); __try { //獲得 LDR 地址 Ldr = Peb + (ULONG64)LdrInPebOffset; //測試是否可讀,不可讀則拋出異常退出 ProbeForRead((CONST PVOID)Ldr, 8, 8); //獲得鏈表頭 ModListHead = (PLIST_ENTRY)(*(PULONG64)Ldr + ModListInPebOffset); //再次測試可讀性 ProbeForRead((CONST PVOID)ModListHead, 8, 8); //獲得第一個模塊的信息 Module = ModListHead->Flink; while (ModListHead != Module) { //打印信息:基址、大小、DLL 路徑 DbgPrint("Base=%p, Size=%ld, Path=%wZ", (PVOID)(((PLDR_DATA_TABLE_ENTRY)Module)->DllBase), (ULONG)(((PLDR_DATA_TABLE_ENTRY)Module)->SizeOfImage), &(((PLDR_DATA_TABLE_ENTRY)Module)->FullDllName)); Module = Module->Flink; //測試下一個模塊信息的可讀性 ProbeForRead((CONST PVOID)Module, 80, 8); } } __except (EXCEPTION_EXECUTE_HANDLER) { DbgPrint("[EnumModule]__except (EXCEPTION_EXECUTE_HANDLER)"); } //取消依附進程 KeUnstackDetachProcess(&ks); } 10.卸載 DLL 模塊。使用 MmUnmapViewOfSection 即可。MmUnmapViewOfSection 的原型如 下。填寫正確的 EPROCESS 和 DLL 模塊基址就能把 DLL 卸載掉。如果卸載 NTDLL 等重要 DLL 將會導致進程崩潰 NTSTATUS MmUnmapViewOfSection (IN PEPROCESS Process, //進程的 EPROCESS IN PVOID BaseAddress) //DLL 模塊基址

