Rootkit 核心技術——利用 nt!_MDL(記憶體描述符鏈表)突破 SSDT(系統服務描述符表)的只讀訪問限制 Part I

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

———————————————————————————————————————————————————————— 在 rootkit 與惡意軟體開發中有一項基本需求,那就是 hook Windows 內核的系統服務描述符表(下稱 SSDT),把該表中的 特定系統服務函數替換成我們自己實現的惡意常式; ...


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

 

在 rootkit 與惡意軟體開發中有一項基本需求,那就是 hook Windows 內核的系統服務描述符表(下稱 SSDT),把該表中的

 

特定系統服務函數替換成我們自己實現的惡意常式;當然,為了確保系統能夠正常運作,我們需要事先用一個函數指針保存原始

的系統服務,並且在我們惡意常式的邏輯中調用這個函數指針,此後才能進行 hook,否則損壞的內核代碼與數據結構將導致

一個 BugCheck(俗稱的藍屏)

 

 

 

儘管 64 位 Windows 引入了像是 PatchGuard 的技術,實時監控關鍵的內核數據,包括但不限於 SSDT,IDT,GDT。。。等等,

 

保證其完整性,但在 32 系統上修改 SSDT 是經常會遇到的場景,所以本文還是對此做出了介紹。

OS 一般在系統初始化階段把 SSDT 設定成只讀訪問,這也是為了避免驅動與其它內核組件無意間改動到它;所以我們的首要任務

就是設法繞過這個只讀屬性。

 

 

 

在此之前,先複習一下與 SSDT 相關的幾個數據結構,並解釋定位 SSDT 的過程。

 

 

我們知道,每個線程的 _KTHREAD 結構中,偏移 0xbc 位元組處是一枚叫做 ServiceTable 的泛型指針(亦即 PVOID 或 void*),

該欄位指向一個全局的數據結構,叫做 KeServiceDescriptorTable,它就是 SSDT,SSDT 中首個欄位又是一枚指針,指向

全局的數據結構 KiServiceTable,而後者是一個數組,其內的每個成員都是一枚函數指針,持有相應的系統服務常式入口地址。

有的時候,用言語來描述內核的一些概念過於抽象和詞窮,還是來看看下圖吧,它很形象地展示了上述關係:

 

 

 

 

 

根據上圖我們有了思路:首先設法獲取當前運行線程的 _KTHREAD 結構,然後即可逐步定位到 KiServiceTable,它就是我們最終

hook 的對象!

鑒於 ServiceTable 是一枚指針,持有另一枚指針 KeServiceDescriptorTable 的地址

(亦即“指向指針的指針”,往後我會不加以區分“持有”與“指向”術語),而 KiServiceTable 則是一個函數指針數組;

在 Rootkit 源碼中,它們可以分別用三個全局變數(在驅動的入口點 DriverEntry() 之外聲明 )表示,如下圖,我使用了

“自註釋”的變數名,很易於理解;而且我把星號緊接類型保留字後面,避免與“解引”操作混淆(所以星號是一個重載的運算

符):

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

 

對於內核模式驅動程式開發人員來講,自己實現一個常式來獲取當前運行線程的 _KTHREAD 結構顯然並不輕鬆,幸運的是,文檔

化的 PsGetCurrentThread() 常式能夠完成這一任務。

