驅動除錯——挫敗 QQ.EXE 的核心模式保護機制(part I)

Editor發表於2018-05-29


QQ 是一款熱門的即時通訊(IM)類工具,在安裝時刻會向系統分割槽的\..\windows\system32\drivers 路徑下生成兩個驅動程式檔案:

QQProtect.sys 與 QQFrmMgr.sys ,前者是 QQProtect.exe(QQ 安全防護程式,又稱 Q 盾)的核心模式元件;後者是一種過濾型驅動。


同時還會向登錄檔位置HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\ 建立對應名稱的鍵,其中有重要的兩個子鍵控制這兩個驅動的載入方式:“type” 與 “start”。


對於 QQProtect.sys ,其鍵值分別為 1 和 2,這意味著由 services.exe(服務控制管理器)自動將 QQProtect.sys 載入核心空間;


對於 QQFrmMgr.sys,其鍵值分別為 1 和 1,這意味著在核心初始化期間,由 ntoskrnl.exe將 QQFrmMgr.sys 載入核心空間,就載入的順序而言,QQFrmMgr.sys 早於 QQProtect.sys,如下圖所示:


驅動除錯——挫敗 QQ.EXE 的核心模式保護機制(part I)


儘管這兩個驅動並非 Rootkit 或者惡意軟體,但它們確實會 hook 系統服務排程表/Shadow、在 System 程式中注入核心執行緒、註冊一些通知回撥......

所以也算是更改了系統的一些關鍵資料結構來進行非正當活動。


本文探討如何使用核心偵錯程式 WinDbg.exe 來檢查諸如此類為保護 QQ 程式而採取的核心空間手段,然後把系統還原至 “乾淨” 狀態。


測試環境是兩臺真實的計算機(雙機物理除錯)——執行 Windows 7 的 宿主機(除錯機),以及執行 Windows 8.1 的目標機(被除錯機,已安裝了 QQ)

兩者通過乙太網線連線進行除錯。


注意,通過乙太網線執行雙機物理除錯時,對除錯機的網路卡無特殊要求;但是被除錯機的網路卡必須被 Debugging Tools for Windows 所支援(亦即 Kd.exe 與 WinDbg.exe),而且被除錯機上的作業系統版本需要是 windows 8 或者更後面的版本;除錯機上的作業系統需要是 windows xp 或更後面的版本。


使用乙太網除錯的一大好處就是,通訊介質獲取方便——相較於老舊的串列埠線(RS-232)以及主機板上基本被淘汰的 COM 模組而言,Cat5 標準以上的網路線隨便在電腦城就能買到,而且主機板上絕不可能沒有網路介面卡使用的 RJ-45 埠。想必乙太網除錯一定會成為日後的標準!


另一方面,我也實施了物理-虛擬機器除錯,虛擬機器作為被除錯機,其上執行 Windows 7,這樣不但能夠對比出,QQ 驅動針對不同核心版本(Windows 7 是核心版本 6.1 ;Windows 8.1 是核心版本 6.3)所表現出來的邏輯差異,還能夠明確 QQ 驅動是否採取了 “反虛擬機器” 技術,並且揭示它在真實機器上的行為!


因此下面的除錯過程中,所有與真實機器上不同的結果我都會另行說明。在開始之前,來過目一下我配置的雙機物理除錯引數:

1 cd "d:\Windows Kits\10\Debuggers\x86" && d: && windbg.exe -n -v -logo d:\networking_physical_host-target_debugging.txt -y SRV*E:\windows8_1_retail_symbols*http://msdl.microsoft.com/download/symbols -k net:port=60111,key=shayi.1983.gmail.com


其中,

❶ 我將 Windows Kits 驅動開發工具包安裝到了  “d:\Windows Kits”  目錄下;

❷ 輸出除錯資訊到指定的日誌檔案;

❸ 指定微軟的符號伺服器 URL,這樣偵錯程式就可以通過 HTTP GET 請求,按需從伺服器下載並解析特定核心模組中的函式符號;

❹ 以及預先儲存在本地的符號檔案(可以從 MSDN 站點下載,整個 MSI 封裝的符號包大小約為五、六百 MB)所在路徑;

(注意,宿主機上核心版本的不同導致需要分別下載對應的符號檔案,並指定為除錯引數)

❺ 指定通過乙太網除錯(net),宿主機上開啟除錯埠為 UDP 的 60111;

❻ 最後的 key 可以任意指定,但其中的 4 個子域之間需要用點號分隔開。


