在內核裏操做進程編程
在內核裏操做進程,相信是不少對 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 模塊基址測試