1. 程式人生 > >學會使用windbg定位程式bug

學會使用windbg定位程式bug

0x1工作環境
系統:win7 32bit sp1
Windbg: 6.12.0002.633 x86
測試程式:通過com介面獲得系統計劃任務

0x2被除錯程式說明
被除錯程式是一個通過com介面獲取windows的計劃任務列表的程式。
崩潰時的提示資訊
 

0x3啟動winDbg開始除錯
個人喜歡使用bat指令碼檔案來開啟windbg除錯程式。
我的指令碼檔案內容如下
start """C:\Program Files\Debugging Tools for Windows (x86)\windbg.exe""C:\Users\ky1-2\Desktop\MyTest\Bin\Debug\GetTaskScheduler.exe"
C:\Program Files\Debugging Tools forWindows (x86)\windbg.exe 是windbg的目錄


C:\Users\ky1-2\Desktop\MyTest\Bin\Debug\GetTaskScheduler.exe是被除錯的程式。
雙擊bat指令碼,啟動windbg除錯GetTaskScheduler.exe
 
F5執行。執行到崩潰。
 
錯誤資訊提示為code c0000005地址訪問失敗。這是第一次丟擲的異常。
此時該此異常還未任何的處理。改異常出現的位置在73700cc3處。
該指令是將一個內容賦值給73700c98。也就是說,在執行73700cc3指令時,向73700c98地址寫入資料時失敗了。
接下來使用!address 命令檢視該地址所在記憶體頁的屬性資訊
 
Allocation Base:  動態分配記憶體單元的始地址。

Base Address:  是基地址,例如,裝入一個模組,從某地址開始存放,而這個始地址就是BaseAddress。
Allocation Base 和Base Address的區別主要在於前者用於模組,可以看成 程式塊始地址;後者用於資料,可以看成資料塊始地址。
Protect: 記憶體頁的屬性資訊。PAGE_EXECUTE_READ是可讀可執行。
通過上面的資訊可以看到73700c98的屬性是可執行、可讀。沒有寫入的屬性。
這時就要通過堆疊檢視呼叫資訊,找到最近的一次非系統模組的呼叫。
這樣做的原因是,系統模組崩潰的可能性比較小(除非系統模組存在漏洞),而且如果系統模組崩潰大多數原因在於使用者呼叫時出現了問題。

所以這裡選擇檢視最近的一次非系統模組的呼叫函式的資訊。
補充:與記憶體相關的指令
.dvalloc申請虛擬記憶體
.dvfree釋放虛擬記憶體
.writemem寫記憶體到檔案
.readmem讀檔案到記憶體
!address 記憶體資訊檢視
!vprot   看虛擬記憶體保護屬性
接下來,通過kn命令檢視呼叫堆疊資訊
 
先來介紹一下kn命令顯示資訊的含義。
kn命令顯示四列:
#           :代表呼叫堆疊中函式的編號;
ChildEBP:代表當前函式的棧底指標,這裡的Child意思是相對上一個呼叫函式而言的;
RetAddr :代表該函式返回到父函式時的地址;
第四列   :當前函式下一次要執行的地址對應的模組資訊。
例如上圖第6行。
03表示在呼叫棧的編號是03;
001df2a4 是編號為03的函式的棧底即EnumTaskFolder函式的EBP指標(之前寫錯了)
013f481e 是編號為04的函式的地址即編號為03的函式執行完後返回到父函式的013f481e地址;
GetTaskScheduler!EnumTaskFolder+0x4d7意思是當前函式下一步要執行的指令地址013f4b07(就是上一行中的RetAddr的地址013f4b07)位於GetTaskScheduler模組的EnumTaskFolder函式偏移0x4d7個位元組處。
好了,瞭解了這些,下面開始分析呼叫棧。
通過上圖的呼叫堆疊定位到最近的一次非系統模組是GetTaskScheduler!FillTaskSchedulerInfoHigh函式。
FillTaskSchedulerInfoHigh函式在0x013f41a4地址指令的上一條指令呼叫了IComHandlerAction的虛擬函式。
所以下一步就要進入 FillTaskSchedulerInfoHigh函式檢視0x013f41a4之前的反彙編程式碼以及FillTaskSchedulerInfoHigh函式裡用到的變數資訊。
補充:
kP命令可以在呼叫堆疊中顯示函式的引數傳遞情況。且引數自動換行對齊。如下圖:
 
使用.frame 2命令切換到FillTaskSchedulerInfoHigh函式。
使用ub 013f41a4檢視呼叫taskschd!ATL::CComObject<CustomTaskImpl<IComHandlerAction,5>>::`scalar deleting destructor'函式的程式碼資訊
使用dv /i /V命令,檢視當前函式用到的變數資訊。
 
通過反彙編程式碼可以看出在Call ecx指令之前訪問了ebp-0x50處的地址,而ebp-0x50正式是pExecAction的地址。
所以Call ecx指令與pExecAction有莫大關係。
接下來分析pExecAction這個區域性變數。
使用dt 命令檢視變數詳細資訊
 
pExecAction的型別是IExecAction*指標。該物件只有一個虛擬函式表。下面來分析該物件的虛擬函式表
 
013f419f 8b4a38          mov     ecx,dword ptr [edx+38h]
[edx+38h]是0x736f2b30。即虛擬函式表的第15(38h/4h+1)個函式。
現在虛擬表的第15個函式是IComHandlerAction類的成員函式。
而IExecAction的第15個虛擬函式是什麼?
還有IExecAction與IComHandlerAction到底有什麼關係哪?
補充1:
如果dt命令使用不了怎麼判斷pExecAction是一個類物件哪?
013f4196 8b4db0   mov     ecx,dword ptr[ebp-50h] 把pExecAction賦值給ecx
013f4199 8b11      mov     edx,dword ptr[ecx] 把pExecAction指標的內容賦值給edx
....//沒有對edx的賦值操作
013f419f8b4a38     mov     ecx,dword ptr [edx+38h]通過edx+38h取址,可以推測pExecAction指向的地址可能是一個類物件或結構體物件。


Msdn一下:
 
 

 
 
通過msdn可知 IComHandlerAction和IExecAction都是IAction的子類
通過虛擬表可知第15個函式是put_Data後面的函式,所以推斷出IExecAction的第15個虛擬函式是get_WorkingDirectory即IExecAction的第5個函式.
補充:虛擬函式表的繼承關係 
1.   一般繼承(無虛擬函式覆蓋)我們可以看到下面幾點:
1)  虛擬函式按照其宣告順序放於表中。
2)  父類的虛擬函式在子類的虛擬函式前面。
2.   一般繼承(有虛擬函式覆蓋)
覆蓋父類的虛擬函式是很顯然的事情,不然,虛擬函式就變得毫無意義。下面,我們來看一下,如果子類中有虛擬函式過載了父類的虛擬函式,會是一個什麼樣子?假設,我們有下面這樣的一個繼承關係。
 
為了讓大家看到被繼承過後的效果,在這個類的設計中,我只覆蓋了父類的一個函式:f()。那麼,對於派生類的例項,其虛擬函式表會是下面的一個樣子:
 
我們從表中可以看到下面幾點,
1)  覆蓋的f()函式被放到了虛表中原來父類虛擬函式的位置。
2)  沒有被覆蓋的函式依舊。
3.   多重繼承(無虛擬函式覆蓋)下面,再讓我們來看看多重繼承中的情況,假設有下面這樣一個類的繼承關係。注意:子類並沒有覆蓋父類的函式。
 
對於子類例項中的虛擬函式表,是下面這個樣子:
 
我們可以看到:
1)  每個父類都有自己的虛表。
2)  子類的成員函式被放到了第一個父類的表中。(所謂的第一個父類是按照宣告順序來判斷的)這樣做就是為了解決不同的父類型別的指標指向同一個子類例項,而能夠呼叫到實際的函式。
4.   多重繼承(有虛擬函式覆蓋)下面我們再來看看,如果發生虛擬函式覆蓋的情況。下圖中,我們在子類中覆蓋了父類的f()函式。
    
下面是對於子類例項中的虛擬函式表的圖:
 

再進一步分析虛擬表的函式:
 
通過紅線標註的位置尤其是put_id, put_ClassId, put_Data幾個函式基本可以斷定,ebp-0x50處的地址的指標應該是IComHandlerAction型別的指標而不是IExecAction型別的指標。
現在找到了問題的真相,下面就要確定問題的原因了。猜測問題是程式將ebp-0x50處的指標強制轉成了IExecAction指標並訪問了IExecAction的成員函式get_WorkingDirectory。
接下來要驗證我們的想法。分析到這裡了應該可以看原始碼了。
0x4原始碼分析
 
可以查出Line192將pExecAction強制轉化成了父指標,且沒有判斷pExecAction的型別是不是IExecAction.所以,當pActions->get_Item()獲得到的IAction不是IExecAction時就會出問題或者崩潰了。
改進方法