調試實戰 | 通過轉儲文件分析程序無響應之使用 windbg + IDA 逆向篇



緣起

最近,接連在項目中遇到了兩個界面無響應的問題。都只發生在客戶特定機器上,不方便直接調試,只能抓取 dump 進行事後分析了。

抓取 dump

遠程連上可以重現問題的機器,使用 process explorer 初步觀察卡死的進程,發現 CPU 佔用率很低,經過一段時間的觀察,基本確定是一個死鎖問題。在卡死的進程上右鍵,保存完整轉儲,壓縮,發回本地進行分析。

使用 windbg 進行分析

雙擊抓取的 dump 文件,因爲之前已經執行過 windbg.exe -IA,所以默認會通過 windbg 打開 dump 文件。先使用 ~*kvn 粗略瀏覽一下每個線程的調用棧(因爲比較長,這裏就不截圖了)。經過觀察,很快鎖定了兩個值得進一步查看的線程:一個是主線程(界面線程),因爲是界面無響應,肯定要關注界面線程。另外一個是 7 號工作線程。分別看一下這兩個線程的調用棧。

主線程的調用棧如下圖所示:

注意上圖紅色高亮部分,主線程通過 SleepConditionVariableCS() 進入等待。

看完主線程,再看 7 號工作線程的調用棧,如下圖所示:

7 號線程對 SendMessage() 的調用非常值得懷疑。

猜測整個流程是這樣的:主線程不知由於什麼原因進入等待狀態,而工作線程由於各種各樣的原因也進入了等待狀態。其中 7 號線程最明顯,因爲它正在發消息,而主線程此時是無論如何也不會響應這個消息的。

於是,典型的死鎖再一次發生了。


加載 AssemblyDesign_Tools.dll 的符號後,查看對應的源碼,消除對 SendMessage() 的調用,問題解決!so easy!

說明:工作線程中並沒有直接調用 SendMessage(),而是調用了操作界面的相關 API,間接調用了 SendMessage()  給界面線程發消息。

死鎖的問題解決了,但是爲什麼向主線程發個消息就死鎖了呢?秉着打破砂鍋問到底的原則,我又開始折騰了。下面的內容適合喜歡調試逆向的極客閱讀。

深入調查

最開始的思路是:查看主線程在等待的條件變量,然後再調查哪個工作線程會喚醒這個條件變量。奈何 64 位下,前四個參數通過寄存器 RCX, RDX, r8, r9 進行傳遞,如果這些寄存器沒有在棧上存儲一份的話,很難查看具體的值。折騰一番後,確實沒找到有用的信息,而且就算找到了,也很難找出是哪個線程會執行喚醒操作。

這個死鎖問題不像關鍵段死鎖解決起來那麼直接。不能直接通過命令(!cs -l),或者查看調用棧就能直接理出頭緒。看來只能硬着頭皮逆向分析相關代碼了。

0 號線程和 7 號線程最值得懷疑,其它線程基本可以排除。先看看主線程爲什麼會等待吧。

主線程邏輯

找到調用 BentleyG!Bentley::BeConditionVariable::WaitOnCondition()  的地方,也就是 5 號棧幀。

IDA 中打開 MobileDgn.dll,並找到這個函數,然後按下神奇的 F5

可以看到,主線程在陷入等待之前,向工作線程發送了一個任務,也就是 sub_7FEDAC749A0 ,傳遞的參數是 v5v5 偏移 88 的位置保存了 BeConditonVariable 類型的變量,也就是 WaitOnCondition() 所等待的變量。 猜測: sub_7FEDAC749A0 內部會喚醒這個 BeConditionVariable , 如果 sub_7FEDAC749A0 被順利執行,那麼主線程的等待自然就結束了。

先看看 sub_7FEDAC749A0 的反彙編代碼,當然是直接看 F5 後的僞代碼了。

可以很明顯的看到 sub_7FEDAC749A0 內部調用了 Bentley::BeConditionVariable::Wake((CMFCRibbonInfo::XElementButtonUndo *)((char *)v1 + 88), 1);

從函數名就可以猜到是用來喚醒 BeConditionVariable 的。

如此看來,sub_7FEDAC749A0 很有可能還沒有被執行,工作線程就掛起了。一起來看看 SendToWorkThread 是怎麼把任務發送到工作線程的。

SendToWorkThread()  會先判斷是否在工作線程運行,如果是則直接執行對應的函數,否則就根據參數生成一個 RpcMessage ,然後發送這個新生成的 RpcMessage 到工作線程的任務隊列中。

再來看一下生成 RpcMessage 的函數,我把這個函數命名爲 MakeMobileDgnRPCMessage()

一定要記住這裏的關鍵信息,後面會根據這裏的關鍵信息驗證。 HandlePaint() 傳過來的函數地址是 0x7FEDAC749A0 ,保存在了 rpcMsg 偏移 128 的位置。 MobileDgnRPCGenericMessage 類的虛表地址是 000007FEDAD19658

繼續追蹤 SendAsynToWorkThread(),如下圖:

繼續追蹤 HandleRpcMessage() (這個名字是我命名的,不是 IDA 識別的),傳給它的第一個參數是一個全局變量(姑且命名爲 g_taskQueueManager ),第二個參數是要發送的 rpcMsg ,第三個參數 v3 的值是 2 ,第四個參數是 1

這個函數比較長,我只逆了個大概,大體思路是先檢查是否存在,不存在則插入。如果隊列已經滿了,需要等待工作線程從隊列中取走一些任務才返回。

