• [转载]关于SSDT的详解


    关于SSDT的详解(1)

    引子

    2006年,中国互联网上的斗争硝烟弥漫。这时的战场上,先前颇为流行的窗口挂钩、API挂钩、进程注入等技术已然成为昨日黄花,大有逐渐淡出之势;取而代之的,则是更狠毒、更为赤裸裸的词汇:驱动、隐藏进程、Rootkit……
    前不久,我不经意翻出自己2005年9月写下的一篇文章《DLL的远程注入技术》,在下面看到了一位名叫L4bm0s的网友说这种技术已经过时了。虽然我也曾想过拟出若干辩解之词聊作应对,不过最终还是作罢了——毕竟,拿出些新的、有技术含量的东西才是王道。于是这一次,李马首度从ring3(应用层)的围城跨出,一跃而投身于ring0(内核层)这一更广阔的天地,便有了这篇《城里城外看SSDT》。——顾名思义,城里和城外的这一墙之隔,就是ring3与ring0的分界。
    在这篇文章里,我会用到太多杂七杂八的东西,比如汇编,比如内核调试器,比如DDK。这诚然是一件令我瞻前顾后畏首畏尾的事情——一方面在ring0我不得不依靠这些东西,另一方面我实在担心它们会导致我这篇文章的阅读门槛过高。所以,我决定尽可能少地涉及驱动、内核与DDK,也不会对诸如如何使用内核调试器等问题作任何讲解——你只需要知道我大概在做些什么,这就足够了。

    什么是SSDT?

    什么是SSDT?自然,这个是我必须回答的问题。不过在此之前,请你打开命令行(cmd.exe)窗口,并输入“dir”并回车——好了,列出了当前目录下的所有文件和子目录。
    那么,以程序员的视角来看,整个过程应该是这样的:

    1. 由用户输入dir命令。
    2. cmd.exe获取用户输入的dir命令,在内部调用对应的Win32 API函数FindFirstFile、FindNextFile和FindClose,获取当前目录下的文件和子目录。
    3. cmd.exe将文件名和子目录输出至控制台窗口,也就是返回给用户。

    到此为止我们可以看到,cmd.exe扮演了一个非常至关重要的角色,也就是用户与Win32 API的交互。——你大概已经可以猜到,我下面要说到的SSDT亦必将扮演这个角色,这实在是一点新意都没有。
    没错,你猜对了。SSDT的全称是System Services Descriptor Table,系统服务描述符表。这个表就是一个把ring3的Win32 API和ring0的内核API联系起来的角色,下面我将以API函数OpenProcess为例说明这个联系的过程。
    你可以用任何反汇编工具来打开你的kernel32.dll,然后你会发现在OpenProcess中有类似这样的汇编代码:

    汇编代码

    1. call ds:NtOpenProcess  

    这就是说,OpenProcess调用了ntdll.dll的NtOpenProcess函数。那么继续反汇编之,你会发现ntdll.dll中的这个函数很短:

    汇编代码

    1. mov eax, 7Ah   
    2. mov edx, 7FFE0300h   
    3. call dword ptr [edx]   
    4. retn 10h  

    另外,call的一句实质是调用了KiFastSystemCall:

    C++代码

    1. mov edx, esp   
    2. sysenter  

    上面是我的XP Professional sp2中ntdll.dll的反汇编结果,如果你用的是2000系统,那么可能是这个样子:

    C++代码

    1. mov eax, 6Ah   
    2. lea edx, [esp+4]   
    3. int 2Eh   
    4. retn 10h  

    虽然它们存在着些许不同,但都可以这么来概括:

    1. 把一个数放入eax(XP是0x7A,2000是0x6A),这个数值称作系统的服务号。
    2. 把参数堆栈指针(esp+4)放入edx。
    3. sysenter或int 2Eh。

    好了,你在ring3能看到的东西就到此为止了。事实上,在ntdll.dll中的这些函数可以称作真正的NT系统服务的存根(Stub)函数。分隔ring3与ring0城里城外的这一道叹息之墙,也正是由它们打通的。接下来SSDT就要出场了,come some music。

    站在城墙看城外

    插一句先,貌似到现在为止我仍然没有讲出来SSDT是个什么东西,真正可以算是“犹抱琵琶半遮面”了。——书接上文,在你调用sysenter或int 2Eh之后,Windows系统将会捕获你的这个调用,然后进入ring0层,并调用内核服务函数NtOpenProcess,这个过程如下图所示。

    SSDT在这个过程中所扮演的角色是至关重要的。让我们先看一看它的结构,如下图。

    当程序的处理流程进入ring0之后,系统会根据服务号(eax)在SSDT这个系统服务描述符表中查找对应的表项,这个找到的表项就是系统服务函数NtOpenProcess的真正地址。之后,系统会根据这个地址调用相应的系统服务函数,并把结果返回给ntdll.dll中的NtOpenProcess。图中的“SSDT”所示即为系统服务描述符表的各个表项;右侧的“ntoskrnl.exe”则为Windows系统内核服务进程(ntoskrnl即为NT OS KerneL的缩写),它提供了相对应的各个系统服务函数。ntoskrnl.exe这个文件位于Windows的system32目录下,有兴趣的朋友可以反汇编一下。
    附带说两点。根据你处理器的不同,系统内核服务进程可能也是不一样的。真正运行于系统上的内核服务进程可能还有ntkrnlmp.exe、ntkrnlpa.exe这样的情况——不过为了统一起见,下文仍统称这个进程为ntoskrnl.exe。另外,SSDT中的各个表项也未必会全部指向ntoskrnl.exe中的服务函数,因为你机器上的杀毒监控或其它驱动程序可能会改写SSDT中的某些表项——这也就是所谓的“挂钩SSDT”——以达到它们的“主动防御”式杀毒方式或其它的特定目的。

    KeServiceDescriptorTable

    事实上,SSDT并不仅仅只包含一个庞大的地址索引表,它还包含着一些其它有用的信息,诸如地址索引的基地址、服务函数个数等等。ntoskrnl.exe中的一个导出项KeServiceDescriptorTable即是SSDT的真身,亦即它在内核中的数据实体。SSDT的数据结构定义如下:

    C++代码

      1. typedef struct _tagSSDT {   
      2.     PVOID pvSSDTBase;   
      3.     PVOID pvServiceCounterTable;   
      4.     ULONG ulNumberOfServices;   
      5.     PVOID pvParamTableBase;   
      6. } SSDT, *PSSDT;  
      7. 其中,pvSSDTBase就是上面所说的“系统服务描述符表”的基地址。pvServiceCounterTable则指向另一个索引表,该表包含了每个服务表项被调用的次数;不过这个值只在Checkd Build的内核中有效,在Free Build的内核中,这个值总为NULL(注:Check/Free是DDK的Build模式,如果你只使用SDK,可以简单地把它们理解为Debug/Release)。ulNumberOfServices表示当前系统所支持的服务个数。pvParamTableBase指向SSPT(System Service Parameter Table,即系统服务参数表),该表格包含了每个服务所需的参数字节数。
        下面,让我们开看看这个结构里边到底有什么。打开内核调试器(以kd为例),输入命令显示KeServiceDescriptorTable,如下。

        WinDbg输出
        1. lkd> dd KeServiceDescriptorTable l4   
        2. 8055ab80 804e3d20 00000000 0000011c 804d9f48  

        接下来,亦可根据基地址与服务总数来查看整个服务表的各项:

        WinDbg输出
        1. lkd> dd 804e3d20 l11c   
        2. 804e3d20 80587691 f84317aa f84317b4 f84317be   
        3. 804e3d30 f84317c8 f84317d2 f84317dc f84317e6   
        4. 804e3d40 8057741c f84317fa f8431804 f843180e   
        5. 804e3d50 f8431818 f8431822 f843182c f8431836   
        6. ...  

        你获得的结果可能和我会有不同——我指的是那堆以十六进制f开头的地址项,因为我的SSDT被System Safety Monitor接管了,没留下几个原生的ntoskrnl.exe表项。
        现在是写些代码的时候了。KeServiceDescriptorTable及SSDT各个表项的读取只能在ring0层完成,于是这里我使用了内核驱动并借助DeviceIoControl来完成。其中DeviceIoControl的分发代码实现如下面的代码所示,没有什么技术含量,所以不再解释。

        C++代码
        1. switch ( IoControlCode )   
        2. {   
        3. case IOCTL_GETSSDT:   
        4.      {   
        5.         __try  
        6.          {   
        7.              ProbeForWrite( OutputBuffer, sizeof( SSDT ), sizeof( ULONG ) );   
        8.              RtlCopyMemory( OutputBuffer, KeServiceDescriptorTable, sizeof( SSDT ) );   
        9.          }   
        10.          __except ( EXCEPTION_EXECUTE_HANDLER )   
        11.          {   
        12.              IoStatus->Status = GetExceptionCode();   
        13.          }   
        14.      }   
        15.     break;   
        16. case IOCTL_GETPROC:   
        17.      {   
        18.         ULONG uIndex = 0;   
        19.         PULONG pBase = NULL;   
        20.   
        21.         __try  
        22.          {   
        23.              ProbeForRead( InputBuffer, sizeof( ULONG ), sizeof( ULONG ) );   
        24.              ProbeForWrite( OutputBuffer, sizeof( ULONG ), sizeof( ULONG ) );   
        25.          }   
        26.          __except( EXCEPTION_EXECUTE_HANDLER )   
        27.          {   
        28.              IoStatus->Status = GetExceptionCode();   
        29.             break;   
        30.          }   
        31.   
        32.          uIndex = *(PULONG)InputBuffer;   
        33.         if ( KeServiceDescriptorTable->ulNumberOfServices <= uIndex )   
        34.          {   
        35.              IoStatus->Status = STATUS_INVALID_PARAMETER;   
        36.             break;   
        37.          }   
        38.          pBase = KeServiceDescriptorTable->pvSSDTBase;   
        39.          *((PULONG)OutputBuffer) = *( pBase + uIndex );   
        40.      }   
        41.     break;   
        42. // ...   
        43. }  

        补充一下,再。DDK的头文件中有一件很遗憾的事情,那就是其中并未声明KeServiceDescriptorTable,不过我们可以自己手动添加之:

        C++代码
        1. extern PSSDT KeServiceDescriptorTable;  

        ——当然,如果你对DDK开发实在不感兴趣的话,亦可以直接使用配套代码压缩包中的SSDTDump.sys,并使用DeviceIoControl发送IOCTL_GETSSDT和IOCTL_GETPROC控制码即可;或者,直接调用我为你准备好的两个函数:

        C++代码
        1. BOOL GetSSDT( IN HANDLE hDriver, OUT PSSDT buf );   
        2. BOOL GetProc( IN HANDLE hDriver, IN ULONG ulIndex, OUT PULONG buf );  
  • 相关阅读:
    freopen
    字符
    map映射
    P3512 [POI2010]PIL-Pilots-洛谷luogu
    快读
    单调队列&单调栈
    简写
    邻接表&链式前向星
    mysql参数详解
    网络管理指南
  • 原文地址:https://www.cnblogs.com/02xiaoma/p/2754558.html
Copyright © 2020-2023  润新知