SSDT 中文称号为体系服务描绘符表,该表的作用是将Ring3应用层与Ring0内核层,两者的API函数连接起来,起到承上启下的作用,SSDT并不仅仅只包括一个巨大的地址索引表,它还包括着一些其它有用的信息,诸如地址索引的基址、服务函数个数等,SSDT 经过修正此表的函数地址能够对常用 Windows 函数进行内核级的Hook,然后完结对一些中心的体系动作进行过滤、监控的意图,接下来将演示怎么经过编写简略的驱动程序,来完结查找 SSDT 函数的地址,并能够完结简略的内核 Hook 挂钩。

在开端编写驱动之前,咱们先来剖析一下Ring3到Ring0是怎么协作的,这儿经过C言语调用 OpenProcess 函数,并剖析它的履行进程,先来创立一个C程序。

#include <windows.h>
int main(int argc, char* argv[])
{
    HANDLE handle = OpenProcess(PROCESS_ALL_ACCESS,FALSE,2548);
    return 0;
}

经过VC6编译器编译,并运用OD载入程序,找到程序的OEP,剖析第一次调用,能够看到CALL的地址是 <&KERNEL32.OpenProcess> 此处咱们F7直接跟进这个CALL。

00401028  |.  8BF4          mov     esi, esp
0040102A  |.  68 F4090000   push    0x9F4                                       ; /ProcessId = 0x9F4
0040102F  |.  6A 00         push    0x0                                         ; |Inheritable = FALSE
00401031  |.  68 FF0F1F00   push    0x1F0FFF                                    ; |Access = PROCESS_ALL_ACCESS
00401036  |.  FF15 4CA14200 call    dword ptr [<&KERNEL32.OpenProcess>]         ; \OpenProcess

此刻咱们现已进入到了 00401036这个地址中,调查下方的代码,发现其调用了&ntdll.NtOpenProcess 这个函数,咱们继续F7跟进。

75BC83C3    50              push    eax
75BC83C4    8975 FC         mov     dword ptr [ebp-0x4], esi
75BC83C7    C745 E0 1800000>mov     dword ptr [ebp-0x20], 0x18
75BC83CE    8975 E4         mov     dword ptr [ebp-0x1C], esi
75BC83D1    8975 E8         mov     dword ptr [ebp-0x18], esi
75BC83D4    8975 F0         mov     dword ptr [ebp-0x10], esi
75BC83D7    8975 F4         mov     dword ptr [ebp-0xC], esi
75BC83DA    FF15 D411BC75   call    dword ptr [<&ntdll.NtOpenProcess>]   ; ntdll.ZwOpenProcess

当咱们进入到NtOpenProcess这个函数时,会看到以下代码,其间0xBE将其转换成十进制是190

77A05D88 >  B8 BE000000     mov     eax, 0xBE
77A05D8D    BA 0003FE7F     mov     edx, 0x7FFE0300
77A05D92    FF12            call    dword ptr [edx]
77A05D94    C2 1000         retn    0x10

驱动开发:挂接SSDT内核钩子

经过运用Xuetr东西比照,能够发现这个0xBE正好便是 NtOpenProcess函数在内核中的调用号,此刻咱们继续F7进入到call dword ptr [edx] 地址中,能够看到以下代码片段。

77A070AC    8D6424 00       lea     esp, dword ptr [esp]
77A070B0 >  8BD4            mov     edx, esp
77A070B2    0F34            sysenter
77A070B4 >  C3              retn

发现现已到达Ring3层的终点了,其间 sysenter 指令便是用来快速调用一个 Ring0 层的体系进程,简略来说便是将用户层代码向内核层建议的体系调用,由 ntoskrnl.exe 程序向内核发送IO请求,然后内核与驱动程序回来履行的成果。

网上找到一张图,能够很好的解说这个调用的次序。

进入用户层:kernel32(OpenProcess) -> ntdll(NTOpenProcess)->ntdll(SysEnter) 进入内核层:ntoskrnl.exe(nt!ZWOpenProcess) -> ntoskrnl.exe(nt!KiSystemService) -> ntoskrnl.exe(nt!NtOpenProccess)

