日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

城里城外看SSDT[转]

發布時間:2024/8/5 编程问答 48 豆豆
生活随笔 收集整理的這篇文章主要介紹了 城里城外看SSDT[转] 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

點這里下載本文的配套代碼

引子

2006年,中國互聯網上的斗爭硝煙彌漫。這時的戰場上,先前頗為流行的窗口掛鉤、API掛鉤、進程注入等技術已然成為昨日黃花,大有逐漸淡出之勢;取而代之的,則是更狠毒、更為赤裸裸的詞匯:驅動、隱藏進程、Rootkit……

前不久,我不經意翻出自己2005年9月寫下的一篇文章《DLL的遠程注入技術》,在下面看到了一位名叫L4bm0s的網友說這種技術已經過時了。雖然我也曾想過擬出若干辯解之詞聊作應對,不過最終還是作罷了——畢竟,拿出些新的、有技術含量的東西才是王道。于是這一次,李馬首度從ring3(應用層)的圍城跨出,一躍而投身于ring0(內核層)這一更廣闊的天地,便有了這篇《城里城外看SSDT》。——顧名思義,城里和城外的這一墻之隔,就是ring3與ring0的分界。

在這篇文章里,我會用到太多雜七雜八的東西,比如匯編,比如內核調試器,比如DDK。這誠然是一件令我瞻前顧后畏首畏尾的事情——一方面在ring0我不得不依靠這些東西,另一方面我實在擔心它們會導致我這篇文章的閱讀門檻過高。所以,我決定盡可能少地涉及驅動、內核與DDK,也不會對諸如如何使用內核調試器等問題作任何講解——你只需要知道我大概在做些什么,這就足夠了。

什么是SSDT?

什么是SSDT?自然,這個是我必須回答的問題。不過在此之前,請你打開命令行(cmd.exe)窗口,并輸入“dir”并回車——好了,列出了當前目錄下的所有文件和子目錄。

