[Windows] [Debug] 記憶體無痕鉤子 - 硬體斷點 (C++) 實作 Ring3 進程防殺


前言

其實三年前就土炮過調試器了,不過最近要做碩論(?)會用到動態路徑分析所以又重新回頭來玩一些以前寫外掛用到的技能,而翻了一下繁體中文的文獻好像對這部分沒什麼提及就寫了一篇做一個簡單整理(主要是方便我以後回頭拿來當工具用XD)

本文實作做一個標準調試器自動下硬體斷點、然後修改暫存器內容再恢復執行緒運作,藉此來達成記憶體程式碼不破損情況下做到修改程式邏輯,完成自我進程防殺(防止工作管理員殺除)

Debugger

在 Windows 進程權級分割下如果想除錯其他進程要先用 AdjustTokenPrivileges 拿取 SE_DEBUG 令牌,有了這張令牌就可以除錯其他進程(不過拿這張令牌要先過 UAC 就是)接著可以用 DebugActiveProcess 函數 attach 上對應你想動態除錯的進程、接著你除錯的進程就會被掛起成 DEBUG_MODE 接著執行緒會被排程跳到 DbgBreakPoint 上踩到 \xCC 然後就拋出一個異常可以讓 Debugger 接收負責接下來對應處理。 (所以一些殼在處理 anti-attach 主要就是在 DbgBreakPoint 上一個 hotpatch 去自殺)

而接下來 Debugger 負責做的處理就是一個無限迴圈:以 WaitForDebugEvent 向系統拿取當前被掛載進程的異常事件(這個概念有點像 Win32 視窗程式的 GetMessage 架構拿消息)當被掛載進程拋錯時候,WaitForDebugEvent 就可以拿到異常事件、Debugger 負責接手接下來的事情。

所以講到這邊原理就很淺而易見可以知道:如果外掛要修改邏輯(以除錯器模式實作)就會有很基礎的三種方式去讓程式執行到想攔截的函數時,就必須拋錯,主流方法就三種:
  1. int 3 讓 CPU 踩上去時候自動拋出異常
  2. 記憶體改成不可執行的分頁區讓 DEP 防護拋出異常
  3. 或者本文要講的硬體斷點手段

硬體斷點(DR0 → DR4)

可以查閱 wiki 知道:wiki/X86_debug_register,總之 x86 CPU 提供了每個 Thread 有四個「當前正在除錯的地址」可以指示當 CPU 正在對某塊地址進行 讀/寫/執行 時「就必須拋錯」。那麼這四個地址就分別放在 DR0 ~ DR4(對,就把你想攔截的地址放上去就對了沒什麼技巧)

那麼怎麼讓 CPU 知道到底是對記憶體 執行、讀、還是寫的時候才要斷下來呢?甚至我可能先寫上地址但不想馬上斷下來需要有個開關嘛,這部分可以看到 wiki:
Bits 16-17 (DR0), 20-21 (DR1), 24-25 (DR2), 28-29 (DR3), define when breakpoints trigger. Each breakpoint has a two-bit entry that specifies whether they break on execution (00b), data write (01b), data read or write (11b). 10b is defined to mean break on IO read or write but no hardware supports it.[citation needed] Bits 18-19 (DR0), 22-23 (DR1), 26-27 (DR2), 30-31 (DR3), define how large an area of memory is watched by breakpoints. Again each breakpoint has a two-bit entry that specifies whether they watch one (00b), two (01b), eight (10b)[1] or four (11b) bytes.[2]
簡單來說 DR7 這樣一個 int32_t 裡面可以配置:bit[16:17] 是 DR0 所記憶體地址 hook 的開或關(分別對應要全域還是區域 hook)bit[20:21] 對應 DR1... 依此類推。那麼怎麼配置讀/寫/執行 時候才要斷下來呢?分別對應在 bit[18:19] 是 DR0 讀寫還是執行(00b 是執行、01b 是讀、10b是寫、11是唯有讀寫時才拋錯)
所以可以大概理解為:bit[16] 後每 4 bit 分別對應 DR0、DR1、DR2、DR3 的讀寫開關與拋錯條件。

PoC




由於砍進程通常要透過 WinAPI -- TerminateProcess,調用此 API 需要給定 hProcess 是你用 OpenProcess 去跟系統要回來的令牌(亦即你權限不夠就拿不到這張令牌)所以大部分做進程防殺都在此 API 做攔截。

那可以看到 ntdll!NtOpenProcess 的原型第四個參數是 PCLIENT_ID 根據 x64 Calling Convention 可以知道 R9 上應該會拿到一個 uint64_t 的 address 裡面含有當前要拿令牌的 Proccess ID。

講到這邊廢話不多說,觀念都很簡單就可以直接上實戰 PoC 惹:
效果如下圖:
其實調試器做的事情很簡單,就負責對工作管理員的每一個 Thread 的 DR0 上一個 NtOpenProcess 的 hook,配置好後就等待 Process 執行到此 API 拋錯回來,我們就可以透過 ReadProcessMemory 把 R9 內容讀回來看是不是我們自己的 Proccess Id,如果是、我們就可以把 Rcx(第一個參數)清掉讓它拿不到令牌達成進程免殺。

而這個實作因為不牽涉上記憶體 patch 所以可以看到記憶體上面是沒有任何痕跡的,這也是為啥線上遊戲外掛那麼愛用XD,大部分反外掛防護要防止這類型手段通常都避免其他進程可以 DEBUG 遊戲程式來下硬體斷點。不過只要這一層繞掉,基本上在暫存器上面的直接上 DRn Hook 檢測起來是相當費力的。

留言

這個網誌中的熱門文章

[C#] Lambda花式應用噁爛寫法(跨UI委派秒幹、多線程處理...etc)

[Black Asia Arsenal] puzzCode: 專注開發後門的編譯器, 自帶反逆向、對抗病毒特徵碼定位技術

[Windows] 逆向工程 C++ 中入口函數參數 main(argc, argv) 與如何正確的進行參數劫持