至此,基本理清了主線程相關的邏輯。 大體是這樣的: 主線程在處理 HandlePaint() 的時候,先發送一個任務給工作線程,(通過 SendToWorkThread() 發送到工作線程的任務隊列中),然後通過 BeConditionVariable::WaitOnCondition() 等待這個任務結束。

看完界面線程,再來再看 7 號工作線程的相關邏輯。

工作線程邏輯

7 號工作線程,只需要關心 9 號棧幀對應的函數。

注意, _RunThread+0x1a3c ,這個偏移有點大。 由於缺少符號,這裏很有可能只是以 _RunThread 作爲參照得到的一個偏移,實際對應的是另外一個函數的代碼。 使用 ub 向前查看反彙編,很快定位到正確的函數首地址。 從上圖可知, 9 號棧幀對應的函數起始地址是 000007fe dac5fee0 。 怎麼在 IDA 中找到這個函數呢? 如果知道這個函數相對於其所在模塊的偏移,就可以算出在 IDA 中的地址了。 該怎麼獲取這個地址對應的模塊基址呢? 在 windbg 中執行 lma address 就可以知道一個地址對應的模塊信息了。 得到模塊基址( 0x000007fe dac10000 )後,可以很簡單的計算出偏移量爲 0x4fee0 。 有了這些信息就可以在 IDA 中找到這個函數了。

小貼士:也可以在 IDA 中調整模塊基址,使其與 windbg 保持一致。這樣就不用根據偏移在 IDA 中手動計算地址了。

得到要查看的函數地址後(我在 IDA 中執行了 Rebase program,所以是 000007fe dac10000),在 IDA 中直接按快捷鍵 g,輸入地址後即可跳轉到輸入的地址處。

注意:如果在 IDA 中以 16 進制輸入地址,請加上 0x 前綴,而且不要帶重音連接符。

再次按下神奇的 F5

至此,工作線程的邏輯也理清了。 簡單總結如下: 工作線程是一個循環,不斷從任務隊列取任務執行,如果設置了喚醒標記位,那麼需要在執行完任務函數後,喚醒等待的線程。

驗證猜想

好了,花費了這麼多精力終於理清了主線程和工作線程的交互邏輯。目的只有一個,就是爲了更好的驗證之前的猜想:工作線程還沒有來得及執行主線程過來的任務就掛起了。

如果猜測是正確的,那麼工作線程的任務隊列中應該還保留着這個未執行的任務。接下來的任務就是來找到這個未執行的任務。

通過上面對主線程和工作線程的分析,工作線程的任務隊列中應該有類型爲 MobileDgnRPCGenericMessage 的對象,並且該對象偏移 128 的位置的值爲 0x7FEDAC749A0。根據這兩條關鍵信息在內存中搜尋一下符合條件的記錄。

windbg 中輸入命令 s -q 0 L?fffffffffffffff 000007FEDAD19658,根據虛表地址搜尋 MobileDgnRPCGenericMessage 類型的對象。找到了兩條符合條件的記錄。

再輸入 s -q 0 L?fffffffffffffff 7FEDAC749A0 根據 sub_7FEDAC749A0 的地址搜尋包含這個地址的對象。找到四條符合條件的記錄。

我們關心的是這兩次搜尋結果相差 128 的記錄(因爲根據之前的分析,workProc 存儲在偏移爲 128 的位置)。經過肉眼觀察及在 windbg 中計算(? 00000000300b4ea0 - 00000000300b4f20),得到了一個符合條件的對象的地址 00000000 300b4ea0。整個過程如下圖:

找到了滿足上面條件的對象地址,還需要確認這個對象是否在工作線程的任務隊列中。 工作線程的任務隊列由全局變量 g_taskQueueManager 管理,該變量是一個指針,指針所在的地址是 000007FEDADAA380 ,指針的值是 00000000024d9fa0

根據之前的分析猜測,偏移爲 8 的位置記錄了任務隊列的開始位置,偏移 16 的位置記錄了任務隊列的結束位置,偏移 24 的位置記錄了任務隊列緩衝區結尾的位置,這個任務隊列很有可能是通過 vector 管理的。在 windbg 中查看 g_taskQueueManager 的內容。

查看任務隊列起始位置保存的記錄信息,輸入 dq 0000000037e33ce0 ,然後與 s -q 0 L?fffffffffffffff 00000000300b4ea0 得到的搜索記錄對比,發現有一條是吻合的。 看來,主線程發送給工作線程的任務確實還沒有被執行,工作線程就掛起了。

總結

這個偶發的掛起 bug 終於算是解決了。整個過程多虧了強大的 IDA 的強力支持。使整個分析過程簡單了 N 倍。最後,對整個分析過程中用到的技術點做一個簡單的總結:

  • IDAF5 真香。

  • 可以在 IDA 中通過 Rebase program 調整模塊基址。

  • IDA 中按 g 可以跳轉到輸入的地址。

  • IDA 中的地址需要有 0x 前綴,不要包含 windbg64 位地址的地址連接符。

  • 可以在 windbg 中通過 lm a address 得到一個地址對應的模塊信息。

  • windbg 中可以通過 ub 進行反向反彙編。

  • windbg 中可以根據虛表地址搜尋對應類型的變量在內存中的位置。

  • 在工作線程調用界面相關的 API 時,是通過給界面線程發消息的方式實現的。

參考資料

  • SleepConditionVariableCS msdn[1]

References:

[1]

SleepConditionVariableCS msdn: https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-sleepconditionvariablecs

感謝你的分享,點贊和在看

歡迎留言交流!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章