驱动开发:挂接SSDT内核钩子


读取 SSDT 取得函数地址

上面的试验咱们经过一个函数的调用流程了解到了用户层与内核层的通信进程,其间提到了SSDT索引号的相关概念,SSDT索引号在体系中是固定不变的,运用这个特性就能够定位到原始API函数地址。

Windows 体系供给的SSDT表其作用便是便利应用层之间API的调用,一切的API调用都会转到SSDT这张表中进行参考,这样就能够使不同的API调用全部都转到对应的SSDT表中,然后便利办理。

在SSDT表中有一个 KeServiceDescriptorTable的结构,该结构是由内核导出的表,该表具有一个指针,指向SSDT中包括由 Ntoskrnl.exe 完结的中心体系服务的相应部分,ntoskrnl.exe 中导出了PSERVICE_DESCRPITOR_TABLE类型指针,变量KeServiceDescriptorTable 它是内核的主要组成部分,该表结构如下:

typedef struct _SERVICE_DESCRIPTOR_TABLE
{
    PULONG ServiceTableBase;                      // SSDT 指针,服务表基址
    PULONG ServiceCounterTableBase;         // 包括 SSDT 中每个服务被调用次数的计数器
    ULONG  NumberOfService;                        // SSDT 索引数目
    PUCHAR ParamTableBase;             // 包括每个体系服务参数字节数表的基地址-体系服务参数表 
} SERVICE_DESCRIPTOR_TABLE,*PSERVICE_DESCRIPTOR_TABLE;

表结构中的 SERVICE_DESCRIPTOR_TABLE 包括了一切内核导出函数的地址,在32位体系中每个地址长度为4个字节,所以要取得某个函数在SSDT中的偏移量,能够运用 KeServiceDescriptorTable->ServiceTableBase + 函数ID * 4 的方法来得到。

上方都是一些理论部分,接着咱们经过运用WinDBG来具体查看一些这个表的一些结构信息,此处测试体系是XP

翻开WinDBG调试器,选择【File -> Kernel Debug -> Local -> OK】输入以下指令完结符号文件的加载。

lkd> .sympath srv*d:\symbols*http://msdl.microsoft.com/download/symbol
lkd> .reload
Connected to Windows XP 2600 x86 compatible target at (Sat Sep 21 07:23:56.796 2019 (UTC + 8:00)), ptr64 FALSE
Loading Kernel Symbols

当符号文件加载完结以后,在指令窗口输入 dd KeServiceDescriptorTable 指令。

lkd> dd KeServiceDescriptorTable
8055d700  80505570 00000000 0000011c 805059e4
8055d710  00000000 00000000 00000000 00000000
8055d720  00000000 00000000 00000000 00000000
8055d730  00000000 00000000 00000000 00000000
8055d740  00000002 00002710 bf80c401 00000000
8055d750  b69c4a80 b8e4ab60 8ad620f0 806f80c0
8055d760  00000000 00000000 fee134ac ffffffff
8055d770  5a5a626c 01d56f51 00000000 00000000

从以上结构界说可看出,SSDT 的首地址为 80505570 该地址对应结构中的 ServiceTableBase,可索引的函数有11c对应结构中的NumberOfService,因为SSDT是数组结构,所以里边寄存了一切的 nt!nt* 函数的地址,运用 dd kiservicetable 查看 SSDT 下的一切数组成员信息。

lkd> dd Kiservicetable
80505570  805a5664 805f23ea 805f5c20 805f241c
80505580  805f5c5a 805f2452 805f5c9e 805f5ce2
80505590  80616e80 806180e4 805ed7e8 805ed440
805055a0  805d5c0c 805d5bbc 806174a6 805b6fea
805055b0  80616ac2 805a9aee 805b15fe 805d76d0
805055c0  805028e8 805c96a4 80577b04 80539d88
805055d0  80610090 805bd564 805f615a 80624e3a
805055e0  805fa66e 805a5d52 8062508e 805a5604

为了能够定位到咱们所需求的函数调用号,咱们还需求手动查找一下 ZwOpenProcess 这个函数的ID号,能够运用WinDBG来获取,如下显示调用号为 7A