那么,以程序員的視角來看,整個過程應該是這樣的:

  • 由用戶輸入dir命令。
  • cmd.exe獲取用戶輸入的dir命令,在內部調用對應的Win32 API函數FindFirstFile、FindNextFile和FindClose,獲取當前目錄下的文件和子目錄。
  • cmd.exe將文件名和子目錄輸出至控制臺窗口,也就是返回給用戶。
  • 到此為止我們可以看到,cmd.exe扮演了一個非常至關重要的角色,也就是用戶與Win32 API的交互。——你大概已經可以猜到,我下面要說到的SSDT亦必將扮演這個角色,這實在是一點新意都沒有。

    沒錯,你猜對了。SSDT的全稱是System Services Descriptor Table,系統服務描述符表。這個表就是一個把ring3的Win32 API和ring0的內核API聯系起來的角色,下面我將以API函數OpenProcess為例說明這個聯系的過程。

    你可以用任何反匯編工具來打開你的kernel32.dll,然后你會發現在OpenProcess中有類似這樣的匯編代碼:

    call ds:NtOpenProcess

    這就是說,OpenProcess調用了ntdll.dll的NtOpenProcess函數。那么繼續反匯編之,你會發現ntdll.dll中的這個函數很短:

    mov eax, 7Ah
    mov edx, 7FFE0300h
    call dword ptr [edx]
    retn 10h

    另外,call的一句實質是調用了KiFastSystemCall:

    mov edx, esp
    sysenter

    上面是我的XP Professional sp2中ntdll.dll的反匯編結果,如果你用的是2000系統,那么可能是這個樣子:

    mov eax, 6Ah
    lea edx, [esp+4]
    int 2Eh
    retn 10h

    雖然它們存在著些許不同,但都可以這么來概括:

  • 把一個數放入eax(XP是0x7A,2000是0x6A),這個數值稱作系統的服務號。
  • 把參數堆棧指針(esp+4)放入edx。
  • sysenter或int 2Eh。
  • 好了,你在ring3能看到的東西就到此為止了。事實上,在ntdll.dll中的這些函數可以稱作真正的NT系統服務的存根(Stub)函數。分隔ring3與ring0城里城外的這一道嘆息之墻,也正是由它們打通的。接下來SSDT就要出場了,come some music。

    站在城墻看城外

    插一句先,貌似到現在為止我仍然沒有講出來SSDT是個什么東西,真正可以算是“猶抱琵琶半遮面”了。——書接上文,在你調用sysenter或int 2Eh之后,Windows系統將會捕獲你的這個調用,然后進入ring0層,并調用內核服務函數NtOpenProcess,這個過程如下圖所示。

    SSDT在這個過程中所扮演的角色是至關重要的。讓我們先看一看它的結構,如下圖。

    當程序的處理流程進入ring0之后,系統會根據服務號(eax)在SSDT這個系統服務描述符表中查找對應的表項,這個找到的表項就是系統服務函數NtOpenProcess的真正地址。之后,系統會根據這個地址調用相應的系統服務函數,并把結果返回給ntdll.dll中的NtOpenProcess。圖中的“SSDT”所示即為系統服務描述符表的各個表項;右側的“ntoskrnl.exe”則為Windows系統內核服務進程(ntoskrnl即為NT OS KerneL的縮寫),它提供了相對應的各個系統服務函數。ntoskrnl.exe這個文件位于Windows的system32目錄下,有興趣的朋友可以反匯編一下。

    附帶說兩點。根據你處理器的不同,系統內核服務進程可能也是不一樣的。真正運行于系統上的內核服務進程可能還有ntkrnlmp.exe、ntkrnlpa.exe這樣的情況——不過為了統一起見,下文仍統稱這個進程為ntoskrnl.exe。另外,SSDT中的各個表項也未必會全部指向ntoskrnl.exe中的服務函數,因為你機器上的殺毒監控或其它驅動程序可能會改寫SSDT中的某些表項——這也就是所謂的“掛鉤SSDT”——以達到它們的“主動防御”式殺毒方式或其它的特定目的。

    KeServiceDescriptorTable

    事實上,SSDT并不僅僅只包含一個龐大的地址索引表,它還包含著一些其它有用的信息,諸如地址索引的基地址、服務函數個數等等。ntoskrnl.exe中的一個導出項KeServiceDescriptorTable即是SSDT的真身,亦即它在內核中的數據實體。SSDT的數據結構定義如下:

    typedef struct _tagSSDT {
    ??? PVOID pvSSDTBase;
    ??? PVOID pvServiceCounterTable;
    ??? ULONG ulNumberOfServices;
    ??? PVOID pvParamTableBase;
    } SSDT, *PSSDT;

    其中,pvSSDTBase就是上面所說的“系統服務描述符表”的基地址。pvServiceCounterTable則指向另一個索引表,該表包含了每個服務表項被調用的次數;不過這個值只在Checkd Build的內核中有效,在Free Build的內核中,這個值總為NULL(注:Check/Free是DDK的Build模式,如果你只使用SDK,可以簡單地把它們理解為Debug/Release)。ulNumberOfServices表示當前系統所支持的服務個數。pvParamTableBase指向SSPT(System Service Parameter Table,即系統服務參數表),該表格包含了每個服務所需的參數字節數。

    下面,讓我們開看看這個結構里邊到底有什么。打開內核調試器(以kd為例),輸入命令顯示KeServiceDescriptorTable,如下。

    lkd> dd KeServiceDescriptorTable l4
    8055ab80 804e3d20 00000000 0000011c 804d9f48

    接下來,亦可根據基地址與服務總數來查看整個服務表的各項:

    lkd> dd 804e3d20 l11c
    804e3d20 80587691 f84317aa f84317b4 f84317be
    804e3d30 f84317c8 f84317d2 f84317dc f84317e6
    804e3d40 8057741c f84317fa f8431804 f843180e
    804e3d50 f8431818 f8431822 f843182c f8431836
    ...

    你獲得的結果可能和我會有不同——我指的是那堆以十六進制f開頭的地址項,因為我的SSDT被System Safety Monitor接管了,沒留下幾個原生的ntoskrnl.exe表項。

    現在是寫些代碼的時候了。KeServiceDescriptorTable及SSDT各個表項的讀取只能在ring0層完成,于是這里我使用了內核驅動并借助DeviceIoControl來完成。其中DeviceIoControl的分發代碼實現如下面的代碼所示,沒有什么技術含量,所以不再解釋。

    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開發實在不感興趣的話,亦可以直接使用配套代碼壓縮包中的SSDTDump.sys,并使用DeviceIoControl發送IOCTL_GETSSDT和IOCTL_GETPROC控制碼即可;或者,直接調用我為你準備好的兩個函數:

    BOOL GetSSDT( IN HANDLE hDriver, OUT PSSDT buf );
    BOOL GetProc( IN HANDLE hDriver, IN ULONG ulIndex, OUT PULONG buf );

    獲取詳細模塊信息

    雖然我們現在可以獲取任意一個服務號所對應的函數地址了已經,但是你可能仍然不滿意,認為只有獲得了這個服務函數所在的模塊才是王道。換句話說,對于一個干凈的SSDT表來說,它里邊的表項應該都是指向ntoskrnl.exe的;如果SSDT之中有若干個表項被改寫(掛鉤),那么我們應該知道是哪一個或哪一些模塊替換了這些服務。

    首先我們需要獲得當前在ring0層加載了那些模塊。如我在本文開頭所說,為了盡可能地少涉及ring0層的東西,于是在這里我使用了ntdll.dll的NtQuerySystemInformation函數。關鍵代碼如下:

    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 )
    {
    // 緩沖區太小,重新分配
    delete pRet;
    ??? pRet = (PSYSMODULELIST)new BYTE[nRetSize];
    ??? s = NtQuerySystemInformation( SystemModuleInformation, pRet,
    ??????? nRetSize, &nRetSize );
    }

    需要說明的是,這個函數是利用內核的PsLoadedModuleList鏈表來枚舉系統模塊的,因此如果你遇到了能夠隱藏驅動的Rootkit,那么這種方法是無法找到被隱藏的模塊的。在這種情況下,枚舉系統的“\Driver”目錄對象可能可以更好解決這個問題,在此不再贅述了就。

    接下來,是根據SSDT中的地址表項查找模塊。有了SYSTEM_MODULE_INFORMATION結構中的模塊基地址與模塊大小,這個工作完成起來也很容易:

    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;
    }

    詳細枚舉系統服務項

    到現在為止,還遺留有一個問題,就是獲得服務號對應的服務函數名。比如XP下0x7A對應著NtOpenProcess,但是到2000下,NtOpenProcess就改為0x6A了。

    ——有一個好消息一個壞消息,你先聽哪個?

    ——什么壞消息?

    ——Windows并沒有給我們開放這樣現成的函數,所有的工作都需要我們自己來做。

    ——那好消息呢?

    ——牛糞有的是。

    壞了,串詞兒了。好消息是我們可以通過枚舉ntdll.dll的導出函數來間接枚舉SSDT所有表項所對應的函數,因為所有的內核服務函數對應于ntdll.dll的同名函數都是這樣開頭的:

    mov eax, <ServiceIndex>

    對應的機器碼為:

    B8 <ServiceIndex>

    再說一遍:非常幸運,僅就我手頭上的2000 sp4、XP、XP sp1、XP sp2、2003的ntdll.dll而言,無一例外。不過Mark Russinovich的《深入解析Windows操作系統》一書中指出,IA64的調用方式與此不同——由于手頭上沒有相應的文件,所以在這里不進行討論了就。

    接著說。我們可以把mov的一句用如下的一個結構來表示:

    #pragma pack( push, 1 )
    typedef struct _tagSSDTEntry {
    ??? BYTE? byMov;?? // 0xb8
    ??? DWORD dwIndex;
    } SSDTENTRY;
    #pragma pack( pop )

    那么,我們可以對ntdll.dll的所有導出函數進行枚舉,并篩選出“Nt”開頭者,以SSDTENTRY的結構取出其開頭5個字節進行比對——這就是整個的枚舉過程。相關的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 );
    // 到第一個數據目錄
    ??? 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;
    // 到函數名RVA數組
    ??? pNameArray = (LPDWORD)( (DWORD)hNtDll + pExpDir->AddressOfNames );
    // 初始化系統模塊鏈表
    ??? PSYSMODULELIST pList = CreateModuleList( hNtDll );
    // 循環查找函數名
    for ( i = 0; i < nNameCnt; ++i )
    ??? {
    ??????? PCSTR pszName = (PCSTR)( pNameArray[i] + (DWORD)hNtDll );
    if ( 'N' == pszName[0] && 't' == pszName[1] )
    ??????? {
    // 找到了函數,則定位至查找表
    ??????????? LPWORD pOrdNameArray = (LPWORD)( (DWORD)hNtDll + pExpDir->AddressOfNameOrdinals );
    // 定位至總表
    ??????????? LPDWORD pFuncArray?? = (LPDWORD)( (DWORD)hNtDll + pExpDir->AddressOfFunctions );
    ??????????? LPCVOID pFunc??????? = (LPCVOID)( (DWORD)hNtDll + pFuncArray[pOrdNameArray[i]] );
    // 解析函數,獲取服務名
    ??????????? 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的基地址、服務個數,以及各個表項所對應的服務號、所在模塊、地址和服務名。

    結語

    ring3與ring0,城里與城外之間為一道嘆息之墻所間隔,SSDT則是越過此墻的一道必經之門。因此,很多殺毒軟件也勢必會圍繞著它大做文章。無論是System Safety Monitor的系統監控,還是卡巴斯基的主動防御,都是掛鉤了SSDT。這樣,病毒尚在ring3內發作之時,便被扼殺于搖籃之內。

    內核最高權限,本就是兵家必爭之地,魔高一尺道高一丈的爭奪于此亦已變成頗為稀松平常之事。可以說和這些爭奪比起來,SSDT的相關技術簡直不值一提。但最初發作的病毒體總是從ring3開始的——換句話說,任你未來會成長為何等的武林高手,我都可以在你學走路的時候殺掉你——知曉了SSDT的這點優勢,所有的病毒咂吧咂吧也就都沒味兒了。所以說么,殺毒莫如防毒。

    ——就此打住罷,貌似扯遠大發了。

    轉載于:https://www.cnblogs.com/flying_bat/archive/2007/11/07/951895.html

    總結

    以上是生活随笔為你收集整理的城里城外看SSDT[转]的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。