對於windwos逆向人員來說,不論是寫外掛、寫病毒/木馬,都需要打開其他內存的空間,改寫某些關鍵數據,達到改變其原有執行流程的目的。那么日常的工作肯定涉及到openprocess、readprocessmemory、writeprocessmemory等函數;這些函數都是怎么被調用的了?
1、windows提供了大量的系統函數供3層的應用調用。這些函數被統一編號,入口地址放在一張表里,編號就是索引,通過編號就能找到函數的入口地址;64位的表結構可以通過windbg查看,如下:
x64的SSDT表和32位比復雜了一些,為了便於讀者理解,我用不同顏色(黃、綠、藍、橙)做了標注;開發人員在3環調用openprocess、readprocessmemory、writeprocessmemory等函數,最終都會通過這個表找到對應的內核入口地址,進而跳轉到內核空間執行;具體的函數實現可以通過逆向ntdll.dll、kerner32.dll、ntoskrl.exe等內核文件查看,這里不贅述(SSDT hook已經爛大街了,google一下資料大堆);各大廠商最初的驅動保護就是hook SSDT表的關鍵函數,一旦發現第三方程序打開自己的進程,直接返回false,達到保護自己進程數據不被篡改的目的;今天演示一下hook terminalprocess函數,讓其無法關閉計算器或記事本的進程;
2、通過微軟官網查詢得知:windwos提供的terminalProcess函數在kerner32.dll中:

用IDA打開kernerl32.dll,切換到import,發現terminalProcess是從ntdll.dll導入的
繼續追查ntdll.dll,在export找到目標函數,雙擊進入函數體,如下:
這個函數有兩個重要信息:
(1)mov eax, 2Ch: 2c=44,是系統調用號(同一函數在windwos不同版本的調用號是不一樣的,我剛開始做實驗時總是藍屏,調試了好長時間才發現是調用號搞錯了),也就是terminalprocess在SSDT中的編號,根據這個編號就能找到函數的入口地址(當然不是直接現成地展示在表內,而要經過一些簡單地計算)
(2)通過syscall進入內核
3、核心代碼(下面的參考【3】);注意:本人的測試環境是win10.0.16299.125,調用號是0x2c;其他版本的系統可能不一樣,建議讀者自己用IDA查查ntdll.dll,否則直接藍屏;
#include "hook.h" #include "asmUtil.h" PSYSTEM_SERVICE_TABLE KeServiceDescriptorTable; NTTERMINATEPROCESS NtTerminateProcess = NULL; ULONG OldTpVal; /* 用戶點擊關閉,系統會調用原NtTerminateProcess,並傳遞ProcessHandle和ExitStatus兩個參數;但SSDT已經被改成了KeBugCheckEx,所以 會先執行KeBugCheckEx。進入后又執行jmp,跳轉到我們自己定義的Fake_NtTerminateProcess。這時EIP變了好幾次,但是堆棧一直沒變,所以 Fake_NtTerminateProcess的參數就是原NtTerminateProcess的參數ProcessHandle和ExitStatus;所以后續也能重新調回NtTerminateProcess 走原來正常的流程; */ NTSTATUS __fastcall Fake_NtTerminateProcess(IN HANDLE ProcessHandle, IN NTSTATUS ExitStatus) { //Dbg_Break(); PEPROCESS Process; // 通過進程句柄來獲取該進程所對應的FileObject對象,由於這里是進程對象,自然獲得的是EPROCESS對象 NTSTATUS st = ObReferenceObjectByHandle(ProcessHandle, 0, *PsProcessType, KernelMode, &Process, NULL); DbgPrint("\r\n-------Fake_NtTerminateProcess called! NT_SUCCESS(st):% d------------------------\r\n",NT_SUCCESS(st)); DbgPrint("\r\n-------Fake_NtTerminateProcess called! st:% d------------------------\r\n", st); if (NT_SUCCESS(st)) //#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) { //if (!_stricmp(PsGetProcessImageFileName(Process), "Calculator.exe")) //if (strcmp(PsGetProcessImageFileName(Process), "Calculator.exe") == 0) DbgPrint("\r\n-------PsGetProcessImageFileName(Process):% s------------------------\r\n", PsGetProcessImageFileName(Process)); if ((!_stricmp(PsGetProcessImageFileName(Process), "Calculator.exe")) || (!_stricmp(PsGetProcessImageFileName(Process), "notepad.exe"))) { //ObDeReferenceObject(&Process); return STATUS_ACCESS_DENIED; } else { //ObDeReferenceObject(&Process); /*這個已經被掛鈎了,會不會形成死循環????*/ return NtTerminateProcess(ProcessHandle, ExitStatus); } } else { return STATUS_ACCESS_DENIED; } } /*關閉內核頁面寫保護*/ KIRQL WPOFFx64() { KIRQL irql = KeRaiseIrqlToDpcLevel(); UINT64 cr0 = __readcr0(); cr0 &= 0xfffffffffffeffff; __writecr0(cr0); _disable(); return irql; } /*打開內核頁面寫保護*/ void WPONx64(KIRQL irql) { UINT64 cr0 = __readcr0(); cr0 |= 0x10000; _enable(); __writecr0(cr0); KeLowerIrql(irql); } // win10的變了,用下面的替代 ULONGLONG GetKeServiceDescriptorTable64_win10() { PUCHAR StartSearchAddress = (PUCHAR)__readmsr(0xC0000082); PUCHAR EndSearchAddress = StartSearchAddress + 0x500; PUCHAR i = NULL; UCHAR b1 = 0, b2 = 0, b3 = 0; ULONG templong = 0; ULONGLONG addr = 0; for (i = StartSearchAddress; i < EndSearchAddress; i++) { if (MmIsAddressValid(i) && MmIsAddressValid(i + 1) && MmIsAddressValid(i + 2)) { b1 = *i; b2 = *(i + 1); b3 = *(i + 2); if (b1 == 0x4c && b2 == 0x8d && b3 == 0x15) //4c8d15 { memcpy(&templong, i + 3, 4); addr = (ULONGLONG)templong + (ULONGLONG)i + 7; return addr; } } } return 0; } /* 根據調用號找到目標內核函數地址 kd> x nt!KeServiceDescriptorTable fffff803`4e9a1880 nt!KeServiceDescriptorTable = <no type information> kd> dq fffff803`4e9a1880 fffff803`4e9a1880 fffff803`4e839c10 00000000`00000000 fffff803`4e9a1890 00000000`000001d0 fffff803`4e83a354 注意事項: 1、這4個都是指針,都是8字節的; ServiceTableBase:fffff803`4e839c10 ServiceCounterTableBase:00000000`00000000 NumberOfServices:00000000`000001d0 ParamTableBase:fffff803`4e83a354 2、ServiceTableBase存儲的是4字節的偏移: (2.2) 第0x2c=44號函數NtTerminateProcess偏移: kd> dd fffff803`4e839c10+0x29*4 fffff803`4e839cb4 fd9c8d00 01a27c00 01a99001 02150f00;注意低位在后面 0x29函數偏移:fffff803`4e839c10 + 02150f00>>4 =fffff803`4e839c10 + 2150F0 = FFFF F803 4EA4 ED00,和下面NtTerminateProcess的起始地址是吻合的: kd> u nt!NtTerminateProcess nt!NtTerminateProcess: fffff803`4ea4ed00 4c8bdc mov r11,rsp fffff803`4ea4ed03 49895b10 mov qword ptr [r11+10h],rbx fffff803`4ea4ed07 49897320 mov qword ptr [r11+20h],rsi */ ULONGLONG GetSSDTFuncCurAddr(ULONG id) { LONG dwtmp = 0; PULONG ServiceTableBase = NULL; ServiceTableBase = (PULONG)KeServiceDescriptorTable->ServiceTableBase; dwtmp = ServiceTableBase[id]; dwtmp = dwtmp >> 4; return (LONGLONG)dwtmp + (ULONGLONG)ServiceTableBase; } /* (2.3)反過來求偏移 (2.3.1)kd> u nt!NtTerminateProcess nt!NtTerminateProcess: fffff803`4ea4ed00 4c8bdc mov r11,rsp (2.3.2)差距: nt!NtTerminateProcess:fffff803`4ea4ed00 - ServiceTableBase:fffff803`4e839c10 = 21 50F0 (2.3.3)偏移: 21 50F0 << 4 = 215 0F00 */ ULONG GetOffsetAddress(ULONGLONG FuncAddr) { ULONG dwtmp = 0; PULONG ServiceTableBase = NULL; ServiceTableBase = (PULONG)KeServiceDescriptorTable->ServiceTableBase; dwtmp = (ULONG)(FuncAddr - (ULONGLONG)ServiceTableBase); return dwtmp << 4; } /* SSDT在ntoskrnl中;內核函數和用戶自己的驅動不在一個4GB空間,32位的偏移是直接跳不過去的; 修改這個偏移地址的值,使之跳轉到 KeBugCheckEx ,然后在 x KeBugCheckEx 的頭部寫一個 2 12 字節的 mov - - jmp ,這是一個可以跨越 4GB ! 的跳轉,跳到我們的函數里! */ VOID FuckKeBugCheckEx() { KIRQL irql; ULONGLONG myfun; UCHAR jmp_code[] = "\x48\xB8\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00\xFF\xE0"; /*通過jmp跳轉,而不是call,可以讓Fake_NtTerminateProcess直接利用原NtTerminateProcess 留下的參數*/ myfun = (ULONGLONG)Fake_NtTerminateProcess; memcpy(jmp_code + 2, &myfun, 8); irql = WPOFFx64(); memset(KeBugCheckEx, 0x90, 15); memcpy(KeBugCheckEx, jmp_code, 12); WPONx64(irql); } /* 填寫KeBugCheckEx的地址 在KeBugCheckEx填寫jmp,跳到Fake_NtTerminateProcess 不能直接填寫Fake_NtTerminateProcess的地址,因為它們不再同一個4GB */ VOID HookSSDT(PSYSTEM_SERVICE_TABLE received) { KIRQL irql; ULONGLONG dwtmp = 0; PULONG ServiceTableBase = NULL; KeServiceDescriptorTable = received; //get old address //Dbg_Break(); NtTerminateProcess = (NTTERMINATEPROCESS)GetSSDTFuncCurAddr(44); DbgPrint("\r\n------------------------Old_NtTerminateProcess: %llx-----------------------\r\n", (ULONGLONG)NtTerminateProcess); //set kebugcheckex //Dbg_Break(); FuckKeBugCheckEx(); //show new address ServiceTableBase = (PULONG)KeServiceDescriptorTable->ServiceTableBase; //OldTpVal = ServiceTableBase[41]; //win7編號是0x29 = 41 OldTpVal = ServiceTableBase[44]; //win10逆向ntdll的時候發現編號是0x2c = 44 irql = WPOFFx64(); /* 我們掛鈎的函數是KeBugCheckEx,所以把該函數的偏移算出來(只有32位,在4GB內) 把SSDT原本terminalProcess的地方替換掉(都在SSDT,在同一個4GB范圍內) 這樣一旦調用terminalProcess,實際會調用KeBugCheckEx,然后再到我們自己的代碼; */ ServiceTableBase[44] = GetOffsetAddress((ULONGLONG)KeBugCheckEx); WPONx64(irql); DbgPrint("\r\n------------------------KeBugCheckEx: %llx-----------------------\r\n", (ULONGLONG)KeBugCheckEx); DbgPrint("\r\n------------------------New_NtTerminateProcess: %llx-----------------------\r\n", GetSSDTFuncCurAddr(44)); } VOID UnhookSSDT() { KIRQL irql; PULONG ServiceTableBase = NULL; ServiceTableBase = (PULONG)KeServiceDescriptorTable->ServiceTableBase; //set value irql = WPOFFx64(); ServiceTableBase[44] = GetOffsetAddress((ULONGLONG)NtTerminateProcess); //OldTpVal;//直接填寫這個舊值也行 WPONx64(irql); //沒必要恢復KeBugCheckEx的內容了,反正執行到KeBugCheckEx時已經完蛋了。 DbgPrint("\r\n------------------------NtTerminateProcess: %llx-----------------------\r\n", GetSSDTFuncCurAddr(44)); } ULONGLONG SearchforKeServiceDescriptorTable64(ULONGLONG StartSearchAddress, ULONGLONG EndSearchAddress) { UCHAR b1 = 0, b2 = 0, b3 = 0; ULONG templong = 0; ULONGLONG KeServiceDescriptorTable = 0; //地址效驗 if (MmIsAddressValid(StartSearchAddress) == FALSE)return NULL; if (MmIsAddressValid(EndSearchAddress) == FALSE)return NULL; for (PUCHAR i = StartSearchAddress; i < EndSearchAddress; i++) { if (MmIsAddressValid(i) && MmIsAddressValid(i + 1) && MmIsAddressValid(i + 2)) { b1 = *i; b2 = *(i + 1); b3 = *(i + 2); //if (b1 == 0x4c && b2 == 0x8d && b3 == 0x15);//4c8d15 //if (b1 == "4c" && b2 == "8d" && b3 == "15" );//4c8d15 //if (*i == 0x4c && *(i + 1) == 0x8d && *(i + 2) == 0x15);// 不能有;號,否則下面的代碼一定會執行 if (b1 == 0x4c && b2 == 0x8d && b3 == 0x15) { DbgPrint("\r\n--------- StartSearchAddress: %llx----------------- -------------\r\n", StartSearchAddress); DbgPrint("\r\n--------- matched targetAddress: %llx----------------- -------------\r\n", i); DbgPrint("\r\n--------- targetAddress offset: %d----------------- -------------\r\n", (i - StartSearchAddress)); //Dbg_Break(); memcpy(&templong, i + 3, 4); KeServiceDescriptorTable = (ULONGLONG)templong + (ULONGLONG)i + 7;//i是當前地址,templong是相對SSDT的偏移 DbgPrint("\r\n--------- KeServiceDescriptorTable: %llx----------------- -------------\r\n", KeServiceDescriptorTable); return KeServiceDescriptorTable; //當前地址 + 長度 + 數值 //fffff800`03c8c772+7 + 002320c7 = FFFFF80003EBE840 /* fffff800`03c8c772 4c8d15c7202300 lea r10,[nt!KeServiceDescriptorTable (fffff800`03ebe840)] fffff800`03c8c779 4c8d1d00212300 lea r11,[nt!KeServiceDescriptorTableShadow (fffff800`03ebe880)] */ } } } return NULL; } //獲取SSDT KeServiceDescriptorTable ULONGLONG GetKeServiceDescriptorTable64() { PUCHAR pKiSystemCall64 = (PUCHAR)__readmsr(0xc0000082);//rdmsr c0000082 //定位KiSystemCall64 PUCHAR EndSearchAddress = pKiSystemCall64 + 0x500;//在1280個字節的范圍內搜索 ULONGLONG KeServiceDescriptorTable = 0; KeServiceDescriptorTable = SearchforKeServiceDescriptorTable64(pKiSystemCall64, EndSearchAddress); if (KeServiceDescriptorTable) { return KeServiceDescriptorTable; } //msr[0xc0000082]變成了KiSystemCall64Shadow函數 //原來我們64位搜索KeServiceDescriptorTable是通過msr的0xc0000082獲得KiSystemCall64字段, //但是現在msr[0xc0000082]變成了KiSystemCall64Shadow函數, 而且這個函數無法直接搜索到KeServiceDescriptorTable。 ULONGLONG KiSystemServiceUser = 0; ULONGLONG templong = 0xffffffffffffffff; for (PUCHAR i = pKiSystemCall64; i < EndSearchAddress + 0xff; i++)//在pKiSystemCall64的0x5ff=1535字節范圍內查找 { if (*(PUCHAR)i == 0xe9 && *(PUCHAR)(i + 5) == 0xc3)//找到KiSystemServiceUser //if (*(PUCHAR)i == "e9" && *(PUCHAR)(i + 5) == "c3")//找到KiSystemServiceUser { //fffff803`23733383 e9631ae9ff jmp nt!KiSystemServiceUser(fffff803`235c4deb) //fffff803`23733388 c3 ret RtlCopyMemory(&templong, (PUCHAR)(i + 1), 4); KiSystemServiceUser = templong + 5 + i;//KiSystemServiceUser EndSearchAddress = KiSystemServiceUser + 0x500; KeServiceDescriptorTable = SearchforKeServiceDescriptorTable64(KiSystemServiceUser, EndSearchAddress); return KeServiceDescriptorTable; } } return 0; }
4、效果:想要關閉計算器,直接彈框拒絕訪問;
windbg也看到了打印的日志,說明自己寫的Fake_NtTerminateProcess函數已經被調用;
其他的窗口能夠隨意結束;
這次沒刻意做驅動隱藏,還是被PCHUNTER發現了:
SSDT hook是好多年以前的老辦法了;因為驅動在0環,和windows 內核平起平坐,權力相當大。為了保護自己的客戶端,各個廠家都在爭先恐后地hook,把內核搞得一團糟,嚴重影響了用戶體驗;微軟終於坐不住了,近些年在64位的windows做了以下改動:
- 增加PG保護,一旦發現自己的內核代碼被改,大概率會直接藍屏
- 增加驅動簽名。運行的驅動必須強制簽名。一旦發現某些驅動改內核,直接吊銷簽名的資格
那么問題又來了,既然不讓hook SSDT,各大廠家怎么知道自己的客戶端有沒有被逆向人員搞了?微軟又提供了新的解決方案:注冊回調函數;一旦第三方調用openprocess、readprocessmemory、writeprocessmemory等函數搞事,自己的客戶端就能收到通知,然后采取響應的措施;回調函數的具體用法見下方【1】;
最后,系統調用的好處/意義:
- 3環應用只需要知道調用號就可以調用系統提供的服務,不需要知道這些服務是怎么實現的,有效地保護了系統服務代碼不被看見;
- 3環的權限下也不能修改系統調用!
syscall和中斷實現的系統調用對比:
參考:
1、https://www.write-bug.com/article/2170.html 基於ObRegisterCallbacks實現的線程和進程監控及其保護
2、https://www.cnblogs.com/freesec/p/7623675.html windows 64位 系統非HOOK方式監控進程創建
3、http://www.m5home.com/bbs/thread-8378-1-1.html x64 SSDT正確的偏移計算,一鍵恢復SSDT表中的所有HOOK