天天看點

核心驅動程式完整性校驗的原理分析

在上一篇文章中提到了 Windows Vista 及之後版本的 Windows 作業系統在驅動程式加載完成後,驅動中調用的一些系統回調函數(如 ObRegisterCallbacks,可用來監控系統中對進線程句柄的操作,如打開程序、複制線程句柄等)等 API 中會通過 MmVerifyCallbackFunction 函數對該驅動程式進行完整性檢查,檢測未通過則會傳回 0xC0000022 拒絕通路的傳回值。在這篇文章中将會對這個函數進行簡單的分析,以明确其原理。

0x0 擷取函數位址

通過 Windbg 連接配接 64 位的 Windows 7 SP1 虛拟機,并通過 u nt!MmVerifyCallbackFunction 指令得到該函數的基位址。接下來使用 lm 指令獲得 nt 核心子產品的基位址,通過與前面的函數首位址相減得到函數的位址偏移 4700B0。

在 IDA 中加載 64 位 Windows 7 SP1 的 ntoskrnl.exe 檔案并指定 pdb 檔案,在 IDA View-A 頁面中定位到前面獲得的函數位址偏移位置 PAGE:00000001404700B0,即定位到該函數所在。

核心驅動程式完整性校驗的原理分析

0x1 簡單分析

如果 pdb 檔案正确加載的話會在 IDA View-A 頁面中看到 IDA 已正确識别該函數的名稱符号。通過 IDA 獲得初步的 C 代碼,對其進行一些修正後得到下述代碼。

ULONG64 __fastcall MmVerifyCallbackFunction(PVOID pAddr)
{
  PVOID    address;  // rsi@1
  ULONG64  result;   // rax@2
  PVOID    thread;   // rbx@3
  BOOLEAN  status;   // edi@3
  PVOID    ldrentry; // rax@3
  bool     disable;  // zf@6

  address = pAddr;
  if ( (ULONG64)(pAddr + 0x70000000000) > 0x7FFFFFFFFF )
  {
    thread = *MK_FP(__GS__, 0x188);  // get _ETHREAD pointer from KPCR
    --*(WORD *)(thread + 0x1C4);     // Disable Kernel APCs // Thread->KernelApcDisable--;
    status = FALSE;
    ExAcquireResourceSharedLite(&PsLoadedModuleResource, TRUE);
    ldrentry = MiLookupDataTableEntry(address, TRUE);
    if ( ldrentry && *(BYTE *)(ldrentry + 0x68) & 0x20 )
      status = TRUE;
    ExReleaseResourceLite(&PsLoadedModuleResource);
    disable = (*(WORD *)(thread + 0x1C4))++ == -1;  // Enable Kernel APCs // Thread->KernelApcDisable++;
    // 1. Thread->KernelApcDisable == -1 before ++;
    // 2. EThread->ApcState->ApcListHead does not point to itself;
    // 3. EThread->SpecialApcDisable == 0;
    // means KernelApc/SpecialApc enabled && ApcList not empty now.
    if ( disable && *(QWORD *)(thread + 0x50) != thread + 0x50 && !*(WORD *)(thread + 0x1C6) )
      KiCheckForKernelApcDelivery();
    result = (ULONG)status;
  }
  else
  {
    result = FALSE;
  }
  return result;
}      

0x2 代碼解釋

首先是判斷傳入位址參數的有效性。為了更精确地了解 if ( (ULONG64)(pAddr + 0x70000000000) > 0x7FFFFFFFFF ) 這行語句執行的操作和作用,下面貼出其彙編指令代碼。

mov     rax, 70000000000h
mov     rsi, rcx
add     rax, rcx
mov     rcx, 7FFFFFFFFFh
cmp     rax, rcx
ja      nt!MmVerifyCallbackFunction+0x3c      

其中 rcx 初始值是傳入的位址參數的值。開始時并未明确這幾行指令代碼這樣判斷的目的是什麼,通過 Windbg 跟蹤 PsSetCreateProcessNotifyRoutineEx 等 API 函數對 MmVerifyCallbackFunction 的調用,發現 ecx 都是 0xfffff8800373c620 之類正常的位址數值,通過和 0x70000000000 相加得到的值 0xffffff800373c620 也遠大于 0x7FFFFFFFFF 這個比較小的數。

