博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
驱动保护:挂接SSDT内核钩子(1)
阅读量:5117 次
发布时间:2019-06-13

本文共 12888 字,大约阅读时间需要 42 分钟。

SSDT 中文名称为系统服务描述符表,该表的作用是将Ring3应用层与Ring0内核层,两者的API函数连接起来,起到承上启下的作用,SSDT并不仅仅只包含一个庞大的地址索引表,它还包含着一些其它有用的信息,诸如地址索引的基址、服务函数个数等,SSDT 通过修改此表的函数地址可以对常用 Windows 函数进行内核级的Hook,从而实现对一些核心的系统动作进行过滤、监控的目的,接下来将演示如何通过编写简单的驱动程序,来实现搜索 SSDT 函数的地址,并能够实现简单的内核 Hook 挂钩。

在开始编写驱动之前,我们先来分析一下Ring3到Ring0是如何协作的,这里通过C语言调用 OpenProcess 函数,并分析它的执行过程,先来创建一个C程序。

#include 
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, esp0040102A  |.  68 F4090000   push    0x9F4                                       ; /ProcessId = 0x9F40040102F  |.  6A 00         push    0x0                                         ; |Inheritable = FALSE00401031  |.  68 FF0F1F00   push    0x1F0FFF                                    ; |Access = PROCESS_ALL_ACCESS00401036  |.  FF15 4CA14200 call    dword ptr [<&KERNEL32.OpenProcess>]         ; \OpenProcess

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

75BC83C3    50              push    eax75BC83C4    8975 FC         mov     dword ptr [ebp-0x4], esi75BC83C7    C745 E0 1800000>mov     dword ptr [ebp-0x20], 0x1875BC83CE    8975 E4         mov     dword ptr [ebp-0x1C], esi75BC83D1    8975 E8         mov     dword ptr [ebp-0x18], esi75BC83D4    8975 F0         mov     dword ptr [ebp-0x10], esi75BC83D7    8975 F4         mov     dword ptr [ebp-0xC], esi75BC83DA    FF15 D411BC75   call    dword ptr [<&ntdll.NtOpenProcess>]   ; ntdll.ZwOpenProcess

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

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

1379525-20190920183542968-1192444598.png

通过使用Xuetr工具对比,可以发现这个0xBE正好就是 NtOpenProcess函数在内核中的调用号,此时我们继续F7进入到call dword ptr [edx] 地址中,可以看到以下代码片段。

77A070AC    8D6424 00       lea     esp, dword ptr [esp]77A070B0 >  8BD4            mov     edx, esp77A070B2    0F34            sysenter77A070B4 >  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)

1379525-20190920185230141-706656584.png

读取 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/symbollkd> .reloadConnected to Windows XP 2600 x86 compatible target at (Sat Sep 21 07:23:56.796 2019 (UTC + 8:00)), ptr64 FALSELoading Kernel Symbols

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

lkd> dd KeServiceDescriptorTable8055d700  80505570 00000000 0000011c 805059e48055d710  00000000 00000000 00000000 000000008055d720  00000000 00000000 00000000 000000008055d730  00000000 00000000 00000000 000000008055d740  00000002 00002710 bf80c401 000000008055d750  b69c4a80 b8e4ab60 8ad620f0 806f80c08055d760  00000000 00000000 fee134ac ffffffff8055d770  5a5a626c 01d56f51 00000000 00000000

从以上结构定义可看出,SSDT 的首地址为 80505570 该地址对应结构中的 ServiceTableBase,可索引的函数有11c对应结构中的NumberOfService,由于SSDT是数组结构,所以里面存放了所有的 nt!nt* 函数的地址,使用 dd kiservicetable 查看 SSDT 下的所有数组成员信息。

lkd> dd Kiservicetable80505570  805a5664 805f23ea 805f5c20 805f241c80505580  805f5c5a 805f2452 805f5c9e 805f5ce280505590  80616e80 806180e4 805ed7e8 805ed440805055a0  805d5c0c 805d5bbc 806174a6 805b6fea805055b0  80616ac2 805a9aee 805b15fe 805d76d0805055c0  805028e8 805c96a4 80577b04 80539d88805055d0  80610090 805bd564 805f615a 80624e3a805055e0  805fa66e 805a5d52 8062508e 805a5604

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

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

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

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

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