lkd> u ZwOpenProcess
ntdll!ZwOpenProcess:
7c92d5fe b87a000000      mov     eax,7Ah
7c92d603 ba0003fe7f      mov     edx,offset SharedUserData!SystemCallStub (7ffe0300)
7c92d608 ff12            call    dword ptr [edx]
7c92d60a c21000          ret     10h
7c92d60d 90              nop

上方代码能够得到 nt!NTOpenProcess地址在SSDT表中的索引号。

lkd> dd kiservicetable +0x7A * 4 l 1
80502d74  805c2296
lkd> u 805c2296
nt!NtOpenProcess:
805c2296 68c4000000      push    0C4h
805c229b 68a8aa4d80      push    offset nt!ObWatchHandles+0x25c (804daaa8)
805c22a0 e86b6cf7ff      call    nt!_SEH_prolog (80538f10)

如果符号文件没有加载成功,能够运用下面的方法来查询,找到结构的首地址,然后与函数编号相加来获取。

lkd> dd KeServiceDescriptorTable
80553fa0  80502b8c 00000000 0000011c 80503000
lkd> dd 80502b8c+0x7A*4
80502d74  805c2296 805e49fc 805e4660 805a0722
lkd> u 805c2296
nt!NtOpenProcess:
805c2296 68c4000000      push    0C4h
805c229b 68a8aa4d80      push    offset nt!FsRtlLegalAnsiCharacterArray+0x2008 (804daaa8)
805c22a0 e86b6cf7ff      call    nt!wctomb+0x45 (80538f10)
805c22a5 33f6            xor     esi,esi

留意:在验证的时分需求请封闭杀毒软件,因为杀毒软件会Hook这些地址来到达防护的意图,Hook后这些地址会发生变化无法完结整个查询进程,别的ZwOpenProcessNtOpenProcess其实是一回事。

编写驱动程序: 接着咱们别离运用C言语和汇编完结读取体系的SSDT表,此处运用的体系是Win7,因为 Win7 体系默许情况下本地内核调试功用被屏蔽了,所以有必要在操控台下运转 bcdedit -debug on 指令而且重启来进入调试形式。

进入调试形式后,咱们首要经过WinDBG调试器,来查询一下ZwOpenProcess函数的调用号,履行指令如下。

lkd> u ZwOpenProcess
nt!ZwOpenProcess:
83c8a62c b8be000000      mov     eax,0BEh
83c8a631 8d542404        lea     edx,[esp+4]
83c8a635 9c              pushfd
83c8a636 6a08            push    8
83c8a638 e8b1190000      call    nt!ZwYieldExecution+0x95a (83c8bfee)
83c8a63d c21000          ret     10h
nt!ZwOpenProcessToken:
83c8a640 b8bf000000      mov     eax,0BFh
83c8a645 8d542404        lea     edx,[esp+4]

上方代码中能够看到mov eax,0BEh其间的BE便是ZwOpenProcess函数在当时体系下的调用号,咱们将其转换为十进制是190 当然也能够运用Xuetr等东西来查询。

驱动开发:挂接SSDT内核钩子

接着咱们来编译以下驱动代码,重要的内容现已备注好了,唯一需求更改的当地是 SSDT_Adr = (PLONG)(STB_addr + 0x7A * 4); 其间的0x7A需求改为0xBE