後經過計算得知,相加後的值在此處隻有在一種情況才會小于 0x7FFFFFFFFF 值。位址參數是個 ULONG64 長度的數字,其值位于 [0xFFFFF900`00000000, 0xFFFFF97F`FFFFFFFF] 區間時,相加後的數值對 ja 條件成立,函數跳轉到檢測失敗并傳回的位置。後通過查閱資料得知,在 64 位 Windows 作業系統中,該位址空間區間範圍正是核心位址空間中的會話空間(Session Space)。

Session Space

Session Data Structures, Session Pool and Session Images are loaded in this area.

The session image space contains driver images like Win32K.sys (Window Manager), CDD.DLL (Canonical Display Driver), TSDDD.dll (Frame Buffer Display Driver), DXG.sys (DirectX Graphics Driver) etc.

For any process that belongs to a session the field EPROCESS->Session points to a MM_SESSION_SPACE structure for that session. Session paged pool limits are pointed to by MM_SESSION_SPACE->PagesPoolStart and MM_SESSION_SPACE->PagesPoolEnd.

在判斷位址參數的有效性之後,首先通過 EThread->KernelApcDisable 禁用 Normal Kernel APC。GS 寄存器存儲目前 CPU 核心的 _KPCR 結構位址。目前線程 EThread 位址位于 KPCR->Prcb->CurrentThread 位置,根據偏移 +0x188 擷取到 EThread 的位址。對偏移為 0x1C4 的 EThread->KernelApcDisable 進行累減運算操作。

在 EThread 結構中有個 ApcState 域,以及 KernelApcDisable 和 SpecialApcDisable 域。

KernelApcDisable 和 SpecialApcDisable 域可以合并成一個 CombinedApcDisable 域,兩者都是 16 位的整數值,0 表示不禁止 APC,負數表示禁止 APC。一個線程在執行過程中可以有多種因素要禁止 APC,這些因素以負值來表示,并累加起來,當因素消除的時候再減去相應的負值。

隻有當 KernelApcDisable 或 SpecialApcDisable 為 0 的時候,該線程才允許插入或送出 APC。這兩個值分别控制普通的核心 APC 和特殊的核心 APC。

ApcState 是一個結構成員,指定了一個線程的 APC 資訊,包括 APC 連結清單、是否正在處理 APC 或者是否有核心 APC 或使用者 APC 正在等待等資訊。APC 連結清單頭指針 ApcListHead 位于其結構第一個成員位置。

為調用線程的共享讀通路請求指定的資源。PsLoadedModuleResource 是一個 ERESOURCE 結構體資料類型的全局變量,在 MiInitializeLoadedModuleList 函數中初始化。通過 ExAcquireResourceSharedLite 函數請求 PsLoadedModuleResource 系列資源的讀通路權限。根據 MSDN 上的描述,調用該函數之前必須禁用 Normal Kernel APC。直到資源釋放之前,APC 投遞必須保持禁用狀态。以下是該函數的聲明。

BOOLEAN 
ExAcquireResourceSharedLite(
  _Inout_ PERESOURCE Resource,
  _In_    BOOLEAN    Wait
);      

ERESOURCE 結構類型是 Windows 作業系統核心中的讀寫鎖對象類型。通過它和一系列的 Resource 例程可以實作同時隻有一個 Writer 寫入,多個 Reader 讀通路的機制。

詳細資訊:​​http://msdn.microsoft.com/en-us/library/windows/hardware/ff544363(v=vs.85).aspx​​

在對指定資源的讀通路請求完成後,通過 MiLookupDataTableEntry 函數對 PsLoadedModuleList 指針指向的已加載的核心子產品連結清單進行周遊。MSDN 和其他文檔中沒有查到有關這個函數的定義資訊,以下函數定義是通過 IDA 逆向 ntoskrnl.exe 推測得到的。其中的第二個參數 BOOLEAN bAcquiredResource 表示目前調用環境是否已擷取到前述的系列資源的讀通路權限,如果傳入 FALSE 則在該函數内部會調用 ExAcquireResourceSharedLite 函數進行這些資源的讀通路權限的擷取。

PLDR_DATA_TABLE_ENTRY
NTAPI
MiLookupDataTableEntry(
  IN PVOID Address, 
  IN BOOLEAN bAcquiredResource
);      