關於目標機上的對應配置,請各位參見 MSDN 文件,這裡就不再贅述。



首先在 Windows 8.1 目標機上通過 Process Explorer 瀏覽到 System 程式中的系統執行緒,其中有一個 QQ 核心執行緒是由 QQFrmMgr.sys 建立的,該執行緒的啟動地址距離所屬模組被載入基址的偏移量為 0x5e34 :


驅動除錯——挫敗 QQ.EXE 的核心模式保護機制(part I)


我們的目標是結束該執行緒的執行,通常的做法是用系統內建的APC(非同步過程呼叫)機制來實現。APC 就是執行在特定執行緒上下文中的例程。


從程式設計角度來講,呼叫 KeInitializeApc() 初始化一個 nt!_KAPC 結構,並將其關聯到該 QQ 核心執行緒的 nt!_KTHREAD 結構,設定該 APC 例程回撥為 PspExitThread();


然後利用 KeInsertQueueApc() 通過這個 nt!_KTHREAD 結構來排入該 QQ 核心執行緒的APC 佇列,如此一來,當該 APC 被交付時,就會在該 QQ 核心執行緒的執行上下文中呼叫PspExitThread(),從而終止掉該 QQ 核心執行緒。


而在除錯環境下,沒有對應的核心 API 可用,所以我們必須手工構造 APC、指定回撥函式、關聯執行緒、以及排入佇列,如下步驟所示:

第一步:查詢 System 程式的 nt!_EPROCESS 結構地址;

第二步:定位到其中的執行緒雙向連結串列頭部,然後開始遍歷這個連結串列中的每一個nt!_ETHREAD 結構,找出那些啟動地址位於 QQFrmMgr.sys 模組空間內的執行緒:

驅動除錯——挫敗 QQ.EXE 的核心模式保護機制(part I)


相應的 WinDbg 命令如下:

1 !list "-t nt!_LIST_ENTRY.FLink -e -x \"r @$t3=@$extret-@$t1;

2 r @$t4= @$t3+@$t2;

3 r @$t5=poi(@$t4);

4 .if(@@((unsigned long)@$t5>(unsigned long)0x82000000 && (unsigned long)@$t5<(unsigned long)0x82017b00)){r @$t3;dt -b nt!_ETHREAD Cid. @$t3; dds @$t4 l1;};

5 \" 8565e5c0+@$t0"


驅動除錯——挫敗 QQ.EXE 的核心模式保護機制(part I)

驅動除錯——挫敗 QQ.EXE 的核心模式保護機制(part I)


第三步:手工構建一個 nt!_KAPC 結構,指定回撥函式、關聯執行緒、以及排入佇列:


(3-1):查詢 QQFrmMgr.sys 模組內部的 section 資訊,注意到其中的 .data section 後緊接 INIT section:


驅動除錯——挫敗 QQ.EXE 的核心模式保護機制(part I)


從上圖可知,.data section 起始 RVA 為 11800,大小 3C80,結束 RVA 為 15480,這剛好是 INIT section 的起始 RVA。


INIT section 的屬性中,“Discardable”  與  “Execute Read Write” 完美匹配了手工構建 APC 需要的寫屬性,以及回撥函式需要的執行屬性,所以它是理想的目標 section。


(3-2):從 INIT section 起始地址初始化 0x200 位元組記憶體,此塊區域用於 nt!_KAPC 結構和回撥函式(nt!_KAPC 的KernelRoutine欄位)。如下圖所示,

我們在地址 82015480 處構造的回撥函式呼叫 PspExitThread() 來結束當前執行緒的執行;然後在地址 82015500 處構造一個 nt!_KAPC 結構;


驅動除錯——挫敗 QQ.EXE 的核心模式保護機制(part I)

1 r @$t0=82015500;

2 r @$t1=8b9ccbc0;

3 r@$t2=82015480;

4 ?? ((nt!_KAPC*)@$t0)->Type=18;

5 ?? ((nt!_KAPC*)@$t0)->Size=sizeof(nt!_KAPC);

6 ?? ((nt!_KAPC*)@$t0)->Thread=@$t1;

7 ?? ((nt!_KAPC*)@$t0)->KernelRoutine=@$t2;

8 ?? ((nt!_KAPC*)@$t0)->Inserted=1;

9 r @$t3=@@(&(((nt!_ETHREAD*)@$t1)->Tcb.ApcState.ApcListHead[0]));

