-------- Rootkit 核心技術——利用 nt!_MDL 突破 KiServiceTable 的只讀訪問限制 Part II --------

来源:https://www.cnblogs.com/flying-shark/archive/2018/01/28/8372956.html
-Advertisement-
Play Games

——————————————————————————————————————————————————————————————————————————————————————————— 本篇開始進入正題,因為涉及 MDL,所以相關的背景知識是必須的: nt!_MDL 代表一個“記憶體描述符鏈表”結構,它 ...


———————————————————————————————————————————————————————————————————————————————————————————

本篇開始進入正題,因為涉及 MDL,所以相關的背景知識是必須的:

 

nt!_MDL 代表一個“記憶體描述符鏈表”結構,它描述著用戶或內核模式虛擬記憶體(亦即緩衝區),其對應的那些物理頁被鎖定住,

無法換出。

因為一個虛擬的,地址上連續的用戶或內核緩衝區可能映射到多個不連續的物理頁,所以 nt!_MDL 定長(0x1c 位元組)的頭部後緊

數量可變的頁框號(Page Frame Numbers),MDL 描述的每個物理頁面都有一個頁框號,

於是這些頁框號引用的物理地址範圍就對應了一片特定的用戶或內核模式緩衝區。

通常虛擬和物理頁的大小為 4 KB,KiServiceTable 中的系統服務數量為 401 個,每函數的入口點占用 4 位元組,整張調用表大小

為 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 了。。。。。

——————————————————————————————————————————————————————————————————————————————————————

to be continued

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 這個消息比較實用也很關鍵,它代表非顯示區域命中測試。這個消息優先於所有其他的顯示區域和非顯示區域滑鼠消息。其中lParam參數含有滑鼠位置的x和y屏幕坐標,wParam 這裡沒有用。 Windows應用程式通常把這個消息傳送給DefWindowProc,然後Windows用WM_NCHITTEST消 ...
  • 1、connect描述:實例連接到一個Redis.參數:host: string,port: int返回值:BOOL 成功返回:TRUE;失敗返回:FALSE示例:$redis = new redis(); $result = $redis->connect('127.0.0.1', 6379); ...
  • 因為每個枚舉常量只有一個實例,所以如果在比較兩個參考值,至少有一個涉及到枚舉常量時,允許使用“==”代替equals() ...
  • 作者: "nnngu" 項目源代碼:https://github.com/nnngu/nguSeckill 首先在編寫 層代碼前,我們應該首先要知道這一層到底是乾什麼的。 層主要負責業務模塊的邏輯應用設計。同樣是首先設計介面,再設計其實現的類,接著在 的配置文件中配置其實現的關聯。這樣我們就可以在應 ...
  • 使用正則表達式的幾個步驟: 1、用import re 導入正則表達式模塊; 2、用re.compile()函數創建一個Regex對象; 3、用Regex對象的search()或findall()方法,傳入想要查找的字元串,返回一個Match對象; 4、調用Match對象的group()方法,返回匹配 ...
  • redis的使用和安裝,redis基礎和高級部分 在後端開發中,為了提高性能,對於一些經常查詢但是又不太變化的內容會使用redis,比如前端的列表展示項等,如果數據有變化也可以清空緩存,讓前端查一次資料庫,所以使用redis相對高效和靈活.本文主要對於redis在linux上的使用和安裝進行說明。 ...
  • jsp jsp簡介: JSP全名為Java Server Pages,中文名叫java伺服器頁面,其根本是一個簡化的Servlet設計,在jsp中既可以寫html 代碼 ,又可以寫java代碼 作用:將頁面顯示與業務邏輯相分離; 通常分為三部分: java 代碼 html代碼 jsp指令 jsp本質 ...
  • 1.flask特有的變數和函數: 變數:g、session、request、config 函數:url_for()、get_flashed_messages()這個函數註意了啊,記住這是個函數,別忘了寫括弧!!!!!!!!! 廢話不多說,直接上代碼體驗一下: 先解釋一個bug,當我們設置了# -*- ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...