PsLoadedModuleList 是一個 PLDR_DATA_TABLE_ENTRY 類型的全局指針,指向目前核心中已加載的核心子產品的 LDR_DATA_TABLE_ENTRY 環形連結清單的第一個節點。每個節點是一個 LDR_DATA_TABLE_ENTRY 類型的結構體對象。以下是在 Windows 7 x64 SP1 作業系統環境下該結構體的資料類型定義。

typedef struct _LDR_DATA_TABLE_ENTRY
{
    LIST_ENTRY InLoadOrderLinks;
    LIST_ENTRY InMemoryOrderLinks;
    LIST_ENTRY InInitializationOrderLinks;
    PVOID DllBase;
    PVOID EntryPoint;
    ULONG SizeOfImage;
    UNICODE_STRING FullDllName;
    UNICODE_STRING BaseDllName;
    ULONG Flags;
    UINT16 LoadCount;
    UINT16 TlsIndex;
    union
    {
        LIST_ENTRY HashLinks;
        struct
        {
            PVOID SectionPointer;
            ULONG CheckSum;
        };
    };
    union
    {
        ULONG TimeDateStamp;
        PVOID LoadedImports;
    };
    PVOID EntryPointActivationContext;
    PVOID PatchInformation;
    LIST_ENTRY ForwarderLinks;
    LIST_ENTRY ServiceTagLinks;
    LIST_ENTRY StaticLinks;
    PVOID ContextInformation;
    ULONG_PTR OriginalBase;
    LARGE_INTEGER LoadTime;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;      

在 MiLookupDataTableEntry 函數中周遊連結清單時,通過結構體成員 DllBase 和 SizeOfImage 判斷 Address 參數是否在該核心子產品位址區間範圍内。如果命中,則傳回該 LDR_DATA_TABLE_ENTRY 連結清單節點的位址指針。

在 MiLookupDataTableEntry 函數傳回後,ldrentry 變量取得對應核心子產品的 LDR_DATA_TABLE_ENTRY 指針。判斷 lprentry + 0x68 位置的 BYTE 類型的資料是否對 0x20 标志位置位。通過上面的 LDR_DATA_TABLE_ENTRY 結構體定義發現,0x68 位置是 ULONG Flags 成員。該成員存儲一些對應核心子產品的屬性标志。

判斷為真則指派 status = TRUE,其在後面會用來指派 result 并作為 MmVerifyCallbackFunction 函數的傳回值。那麼到現在可知該函數的關鍵判定就是在這一步了。如果在調用該函數之前就将對應的核心子產品 LDR_DATA_TABLE_ENTRY 節點的 Flags 的 0x20 标志位置位,則會得到 MmVerifyCallbackFunction 函數校驗通過的結果,達到繞過強制簽名校驗的目的。

在判斷标志位之後,函數會執行一些諸如資源權限釋放、恢複 Normal Kernel APC、Kernel APC 投遞檢查并繼續投遞等操作。具體可以參考前面部分的内容,代碼的注釋已經寫清楚。

具體的驗證可以通過 Windbg 跟一下。

現在回想在編譯連結驅動程式的時候,在 sources 檔案中可選添加的 LINKER_FLAGS=/INTEGRITYCHECK 連結标記,其實就是給生成的 sys 檔案的 PE 檔案頭中對應的 Flags 資料置位 0x20 标志位。

0x3 備注

文章中對部分 Windows 作業系統核心結構的解釋參考自《Windows核心原理與實作(潘愛民著)》一書。

====================================================================================================

部落客後記:

    部落客同樣遇到了調用PsSetCreateProcessNotifyRoutineEx注冊回調失敗的情況。失敗的機器運作在"Test Mode"下,并處于被調試狀态。原作者可能遺漏了對這種情況的說明。下面分類說明:

1.編譯過程中不加/INTEGRITYCHECK标志。x64位調試機在Test Mode下可以加載驅動,但調用PsSetCreateProcessNotifyRoutineEx後失敗;

2.編譯過程中添加加/INTEGRITYCHECK标志。x64位調試機在Test Mode下可以加載驅動,調用PsSetCreateProcessNotifyRoutineEx成功

原因分析:

出現上述原因正如​​MSDN​​所說:

繼續閱讀