————————————————————————————————————————————————————————————————————————編程
QQ 是一款熱門的即時通訊(IM)類工具,在安裝時刻會向系統分區的 \..\windows\system32\drivers 路徑下生成兩個驅動程序文件:windows
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 ,以下圖所示:異步
儘管這兩個驅動並不是 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 封裝的符號包大小約爲5、六百 MB)所在路徑;
(注意,宿主機上內核版本的不一樣致使須要分別下載對應的符號文件,並指定爲調試參數)
❺ 指定經過以太網調試(net),宿主機上開啓調試端口爲 UDP 的 60111;
❻ 最後的 key 能夠任意指定,但其中的 4 個子域之間須要用點號分隔開。
關於目標機上的對應配置,請各位參見 MSDN 文檔,這裏就再也不贅述。
——————————————————————————————————————————————————————————
首先在 Windows 8.1 目標機上經過 Process Explorer 瀏覽到 System 進程中的系統線程,其中有一個 QQ 內核線程是由
QQFrmMgr.sys 建立的,該線程的啓動地址距離所屬模塊被載入基址的偏移量爲 0x5e34 :
咱們的目標是結束該線程的執行,一般的作法是用系統內置的 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 模塊空間內的線程:
相應的 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"
第三步:手工構建一個 nt!_KAPC 結構,指定回調函數、關聯線程、以及排入隊列:
(3-1):查詢 QQFrmMgr.sys 模塊內部的 section 信息,注意到其中的 .data section 後緊接 INIT section:
從上圖可知,.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 結構;
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;
驗證咱們的操做是否正確:
整個過程的形象圖示:
爲了理解 APC 交付的機制,咱們在回調函數入口處設置一個斷點,而後就可以經過棧回溯信息得知該回調是如何被調用的,按
下「g」鍵恢復目標機器的執行,等待 APC 交付時觸發斷點:
從上圖能夠看到,這種 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"
——————————————————————————————————————————————————————————————————————————————————
小結:本篇討論瞭如何利用內核提供的基礎設施——APC——來挫敗 QQ 過濾驅動向內核空間注入的可執行代碼,並在基於 Windows 8.1(NT 6.3 版內
核)的真實機器上成功實踐,限於篇幅,後續博文將介紹如何檢測並還原 QQ 驅動修改的其它內核數據結構,以及清除它安裝的鉤子例程!
——————————————————————————————————————————————————————————————————————————————————