首页 > 代码库 > 挂钩SSDT详解附源代码

挂钩SSDT详解附源代码

源代码下载地址:挂钩SSDT源代码

 

    据微软所言,服务描述符表是一个由四个结构组成的数组,其中的每一个结构都是由四个双字项组成。因此,我们可以将服务描述符表表示为:

typedef struct ServiceDescriptorTable

{

      SDE ServiceDescriptor[4];

}SDT;

其中,其中的每一个服务描述符呈现出四个双字的形式,它的结构为:

#pragma pack(1)

typedef struct ServiceDescriptorEntry

{

      unsigned int *ServiceTableBase;

      unsigned int *ServiceCounterTableBase; //Used only in checked build

      unsigned int NumberOfServices;

      unsigned char *ParamTableBase;

} SDE,*PSDE;

#pragma pack()

我们寻找的数据结构SSDT是由第一个域所引用的调用表(可以在调试器中用命令dps nt!KiServiceTable查看)

 

一、      禁用WP

如果我们能够简单的将值换人并换出SSDT该有多好,然而障碍在于SSDT存在于只读内存中。因此,为了挂钩由SSDT引用的例程,我们一般的策略看起来应该类似于以下形式(以伪代码表示):

DisableReadProtection();

ModifySSDT();

EnableReadProtection();

英特尔的文档写明:“如果CR0.WP=1,访问类型由页目录和页表项的R/W标志位决定。如果CR0.WP=0,超级用户权限允许读写访问。”因此,为破坏SSDT上的写保护,我们需要临时清除写保护(Write Protect,WP)标志。

通过分配自己的MDL来描述SSDT,我们可以禁用读保护。MDL与存储SSDT内容的物理内存页相关联,MDL元素结构在WDK附带的wdm.h头文件中的定义为:

typedef struct _MDL

{

      struct _MDL *Next;

      CSHORT Size;

      CSHORT MdlFlags;

      struct _EPROCESS *Process;

      PVOID MappedSystemVa;

      PVOID StartVa;

      ULONG ByteCount;

      ULONG ByteOffset;

}MDL,*PMDL;

对于物理内存的这一区域,一旦我们添加了自己的私有描述,就可以使用按位OR以及MDL_MAPPED_TO_SYSTEM_VA宏调整MDL的权限。再一次,我们可以成功实现,因为我们拥有自己的MDL对象。最后,我们使用SSDT在物理内存中的位置与MDL之间的映射正式化。

然后,锁定我们在线性空间创建的MDL缓冲区。作为回报,我们得到一个新的线性地址,它也指向SSDT,而且能够对其进行修改。

简而言之,使用MDL,我们在系统的线性地址空间中创建了新的可写缓冲区,它恰好解析成存储SSDT的物理内存。只要两个区域解析成同样的物理内存区域,它就没什么不同。它只是一个数字游戏,纯粹而简单。如果你不能写入线性内存的给定区域,那么就创建自己的区域并且向其写入。

WP_GLOBALS disableWP_MDL(unsigned int *ssdt,unsigned int nServices)

{

  WP_GLOBALS wpGbs;

  DbgPrint("[disableWP_MDL] SSDT=%\n",ssdt);

  DbgPrint("[disableWP_MDL] nServices=%\n",nServices);

 

  wpGbs.pMDL = MmCreateMdl(NULL, (PVOID)ssdt, (SIZE_T)nServices*4);

  if(wpGbs.pMDL==NULL)

  {

        DbgPrint("[disableWP_MDL] %\n","call to MmCreateMdl failed");

        return (wpGbs);

  }

 

  MmBuildMdlForNonPagedPool(wpGbs.pMDL);

  wpGbs.pMDL->MdlFlags = wpGbs.pMDL->MdlFlags | MDL_MAPPED_TO_SYSTEM_VA;

  wpGbs.callTable = (unsigned char*)MmMapLockedPages(wpGbs.pMDL, KernelMode);

  if(wpGbs.callTable==NULL)

  {

        DbgPrint("[disableWP_MDL] %\n","call to MmMapLockedPages failed");

        return (wpGbs);

  }

 

 

  return (wpGbs);

}

这个例程返回一个结构,它只是一个指向我们MDL和SSDT指针的包装器。

typedef struct _WP_GLOBALS

{

      unsigned char *callTable;

      PMDL pMDL;

}WP_GLOBALS;

我们通过前面的函数返回该结构,以便访问可写版本的SSDT,而且当不再需要MDL缓冲区时,我们可以复原事务的原始状态。为复原系统状态,我们使用以下函数:

void enableWP_MDL(PMDL mdlPtr,unsigned char *callTable)

{

      if(mdlPtr != NULL)

      {

           MmUnmapLockedPages((PVOID)callTable, mdlPtr);

           IoFreeMdl(mdlPtr);

      }   

}

 

二、      挂钩SSDT

一旦禁用了写保护,我们可以使用以下例程将新的函数地址换人SSDT

unsigned char* hookSSDT(unsigned char *apiCall,unsigned char *newAddr,unsigned int *callTable)

{

      PLONG target;

      unsigned int indexValue;

 

      indexValue = http://www.mamicode.com/getSSDTIndex(apiCall);

      target = (PLONG)&(callTable[indexValue]);

      return ((unsigned char*)InterlockedExchange(target,(LONG)newAddr));

}

该例程接收挂钩例程的地址、现有例程的地址以及指向SSDT的指针,返回现有例程的地址(以便完成任务后恢复SSDT)。