10 r @$t4=@@(&(((nt!_KAPC*)@$t0)->ApcListEntry));

11 r @$t5=@@(((nt!_LIST_ENTRY*)@$t3)->Flink);

12 ?? ((nt!_LIST_ENTRY*)@$t4)->Flink=@$t5;

13 ?? ((nt!_LIST_ENTRY*)@$t4)->Blink=@$t3;

14 ?? ((nt!_LIST_ENTRY*)@$t5)->Blink=@$t4;

15 ?? ((nt!_LIST_ENTRY*)@$t3)->Flink=@$t4;

16 ?? ((nt!_ETHREAD*)@$t1)->Tcb.ApcState.KernelApcPending=1;


變數“t1”的值是前面查詢到的 QQ 核心執行緒的 nt!_ETHREAD 結構;


變數“t3”用來定位到 nt!_ETHREAD 結構中的第一個 APC 佇列頭部(Tcb.ApcState.ApcListHead[0]);這個佇列頭部的“Flink”欄位(指向下一個 nt!_KAPC 結構)由變數 “t5” 儲存;


變數 “t4” 亦即我們構建的 nt!_KAPC 結構中的“ApcListEntry”欄位,它被用來初始化 “t5”;


這種初始化邏輯類似於下面的 C 程式碼:

1  KTHREAD.ApcState.ApcListHead[0]->Flink = KAPC->ApcListEntry;


驗證我們的操作是否正確:


驅動除錯——挫敗 QQ.EXE 的核心模式保護機制(part I)


整個過程的形象圖示:


驅動除錯——挫敗 QQ.EXE 的核心模式保護機制(part I)


為了理解 APC 交付的機制,我們在回撥函式入口處設定一個斷點,然後就能夠通過棧回溯資訊得知該回撥是如何被呼叫的,按下 “g” 鍵恢復目標機器的執行,等待 APC 交付時觸發斷點:


驅動除錯——挫敗 QQ.EXE 的核心模式保護機制(part I)


從上圖可以看到,這種 APC 交付機制其實並不神祕—— 傳遞給PspSystemThreadStartup() 的首個引數就是 QQFrmMgr.sys 建立的 QQ 核心執行緒的啟動地址,表明它被排程執行了;


經過一系列呼叫後,KiSwapThread() 從它接收到的首個引數(0x8b9ccbc0,亦即 QQ 核心執行緒的 nt!_ETHREAD 結構地址)中,定位到其 APC 佇列頭部,然後呼叫連結串列中第一個 nt!_KAPC結構的 “KernelRoutine” 回撥,從而觸發我們先前設定的斷點。


按下 “g” 鍵繼續執行,導致 PspExitThread() 把 QQ 核心執行緒終止掉然後返回,現在通過Process Explorer 瀏覽目標機器上,System 程式中的系統執行緒們,已經找不到QQFrmMgr.sys+0x5e34 那個執行緒了,另一方面,也可以在除錯機器上驗證:


1 r @$t0=@@(#FIELD_OFFSET(nt!_EPROCESS, ThreadListHead));

2 r @$t1= @@(#FIELD_OFFSET(nt!_ETHREAD, ThreadListEntry));

3 r @$t2=@@(#FIELD_OFFSET(nt!_ETHREAD, StartAddress));

4 !list "-t nt!_LIST_ENTRY.FLink -e -x \"r @$t3=@$extret-@$t1;

5 r @$t4= @$t3+@$t2;

6 r @$t5=poi(@$t4);

7 .if(@@((unsigned long)@$t5>(unsigned long)0x82000000 && (unsigned long)@$t5<(unsigned long)0x82017b00)){r @$t3;dt -b nt!_ETHREAD Cid. ExitStatus @$t3; dt -b nt!_KTHREAD Header. @$t3; };

8 \" 8565e5c0+@$t0"


驅動除錯——挫敗 QQ.EXE 的核心模式保護機制(part I)


小結


本篇討論瞭如何利用核心提供的基礎設施——APC——來挫敗 QQ 過濾驅動向核心空間注入的可執行程式碼,並在基於 Windows 8.1(NT 6.3 版核心)的真實機器上成功實踐,限於篇幅,後續將介紹如何檢測並還原 QQ 驅動修改的其它核心資料結構,以及清除它安裝的鉤子例程!


原文連結:[原創]---驅動除錯--挫敗 QQ.EXE 的核心模式保護機制(part I)---


本文由看雪論壇 shayi  原創


轉載請註明來自看雪社群

相關文章