黑客防线2011合订本(下半年)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

详解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+结构的问题,又是一个全新的话题了,不是一两句话能讲完的,这只能留待日后再讲了。

(编辑提醒:本文涉及的代码可以到黑防官方网站下载