———————————————————————————————————————————————————————————————————————— QQ 是一款熱門的即時通信(IM)類工具,在安裝時刻會向系統分區的 \..\windows\system32\drivers 路徑下生成兩個驅動程式文件: ...
————————————————————————————————————————————————————————————————————————
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 ,如下圖所示:
儘管這兩個驅動並非 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 :
我們的目標是結束該線程的執行,通常的做法是用系統內置的 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 驅動修改的其它內核數據結構,以及清除它安裝的鉤子常式!
——————————————————————————————————————————————————————————————————————————————————