前言
進程保護是眾多AV或者病毒都要所具備的基礎功能,本文就0環下通過SSDT來對進程進行保護進行探究,SSDT也不是什么新技術,但作為學習,老的技術我們同樣需要掌握。
什么是SSDT
SSDT 的全稱是System Services Descriptor Table,系統服務描述符表。
首先要明確的是他是一張表,通過windbg查看這張表。
dd KeServiceDescriptorTable
這個表就是一個把 Ring3 的 Win32 API 和 Ring0 的內核 API 聯系起來。
當我們在r 3調用一個API時,實際上調用的是一個接口,這里拿ReadProcessMemory舉例。
ReadProcessMemory函數在kernel32.dll中導出,通過斷點可以找到對應的反匯編代碼。在匯編代碼中,可以看到ReadProcessMemory調用了ntdll.dll中的ZwReadVirtualMemory函數。
在ZwReadVirtualMemory函數開始的地方下斷點。
bp ZwReadVirtualMemory
實際上功能代碼也沒有在ZwReadVirtualMemory函數中實現,只是拿著一個索引號并跳轉到一個地址。
這個索引號實際上就是SSDT表中的索引號,回到windbg,我們現在拿到索引號0xBA去SSDT表中找。
kd> dd KeServiceDescriptorTable
80553fa0 80502b8c 00000000 0000011c 80503000
80553fb0 00000000 00000000 00000000 00000000
80553fc0 00000000 00000000 00000000 00000000
80553fd0 00000000 00000000 00000000 00000000
80553fe0 00002710 bf80c0b6 00000000 00000000
80553ff0 f8b67a80 f82e7b60 821bfa90 806e2f40
80554000 00000000 00000000 22bc349b 00000001
80554010 afa8a15b 01d7eb4f 00000000 00000000
kd> dd 80502b8c + 0xba*4
80502e74 805aa712 805c99e0 8060ea76 8060c43c
80502e84 8056f0d2 8063ab56 8061aca8 8061d332
80502e94 8059b804 8059c7cc 8059c1d4 8059baee
80502ea4 805bf456 80598d62 8059908e 805bf264
80502eb4 806064b6 8051ee82 8061cc3e 805cbd40
80502ec4 805cbc22 8061cd3a 8061ce20 8061cf48
80502ed4 8059a07c 8060db50 8060db50 805c892a
80502ee4 8063d80e 8060be28 80607fb8 8060882a
kd> u 805aa712
可以看到在0環調用的是NtReadVirtualMemory,這實際上才是真正實現功能的地方。而SSDT將r 3和r 0聯系到一起。
SSDT結構
在 NT 4.0 以上的 windows 操作系統中,默認就存在兩個系統服務描述表,這兩個調度表對應了兩類不同的系統服務,
這兩個調度表為:KeServiceDescriptorTable 和 KeServiceDescriptorTableShadow,
其中 KeServiceDescriptorTable 主要是處理來自 Ring3 層 Kernel32.dll 中的系統調用,
而 KeServiceDescriptorTableShadow 則主要處理來自 User32.dll 和 GDI32.dll 中的系統調用,
并且 KeServiceDescriptorTable 在 ntoskrnl.exe(Windows 操作系統內核文件,包括內核和執行體層)是導出的,
而 KeServiceDescriptorTableShadow 則是沒有被 Windows 操作系統所導出,
而關于 SSDT 的全部內容則都是通過 KeServiceDescriptorTable 來完成的。
SSDT表的結構通過結構體表示為如下:
typedef struct _KSERVICE_TABLE_DESCRIPTOR
{
KSYSTEM_SERVICE_TABLE ntoskrnl; // ntoskrnl.exe 的服務函數
KSYSTEM_SERVICE_TABLE win32k; // win32k.sys 的服務函數(GDI32.dll/User32.dll 的內核支持)
KSYSTEM_SERVICE_TABLE notUsed1;
KSYSTEM_SERVICE_TABLE notUsed2;
} KSERVICE_TABLE_DESCRIPTOR, * PKSERVICE_TABLE_DESCRIPTOR;
其中每一項又是一個結構體:KSYSTEM_SERVICE_TABLE。通過結構體表示為如下:
typedef struct _KSYSTEM_SERVICE_TABLE
{
PULONG ServiceTableBase; // SSDT (System Service Dispatch Table)的基地址
PULONG ServiceCounterTableBase; // 用于 checked builds, 包含 SSDT 中每個服務被調用的次數
ULONG NumberOfService; // 服務函數的個數, NumberOfService * 4 就是整個地址表的大小
ULONG ParamTableBase; // SSPT(System Service Parameter Table)的基地址
} KSYSTEM_SERVICE_TABLE, * PKSYSTEM_SERVICE_TABLE;
通過看圖形化界面可以更加直觀,下圖是ntoskrnl.exe和win32k.sys的服務函數結構。
HOOK SSDT
有了上面的知識儲備,理解SSDT HOOK就很容易了。
當3環程序執行后,操作系統拿著索引去SSDT表中找對應的0環程序,這時我們就可以在SSDT表中做點手腳,將某一個api函數的指針改成我們自己函數的指針,這樣執行的將會是我們自己的代碼。
首先需要定義我們自己的函數
ULONG g_Pid = 568;
NTSTATUS NTAPI MyOpenProcess(PHANDLE ProcessHandle, ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes, PCLIENT_ID ClientId)
{
NTSTATUS status;
status = STATUS_SUCCESS;
//當此進程為要保護的進程時
if (ClientId->UniqueProcess == (HANDLE)g_Pid)
{
//設為拒絕訪問
DesiredAccess = 0;
}
return NtOpenProcess(ProcessHandle, DesiredAccess, ObjectAttributes, ClientId);
}
g_Pid定義為全局的,我們想保護哪個進程就將該進程的pid賦值給g_Pid。
比如這里就保護Dbgview.exe。
這里函數準備好以后,就要將該函數的指針覆蓋原來NtOpenProcess的指針。但是需要注意的是:我們自己改自己的代碼是不用管權限的,改別人的代碼很有可能這塊內存是只讀的,并不可寫。
那么本質上就是SSDT對應的物理頁是只讀的,這里有兩種辦法,我們都知道物理頁的內存R/W位的屬性是由PDE和PTE相與而來的,那么我們就可以改變SSDT對應的PDE和PTE的R/W屬性,將物理頁設置為可讀可寫的。通過CR4寄存器判斷是2-9-9-12分頁還是10-10-12分頁。
if(RCR4 & 0x00000020)
{//說明是2-9-9-12分頁
KdPrint(("2-9-9-12分頁 %pn",RCR4));
KdPrint(("PTE1 %pn",*(Dword*)(0xC0000000 + ((HookFunAddr >> 9) & 0x007FFFF8))));
*(DWORD64*)(0xC0000000 + ((HookFunAddr >> 9) & 0x007FFFF8)) |= 0x02;
KdPrint(("PTE1 %pn",*(DWORD*)(0xC0000000 + ((HookFunAddr >> 9) & 0x007FFFF8))));
}
else
{//說明是10-10-12分頁
KdPrint(("10-10-12分頁n"));
KdPrint(("PTE1 %pn",*(DWORD*)(0xC0000000 + ((HookFunAddr >> 10) & 0x003FFFFC))));
*(DWORD*)(0xC0000000 + ((HookFunAddr >> 10) & 0x003FFFFC)) |= 0x02;
KdPrint(("PTE2 %pn",*(DWORD*)(0xC0000000 + ((HookFunAddr >> 10) & 0x003FFFFC))));
}
還有一種方式就是通過Cr0寄存器。CR0寄存器的第16位叫做保護屬性位,控制著頁的讀或寫屬性。
WP為1 時, 不能修改只讀的內存頁 , WP為0 時, 可以修改只讀的內存頁。那么我們就可以將WP位置為0,暫時關閉可讀屬性。
VOID PageProtectOn()
{
__try
{
_asm
{
mov eax, cr0
or eax, 10000h
mov cr0, eax
sti
}
}
__except (1)
{
DbgPrint("PageProtectOn執行失敗!");
}
}
VOID PageProtectOff()
{
__try
{
_asm
{
cli
mov eax, cr0
and eax, not 10000h //and eax,0FFFEFFFFh
mov cr0, eax
}
}
__except (1)
{
DbgPrint("PageProtectOff執行失敗!");
}
}
可以修改SSDT表后,就要寫函數來修改NtOpenProcess指針,也就是我們的HOOK函數。
NTSTATUS _hook()
{
NTSTATUS status;
status = STATUS_SUCCESS;
PageProtectOff();
PoldAddress = KeServiceDescriptorTable->ntoskrnl.ServiceTableBase[0x7a];
KeServiceDescriptorTable->ntoskrnl.ServiceTableBase[0x7a] = (ULONG)MyOpenProcess;
PageProtectOn();
return status;
}
在修改SSDT表前先關閉物理頁保護,修改完后要開啟物理頁保護,保證其他任務能夠順利完成。這里的索引可以通過ida或者debug工具去看。
然后就是卸載鉤子,用于驅動卸載的時候使用。
NTSTATUS _unhook()
{
NTSTATUS status;
status = STATUS_SUCCESS;
PageProtectOff();
KeServiceDescriptorTable->ntoskrnl.ServiceTableBase[0x7a] = PoldAddress;
PageProtectOn();
return status;
}
最后是入口和卸載函數
VOID DriverUnload(PDRIVER_OBJECT driver)
{
_unhook();
DbgPrint("卸載了。。。。。n");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path)
{
_hook();
DbgPrint("跑起來了。。。n");
driver->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}
最后編譯,加載驅動,當我們嘗試用任務管理器殺死Dbgview時會被拒絕。
如果通過taskkill同樣不行。
后記
在SSDT上寫鉤子,在0環是最低級的方式,可以看到編寫代碼十分簡單,但是也是非常容易被檢測的,比如我們通過PChunter這樣的內核工具去看一下。
可以看到NtOpenProcess赫然在列。實際上SSDT已作為基礎需要被了解。
本文由SD原創發布
轉載,請參考轉載聲明,注明出處: https://www.anquanke.com/post/id/262577
安全客 - 有思想的安全新媒體