天天看點

程序建立通知回調通知例程的學習筆記

在 Windows 作業系統中可以通過 PsSetCreateProcessNotifyRoutine 函數注冊或移除一個程序建立通知回調例程。在 Vista 以及之後的版本中,微軟加入 PsSetCreateProcessNotifyRoutineEx 新的函數來注冊建立程序通知。通過判斷系統版本來對應不同的作業系統調用不同的注冊函數。

而在 Vista 之前的系統版本(如 Windows XP)中由于沒有 PsSetCreateProcessNotifyRoutineEx 這個函數,會驅動加載的時候導緻加載失敗。那麼通過 MmGetSystemRoutineAddress 函數可以動态地擷取 PsSetCreateProcessNotifyRoutineEx 函數。

typedef 
NTSTATUS (_stdcall *PPsSetCreateProcessNotifyRoutineEx)(
    IN PCREATE_PROCESS_NOTIFY_ROUTINE_EX  NotifyRoutine,
    IN BOOLEAN  Remove
    );

PPsSetCreateProcessNotifyRoutineEx g_pPsSetCreateProcessNotifyRoutineEx = NULL;
BOOLEAN g_bSucReg = FALSE;
BOOLEAN g_bUsedEx = FALSE;

NTSTATUS
SetProcessCallBack ()
{
    NTSTATUS nStatus = STATUS_UNSUCCESSFUL;
    do 
    {
        UNICODE_STRING uFuncName = {0};
        RtlInitUnicodeString(&uFuncName,L"PsSetCreateProcessNotifyRoutineEx");
        g_pPsSetCreateProcessNotifyRoutineEx = (PPsSetCreateProcessNotifyRoutineEx)MmGetSystemRoutineAddress(&uFuncName);
        if( g_pPsSetCreateProcessNotifyRoutineEx == NULL )
        {
            break;
        }
        if( STATUS_SUCCESS != g_pPsSetCreateProcessNotifyRoutineEx(CreateProcessNotifyEx,FALSE) )
        {
            break;
        }
        g_bSucReg = TRUE;
        g_bUsedEx = TRUE;
        nStatus = STATUS_SUCCESS;
    } while (FALSE);

    do 
    {
        if ( TRUE == g_bUsedEx )
        {
            break;
        }
        if( STATUS_SUCCESS != PsSetCreateProcessNotifyRoutine(CreateProcessNotify,FALSE) )
        {
            break;
        }
        g_bSucReg = TRUE;
        g_bUsedEx = FALSE;
        nStatus = STATUS_SUCCESS;
    } while (FALSE);

    return nStatus;
}      

通知例程處理函數也需要同時配套地使用新的 CreateProcessNotifyEx 函數定義格式和函數體。與舊版本 CreateProcessNotify 通過 BOOLEAN Create 參數判斷是建立還是銷毀程序不同的是,CreateProcessNotifyEx 是通過參數中指向 PS_CREATE_NOTIFY_INFO 類型的結構體指針 PPS_CREATE_NOTIFY_INFO CreateInfo 來進行判斷。

VOID 
CreateProcessNotifyEx (
                      __inout PEPROCESS  Process,
                      __in HANDLE  ProcessId,
                      __in_opt PPS_CREATE_NOTIFY_INFO  CreateInfo
                      )
{
    HANDLE hCurrentThreadID = NULL;
    hCurrentThreadID = PsGetCurrentThreadId();
    if( CreateInfo == NULL )
    {
        DbgPrint("程序銷毀: %08X %08X\n", ProcessId, hCurrentThreadID);
        return;
    }

    DbgPrint("程序建立: %08X %08X\n", ProcessId, hCurrentThreadID);
    return;
}
VOID 
CreateProcessNotify (
                    IN HANDLE   ParentId,
                    IN HANDLE   ProcessId,
                    IN BOOLEAN  Create
                    )
{
    HANDLE hCurrentThreadID = NULL;
    hCurrentThreadID = PsGetCurrentThreadId();
    if( !Create )
    {
        DbgPrint("程序銷毀: %08X %08X\n", ProcessId, hCurrentThreadID);
        return;
    }
    DbgPrint("程序建立: %08X %08X\n", ProcessId, hCurrentThreadID);
    return;
}      

PS_CREATE_NOTIFY_INFO 結構體類型定義如下:

typedef struct _PS_CREATE_NOTIFY_INFO {
  SIZE_T Size;
  union {
    ULONG Flags;
    struct {
      ULONG FileOpenNameAvailable :1;
      ULONG Reserved :31;
    };
  };
  HANDLE ParentProcessId;
  CLIENT_ID CreatingThreadId;
  struct _FILE_OBJECT *FileObject;
  PCUNICODE_STRING ImageFileName;
  PCUNICODE_STRING CommandLine;
  NTSTATUS CreationStatus;
} PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO;      

如果為建立程序,則該參數指針指向該結構體的一個結構體對象,可通過該對象獲得線程ID、父程序ID、檔案對象、映像檔案名、指令行字元串等程序資訊;而如果是銷毀程序,則該參數指針指向 NULL。