#include <ntddk.h>
//声明:服务描绘表 结构 4个参数
typedef struct _ServiceDescriptorTable {
	PULONG ServiceTableBase;                  // 服务表基址 
	PULONG ServiceCounterTable;               // 服务计数器
	ULONG NumberOfServices;           // 服务的数目  
	PUCHAR ParamTableBase;                    // 体系服务参数表 
}*PServiceDescriptorTable;
// 用指针PServiceDescriptorTable指向:_ServiceDescriptorTable服务描绘表结构
// 有必要extern "C" ,因为文件为CPP
extern "C" PServiceDescriptorTable KeServiceDescriptorTable;
void UnloadDriver(PDRIVER_OBJECT pDriver)
{
	DbgPrint("驱动已卸载!\n");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING str)
{
	//*SSDT_Adr  寄存体系描绘符号表地址。
	//STB_addr  寄存 ServiceTableBase 服务表基址。
	//SDT_Nt    函数 NtOpenProcess的当时地址。
	LONG *SSDT_Adr, STB_addr, SSDT_NtOpenProcess_Addr;
	DbgPrint("驱动程序已加载! \n\r");
	STB_addr = (LONG)KeServiceDescriptorTable->ServiceTableBase;
	DbgPrint("当时服务表基址址 %x \n", STB_addr);
	SSDT_Adr = (PLONG)(STB_addr + 0xBE * 4);                          // 此处需求修正
	DbgPrint("当时STB_addr+0xBE*4= %x \n", SSDT_Adr);
	SSDT_NtOpenProcess_Addr = *SSDT_Adr;
	DbgPrint("当时NtOpenProcess地址 %x \n", SSDT_NtOpenProcess_Addr);
	pDriver->DriverUnload = UnloadDriver;
	return STATUS_SUCCESS;
}

编译程序以后,将其拖入虚拟机,翻开DebugVIew东西,然后加载这个驱动程序,调查是否能够读取到咱们想要的数据。

驱动开发:挂接SSDT内核钩子

再次编译下方的汇编版别,调试调查,成果与C版别相同。

#include <ntddk.h>
extern "C" LONG KeServiceDescriptorTable;
void UnloadDriver(PDRIVER_OBJECT pDriver)
{
	DbgPrint("驱动卸载成功\n\r");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING str)
{
	ULONG SSDT_Addr;
	DbgPrint("驱动加载成功! \n");
	__asm
	{
		push ebx
		push eax
		mov ebx, KeServiceDescriptorTable    // 体系描绘符号表的地址
		mov ebx, [ebx]                       // 取服务表基址给EBX
		mov eax, 0xBE                        // NtOpenProcess=转成十六进制等于BE
		imul eax, eax, 4                      // eax=eax*4 -> 7a*4=1e8
		add ebx, eax                         // eax=1e8与服务表基址EBX相加
		mov ebx, [ebx]                       // 取ebx里边的内容给EBX
		mov SSDT_Addr, ebx                   // 将得到的基址给变量
		pop eax
		pop ebx
	}
	DbgPrint("读取SSDT_NtOpenProcess_Addr=%0x \n", SSDT_Addr);
	pDriver->DriverUnload = UnloadDriver;
	return STATUS_SUCCESS;
}

驱动开发:挂接SSDT内核钩子


Hook 挂钩内核函数

挂钩函数有多种用途,下面的第一种挂钩方法能够完结对特定内核函数的重写,而第二种挂钩方法则能够用于驱动维护。

Hook挂钩重写函数: 挂钩代码如下,关键点现已备注清楚了,编译这段代码,并放入虚拟机履行。

#include <ntddk.h>
#ifdef __cplusplus
extern "C" NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING  RegistryPath);
#endif
void SSDTHookUnload(PDRIVER_OBJECT);
// ******************************************************************************
//这个结构是服务描绘表
typedef struct _SERVICE_DESCRIPTOR_TABLE
{
	PULONG ServiceTableBase;
	PULONG ServiceCounterTableBase;
	ULONG  NumberOfService;
	PUCHAR ParamTableBase;
}SERVICE_DESCRIPTOR_TABLE, *PSERVICE_DESCRIPTOR_TALBE;
extern "C" PSERVICE_DESCRIPTOR_TALBE KeServiceDescriptorTable;
// ******************************************************************************
// 此处为函数的原型声明部分,可经过百度查询到
typedef NTSTATUS(*NtOpenProcessEx)(
	OUT PHANDLE ProcessHandle,
	IN ACCESS_MASK AccessMask,
	IN PVOID ObjectAttributes,
	IN PCLIENT_ID Clientld);