(事實上,PsGetCurrentThread()的反彙編代碼恰恰說明瞭這很簡單,如下代碼,僅僅

只是把 fs:[00000124h] 地址處的內容移動到 eax 寄存器作為返回值,而且 KeGetCurrentThread() 的邏輯與它如出一撤!

 

 1 kd> u PsGetCurrentThread
 2 
 3 nt!PsGetCurrentThread:
 4 83c6cd19 64a124010000    mov     eax,dword ptr fs:[00000124h]
 5 83c6cd1f c3              ret
 6 83c6cd20 90              nop
 7 83c6cd21 90              nop
 8 83c6cd22 90              nop
 9 83c6cd23 90              nop
10 83c6cd24 90              nop
11 nt!KeReadStateMutant:
12 83c6cd25 8bff            mov     edi,edi
13 
14 
15 kd> u KeGetCurrentThread
16 
17 nt!PsGetCurrentThread:
18 83c6cd19 64a124010000    mov     eax,dword ptr fs:[00000124h]
19 83c6cd1f c3              ret
20 83c6cd20 90              nop
21 83c6cd21 90              nop
22 83c6cd22 90              nop
23 83c6cd23 90              nop
24 83c6cd24 90              nop

 

 

 

 

 

老生常談,fs 寄存器通常用來存放“段選擇符”,“段選擇符”用來索引 GDT 中的一個“段描述符”,後者有一個“段基址”

屬性,也就是 KPCR(Kernel Processor Control Region,內核處理器控制區域)結構(nt!_KPCR)的起始地址;nt!_KPCR

偏移 0x120 位元組處是一個 nt!_KPRCB 結構,後者偏移 0x4 位元組處的“CurrentThread”欄位就是一個 _KTHREAD 結構,每次

線程切換都會更新該欄位,這就是 fs:[00000124h] 簡潔的背後隱藏的強大設計思想!

 

 

註意,PsGetCurrentThread() 返回一枚指向 _ETHREAD 結構的指針(亦即“PETHREAD”,如你所見,微軟喜歡在指針這一概念

上大玩頭文字 P”游戲),而 _ETHREAD 結構的首個欄位 Tcb 就是一個 _KTHREAD 實例——這意味著,我們無需計算額外的

偏移量,只要考慮那個 ServiceTable 的偏移量 0xbc 即可,如下圖:

 

 

 

而我們需要在這枚指針上執行加法運算,移動它到 ServiceTable 欄位處,所以不能聲明一個 PETHREAD 變數來存儲

PsGetCurrentThread() 的返回值,因為“指針加上數值 n ”會把指針當前持有的地址加上( n * 該指針所指的數據類型大小 )個

位元組—— 表達式 

1 PETHREAD ethread_ptr += 0xbc;

 

實際上把起始地址加上了 0xbc * sizeof(ETHREAD) 個位元組,遠遠超出了我們的預期。。。。

怎麼辦呢?好辦,聲明一個位元組型指針來保存 PsGetCurrentThread() 的返回值,同時把返回值強制轉型為一致的即可!

如此一來,表達式

1 BYTE* byte_ptr += 0xbc;

 

就是把起始地址加上 0xbc * sizeof(BYTE) 個位元組,符合我們的預期。

註意,這要求我們添加相關的類型定義,如下圖:

 

 

這表明 BYTE 與 無符號字元型等價(還等於微軟自家的 UCHAR),大小都是單位元組;DWORD 則與無符號長整型等價,大小都是

四位元組——我們用一個 DWORD 變數存儲數組 KiServiceTable 的地址。

 

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

 

接下來就是通過一系列的指針轉型和解引操作,定位到 KiServiceTable 的過程,再次凸顯了指針在 C 編程中的地位,無論是應用

程式還是內核。。。。。經過如下圖的賦值運算,最終,全局變數 os_ki_service_table 持有了 KiServiceTable 的地址。註意,除

了那偏移量的巨集定義外,所有的運算都在我們的驅動入口常式 DriverEntry() 中完成,而且為了支持動態卸載,我註冊了

Unload() 回調,稍後你會看到 Unload() 的內部實現——大致就是卸載時取消對 KiServiceTable 的寫許可權映射。

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

 

為了驗證定位 KiServiceTable 過程的準確性,我添加了下列列印輸出語句,註意,DbgPrint() 的輸出需要在被調試機器上以

DbgView.exe 查看;抑或直接輸出到調試機器上的 windbg.exe/kd.exe 屏幕上:

 

 

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

結合上圖,在調試器中進行驗證——“dd”命令可以按雙字(四位元組)顯示給定虛擬記憶體地址處的內容;“dps”命令可以按照函

數符號顯示從給定記憶體地址開始的常式地址——它就是專為函數指針數組(例如 KiServiceTable)設計的,如下圖:

 

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

現在,KiServiceTable 可以經由全局變數 os_ki_service_table 以只讀形式訪問,在我們 hook 它之前,需要設法更改為可寫。

先來看看嘗試向只讀的 KiServiceTable 寫入時會發生什麼事情,如下圖所示,我通過 RtlFillMemory() 試圖向 KiServiceTable

持有的第一個四位元組(亦即系統服務 nt!NtAcceptConnectPort )填充 4 個 ASCII 字元“A”:

 

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

 

註意,RtlFillMemory() 的第一個參數是一個指針,指向要被填充的記憶體塊,後面二個參數分別是填充的長度與數據;由於我們的

變數 os_ki_service_table 是 DWORD 型,所以我把它強制轉型為匹配的指針,再作為實參傳入。。。。重新構建驅動,

放入以調試模式運行的虛擬機中載入,宿主機中發生的情況如下圖所示,假設我們編譯好的 rootkit 名稱為

UseMdlMappingSSDT.sys ,

圖中表明出現一個致命系統錯誤,代碼為 0x000000BE,圓括弧裡邊是攜帶錯誤信息的四個參數,在故障排查時會用到它們。

事實上,這就是一個 BugCheck,當錯誤檢查發生時,如果目標系統連接著宿主機上的調試器,就斷入調試器,否則目標系統

上將執行 KeBugCheckEx() 常式,後者會屏蔽掉所有處理器核上的中斷事件,然後將顯示器切換到低分辯率的 VGA 圖形模式下,

繪製一個藍色背景,然後向用戶顯示 “檢查結果” 對應的停機代碼。這就是“藍屏”的由來。

 

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

在此場景中,我們得到一個 0x000000BE 的停機代碼,將其作為關鍵字串搜索 MSDN 文檔,給出的描述如下圖:

 

 

 

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

 

 

官方講解的很清楚:0x000000BE(ATTEMPTED_WRITE_TO_READONLY_MEMORY)停機代碼是由於驅動程式嘗試向一個只讀

 

的記憶體段寫入導致的;第一個參數是試圖寫入的虛擬地址,第二個參數是描述該虛擬地址所在虛擬頁-物理頁的 PTE(頁表項)

內容;後面兩個參數為保留未來擴展使用,所以被我截斷了。結合前面一張圖我們知道,嘗試寫入的虛擬地址為

0x83CAFF7C,描述映射它的物理頁的 PTE 內容是 0x03CAF121,後面兩個參數就目前而言可以忽略。

如下圖所示,0x83CAFF7C 就是 KiServiceTable 的起始地址;描述它的 PTE 經解碼後的標誌部分有一個“R”屬性,表示

只讀;BugCheck 時刻的棧回溯信息顯示,內核中通用的異常處理程式 MmAccessFault() 負責處理與記憶體訪問相關的錯誤,

它是一個前端解析常式,如果異常或錯誤能夠處理,它就分發至實際的處理函數,否則,它調用 KeBugCheck*() 系列函數,

該家族函數會根據調試器的存在與否作出決定——要麼調用 KiBugCheckDebugBreak() 斷入調試器;要麼執行如前文所述的操作

流程來繪製藍屏:

 

 

 

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

 

至此確定了 BugCheck 是由於在驅動中調用 RtlFillMemory() 寫入只讀的內核記憶體引發的。另一個更強大的調試器擴展命令

“!analyze -v”可以輸出詳細的信息,包括 BugCheck “現場”的指令地址和寄存器狀態,如下圖所示,導致 BugCheck 的

指令地址為 0x9ff990b4,該指令把 eax 寄存器的當前值(0x41414141,亦即我們調用 RtlFillMemory() 傳入的 4 個 ASCII 字

符“A”)寫入 ecx 寄存器持有的記憶體地址處,試圖把 nt!NtAcceptConnectPort() 的入口點地址替換成 0x41414141 ;另外它會

給出驅動源碼中對應的行號——也就是第 137 行的 RtlFillMemory() 調用:

 

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

 

如你所見,微軟 C/C++ 編譯器(cl.exe)把 RtlFillMemory() 內聯在它的調用者內部,換言之,儘管有公開的文檔描述它的

返回值,參數。。。。具體的實現還是由編譯器說了算——為了性能優化,RtlFillMemory() 直接實現為一條簡潔的數據移動

指令,相關的參數由寄存器傳遞,沒有因函數調用創建與銷毀棧幀帶來的額外開銷!

到目前為止,儘管我們通過一系列步驟從 _KTHREAD 定位到了系統服務指針表,但以常規手段卻無法 hook 其中的系統服務函

數,因為它是只讀的。

 

下一篇文章我將討論如何使用 MDL(Memory Descriptor List,記憶體描述符鏈表)來繞過這種限制,隨心所欲地讀寫

KiServiceTable!

 


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

-Advertisement-
Play Games
更多相關文章
  • win10系統配置虛擬主機1.用記事本或Sublime Text打開httpd.confctrl + f 搜索httpd-vhosts.conf 將#Include conf/extra/httpd-vhosts.conf的#號去掉保存 2.打開extra/httpd-vhosts.conf添加如下 ...
  • Relatives Time Limit: 1000MS Memory Limit: 65536K Total Submissions: 15566 Accepted: 7900 Description Given n, a positive integer, how many positive i ...
  • 一.路由配置系統 1.1 基本認識 a. URL配置(URLconf)就像Django 所支撐網站的目錄。它的本質是URL與要為該URL調用的視圖函數之間的映射表 b. URLconf 在請求的URL 上查找,將它當做一個普通的Python 字元串。不包括GET和POST參數以及功能變數名稱。 例如,htt ...
  • 一.介紹 在HTML的基礎上加上模板語言(包括:會被替換掉的變數和標簽) 二.模板語法之變數( {{ }} ) 2.1 基本用法 views: template: 2.2 過濾器 語法: 三.模板語法之標簽( {% %} ) 3.1 標簽操作 3.2 模板繼承 模版繼承可以讓您創建一個基本的“骨架” ...
  • 1.開啟apache模塊 在apache配置文件 #LoadModule rewrite_module modules/mod_rewrite.so 前面的#去掉 然後添加 我的E:/Apache/Apache24/htdocs/ 為網站目錄 到apache配置文件,隨便放,為了好找我放在最下麵 < ...
  • 迭代器 迭代器是一種更抽象的概念。 迭代是訪問數據的一種方式。迭代器是一個可以記住遍歷位置的對象,迭代器對象從集合的第一個元素開始訪問,直到所有的元素訪問完後結束。迭代器只能往前去訪問下一個元素,不能後退。 迭代器就是可迭代對象調用iter()方法,返回了一個迭代器 那麼什麼是可迭代對象呢? 像字元 ...
  • 華為研發部門,每年都會在部門內部舉辦一屆編程大賽。旨在讓開發人員在工作之餘,通過游戲編程的切磋,提高技術和協作能力。在入職華為的第四個年頭,我終於如願拿到了部門編程大賽的冠軍。之前的每一年也都會參加,其中兩次抱大腿拿到了亞軍,一次因為太忙棄權了。這一屆終於帶隊拿到了冠軍,了卻了一樁心事。在此,對之前 ...
  • 所有的偉大源於一個勇敢的開始 數據結構預備知識 指針 1.指針:是C語言的靈魂,指針=地址 地址:記憶體單元的編號 指針變數:存放記憶體單元地址的變數 int *p;//p是指針變數,int *表示該p變數只能存儲int類型變數的地址,不能存放別的類型的 int i=10; p=&i 詳細這兩部操作: ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...