編譯驅動程式并在虛拟機系統中進行調試。調試過程中發現 PsSetCreateProcessNotifyRoutineEx 調用失敗,傳回值為 STATUS_ACCESS_DENIED 即 0xC00000CC 錯誤碼。WDK 解釋為是由于生成的驅動程式檔案 PE 頭中沒有被設定 IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY 标志導緻。

作業系統通過核心函數 MmVerifyCallbackFunction 對加載完成的驅動進行完整性校驗标志位的檢測,該标志位未被置位的驅動子產品會被禁止使用某些函數,如上面提到的 PsSetCreateProcessNotifyRoutineEx。

程式建立通知回調通知例程的學習筆記

通過反彙編發現,在 PsSetCreateProcessNotifyRoutineEx 中調用 PspSetCreateProcessNotifyRoutine 函數,在其中通過調用 MmVerifyCallbackFunction 對驅動的完整性校驗标志位進行檢查,檢查失敗時則會使目前函數傳回 0xC00000CC 錯誤碼。

解決方法是在 sources 檔案中加入一行:LINKER_FLAGS=/INTEGRITYCHECK 以開啟驅動程式的完整性校驗,這個方法适用于通過 WDK 編譯器編譯環境進行編譯的情況。如果是通過 VisualStudio 自身編譯器作為交叉編譯工具鍊,則需在“項目-屬性-連結器-指令行”位置添加 /INTEGRITYCHECK 即可。

這時候再進行測試運作,會發現在 Windows 7 的非測試模式的環境下驅動程式會加載失敗。查閱資料發現是由于 INTEGRITYCHECK 标志導緻強制進行驅動簽名校驗的問題;而在測試模式系統環境下不進行簽名校驗,是以會成功加載。

在 32 位版本的 Windows 7 環境中,驅動程式加載時作業系統根據 PE 檔案頭部對應的 Flags 域的值判斷是否置位 INTEGRITYCHECK 标志位,并根據判斷的結果來決定是否要進行代碼簽名校驗操作,在這裡簽名校驗操作并非強制性的。

然而需要注意的是,在 64 位版 Windows 7 系統中,驅動程式加載時的安全性檢查機制有所不同。微軟為 Windows Vista 及後續版本的作業系統的 x64 位版本加強了驅動程式的安全性校驗機制,編譯生成的驅動程式檔案的 PE 頭部對應的 Flags 标志位無論是否已置位 INTEGRITYCHECK(0x20) 标志位,在驅動程式加載時都會執行簽名校驗的操作。

是以在 64 位版本的作業系統中的非測試模式或調試模式環境下,如果需要加載編譯生成的驅動程式,那麼一定需要通過代碼簽名證書對驅動程式進行交叉簽名。

目前的問題是:

1. 如果将驅動檔案的 INTEGRITYCHECK 标志位置位,驅動加載的時候會強制對檔案簽名進行校驗,無簽名或簽名無效的驅動會被禁止加載.

2. 而如果不将 INTEGRITYCHECK 标志位置位,MmVerifyCallbackFunction 校驗函數會造成部分核心函數的調用失敗。

這樣的話就需要找到一種既能使驅動成功加載、又能繞過完整性校驗标志位檢測的方法。

首先,移除上文中在 source 檔案中添加的 LINKER_FLAGS=/INTEGRITYCHECK 這行,然後将以下代碼放置在 DriverEntry 函數中。

PLDR_DATA_TABLE_ENTRY ldr;
ldr = (PLDR_DATA_TABLE_ENTRY)DriverObject->DriverSection;
ldr->Flags |= 0x20;      

這裡的 PLDR_DATA_TABLE_ENTRY 需要自行定義,根據系統版本和位數的不同可能會有差異。在實際應用中可以使用以下定義:

typedef struct _LDR_DATA                           // 24 elements, 0xE0 bytes (sizeof)
{
    struct _LIST_ENTRY InLoadOrderLinks;           // 2 elements, 0x10 bytes (sizeof)
    struct _LIST_ENTRY InMemoryOrderLinks;         // 2 elements, 0x10 bytes (sizeof)
    struct _LIST_ENTRY InInitializationOrderLinks; // 2 elements, 0x10 bytes (sizeof)
    VOID*        DllBase;
    VOID*        EntryPoint;
    ULONG32      SizeOfImage;
#ifdef _WIN64
    UINT8        _PADDING0_[0x4];
#endif
    struct _UNICODE_STRING FullDllName;            // 3 elements, 0x10 bytes (sizeof)
    struct _UNICODE_STRING BaseDllName;            // 3 elements, 0x10 bytes (sizeof)
    ULONG32      Flags;
}LDR_DATA, *PLDR_DATA;      

至于該結構體的完整定義可通過 WinDbg 指令 dt nt!_LDR_DATA_TABLE_ENTRY 在對應的系統中檢視。

至于 MmVerifyCallbackFunction 具體執行了那些操作,我們該怎麼繞過其驅動程式強制簽名校驗?在下一篇文章中将嘗試做一個簡單的分析。