lkd> dd KeServiceDescriptorTable80553fa0  80502b8c 00000000 0000011c 80503000lkd> dd 80502b8c+0x7A*480502d74  805c2296 805e49fc 805e4660 805a0722lkd> u 805c2296nt!NtOpenProcess:805c2296 68c4000000      push    0C4h805c229b 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 ZwOpenProcessnt!ZwOpenProcess:83c8a62c b8be000000      mov     eax,0BEh83c8a631 8d542404        lea     edx,[esp+4]83c8a635 9c              pushfd83c8a636 6a08            push    883c8a638 e8b1190000      call    nt!ZwYieldExecution+0x95a (83c8bfee)83c8a63d c21000          ret     10hnt!ZwOpenProcessToken:83c8a640 b8bf000000      mov     eax,0BFh83c8a645 8d542404        lea     edx,[esp+4]

上方代码中可以看到mov eax,0BEh其中的BE就是ZwOpenProcess函数在当前系统下的调用号,我们将其转换为十进制是190 当然也可以使用Xuetr等工具来查询。

1379525-20190921085430886-457704583.png

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

#include 
//声明:服务描述表 结构 4个参数typedef struct _ServiceDescriptorTable { PULONG ServiceTableBase; // 服务表基址 PULONG ServiceCounterTable; // 服务计数器 ULONG NumberOfServices; // 服务的数目 PUCHAR ParamTableBase; // 系统服务参数表 }*PServiceDescriptorTable;// 用指针PServiceDescriptorTable指向:_ServiceDescriptorTable服务描述表结构// 必须extern "C" ,因为文件为CPPextern "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工具,然后加载这个驱动程序,观察是否能够读取到我们想要的数据。

1379525-20190921090238781-1996161101.png

再次编译下方的汇编版本,调试观察,结果与C版本相同。

#include 
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;}

1379525-20190921091339096-913056947.png

Hook 挂钩内核函数

挂钩函数有多种用途,下面的第一种挂钩方式可以实现对特定内核函数的重写,而第二种挂钩方式则可以用于驱动保护。

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

#include 
#ifdef __cplusplusextern "C" NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath);#endifvoid 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层出现了红色的钩子。

1379525-20190921124331445-922376356.png

驱动进程保护: 进程的创建离不开 ZwTerminateProcess 这个函数的支持,所以我们只需要Hook这个函数并在其内部判断是否是计算器进程,如果是则返回错误,否则返回原始调用,即可完成进程保护,这里就不演示了,核心伪代码如下。

#define EXE_Name "calc.exe"   // 要检索的进程名PEPROCESS  processEPROCESS = NULL;          // 保存访问者的EPROCESSANSI_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进程调用了该函数        // 直接返回假,否则执行原函数    }}

转载于:https://www.cnblogs.com/LyShark/p/11558680.html

你可能感兴趣的文章
Oracle中包的创建
查看>>
团队开发之个人博客八(4月27)
查看>>
发布功能完成
查看>>
【原】小程序常见问题整理
查看>>
C# ITextSharp pdf 自动打印
查看>>
【Java】synchronized与lock的区别
查看>>
django高级应用(分页功能)
查看>>
【转】Linux之printf命令
查看>>
关于PHP会话:session和cookie
查看>>
Chrome development tools学习笔记(3)
查看>>
软件过程的守护神
查看>>
NAT配置
查看>>
【翻译】Brewer's CAP Theorem CAP定理
查看>>
undefined与null
查看>>
redis总结
查看>>
解决SQL Server 阻止了对组件 'Ad Hoc Distributed Queries' 的 STATEMENT 'OpenRowset/OpenDatasource' 的访问...
查看>>
STM32F10x_RTC秒中断
查看>>
[原创]网站HTML,XHTML,XML,WML,CSS等测试验证工具介绍
查看>>
display:none和visiblity:hidden区别
查看>>
C#double转化成字符串 保留小数位数, 不以科学计数法的形式出现。
查看>>