引子 2006年,中國互聯(lián)網(wǎng)上的斗爭硝煙彌漫,。這時的戰(zhàn)場上,,先前頗為流行的窗口掛鉤、API掛鉤,、進程注入等技術(shù)已然成為昨日黃花,,大有逐漸淡出之勢;取而代之的,,則是更狠毒,、更為赤裸裸的詞匯:驅(qū)動、隱藏進程,、Rootkit…… 什么是SSDT,? 什么是SSDT?自然,,這個是我必須回答的問題,。不過在此之前,請你打開命令行(cmd.exe)窗口,,并輸入“dir”并回車——好了,,列出了當前目錄下的所有文件和子目錄。 到此為止我們可以看到,,cmd.exe扮演了一個非常至關(guān)重要的角色,,也就是用戶與Win32 API的交互?!愦蟾乓呀?jīng)可以猜到,,我下面要說到的SSDT亦必將扮演這個角色,這實在是一點新意都沒有,。 匯編代碼 call ds:NtOpenProcess 這就是說,OpenProcess調(diào)用了ntdll.dll的NtOpenProcess函數(shù),。那么繼續(xù)反匯編之,,你會發(fā)現(xiàn)ntdll.dll中的這個函數(shù)很短: 匯編代碼 mov eax, 7Ah mov edx, 7FFE0300h call dword ptr [edx] retn 10h 另外,call的一句實質(zhì)是調(diào)用了KiFastSystemCall: C++代碼 mov edx, esp sysenter 上面是我的XP Professional sp2中ntdll.dll的反匯編結(jié)果,,如果你用的是2000系統(tǒng),,那么可能是這個樣子: C++代碼 mov eax, 6Ah lea edx, [esp+4] int 2Eh retn 10h 雖然它們存在著些許不同,但都可以這么來概括: 把一個數(shù)放入eax(XP是0x7A,,2000是0x6A),,這個數(shù)值稱作系統(tǒng)的服務(wù)號,。 把參數(shù)堆棧指針(esp+4)放入edx。 sysenter或int 2Eh,。好了,,你在ring3能看到的東西就到此為止了。事實上,,在ntdll.dll中的這些函數(shù)可以稱作真正的NT系統(tǒng)服務(wù)的存根(Stub)函數(shù),。分隔ring3與ring0城里城外的這一道嘆息之墻,也正是由它們打通的,。接下來SSDT就要出場了,,come some music。 站在城墻看城外 插一句先,,貌似到現(xiàn)在為止我仍然沒有講出來SSDT是個什么東西,,真正可以算是“猶抱琵琶半遮面”了?!獣由衔?,在你調(diào)用sysenter或int 2Eh之后,Windows系統(tǒng)將會捕獲你的這個調(diào)用,,然后進入ring0層,,并調(diào)用內(nèi)核服務(wù)函數(shù)NtOpenProcess,這個過程如下圖所示,。 SSDT在這個過程中所扮演的角色是至關(guān)重要的,。讓我們先看一看它的結(jié)構(gòu),如下圖,。 當程序的處理流程進入ring0之后,,系統(tǒng)會根據(jù)服務(wù)號(eax)在SSDT這個系統(tǒng)服務(wù)描述符表中查找對應(yīng)的表項,這個找到的表項就是系統(tǒng)服務(wù)函數(shù)NtOpenProcess的真正地址,。之后,,系統(tǒng)會根據(jù)這個地址調(diào)用相應(yīng)的系統(tǒng)服務(wù)函數(shù),并把結(jié)果返回給ntdll.dll中的NtOpenProcess,。圖中的“SSDT”所示即為系統(tǒng)服務(wù)描述符表的各個表項,;右側(cè)的“ntoskrnl.exe”則為Windows系統(tǒng)內(nèi)核服務(wù)進程(ntoskrnl即為NT OS KerneL的縮寫),它提供了相對應(yīng)的各個系統(tǒng)服務(wù)函數(shù),。ntoskrnl.exe這個文件位于Windows的system32目錄下,,有興趣的朋友可以反匯編一下。 KeServiceDescriptorTable 事實上,,SSDT并不僅僅只包含一個龐大的地址索引表,,它還包含著一些其它有用的信息,諸如地址索引的基地址,、服務(wù)函數(shù)個數(shù)等等,。ntoskrnl.exe中的一個導出項KeServiceDescriptorTable即是SSDT的真身,亦即它在內(nèi)核中的數(shù)據(jù)實體,。SSDT的數(shù)據(jù)結(jié)構(gòu)定義如下: C++代碼 typedef struct _tagSSDT { PVOID pvSSDTBase; PVOID pvServiceCounterTable; ULONG ulNumberOfServices; PVOID pvParamTableBase; } SSDT, *PSSDT; 其中,,pvSSDTBase就是上面所說的“系統(tǒng)服務(wù)描述符表”的基地址。pvServiceCounterTable則指向另一個索引表,,該表包含了每個服務(wù)表項被調(diào)用的次數(shù),;不過這個值只在Checkd Build的內(nèi)核中有效,在Free Build的內(nèi)核中,,這個值總為NULL(注:Check/Free是DDK的Build模式,,如果你只使用SDK,可以簡單地把它們理解為Debug/Release),。ulNumberOfServices表示當前系統(tǒng)所支持的服務(wù)個數(shù),。pvParamTableBase指向SSPT(System Service Parameter Table,即系統(tǒng)服務(wù)參數(shù)表),,該表格包含了每個服務(wù)所需的參數(shù)字節(jié)數(shù),。 WinDbg輸出 lkd> dd KeServiceDescriptorTable l4 8055ab80 804e3d20 00000000 0000011c 804d9f48 接下來,,亦可根據(jù)基地址與服務(wù)總數(shù)來查看整個服務(wù)表的各項: WinDbg輸出 lkd> dd 804e3d20 l11c 804e3d20 80587691 f84317aa f84317b4 f84317be 804e3d30 f84317c8 f84317d2 f84317dc f84317e6 804e3d40 8057741c f84317fa f8431804 f843180e 804e3d50 f8431818 f8431822 f843182c f8431836 ... 你獲得的結(jié)果可能和我會有不同——我指的是那堆以十六進制f開頭的地址項,因為我的SSDT被System Safety Monitor接管了,,沒留下幾個原生的ntoskrnl.exe表項,。 switch ( IoControlCode ) { case IOCTL_GETSSDT: { __try { ProbeForWrite( OutputBuffer, sizeof( SSDT ), sizeof( ULONG ) ); RtlCopyMemory( OutputBuffer, KeServiceDescriptorTable, sizeof( SSDT ) ); } __except ( EXCEPTION_EXECUTE_HANDLER ) { IoStatus->Status = GetExceptionCode(); } } break; case IOCTL_GETPROC: { ULONG uIndex = 0; PULONG pBase = NULL; __try { ProbeForRead( InputBuffer, sizeof( ULONG ), sizeof( ULONG ) ); ProbeForWrite( OutputBuffer, sizeof( ULONG ), sizeof( ULONG ) ); } __except( EXCEPTION_EXECUTE_HANDLER ) { IoStatus->Status = GetExceptionCode(); break; } uIndex = *(PULONG)InputBuffer; if ( KeServiceDescriptorTable->ulNumberOfServices <= uIndex ) { IoStatus->Status = STATUS_INVALID_PARAMETER; break; } pBase = KeServiceDescriptorTable->pvSSDTBase; *((PULONG)OutputBuffer) = *( pBase + uIndex ); } break; // ... } 補充一下,,再。DDK的頭文件中有一件很遺憾的事情,,那就是其中并未聲明KeServiceDescriptorTable,,不過我們可以自己手動添加之: extern PSSDT KeServiceDescriptorTable; ——當然,如果你對DDK開發(fā)實在不感興趣的話,,亦可以直接使用配套代碼壓縮包中的SSDTDump.sys,,并使用DeviceIoControl發(fā)送IOCTL_GETSSDT和IOCTL_GETPROC控制碼即可;或者,,直接調(diào)用我為你準備好的兩個函數(shù): BOOL GetSSDT( IN HANDLE hDriver, OUT PSSDT buf ); BOOL GetProc( IN HANDLE hDriver, IN ULONG ulIndex, OUT PULONG buf ); 獲取詳細模塊信息 雖然我們現(xiàn)在可以獲取任意一個服務(wù)號所對應(yīng)的函數(shù)地址了已經(jīng),,但是你可能仍然不滿意,認為只有獲得了這個服務(wù)函數(shù)所在的模塊才是王道,。換句話說,,對于一個干凈的SSDT表來說,它里邊的表項應(yīng)該都是指向ntoskrnl.exe的,;如果SSDT之中有若干個表項被改寫(掛鉤),,那么我們應(yīng)該知道是哪一個或哪一些模塊替換了這些服務(wù)。 首先我們需要獲得當前在ring0層加載了那些模塊,。如我在本文開頭所說,,為了盡可能地少涉及ring0層的東西,于是在這里我使用了ntdll.dll的NtQuerySystemInformation函數(shù),。關(guān)鍵代碼如下: typedef struct _SYSTEM_MODULE_INFORMATION { ULONG Reserved[2]; PVOID Base; ULONG Size; ULONG Flags; USHORT Index; USHORT Unknown; USHORT LoadCount; USHORT ModuleNameOffset; CHAR ImageName[256]; } SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION; typedef struct _tagSysModuleList { ULONG ulCount; SYSTEM_MODULE_INFORMATION smi[1]; } SYSMODULELIST, *PSYSMODULELIST; s = NtQuerySystemInformation( SystemModuleInformation, pRet, sizeof( SYSMODULELIST ), &nRetSize ); if ( STATUS_INFO_LENGTH_MISMATCH == s ) { // 緩沖區(qū)太小,,重新分配 delete pRet; pRet = (PSYSMODULELIST)new BYTE[nRetSize]; s = NtQuerySystemInformation( SystemModuleInformation, pRet, nRetSize, &nRetSize ); } 需要說明的是,這個函數(shù)是利用內(nèi)核的PsLoadedModuleList鏈表來枚舉系統(tǒng)模塊的,,因此如果你遇到了能夠隱藏驅(qū)動的Rootkit,,那么這種方法是無法找到被隱藏的模塊的。在這種情況下,,枚舉系統(tǒng)的“\Driver”目錄對象可能可以更好解決這個問題,,在此不再贅述了就。 接下來,,是根據(jù)SSDT中的地址表項查找模塊,。有了SYSTEM_MODULE_INFORMATION結(jié)構(gòu)中的模塊基地址與模塊大小,這個工作完成起來也很容易: BOOL FindModuleByAddr( IN ULONG ulAddr, IN PSYSMODULELIST pList, OUT LPSTR buf, IN DWORD dwSize ) { for ( ULONG i = 0; i < pList->ulCount; ++i ) { ULONG ulBase = (ULONG)pList->smi[i].Base; ULONG ulMax = ulBase + pList->smi[i].Size; if ( ulBase <= ulAddr && ulAddr < ulMax ) { // 對于路徑信息,,截取之 PCSTR pszModule = strrchr( pList->smi[i].ImageName, '\\' ); if ( NULL != pszModule ) { lstrcpynA( buf, pszModule + 1, dwSize ); } else { lstrcpynA( buf, pList->smi[i].ImageName, dwSize ); } return TRUE; } } return FALSE; } 詳細枚舉系統(tǒng)服務(wù)項 到現(xiàn)在為止,,還遺留有一個問題,就是獲得服務(wù)號對應(yīng)的服務(wù)函數(shù)名,。比如XP下0x7A對應(yīng)著NtOpenProcess,,但是到2000下,,NtOpenProcess就改為0x6A了。 ——有一個好消息一個壞消息,,你先聽哪個,? ——什么壞消息? ——Windows并沒有給我們開放這樣現(xiàn)成的函數(shù),,所有的工作都需要我們自己來做,。 ——那好消息呢? ——牛糞有的是,。 壞了,串詞兒了,。好消息是我們可以通過枚舉ntdll.dll的導出函數(shù)來間接枚舉SSDT所有表項所對應(yīng)的函數(shù),,因為所有的內(nèi)核服務(wù)函數(shù)對應(yīng)于ntdll.dll的同名函數(shù)都是這樣開頭的: mov eax, <ServiceIndex> 對應(yīng)的機器碼為: B8 <ServiceIndex> 再說一遍:非常幸運,僅就我手頭上的2000 sp4,、XP,、XP sp1、XP sp2,、2003的ntdll.dll而言,,無一例外。不過Mark Russinovich的《深入解析Windows操作系統(tǒng)》一書中指出,,IA64的調(diào)用方式與此不同——由于手頭上沒有相應(yīng)的文件,,所以在這里不進行討論了就。 接著說,。我們可以把mov的一句用如下的一個結(jié)構(gòu)來表示: #pragma pack( push, 1 ) typedef struct _tagSSDTEntry { BYTE byMov; // 0xb8 DWORD dwIndex; } SSDTENTRY; #pragma pack( pop ) 那么,,我們可以對ntdll.dll的所有導出函數(shù)進行枚舉,并篩選出“Nt”開頭者,,以SSDTENTRY的結(jié)構(gòu)取出其開頭5個字節(jié)進行比對——這就是整個的枚舉過程,。相關(guān)的PE文件格式解析我不再解釋,可參考注釋,。整個代碼如下: #define MOV 0xb8 void EnumSSDT( IN HANDLE hDriver, IN HMODULE hNtDll ) { DWORD dwOffset = (DWORD)hNtDll; PIMAGE_EXPORT_DIRECTORY pExpDir = NULL; int nNameCnt = 0; LPDWORD pNameArray = NULL; int i = 0; // 到PE頭部 dwOffset += ((PIMAGE_DOS_HEADER)hNtDll)->e_lfanew + sizeof( DWORD ); // 到第一個數(shù)據(jù)目錄 dwOffset += sizeof( IMAGE_FILE_HEADER ) + sizeof( IMAGE_OPTIONAL_HEADER ) - IMAGE_NUMBEROF_DIRECTORY_ENTRIES * sizeof( IMAGE_DATA_DIRECTORY ); // 到導出表位置 dwOffset = (DWORD)hNtDll + ((PIMAGE_DATA_DIRECTORY)dwOffset)->VirtualAddress; pExpDir = (PIMAGE_EXPORT_DIRECTORY)dwOffset; nNameCnt = pExpDir->NumberOfNames; // 到函數(shù)名RVA數(shù)組 pNameArray = (LPDWORD)( (DWORD)hNtDll + pExpDir->AddressOfNames ); // 初始化系統(tǒng)模塊鏈表 PSYSMODULELIST pList = CreateModuleList( hNtDll ); // 循環(huán)查找函數(shù)名 for ( i = 0; i < nNameCnt; ++i ) { PCSTR pszName = (PCSTR)( pNameArray[i] + (DWORD)hNtDll ); if ( 'N' == pszName[0] && 't' == pszName[1] ) { // 找到了函數(shù),,則定位至查找表 LPWORD pOrdNameArray = (LPWORD)( (DWORD)hNtDll + pExpDir->AddressOfNameOrdinals ); // 定位至總表 LPDWORD pFuncArray = (LPDWORD)( (DWORD)hNtDll + pExpDir->AddressOfFunctions ); LPCVOID pFunc = (LPCVOID)( (DWORD)hNtDll + pFuncArray[pOrdNameArray[i]] ); // 解析函數(shù),獲取服務(wù)名 SSDTENTRY entry; CopyMemory( &entry, pFunc, sizeof( SSDTENTRY ) ); if ( MOV == entry.byMov ) { ULONG ulAddr = 0; GetProc( hDriver, entry.dwIndex, &ulAddr ); CHAR strModule[MAX_PATH] = "[Unknown Module]"; FindModuleByAddr( ulAddr, pList, strModule, MAX_PATH ); printf( "0x%04X\t%s\t0x%08X\t%s\r\n", entry.dwIndex, strModule, ulAddr, pszName ); } } } DestroyModuleList( pList ); } 下圖是示例程序SSDTDump在XP sp2上的部分運行截圖,,顯示了SSDT的基地址,、服務(wù)個數(shù),以及各個表項所對應(yīng)的服務(wù)號,、所在模塊,、地址和服務(wù)名。 下圖是示例程序SSDTDump在XP sp2上的部分運行截圖,,顯示了SSDT的基地址,、服務(wù)個數(shù),以及各個表項所對應(yīng)的服務(wù)號、所在模塊,、地址和服務(wù)名,。 結(jié)語 ring3與ring0,城里與城外之間為一道嘆息之墻所間隔,,SSDT則是越過此墻的一道必經(jīng)之門,。因此,很多殺毒軟件也勢必會圍繞著它大做文章,。無論是System Safety Monitor的系統(tǒng)監(jiān)控,,還是卡巴斯基的主動防御,都是掛鉤了SSDT,。這樣,,病毒尚在ring3內(nèi)發(fā)作之時,便被扼殺于搖籃之內(nèi),。 附件:ssdtdump.zip |
|