NtOpenProcessEx ulNtOpenProcessEx = NULL;       // 函数指针,寄存原始函数地址
ULONG ulNtOpenProcessExAddr = 0;                  // 在SSDT中的函数地址的指针
// ******************************************************************************
// SSDT 内核内存页默许只读,需求修正CR0 WP位完结读写
// CR0  的第16位是WP位,为0可读写,为1则只读
void REMOVE_ONLY_READ()
{
	__asm
	{
		push eax
			mov eax, CR0
			and eax, ~10000h //16th bit is 0
			mov CR0, eax
			pop eax
	}
}
void RESET_ONLY_READ()
{
	__asm
	{
		push eax
			mov eax, CR0
			or eax, 10000h   //16th bit is 1
			mov CR0, eax
			pop eax
	}
}
// ******************************************************************************
// 此处便是咱们DIY的函数,声明要和NtOpenProcessEx保持一致.
NTSTATUS MyNtOpenProcessEx(
	OUT PHANDLE ProcessHandle,
	IN ACCESS_MASK AccessMask,
	IN PVOID ObjectAttributes,
	IN PCLIENT_ID Clientld)
{
	//DbgPrint("履行我自己的驱动函数\r\n");
	NTSTATUS Status = STATUS_SUCCESS;
	Status = ulNtOpenProcessEx(
		ProcessHandle,
		AccessMask,
		ObjectAttributes,
		Clientld
		);
	return Status;
}
// ******************************************************************************
VOID HookOpenProcess()
{
	ULONG ulSsdt = 0;
	ulSsdt = (ULONG)KeServiceDescriptorTable->ServiceTableBase;              // 读取到SSDT表的基地址
	ulNtOpenProcessExAddr = ulSsdt + 0xBE * 4;                               // 索引到指定的函数
	ulNtOpenProcessEx = (NtOpenProcessEx)*(PULONG)ulNtOpenProcessExAddr;     // 存储原始函数地址
	REMOVE_ONLY_READ();                                                      // 封闭只读维护
	*(PULONG)ulNtOpenProcessExAddr = (ULONG)MyNtOpenProcessEx;               // 将新函数地址赋值
	RESET_ONLY_READ();                                                       // 敞开只读维护
}
// ******************************************************************************
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING  RegistryPath)
{
	DbgPrint("驱动加载完结 !\n");
	DriverObject->DriverUnload = SSDTHookUnload;
	HookOpenProcess();
	return STATUS_SUCCESS;
}
void SSDTHookUnload(IN PDRIVER_OBJECT DriverObject)
{
	REMOVE_ONLY_READ();
	*(PULONG)ulNtOpenProcessExAddr = (ULONG)ulNtOpenProcessEx;
	RESET_ONLY_READ();
	DbgPrint("驱动卸载完结 !\n");
}

当驱动被加载时,能够经过Xuetr查看到内核SSDT层呈现了红色的钩子。

驱动开发:挂接SSDT内核钩子

驱动进程维护: 进程的创立离不开 ZwTerminateProcess 这个函数的支持,所以咱们只需求Hook这个函数并在其内部判别是否是计算器进程,如果是则回来错误,不然回来原始调用,即可完结进程维护,这儿就不演示了,中心伪代码如下。

#define EXE_Name "calc.exe"   // 要检索的进程名
PEPROCESS  processEPROCESS = NULL;          // 保存访问者的EPROCESS
ANSI_STRING  p_StrName1, p_StrName2;        // 保存进程称号
__declspec(naked) VOID  inline_Hook()
{
	processEPROCESS = IoGetCurrentProcess();               // 获取调用者的EPROCESS保存
	DbgPrint("EPROCESS=%x", processEPROCESS);              // 打印出来
	// 经过遍历将调用者姓名保存到p_StrName1中,下方+0x174是表结构 ImageFileName 的偏移地址
	RtlInitAnsiString(&p_StrName1, (PUCHAR)processEPROCESS + 0x174);
	// 将欲比照的字符串保存到p_StrName2中,初始化ANSI字符串
	RtlInitAnsiString(&p_StrName2, EXE_Name);
	// 判别是否持平,持平则阐明是calc.exe进程,咱们直接回来假
	if (RtlCompareString(&p_StrName1, &p_StrName2, TRUE) == 0)
	{
		// 持平阐明是Calc进程调用了该函数
		// 直接回来假,不然履行原函数
	}
}