详解Win64上的SSDT
前置知识:汇编
关键词:win64,SSDT
图/文 胡文亮
探究了一年多的Win64内核,终于到了核心部分:SSDT。我估计SSDT这个词对很多底层爱好者都有特殊的含义,绝对不仅仅是“系统服务描述表”这么简单,相信不少人都是从玩SSDT HOOK开始玩Windows内核的,至少我就是如此。大约十年前,网上出现了第一篇详解Win32上SSDT的中文文章。十年之后,让我来用中文来写这篇《详解Win64上的SSDT》。
好了,废话就不说了,说多了估计有读者会拿砖头拍我。言归正传,本文只解决两个问题。第一,如何在内核里动态获得SSDT的基址;第二,如何在内核里动态获得Native API的地址(无论导出与否)。至于如何调用Native API,就不解释了,因为调用API是C语言的问题,和平台无关。
在Win32下,第一个问题就根本不是问题,因为KeService DescriptorTable直接被导出了。但是Win64下,KeServiceDescriptor Table没有被导出。所以我们必须搜索得到它的地址。首先反汇编一下KiSystemCall64:
lkd> uf KiSystemCall64 Flow analysis was incomplete, some code may be missing nt!KiSystemCall64: fffff800`03cc7ec00f01f8 swapgs fffff800`03cc7ec3654889242510000000 mov qword ptr gs:[10h],rsp fffff800`03cc7ecc65488b2425a8010000 movrsp,qword ptr gs:[1A8h] fffff800`03cc7ed56a2b push 2Bh fffff800`03cc7ed765ff342510000000 push qword ptr gs:[10h] fffff800`03cc7edf4153 push r11 fffff800`03cc7ee16a33 push 33h fffff800`03cc7ee351 push rcx fffff800`03cc7ee4498bca mov rcx,r10 fffff800`03cc7ee74883ec08 sub rsp,8 fffff800`03cc7eeb55 push rbp fffff800`03cc7eec4881ec58010000 sub rsp,158h fffff800`03cc7ef3488dac2480000000 lea rbp,[rsp+80h] fffff800`03cc7efb48899dc0000000 mov qword ptr [rbp +0C0h],rbx fffff800`03cc7f024889bdc8000000 mov qword ptr [rbp +0C8h],rdi fffff800`03cc7f094889b5d0000000 mov qword ptr [rbp +0D0h],rsi fffff800`03cc7f10c645ab02 mov byte ptr [rbp-55h],2 fffff800`03cc7f1465488b1c2588010000 mov rbx,qword ptr gs: [188h] fffff800`03cc7f1d0f0d8bd8010000 prefetchw [rbx+1D8h] fffff800`03cc7f240fae5dac stmxcsr dword ptr [rbp-54h] fffff800`03cc7f28650fae142580010000 ldmxcsr dword ptr gs:[180h] fffff800`03cc7f31807b0300 cmp byte ptr [rbx+3],0 fffff800`03cc7f3566c785800000000000 mov word ptr [rbp+80h],0 fffff800`03cc7f3e0f848c000000 je nt!KiSystemCall64+0x110 (fffff800`03cc7fd0) 【省略大量无关代码】 nt!KiSystemCall64+0x110: fffff800`03cc7fd0 fb sti fffff800`03cc7fd1 48898be0010000 mov qword ptr [rbx +1E0h],rcx fffff800`03cc7fd8 8983f8010000 mov dword ptr [rbx +1F8h],eax fffff800`03cc7fde 4889a3d8010000 mov qword ptr [rbx +1D8h],rsp fffff800`03cc7fe5 8bf8 mov edi,eax fffff800`03cc7fe7 c1ef07 shr edi,7 fffff800`03cc7fea 83e720 and edi,20h fffff800`03cc7fed 25ff0f0000 and eax,0FFFh nt!KiSystemServiceRepeat: fffff800`03cc7ff2 4c8d1547782300 lea r10,[nt!KeService DescriptorTable (fffff800`03eff840)] fffff800`03cc7ff9 4c8d1d80782300 lea r11,[nt!KeService DescriptorTableShadow (fffff800`03eff880)] fffff800`03cc8000 f7830001000080000000 test dword ptr [rbx +100h],80h fffff800`03cc800a 4d0f45d3 cmovne r10,r11 fffff800`03cc800e 423b441710 cmp eax,dword ptr [rdi +r10+10h] fffff800`03cc8013 0f83e9020000 jae nt!KiSystemServiceExit+0x1a7 (fffff800`03cc8302) nt!KiSystemServiceRepeat+0x27: fffff800`03cc8019 4e8b1417 mov r10,qword ptr [rdi +r10] fffff800`03cc801d 4d631c82 movsxd r11,dword ptr [r10 +rax*4] fffff800`03cc8021 498bc3 mov rax,r11 fffff800`03cc8024 49c1fb04 sar r11,4 fffff800`03cc8028 4d03d3 add r10,r11 fffff800`03cc802b 83ff20 cmp edi,20h fffff800`03cc802e 7550 jne nt!KiSystemServiceGdiTebAccess+0x49 (fffff800`03cc8080) 【省略大量无关代码】
最终,我们在KiSystemServiceRepeat里找到了KeService DescriptorTable的踪影。可能会有人问,为什么不直接反汇编KiSystemServiceRepeat呢?原因很简单,因为你找不到KiSystem ServiceRepeat的地址。虽然KiSystemCall64和KiSystemService Repeat都没有由ntoskrnl. exe导出,但是我们能找到KiSystemCall64的地址。怎么找?直接读取指定的msr得出。很多人只听过通用寄存器和调试寄存器,其实还有很多其他的寄存器(你想想最古老的586 CPU的一级缓存都有32KB呢,而现在的AMD64 CPU的每个核心的一级缓存正好有64KB)。Msr的中文全称是就是“特别模块寄存器”(model specific register),它控制CPU的工作环境和标示CPU的工作状态等信息(例如倍频、最大TDP、危险警报温度),它能够读取,也能够写入,但是无论读取还是写入,都只能在Ring 0下进行。我们通过读取C0000082寄存器,能够得到KiSystemCall64的地址,然后从KiSystemCall64的地址开始,往下搜索0x500字节左右(特征码是4c8d15),就能得到KeServiceDescriptorTable的地址了。同理,我们换一下特征码(4c8d1d),就能获得KeServiceDescriptor TableShadow的地址了。
先用WinDBG证明一下(输入rdmsr c0000082),如图1所示。
图1
代码实现如下:
ULONGLONG MyGetKeServiceDescriptorTable64() { 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 + (ULONG LONG)i + 7; return addr; } } } return 0; }
计算地址的核心代码是4c8d15后面的那4个字节(正好算是一个long)加上当前指令的起始地址再加上7。为什么要加上7呢?因为[lea r10,XXXXXXXX]指令的长度是7个字节。另外,我在外国的网站上看到了同样功能的另外一段代码,也贴出来给大家看一下:
ULONGLONG GetKeServiceDescriptorTable64() { char KiSystemServiceStart_pattern[13] = "\x8B\xF8\xC1\xEF\ x07\x83\xE7\x20\x25\xFF\x0F\x00\x00"; ULONGLONG CodeScanStart = (ULONGLONG)&_ strnicmp; ULONGLONG CodeScanEnd = (ULONGLONG)&KdDebugger NotPresent; ULONGLONG i, tbl_address, b; for (i = 0; i < CodeScanEnd - CodeScanStart; i++) { if (!memcmp((char*)(ULONGLONG)CodeScanStart +i, (char*)KiSystemServiceStart_pattern,13)) { for (b = 0; b < 50; b++) { tbl_address = ((ULONGLONG)CodeScan Start+i+b); if (*(USHORT*) ((ULONGLONG)tbl_ address)==(USHORT)0x8d4c) return ((LONGLONG)tbl_address +7) + *(LONG*)(tbl_address +3); } } } return 0; }
接下来要获取Native API在内核里的地址了。获取Native API在内核里的地址需要得知Native API的index。由于我还没有摸透PE+格式,所以这个index暂时用硬编码。怎么得知这个index呢?我们还是需要使用WinDBG,不过不需要内核调试,普通调试就可以了。随便创建一个进程,然后使用WinDBG附加,再然后在命令栏里输入:u ntdll!函数名
比如输入u ntdll!NtOpenProcess,出现以下结果:
0:004> u ntdll!ntopenprocess ntdll!ZwOpenProcess: 00000000`772b0110 4c8bd1 mov r10,rcx 00000000`772b0113 b823000000 mov eax,23h 00000000`772b0118 0f05 syscall 00000000`772b011a c3 ret
再输入u ntdll!NtTerminateProcess,出现以下结果:
0:004> u ntdll!NtTerminateProcess ntdll!ZwTerminateProcess: 00000000`772b0170 4c8bd1 mov r10,rcx 00000000`772b0173 b829000000 mov eax,29h 00000000`772b0178 0f05 syscall 00000000`772b017a c3 ret
可以看到两次反汇编的结果几乎完全相同,唯一不同的地方是第二句。XXh就是此函数的index。
接下来的重头戏就是分析怎样由index和SSDT基址得到Native API的地址。可以说,每个Win64系统(XP/2003/VISTA/7)的计算方法都不同。我下面分析的计算方法,是Windows 7 X64的。这个计算方法不难寻找,它就隐藏在KiSystemServiceStart里。先看一段对KiSystemServiceStart的反汇编代码:
nt!KiSystemServiceStart: fffff800`03cc7fde 4889a3d8010000 mov qword ptr [rbx +1D8h],rsp ;Native API Index fffff800`03cc7fe5 8bf8 mov edi,eax ;操作1 fffff800`03cc7fe7 c1ef07 shr edi,7 ;操作2 fffff800`03cc7fea 83e720 and edi,20h ;操作3(和获得地址无关,和对比函数有效性有关) fffff800`03cc7fed 25ff0f0000 and eax,0FFFh nt!KiSystemServiceRepeat: ;取得SSDT地址 fffff800`03cc3ff2 4c8d1547782300 lea r10,[nt!KeService DescriptorTable (fffff800`03efb840)] ;取得SSSDT地址 fffff800`03cc3ff9 4c8d1d80782300 lea r11,[nt!KeServiceDescriptorTableShadow (fffff800`03efb880)] ;判断调用的是ssdt函数还是sssdt函数 fffff800`03cc4000 f7830001000080000000 test dword ptr [rbx +100h],80h ;根据上面的判断把ssdt或sssdt的基址放入r10 fffff800`03cc400a 4d0f45d3 cmovne r10,r11 ;判断函数是否有效 fffff800`03cc400e 423b441710 cmp eax,dword ptr [rdi +r10+10h] ;条件跳转 fffff800`03cc4013 0f83e9020000 jae nt!KiSystemService Exit+0x1a7 (fffff800`03cc4302) ;计算步骤1 fffff800`03cc4019 4e8b1417 mov r10,qword ptr [rdi +r10] ;计算步骤2 fffff800`03cc401d 4d631c82 movsxd r11,dword ptr [r10 +rax*4] ;计算步骤3 fffff800`03cc4021 498bc3 mov rax,r11 ;计算步骤4 fffff800`03cc4024 49c1fb04 sar r11,4 ;计算步骤5 fffff800`03cc4028 4d03d3 add r10,r11 ;edi和0x20对比(和计算函数地址无关) fffff800`03cc402b 83ff20 cmp edi,20h ;条件跳转 fffff800`03cc402e 7550 jne nt!KiSystemService GdiTebAccess+0x49 (fffff800`03cc4080) 【省略大量无关代码】 ;调用Native API fffff800`03cc4150 41ffd2 call r10
一般来说反汇编代码里的精华部分很少,不过这段汇编代码却全部都是精华,它完整地诠释了系统是怎样由SSDT基址和Native API的index获得Native API的地址。我曾经尝试把这段汇编代码变成数学公式,但是算出来的结果不对。为了保证能算对地址,我决定使用原版的汇编代码来计算:
mov rax, rcx ;rcx=Native API的index lea r10,[rdx] ;rdx=ssdt基址 mov edi,eax shr edi,7 and edi,20h mov r10, qword ptr [r10+rdi] movsxd r11,dword ptr [r10+rax*4] mov rax,r11 sar r11,4 add r10,r11 mov rax,r10 ret
由于微软的x64编译器不能内联汇编,所以使用我只能使用Shellcode了:
typedef UINT64 (__fastcall *SCFN)(UINT64,UINT64); SCFN scfn; VOID Initxxxx() { UCHAR strShellCode[36]="\x48\x8B\xC1\x4C\x8D\ x12\x8B\xF8\xC1\xEF\x07\x83\xE7\x20\x4E\x8B\x14\x17\x4 D\x63\x1C\x82\x49\x8B\xC3\x49\xC1\xFB\x04\x4D\x03\xD3\x 49\x8B\xC2\xC3"; scfn=ExAllocatePool(NonPagedPool,36); memcpy(scfn,strShellCode,36); } ULONGLONG GetSSDTFunctionAddress64(ULONGLONG NtApiIndex) { ULONGLONG ret=0; ULONGLONG ssdt=GetKeServiceDescriptorTable64(); if(scfn==NULL) Initxxxx(); ret=scfn(NtApiIndex, ssdt); return ret; }
测试代码和运行结果如图2所示。
DbgPrint("SSDT: %llx[TA's method]",MyGetKeServiceDescriptor Table64()); DbgPrint("SSDT: %llx[Foreigner's method]",GetKeService DescriptorTable64()); DbgPrint("NtOpenProcess: %llx",GetSSDTFunction Address64(0x23)); DbgPrint("NtTerminateProcess: %llx",GetSSDTFunction Address64(0x29));
图2
本文到此结束,至于如何在Win64上开启调试模式和测试签名模式、如何给驱动加上测试签名,如何让DBGVIEW有输出,如何获取X64ASM的Shellcode,我就不赘述了,请参看我以前的文章。文章不算太长,但是我的研究时间很长,几乎长达10天。在此期间遇到了各种莫名其妙的问题,在文中都略过不表了。希望本文能给各位读者带来一些帮助。至于通过Native API名获得Native API Index,这关系到PE+结构的问题,又是一个全新的话题了,不是一两句话能讲完的,这只能留待日后再讲了。
(编辑提醒:本文涉及的代码可以到黑防官方网站下载)