给定某个Nt*()函数,他的地址在SSDT中的什么位置?我们在调试器中使用u nt!Zw*命令查看汇编代码,发现这种函数开始处的汇编代码都是这种格式:mov eax,xxxH,这个xxx就是系统调用的索引号。

unsigned int getSSDTIndex(unsigned char *address)

{

  unsigned char *addressOfIndex;

  unsigned int  indexValue;

 

  addressOfIndex = address + 1;

  indexValue = http://www.mamicode.com/*((PULONG)addressOfIndex);

  return (indexValue);

}

一旦有了索引值,定位表项的地址并且将其换出就是一件简单的事情。但是请注意,我们必须使用InterlockedExchange()锁定对此项的访问,以便我们暂时拥有独占的访问权。与IDT或GDT这样基于处理器的结构不同,不论多少个处理器正在运行,只有一个单独的SSDT。

对SSDT中的系统调用脱钩使用同样的基本机制。唯一的不同之处在于我们不向调用例程返回值。

void unHookSSDT(unsigned char *apiCall,unsigned char *oldAddr,unsigned int *callTable)

{

  PLONG target;

  unsigned int indexValue;

 

  indexValue = http://www.mamicode.com/getSSDTIndex(apiCall);

  target = (PLONG)&(callTable[indexValue]);

  InterlockedExchange(target,(LONG)oldAddr);

}

 

三、      SSDT示例

既然已经分析了组成这首乐曲的各种和弦,让我们将其演奏起来听听看。挂钩ZwTerminateProcess系统调用。

NTSTATUS  DriverEntry (

                  IN PDRIVER_OBJECT    DriverObject,

                  IN PUNICODE_STRING  RegistryPath

                  )

{

      DbgPrint("Load SSDT Driver \n");

      DriverObject ->DriverUnload = UnLoad;

     

      wpGlobals = disableWP_MDL(KeServiceDescriptorTable.ServiceTableBase, KeServiceDescriptorTable.NumberOfServices);

 

      if(wpGlobals.pMDL==NULL || wpGlobals.callTable==NULL)

      {

           return (STATUS_UNSUCCESSFUL);

      }

 

      pMDL = wpGlobals.pMDL;

      systemCallTable = (PVOID *)wpGlobals.callTable;

 

      Old_ZwTerminateProcess =(ZwTerminateProcessPtr)hookSSDT((unsigned char*)ZwTerminateProcess, (unsigned char*)NewZwTerminateProcess, (unsigned int*)systemCallTable);

 

      return STATUS_SUCCESS;

}

KeServiceDescriptorTable是由ntoskrnl.exe导出的符号。要想访问它,我们必须为声明加上__declspec(dllimport)前缀,以便编译器知道我们在做什么。导出的内核符号为我们提供了内存中一个位置的地址。我们提供的数据类型定义(即typedef struct ServiceDescriptorEntry)把某个复合结构强加于这个地址的内存。通过这种通用方法,你能够修改由操作系统导出的任意变量。

我们将返回值保存到三个全局变量中(pMDL,systemCallTable,Old_ZwTerminateProcess),以便能够脱钩系统调用,并且重新启用写保护。

VOID

UnLoad (

        IN PDRIVER_OBJECT      DriverObject

        )

{

  DbgPrint("Unload SSDT Driver \n");

 

  unHookSSDT((unsigned char*)ZwTerminateProcess, (unsigned char*)Old_ZwTerminateProcess, (unsigned int*)systemCallTable);

 

  enableWP_MDL(pMDL,(unsigned char*)systemCallTable);

}

每当ZwTerminateProcess被调用,我们挂钩的函数都会被调用。为了存储实现此接口的现有系统调用的地址,定义以下的函数指针数据类型。

typedef NTSTATUS(*ZwTerminateProcessPtr)(

  IN HAndLE ProcessHAndle OPTIONAL,

 IN NTSTATUS ExitStatus );

要做的事情只剩下实现挂钩例程。我们通过打印输出字符串跟踪调用,然后调用原始的系统调用。

NTSTATUS NewZwTerminateProcess(

        IN HAndLE ProcessHAndle OPTIONAL,

        IN NTSTATUS ExitStatus  )

{

  NTSTATUS ntStatus;

  DbgPrint("NewZwTerminateProcess Called \n");

  ntStatus = ((ZwTerminateProcessPtr)(Old_ZwTerminateProcess))(ProcessHAndle,ExitStatus);

 

  if(!NT_SUCCESS(ntStatus))

  {

        DbgPrint("[NewZwTerminateProcess] %\n","Call to Old_ZwTerminateProcess failed");

  }

  return STATUS_SUCCESS;

}

我们在这个示例中建立的是挂钩SSDT的标准操作程序。不论我们正在拦截哪个例程,挂钩与脱钩的技术细节保持相同。从现在起,无论我们何时想要跟踪或过滤系统调用,必须做的所有工作只有以下几点

 

  • 声明原始系统调用原型(例如ZwTerminateProcess()
  • 声明相应的函数指针数据类型(例如ZwTerminateProcessPtr
  • 定义函数指针(例如Old_ZwTerminateProcess
  • 实现挂钩例程(例如NewZwTerminateProcess()

四、加载驱动

    我们用INSTDRV加载编译好的驱动程序DCSSSDT.sys

 

我们用PCHunter32查看挂钩是否成功

 

 

我们用Dbgview查看调试信息

 

没有问题都成功了。

 

转自  http://www.dcscms.com/article/content.php?seq=13