———————————————————————————————————————————————————————————————————————————————————————————編程
本篇開始進入正題,由於涉及 MDL,因此相關的背景知識是必須的:數組
nt!_MDL 表明一個「內存描述符鏈表」結構,它描述着用戶或內核模式虛擬內存(亦即緩衝區),其對應的那些物理頁被鎖定住,安全
沒法換出。網絡
由於一個虛擬的,地址上連續的用戶或內核緩衝區可能映射到多個不連續的物理頁,因此 nt!_MDL 定長(0x1c 字節)的頭部後緊app
跟數量可變的頁框號(Page Frame Numbers),MDL 描述的每一個物理頁面都有一個頁框號,函數
因而這些頁框號引用的物理地址範圍就對應了一片特定的用戶或內核模式緩衝區。佈局
一般虛擬和物理頁的大小爲 4 KB,KiServiceTable 中的系統服務數量爲 401 個,每函數的入口點佔用 4 字節,整張調用表大小spa
爲 1.6 KB,經過 MDL 僅須要一張物理頁便可描述這個緩衝區;在這種狀況下,該 MDL 後只有一個頁框號。翻譯
儘管 nt!_MDL 是半透明的結構,不過在內核調試器以及 WRK 源碼面前仍是被脫的一絲不掛,以下圖爲 WRK 源碼設計
的「ntosdef.h」頭文件中的定義,如你所見,稱爲「鏈表」乃因它的首個字段「Next」是一枚指針,指向後一個 nt!_MDL 結構。
對於咱們 hook KiServiceTable 的場景而言,無需用到 Next 字段;那什麼狀況下會用到呢?
Windows 中某些類型的驅動程序,例如網絡棧,它們支持 MDL 鏈,其內的多個 MDL 描述的那些緩衝區其實是零散的,
假設棧中每一個驅動都分配一個 MDL,其後跟着一些物理頁框號來描述它們各自用到的虛擬緩衝區,那麼這些緩衝區就經過
每一個 _MDL 的 Next 字段(指向下一個 MDL)連接起來。
————————————————————————————————————————————————————————————
對於描述用戶模式緩衝區的 MDL,其內的 Process 字段指向所屬進程的 EPROCESS 結構,進程中的這塊虛擬地址空間被 MDL 鎖
住。
若是由 MDL 描述的緩衝區映射到內核虛擬地址空間中,_MDL 的 MappedSystemVa 字段指向內核模式緩衝區的基地址。
僅當 _MDL 的 MdlFlags 字段內設置了 MDL_MAPPED_TO_SYSTEM_VA 或 MDL_SOURCE_IS_NONPAGED_POOL 比特位,
MappedSystemVa 字段纔有效。
_MDL 的 Size 字段含有 MDL 頭部加上其後的整個 PFN 數組總大小。
上圖還包含了 MdlFlags 字段的全部標誌宏定義,這個 2 字節的字段能夠是任意宏的組合,用於說明 MDL 的一些狀態與屬性。
MDL 的 StartVa 字段和 ByteOffset 字段共同定義了由該 MDL 鎖定的原始緩衝區的起始地址。
(原始緩衝區可能會映射到其它內核緩衝區或用戶緩衝區)
StartVa 指向虛擬頁的起始地址,ByteOffset 包含實際從 StartVa 開始的緩衝區偏移量
MDL 的 ByteCount 字段描述由該 MDL 鎖定的緩衝區大小(以字節爲單位)
對於咱們要 hook 的 KiServiceTable 而言, KiServiceTable 這片內核緩衝區所在的虛擬頁起點由 StartVa 字段攜帶;
ByteOffset 字段則攜帶 KiServiceTable 的頁內偏移量,ByteCount 字段攜帶 KiServiceTable 這片內核緩衝區的大小。
若是你如今看得雲裏霧裏,不用擔憂,後面咱們在調試時會把描述 KiServiceTable 的一個 nt!_MDL 結構實例拿出來分析,
到時候你就會恍然大悟這些字段的設計思想了。
————————————————————————————————————————————————————————————
經過編程方式使用 MDL 繞過 KiServiceTable 的只讀屬性,須要藉助 Windows 執行體組件中的 I/O 管理器以及
內存管理器導出的一些函數,大體流程以下:
IoAllocateMdl() 分配一個 MDL 來描述 KiServiceTable -> MmProbeAndLockPages() 把該 MDL 描述的 KiServiceTable 所
屬物理頁鎖定在內存中,並賦予對這張頁面的讀寫訪問權限(實際是將描述該頁面的 PTE 內容中的「R」標誌位修改爲「W」)
->MmGetSystemAddressForMdlSafe() 將 KiServiceTable 映射到另外一片內核虛擬地址區域(通常而言,位於 rootkit 被加載
到的內核地址範圍內)。
如此一來,KiServiceTable 的原始虛擬地址與新映射的虛擬地址都轉譯到相同的物理地址,並且描述新虛擬地址的 PTE 內容標記了
寫權限比特位,這樣咱們就可以經過修改這個新的虛擬地址中的系統服務例程實現安全掛鉤 KiServiceTable,不會致使
BugCheck。
以下所示,我把上述涉及的全部操做都封裝到一個自定義的函數 MapMdl() 裏面。因爲整個邏輯比較長,截圖分爲多張說明:
MapMdl() 在咱們的 rootkit 入口點——DriverEntry() 中被調用,而在 DriverEntry() 外部聲明幾個與 MDL 相關的全局變量,
它們被 MapMdl() 與 DriverEntry() 共享。
注意,os_ki_service_table 存儲着 KiServiceTable 的地址(參見前一篇定位 KiServiceTable 的代碼),
把它從 DWORD 轉換爲泛型指針是爲了符合 MapMdl() 中的 IoAllocateMdl() 調用時的形參要求;最後一個參數——表達式
0x191 * 4——就是整個 KiServiceTable 緩衝區的大小:倘若 MapMdl() 成功返回,則全局變量 mapped_ki_service_table
持有 KiServiceTable 新映射到的內核虛擬地址;這些全局變量都是「自注釋」的,pfn_array_follow_mdl 持有的地址處內容
就是 MDL 描述的物理頁框號:
——————————————————————————————————————————————————————————————
MapMdl()第一部分邏輯以下圖所示,局部變量 mapped_addr 預期存放 KiServiceTable 新映射到的內核虛擬地址,並做爲
MapMdl() 的返回值給 DriverEntry(),進一步初始化全局變量 mapped_ki_service_table。
注意,PVOID 能夠賦給其它任意類型的指針,這是合法的。
IoAllocateMdl() 返回一枚指針,指向分配好的 MDL,該 MDL 描述 KiServiceTable 的物理內存佈局;這枚指針被用來初始化
做爲實參傳入的全局變量 mdl_ptr(mdl_pointer 是形參)。
我添加的第一個軟件斷點就是爲了研究 IoAllocateMdl() 分配的 MDL 其中 MappedSystemVa,StartVa,以及 MdlFlags 這些
字段的內容——事實上,這些字段值會在
IoAllocateMdl() -> MmProbeAndLockPages() ->MmGetSystemAddressForMdlSafe()
調用鏈的每一階段發生變化,因此我總共添加了三個斷點在相關的檢查區域,有助於咱們在後面的調試過程當中深刻理解 nt!_MDL
的設計思想。
我把使用 Windows 執行體組件例程進行的操做放入一個 try-except 塊內,以便處理可能出現的異常,except 塊內的邏輯以下
圖,當違法訪問出現時,調用 IoFreeMdl() 釋放咱們的 MDL 指針,而後 MapMdl() 返回 NULL,從而致使 DriverEntry() 打印出
錯信息。
————————————————————————————————————————————————————————————
關於 IoAllocateMdl() 的第二個參數,咱們有必要進一步瞭解,因此我翻譯了 MSDN 文檔上的相關片斷,以下:
IoAllocateMdl() 的第二個參數指定要經過分配的 MDL 描述的緩衝區的大小。若是這個長度小於 4KB,
那麼映射它的 MDL 就只描述了一個被鎖定的物理頁面;
若是長度是 4KB 的整數倍,那麼映射它的 MDL 就描述了相應數量的物理頁面(經過緊接 MDL 後面的 PFN 數組)
對於 Windows Server 2003,Windows XP,以及 Windows 2000,
此例程支持的最大緩衝區長度(以字節爲單位)是:
PAGE_SIZE * (65535 - sizeof(MDL)) / sizeof(ULONG_PTR) (約 67 MB)
對於 Windows Vista 和 Windows Server 2008,可以傳入的最大緩衝區大小爲:
(2 gigabytes - PAGE_SIZE)
對於 Windows 7 和 Windows Server 2008 R2,可以傳入的最大緩衝區大小爲:
(4 gigabytes - PAGE_SIZE)
執行此例程的 IRQL 要求爲 <= DISPATCH_LEVEL
————————————————————————————————————————————————————————————
MapMdl()第二部分邏輯以下圖所示,它緊跟在第一個軟件斷點以後。咱們檢查 MDL 中的 MDL_ALLOCATED_FIXED_SIZE 標誌是
否置位,該標誌因調用 IoAllocateMdl() 傳入第二個參數指示固定大小而置位;MmProbeAndLockPages() 的第三個參數是實現
寫訪問的關鍵所在,可否鎖定內存卻是其次,由於像 KiServiceTable 這種系統範圍的調用表,地位很是重要,若是被換出物理內
存,系統豈不就崩潰了,因此坦白講咱們只是由於須要寫權限才調用它的。
第二個斷點緊跟其後,這樣就能夠在調試器中檢查 MmProbeAndLockPages() 是如何修改 MDL 中的標誌;也可使用編程手段
檢查,如圖中的第二個 if 塊邏輯,事實上 MmProbeAndLockPages() 調用會向 MdlFlags 字段內添加 MDL_WRITE_OPERATION
與 MDL_PAGES_LOCKED 標誌,這就是咱們想要的結果!
最後咱們調用 MmGetSystemAddressForMdlSafe() 把該 MDL 描述的原始虛擬地址映射到內核空間的另外一處,新地址一般位於
驅動加載到的內核空間某處;局部變量 mapped_addr 持有這個新地址,最終用來返回並初始化全局變量
mapped_ki_service_table。
同理咱們能夠檢查 MmGetSystemAddressForMdlSafe() 修改了哪些 MDL 結構成員,對於理解 MDL 的工做機理很是關鍵。
————————————————————————————————————————————————————————————
MapMdl()第三部分邏輯以下圖所示,咱們檢查 MmGetSystemAddressForMdlSafe()是否多添加了一個
MDL_MAPPED_TO_SYSTEM_VA 標誌,而後以 DBG_TRACE 宏打印信息。
全局變量 backup_mdl_ptr 是咱們在調用 IoAllocateMdl() 就作好備份的 MDL 指針,它與 mdl_ptr 指向同一個 nt!_MDL 結構。
接下來的邏輯有助於你理解 MDL 頭部後面的 PFN 數組:mdl_ptr 指向 nt!_MDL 結構頭部,把它加上 1 ,意味着把它持有的
內存地址加上 1 * sizeof(MDL) 個字節,因而就定位到了 MDL 頭部後面的 PFN 數組起始地址——如今全局變量
pfn_array_follow_mdl(一枚 PPFN_NUMBER 型指針)持有這個地址;正如圖中倒數第三條 DbgPrint() 調用所言——
MDL 結構後偏移 xx (0x1b)地址處是一個 PFN 數組,用來存儲該 MDL 描述的虛擬緩衝區映射到的物理頁框號。
最後一條 DbgPrint() 調用經過解引 pfn_array_follow_mdl 來輸出該地址處存放的物理頁框號。
在 return mapped_addr; 語句的後面,則是 try-except 塊的異常捕獲邏輯,請參前面截圖。
————————————————————————————————————————————————————————————
如今,程序訪問可讀寫的 mapped_ki_service_table 與只讀的 os_ki_service_table 都轉譯到同一塊物理內存,
後者就是實際上存儲 KiServiceTable 的地方。接下來,咱們用一枚函數指針保存 KiServiceTable 中某個原始的系統服務,
而後用咱們的鉤子例程地址替換掉該位置處的原始系統服務,而鉤子例程內部僅僅是調用原始系統服務,實現安全轉發。
爲了演示簡單起見,我選取 KiServiceTable 中 0x39(57)號例程,由於它的參數只有一個,方便咱們的鉤子例程仿效一樣的
參數聲明——內核系統服務調度器(nt!KiFastCallEntry())並不知道它調用的目標系統服務已經被替換成咱們的鉤子例程,
因此他會以既定方式使用鉤子例程的返回值和輸出參數,在這種狀況下,只要咱們的鉤子例程原型聲明與被掛鉤系統服務有
細微差異,均可能致使非預期的內核錯誤而藍屏,顯然,那些參數既多又複雜的系統服務不適合我用來演示。
此外,某些系統服務接收的參數類型的定義不在 wdm.h / ntddk.h 頭文件內,講明瞭這些數據類型不是給驅動開發人員使用的,
僅供內核組件使用,爲了引入包含該定義的頭文件則會碰到複雜的頭文件嵌套包含問題,其麻煩程度絲絕不遜於 Linux 平臺上
的「二進制軟件包依賴性地獄」。
57 號系統服務例程亦即 nt!NtCompleteConnectPort(),有且僅有一個文檔化的參數,WRK 源碼中的相關定義以下圖:
————————————————————————————————————————————————————————————
因此咱們的鉤子例程只要徹底仿效它的返回值類型與形參類型便可,而後在內部調用指向原始例程的函數指針實施重定向。
經過 typedef 定義一個函數指針,其返回值類型與形參類型與 NtCompleteConnectPort() 一致,而後聲明一個該函數指針
實例。相關代碼以下圖:
————————————————————————————————————————————————————————————
全局變量 ori_sys_service_ptr 持有 NtCompleteConnectPort() 的入口點地址,前者是在咱們的 rootkit 入口點
DriverEntry() 中初始化的;保存這枚指針後就能夠用鉤子例程替換 NtCompleteConnectPort(),以下圖所示:
須要指出一點,儘管把指針名稱 mapped_ki_service_table 看成數組名稱來訪問 KiServiceTable 是被 C 語言核心規範容許的,
可是上圖那段代碼在編譯器會產生警告,以下:
1 1>warnings in directory d:\kmdsource_use_mdl_mapping_ssdt 2 1>d:\kmdsource_use_mdl_mapping_ssdt\usemdlmappingssdt.c(155) : warning C4047: '=' : 'OriginalSystemServicePtr' differs in levels of indirection from 'DWORD' 3 1>d:\kmdsource_use_mdl_mapping_ssdt\usemdlmappingssdt.c(157) : warning C4047: '=' : 'DWORD' differs in levels of indirection from 'NTSTATUS (__stdcall *)(HANDLE)'
ori_sys_service_ptr 是一枚 OriginalSystemServicePtr 型函數指針( NTSTATUS (__stdcall *)(HANDLE) ),而
mapped_ki_service_table 是普通指針,它的數組名稱表示法結合數組下標,實際上被視爲一個存儲對應元素的 DWORD 變量,
二者的間接尋址級別不一樣。
就目前而言咱們能夠無視這兩條警告,由於含有這段代碼的 rootkit 源碼在編譯後確實可以安全地 hook 目標系統服務函數,系統
正常運做不會有問題,相似的警告能夠經過指定警告級別的編譯選項來過濾掉。
——————————————————————————————————————————————————————————————————————————————————
講到這裏你必定會嫌我既羅嗦又婆婆媽媽的,那麼來看下面這一張簡明扼要的全局概覽,它解釋了 MDL 是如何把一片緩衝區
映射到另外一處,並描述二者相同的物理佈局,注意,圖中的組織結構是執行完 MmGetSystemAddressForMdlSafe() 後纔會產生的。
注意,上圖中我沒有給出 PFN 數組中第一個成員攜帶的具體 20 位物理頁框號,原始和映射到的新內核緩衝區,以及實際 RAM
中的物理頁框號,而「byte within page」就是頁內特定偏移處開始的字節序列,亦即系統服務例程入口點的實際物理地址!
這些「佔位符」我會在第三部分的調試單元內給出,畢竟,驅動開發與調試是相輔相成的,只有理論沒有實踐怎麼行,只有源碼
沒有調試怎知真理,否則,任何人對於內存的需求就真的不會超過 640 K 了。。。。。
——————————————————————————————————————————————————————————————————————————————————————