三年前面朝黃土背朝天的我,寫了一篇如何在Windows 7
系統下列舉核心SSDT
表的文章《驅動開發:核心讀取SSDT表基址》
三年過去了我還是個單身狗
,開個玩笑,微軟的Windows 10
系統已經覆蓋了大多數個人PC終端,以前的方法也該進行迭代更新了,或許在網上你能夠找到類似的文章,但我可以百分百肯定都不能用,今天LyShark
將帶大家一起分析Win10 x64
最新系統SSDT
表的列舉實現。
看一款閉源ARK工具的列舉效果:
直接步入正題,首先SSDT
表中文為系統服務描述符表,SSDT表的作用
是把應用
層與核心
層聯絡起來
起到橋樑
的作用,列舉SSDT表
也是反核心
工具最基本的功能,通常在64位元
系統中要想找到SSDT
表,需要先找到KeServiceDescriptorTable
這個函數,由於該函數沒有被匯出,所以只能動態的查詢它的地址,慶幸的是我們可以通過查詢msr(c0000082)
這個特殊的暫存器來替代查詢KeServiceDescriptorTable
這一步,在新版系統中查詢SSDT可以歸納為如下這幾個步驟。
首先第一步通過rdmsr C0000082
MSR暫存器得到KiSystemCall64Shadow
的函數地址,計算KiSystemCall64Shadow
與KiSystemServiceUser
偏移量,如下圖所示。
6ed53180(KiSystemCall64Shadow) - 6ebd2a82(KiSystemServiceUser) = 1806FE
6ed53180(rdmsr) - 1806FE = KiSystemServiceUser
如上當我們找到了KiSystemServiceUser
的地址以後,在KiSystemServiceUser
向下搜尋可找到KiSystemServiceRepeat
裡面就是我們要找的SSDT
表基址。
其中fffff8036ef8c880
則是SSDT表
的基地址,緊隨其後的fffff8036ef74a80
則是SSSDT表
的基地址。
那麼如果將這個過程通過程式碼的方式來實現,我們還需要使用《驅動開發:核心列舉IoTimer定時器》
中所使用的特徵碼定位技術,如下我們查詢這段特徵。
// 署名權
// right to sign one's name on a piece of work
// PowerBy: LyShark
// Email: [email protected]
#include <ntifs.h>
#pragma intrinsic(__readmsr)
ULONGLONG ssdt_address = 0;
// 獲取 KeServiceDescriptorTable 首地址
ULONGLONG GetLySharkCOMKeServiceDescriptorTable()
{
// 設定起始位置
PUCHAR StartSearchAddress = (PUCHAR)__readmsr(0xC0000082) - 0x1806FE;
// 設定結束位置
PUCHAR EndSearchAddress = StartSearchAddress + 0x100000;
DbgPrint("[LyShark Search] 掃描起始地址: %p --> 掃描結束地址: %p \n", StartSearchAddress, EndSearchAddress);
PUCHAR ByteCode = NULL;
UCHAR OpCodeA = 0, OpCodeB = 0, OpCodeC = 0;
ULONGLONG addr = 0;
ULONG templong = 0;
for (ByteCode = StartSearchAddress; ByteCode < EndSearchAddress; ByteCode++)
{
// 使用MmIsAddressValid()函數檢查地址是否有頁面錯誤
if (MmIsAddressValid(ByteCode) && MmIsAddressValid(ByteCode + 1) && MmIsAddressValid(ByteCode + 2))
{
OpCodeA = *ByteCode;
OpCodeB = *(ByteCode + 1);
OpCodeC = *(ByteCode + 2);
// 對位元徵值 尋找 nt!KeServiceDescriptorTable 函數地址
/*
nt!KiSystemServiceRepeat:
fffff803`6ebd2b94 4c8d15e59c3b00 lea r10,[nt!KeServiceDescriptorTable (fffff803`6ef8c880)]
fffff803`6ebd2b9b 4c8d1dde1e3a00 lea r11,[nt!KeServiceDescriptorTableShadow (fffff803`6ef74a80)]
fffff803`6ebd2ba2 f7437880000000 test dword ptr [rbx+78h],80h
fffff803`6ebd2ba9 7413 je nt!KiSystemServiceRepeat+0x2a (fffff803`6ebd2bbe) Branch
*/
if (OpCodeA == 0x4c && OpCodeB == 0x8d && OpCodeC == 0x15)
{
// 獲取高位地址fffff802
memcpy(&templong, ByteCode + 3, 4);
// 與低位64da4880地址相加得到完整地址
addr = (ULONGLONG)templong + (ULONGLONG)ByteCode + 7;
return addr;
}
}
}
return 0;
}
VOID UnDriver(PDRIVER_OBJECT driver)
{
DbgPrint(("驅動程式解除安裝成功! \n"));
}
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DbgPrint("hello lyshark.com");
ssdt_address = GetLySharkCOMKeServiceDescriptorTable();
DbgPrint("[LyShark] SSDT = %p \n", ssdt_address);
DriverObject->DriverUnload = UnDriver;
return STATUS_SUCCESS;
}
如上程式碼中所提及的步驟我想不需要再做解釋了,這段程式碼執行後即可輸出SSDT表的基址。
如上通過呼叫GetLySharkCOMKeServiceDescriptorTable()
得到SSDT
地址以後我們就需要對該地址進行解密操作。
得到ServiceTableBase
的地址後,就能得到每個服務函數的地址。但這個表存放的並不是SSDT
函數的完整地址,而是其相對於ServiceTableBase[Index]>>4
的資料,每個資料佔四個位元組,所以計算指定Index
函數完整地址的公式是;
如下組合程式碼就是一段解密程式碼,程式碼中rcx
暫存器傳入SSDT的下標,而rdx
暫存器則是傳入SSDT表基址。
48:8BC1 | mov rax,rcx | rcx=index
4C:8D12 | lea r10,qword ptr ds:[rdx] | rdx=ssdt
8BF8 | mov edi,eax |
C1EF 07 | shr edi,7 |
83E7 20 | and edi,20 |
4E:8B1417 | mov r10,qword ptr ds:[rdi+r10] |
4D:631C82 | movsxd r11,dword ptr ds:[r10+rax*4] |
49:8BC3 | mov rax,r11 |
49:C1FB 04 | sar r11,4 |
4D:03D3 | add r10,r11 |
49:8BC2 | mov rax,r10 |
C3 | ret |
有了解密公式以後程式碼的編寫就變得很容易,如下是讀取SSDT的完整程式碼。
// 署名權
// right to sign one's name on a piece of work
// PowerBy: LyShark
// Email: [email protected]
#include <ntifs.h>
#pragma intrinsic(__readmsr)
typedef struct _SYSTEM_SERVICE_TABLE
{
PVOID ServiceTableBase;
PVOID ServiceCounterTableBase;
ULONGLONG NumberOfServices;
PVOID ParamTableBase;
} SYSTEM_SERVICE_TABLE, *PSYSTEM_SERVICE_TABLE;
ULONGLONG ssdt_base_aadress;
PSYSTEM_SERVICE_TABLE KeServiceDescriptorTable;
typedef UINT64(__fastcall *SCFN)(UINT64, UINT64);
SCFN scfn;
// 解密演演算法
VOID DecodeSSDT()
{
UCHAR strShellCode[36] = "\x48\x8B\xC1\x4C\x8D\x12\x8B\xF8\xC1\xEF\x07\x83\xE7\x20\x4E\x8B\x14\x17\x4D\x63\x1C\x82\x49\x8B\xC3\x49\xC1\xFB\x04\x4D\x03\xD3\x49\x8B\xC2\xC3";
/*
48:8BC1 | mov rax,rcx | rcx=index
4C:8D12 | lea r10,qword ptr ds:[rdx] | rdx=ssdt
8BF8 | mov edi,eax |
C1EF 07 | shr edi,7 |
83E7 20 | and edi,20 |
4E:8B1417 | mov r10,qword ptr ds:[rdi+r10] |
4D:631C82 | movsxd r11,dword ptr ds:[r10+rax*4] |
49:8BC3 | mov rax,r11 |
49:C1FB 04 | sar r11,4 |
4D:03D3 | add r10,r11 |
49:8BC2 | mov rax,r10 |
C3 | ret |
*/
scfn = ExAllocatePool(NonPagedPool, 36);
memcpy(scfn, strShellCode, 36);
}
// 獲取 KeServiceDescriptorTable 首地址
ULONGLONG GetKeServiceDescriptorTable()
{
// 設定起始位置
PUCHAR StartSearchAddress = (PUCHAR)__readmsr(0xC0000082) - 0x1806FE;
// 設定結束位置
PUCHAR EndSearchAddress = StartSearchAddress + 0x8192;
DbgPrint("掃描起始地址: %p --> 掃描結束地址: %p \n", StartSearchAddress, EndSearchAddress);
PUCHAR ByteCode = NULL;
UCHAR OpCodeA = 0, OpCodeB = 0, OpCodeC = 0;
ULONGLONG addr = 0;
ULONG templong = 0;
for (ByteCode = StartSearchAddress; ByteCode < EndSearchAddress; ByteCode++)
{
// 使用MmIsAddressValid()函數檢查地址是否有頁面錯誤
if (MmIsAddressValid(ByteCode) && MmIsAddressValid(ByteCode + 1) && MmIsAddressValid(ByteCode + 2))
{
OpCodeA = *ByteCode;
OpCodeB = *(ByteCode + 1);
OpCodeC = *(ByteCode + 2);
// 對位元徵值 尋找 nt!KeServiceDescriptorTable 函數地址
// LyShark.com
// 4c 8d 15 e5 9e 3b 00 lea r10,[nt!KeServiceDescriptorTable (fffff802`64da4880)]
// 4c 8d 1d de 20 3a 00 lea r11,[nt!KeServiceDescriptorTableShadow (fffff802`64d8ca80)]
if (OpCodeA == 0x4c && OpCodeB == 0x8d && OpCodeC == 0x15)
{
// 獲取高位地址fffff802
memcpy(&templong, ByteCode + 3, 4);
// 與低位64da4880地址相加得到完整地址
addr = (ULONGLONG)templong + (ULONGLONG)ByteCode + 7;
return addr;
}
}
}
return 0;
}
// 得到函數相對偏移地址
ULONG GetOffsetAddress(ULONGLONG FuncAddr)
{
ULONG dwtmp = 0;
PULONG ServiceTableBase = NULL;
if (KeServiceDescriptorTable == NULL)
{
KeServiceDescriptorTable = (PSYSTEM_SERVICE_TABLE)GetKeServiceDescriptorTable();
}
ServiceTableBase = (PULONG)KeServiceDescriptorTable->ServiceTableBase;
dwtmp = (ULONG)(FuncAddr - (ULONGLONG)ServiceTableBase);
return dwtmp << 4;
}
// 根據序號得到函數地址
ULONGLONG GetSSDTFunctionAddress(ULONGLONG NtApiIndex)
{
ULONGLONG ret = 0;
if (ssdt_base_aadress == 0)
{
// 得到ssdt基地址
ssdt_base_aadress = GetKeServiceDescriptorTable();
}
if (scfn == NULL)
{
DecodeSSDT();
}
ret = scfn(NtApiIndex, ssdt_base_aadress);
return ret;
}
VOID UnDriver(PDRIVER_OBJECT driver)
{
DbgPrint(("驅動程式解除安裝成功! \n"));
}
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DbgPrint("hello lyshark.com \n");
ULONGLONG ssdt_address = GetKeServiceDescriptorTable();
DbgPrint("SSDT基地址 = %p \n", ssdt_address);
// 根據序號得到函數地址
ULONGLONG address = GetSSDTFunctionAddress(51);
DbgPrint("[LyShark] NtOpenFile地址 = %p \n", address);
// 得到相對SSDT的偏移量
DbgPrint("函數相對偏移地址 = %p \n", GetOffsetAddress(address));
DriverObject->DriverUnload = UnDriver;
return STATUS_SUCCESS;
}
執行後即可得到SSDT
下標為51
的函數也就是得到NtOpenFile
的絕對地址和相對地址。
你也可以開啟ARK工具,對比一下是否一致,如下圖所示,LyShark
的程式碼是沒有任何問題的。