Windbg新手入坑指南
文章目錄
- 前言
- 熟悉理論 提高調試效率
- 定制自己的Windbg界面
- 關于工作空間
- 命令概述
- 標準命令
- 元命令
- 擴展命令
- 調試技巧
- 偽寄存器
- 開始實戰
- 控制調試目標
- 單步步入和單步步過
- 單步執行到指定地址
- 單步執行到下一個函數調用
- 單步執行到下一個分支
- 繼續運行
- 追蹤并監視
- 停止調試
- 總結
- 使用斷點
- 軟件斷點
- 硬件斷點
- 條件斷點
- 地址表達式
- 管理斷點
- 總結
- 觀察棧
- 顯示棧回溯
- 總結
- 分析內存
- 顯示內存
- 顯示字符串
- 顯示數據類型
- 搜索內存
- 修改內存
- 其他命令
- 參考資料
前言
這篇文章是我學習windbg的一個筆記和總結,通過和OD的功能來對比學習windbg的一些理論和命令,達到能調試一個exe或者sys文件的目的。大神請飄過~
熟悉理論 提高調試效率
在開始調試之前,了解以下理論知識可以幫助你大大提高調試效率
定制自己的Windbg界面
windbg默認打開就只有一個Command窗口,但是這樣調試效率很低,所以可以先設置好自己的用戶界面,下面是我的Windbg界面,因為用慣了OD,所以這個完全就是仿制的OD的界面,
所有窗口都可以狀態欄里調出來
設置完成之后,你可以保存到工作空間,這樣下次再次打開時這個界面就會保留
關于工作空間
工作空間保存有斷點 用戶定義的別名 調試器的設置 圖形界面信息 調試會話狀態等等信息,類似VS的項目文件,PS的工作區。
命令概述
WinDBG主要是以命令方式工作的,WinDBG共支持三類命令:標準命令、元命令和擴展命令
標準命令
標準命令通常是一兩個字符(version除外)或者符號,用來提供適用于各種調試目標的最基本調試功能。標準命令是不分大小寫的。比如:
- g 運行
- t 單步步入
- p 單步步過
- r 查看和修改寄存器
元命令
元命令用來提供標準命令沒有提供的調試功能,與標準命令一樣,元命令也是內建在調試器引擎或者WinDBG程序文件中的。
所有元命令都以一個點(.)開始,所以元命令也被稱為點命令,例如:
- .reload 重新載入符號
- .reboot 重啟目標機器
- .restart 重啟調試器
- .logfile 顯示信息
擴展命令
擴展命令用于擴展某一方面的調試功能。與標準命令和元命令是內建在WinDBG程序文件中不同,擴展命令是實現在動態加載的擴展模塊(DLL)文件中的。
所有的擴展命令都以!開頭
通過WinDBG的SDK,用戶可以編寫自己的擴展模塊和擴展命令,例如漏洞測試常用的一個mona插件
- !mona
調試技巧
在開始調試之前,有幾個要點,記住以下幾個點對于調試事半功倍
- 直接按回車可以執行上一條命令
- 使用分分號作為分隔符,可以在同一行輸入多條命令
- 按上下方向鍵可以瀏覽和選擇以前輸入過的命令
- 當命令提示符顯示為BUSY時,即使命令編輯框可以輸入命令,但是這個命令也不會被馬上執行,要等WinDBG恢復到空閑狀態才能執行
- 使用Ctrl+Break 來終止一個長時間未完成的命令。如果使用KD或則CDB,那么用Ctrl+C
- 選擇菜單->Edit_>Write Window Text to File可以把之前敲過的所有命令記錄到文件
偽寄存器
WinDBG自動定義了很多偽寄存器。在命令行和命令文件中都可以使用偽寄存器。WinDBG會自動將其替換(展開)為合適的值。例如下面這個@$scopeip就是一個偽寄存器,它代表當前的eip指針
下表列出了windbg所定義的部分寄存器(字典型知識,需要時查閱即可)
| $ea | 調試目標所執行上一條指令的有效地址 |
| $ea2 | 調試目標所執行上一條指令的第二個有效地址 |
| $exp | 表達式評估器所評估的上一條表達式 |
| $ra | 當前函數的返回地址 |
| $eip | 指令指針寄存器 |
| $eventip | 當前調試事件發生時的指令指針 |
| $previp | 上一事件的指令指針 |
| $relip | 與當前事件關聯的指令指針 |
| $scopeip | 當前上下文的指令指針 |
| $exentry | 當前進程的入口地址 |
| $retreg | 首要的函數返回值寄存器 |
| $retreg64 | 64位格式的首要函數返回寄存器 |
| $csp | 棧頂指針ESP |
| $p | 上一個內存顯示命令所打印的第一個值 |
| $proc | 當前進程EPROCESS結構的指針 |
| $thread | 當前線程ETHREAD結構的指針 |
| $peb | 當前進程的進程環境塊(PEB)的地址 |
| $teb | 當前線程的線程環境塊(TEB)地址 |
| $tpid | 擁有當前線程的進程ID(PID) |
| $tid | 當前線程的線程ID |
| $bpx | X號斷點的地址 |
| $frame | 當前棧幀的序號 |
| $dbgtime | 當前時間 |
| $callret | 使用.call命令調用的上一個函數的返回值 |
| $ptrsize | 調試目標所在系統的指針類型寬度 |
| $pagesize | 調試目標所在的系統的內存頁字節數 |
開始實戰
控制調試目標
控制調試目標是調試器的一個核心任務。其宗旨就是使調試目標始終處于調試器的控制之下,讓調試人員可以可以隨心所欲的控制程序的執行狀態。在OD可以通過圖形界面和各種快捷鍵隨心所欲地控制程序,相比windbg就沒那么方便了。但是WinDBG提供了強大的機制和豐富的命令來控制調試目標,這些命令要比OD的功能豐富的多。
單步步入和單步步過
- t 單步步入
- p 單步步過
命令格式如下:
p|t [r] [=StartAddress] [count] ["Command"]
- r表示禁止顯示寄存器內容
- 默認情況下,調試器總是讓目標從當前位置開始單步執行,但是也可以通過等號(=)來指定一個新的起始地址,讓程序從這個地址開始單步
- count用來指定單步執行的次數
- Command用來指定每次單步執行后要執行的命令
例如:
首先查看一下當前的反匯編,我想從77e40d8e這個位置開始執行單步 單步兩次 不顯示寄存器 單步執行完成之后顯示調用堆棧,就可以執行這么一條命令,執行完成之后如圖:
tr =77e40d8e 2 "kb"
單步執行到指定地址
WinDBG提供了pa和ta命令用來執行到指定的代碼地址,其命令格式為:
pa|ta [r] [=StartAddress] StopAddress
- pa是Step To Address的縮寫,即單步執行到StopAddress參數所代表的地址處
- pa和ta的區別在于 ta在遇到函數時會進入函數,windbg的執行結果會顯示函數內容;而pa則是直接步過函數,windbg的執行結果不顯示函數內容
例如:
查看一下當前的反匯編
如果想直接單步到77e9f137的位置,就可以輸入下面這條命令,執行結果如下
par 77e9f137
單步執行到下一個函數調用
與pa和ta命令類似,pc和tc命令用來單步執行到下一個函數調用指令(call)
pc|tc [r] [=StratAddress] [Count]
- pc或tc命令都是讓調試目標從當前地址或者StartAddress指定的地址恢復執行,直到遇到函數調用時停下來
- Count用來指定遇到的函數調用指令個數
- 這兩個的差別依然是在進入和不進入函數時windbg顯示的結果上有區別
例如:
首先來查看一下當前的反匯編
如果想直接單步到77e9f137這個位置的call,就可以直接用下面一條命令
pcr
單步執行到下一個分支
CPU有分支的監視和記錄功能,利用這一功能可以實現單步執行到分支,但是這個命令有一個缺陷就是不能在x86的用戶態模式下使用,命令格式如下:
tb [r] [=StartAddress] [Count]
繼續運行
g(go)命令的一般形式為:
g[a][=StartAddress] [BreakAddress][;BreakCommands]
- 其實StartAddress用來指定開始執行的起始地址 這個功能有點像OD的此處為新的EIP
- BreakAddress用來指定一個斷點地址
- BreakCommands用來指定斷點命中后所指定命令
- 如果不帶任何參數,那么g命令就是恢復目標運行 相當于OD的F9
- 可以用gu命令來執行到返回 相當于OD的Ctrl+F9
追蹤并監視
如果我們想了解一個函數的執行路徑和它調用了哪些其它函數,每個函數包含了多少條指令,但我們又不想一步步的跟蹤執行,那么可以使用wt命令讓它幫我跟蹤執行并生成一份報告給我
下面通過一個例子來解釋wt命令的用法,注意:wt命令必須在函數的起始位置處,也就是步入call之后的第一條指令時執行
首先單步步入函數。然后使用wt命令生成報告
可以把wt命令的結果分為六個部分
- 第一個部分是標題 顯示了追蹤的函數名和追蹤的結束地址
- 第二部分是詳細的執行情況 包括如下四列:
- 第一列為指令數,這一列的數字就是這個函數從入口進入到下一行所對應的函數入口所執行的指令
- 第二列用來顯示本行所對應的函數調用其他函數時所執行的總指令數
- 第三列表示函數的調用深度 沒進入一個函數深度加一
- 第四列為函數名稱 名稱前的縮進用來表示深度
停止調試
- q 停止調試
- .detach 分離調試器
區別在于停止調試時調試器和目標程序的偶停止運行,而分離調試器則是調試器顯示No Target,而目標程序繼續運行
總結
| p | Step | 單步步過 |
| t | Trace | 單步步入 |
| pa | Step to Address | 單步到指定地址 不進入子函數 |
| ta | Trace to Address | 追蹤到指定地址 進入子函數 |
| pc | Step to Next Call | 單步執行到下一個函數調用 |
| tc | Trace to Next Call | 追蹤執行到下一個函數調用 |
| tb | Trace to Next Branch | 追蹤到下一條分支指令 |
| g | Go | 恢復運行 |
| gu | Go Up | 執行到函數返回 |
| q | Quit | 停止調試 |
| .detach | detach | 分離調試器 |
使用斷點
軟件斷點
WinDBG設計了三條命令來設置軟件斷點,分別是bp、bu和bm。其中bp是基本的而且最常用的,其命令格式如下:
bp[ID] [Options] [Address [Passes]] ["Command String"]
- ID用來指定斷點編號 不指定默認從0開始編排
- Options用來指定選項
- Address用來指定斷點地址
- Passes用來指定經過斷點的次數 默認經過一次斷下
- CommandString用來指定設置斷點后執行的命令 用雙引號包圍命令,多個命令用分號分隔
bu命令用來設置一個延遲的以后再求解的斷點,用于對尚未加載模塊中的代碼設置斷點。當指定的模塊被加載時,WinDBG會真正落實這個斷點。所以bu命令對于調試動態加載模塊的入口函數或者初始化代碼特別有用
bm命令用來設置一批斷點,相當于幫我們自動執行很多次bp或者bu命令。比如以下命令對于msvcr80d模塊中的所有print開頭的函數設置斷點:
bm msvcr80d!print*
bm和bu是命令格式如下:
bu[ID] [Options] [Address [Passes]] ["Command String"] bm[Options] SymbolPattern [Passes] ["Command String"]其中的Options可以為以下內容
- /1如果指定此選項,那么這個斷點命中一次后便會便自動從斷點列表中刪除。這種斷點被稱為一次命中斷點
- /p這個開關只能用在內核調試中,/p后跟一個進程的EPROCESS結構,作用是只有當前進程是指定進程時才觸發這個斷點
- /t與/p開關類似,只能用在內核調試中,用來指定一個ETHREAD結構,作用是只有在執行指定的線程訪問斷點地址時才觸發斷點
- /c和/C這兩個開關后面可以帶一個數字,用來指定中斷給用戶的最大函數調用深度和最小函數調用深度。舉例來說,使用命令bp/c5msvcr80d!printf設置的斷點只有當函數調用深度淺于5時才中斷給用戶
硬件斷點
WinDBG的ba命令用來設置硬件斷點,其格式如下:
ba [ID] Access Size [Option] [Address[Passes]] ["Command String"]
-
ID用來指定斷點序號
-
Access用來指定觸發斷點的訪問方式 可以為以下幾個字母之一
- e 讀取和執行時觸發斷點
- r 讀取和寫入時觸發斷點
- w 寫入時觸發斷點
- i 有IO操作時觸發斷點
-
Size用來指定訪問的長度 x86系統可以為1 2 4三種值
-
Passes參數和CommandString參數的用法與設置軟件斷點命令中的一樣
例如:
ba r1 0x401000
那么對內存地址0041717c的一字節訪問、字訪問、雙字訪問(讀寫)都會觸發這個斷點
如果想要查看硬件斷點和狀態可以直接看寄存器窗口的DR0-DR7寄存器
條件斷點
對 沒有錯!windbg也支持條件斷點,但是這玩意有點復雜,而且實用性好像不大,反正我在OD里從來沒用過,直接PASS吧。另外windbg好像是沒有內存斷點的
地址表達式
可以使用以下三種方法來指定斷點命令中的地址參數
-
直接使用內存地址,比如bp 00411390
-
使用模塊名加函數符號的方式,比如bp dbgee!wmain代表對dbgee模塊中的wmain函數設置斷點。也可以在符號后增加一個地址偏移,比如bp dbgee!wmain+3
-
如果是使用完全的調試符號,調試符號中包含源代碼行信息,那么可以使用如下形式:
`[[Module!]Filename][:LineNumber]`
其中Module為模塊名,Filename為源程序文件名,LInenumber為行號。整個表達式要用兩個波浪號包起來``,要有調試符號才能實現,對于逆向來說沒什么用 這個也pass
-
對于C++的類方法,也可以使用類名雙冒號(::)或者雙下劃線(__)來連接類名和方法名,比如:
bp MyClass__MyMethod bp MyClass:MyMethod bp@@(MyClass::MyMethod)管理斷點
使用bl命令可以列出當前已經設置的所有斷點,例如:
-
第一列是斷點的序號
-
第二列是斷點的狀態
- e代表啟用(enable)
- d代表禁用(disable)
- 對于bu設置的斷點還可能有字母u,表示尚未解決(unresolved)
-
第三列是斷點的地址
-
第四列是斷點觸發剩余次數
-
第五列是斷點的初始計數
-
第六列是斷點所關聯的進程和線程,冒號前是進程號,冒號后是線程號
-
第七列是斷點地址的符號表示
命令bc、bd、be分別用來刪除、禁止和啟用斷點,它們的格式都是:
bc|bd|be 斷點號
其中斷點號可以使用*來通配所有斷點,使用-來表示一個范圍,或者使用逗號來指定多個斷點號。例如以下命令都是有效的:
bd 0-2,4 禁止0 1 2 4號斷點 be * 啟用所有斷點總結
最后來一個表格總結
| bp | 設置軟件斷點 |
| bu | 對未加載的模塊設置斷點 |
| bm | 批量設置斷點 |
| ba | 設置硬件斷點 |
| bl | 列出所有斷點 |
| bc | 刪除斷點 |
| bd | 禁止斷點 |
| be | 啟用斷點 |
觀察棧
顯示棧回溯
WinDBG的k系列命令就是用來幫助我們進行棧回溯的,先來看一個例子
- 其中的每一行代表棧上的一個棧幀 也就是一個函數
- 最上面一行表示的是當前正在執行的函數 每個函數下面一行是上一行的父函數
- 第一列是棧幀的基地址EBP
- 第二列是函數的返回地址
- 第三列是函數名以及執行位置
K命令顯示了函數名信息,但是沒有顯示每個函數的參數。命令kb可以顯示放在棧上的前三個參數,例如(L是不顯示源文件信息):
- 前兩列以及最后一列的內容與k命令結果是一樣的
- 中間三列是函數的參數 只顯示三個,如果要觀察第四個參數可以用 dd ebp+x014
- 這只是棧上的前三個參數,如果函數的調用約定為fastcall,那么前兩個參數還是在ecx和edx
其他K系列的命令:
- kp命令可以把參數和參數值都以函數原型格式顯示出來,但是需要有符號,對于逆向來說沒什么用
- kv命令可以在kb命令的基礎上增加顯示FPO信息和調用約定
- kn命令會在每行前顯示棧幀的序號
總結
| k | 顯示調用堆棧 |
| kb | 顯示調用堆棧和棧上的前三個參數 |
| kp | 參數和參數值都以函數原型格式顯示出來(必須有符號) |
| kv | kb命令的基礎上增加顯示FPO信息和調用約定 |
| kn | 命令會在每行前顯示棧幀的序號 |
分析內存
顯示內存
WinDBG的d系列命令用來顯示指定內存區域的數據內容。這些命令的格式為:
d{a|b|c|d|D|f|p|q|u|w|W} [Options] [Range] dy{b|d} [Options] [Range] d [Options] [Range]其中大括號中的字母(區分大小寫)用來指定數據的顯示方式,含義如下:
- a表示ASCII碼
- b表示字節和ASCII碼
- c表示DWORD和ASCII碼
- d表示DWORD
- D表示雙精度浮點數
- f表示單精度浮點數
- p表示按指針寬度顯示
- q表示四字(8字節)
- u表示UNICODE字符
- w表示字
- W表示字和ASCII碼
- yb表示二進制和字節
- yd表示二進制和雙字
Range參數用來指定要顯示的內存范圍。可以有以下幾種表示方法:
- 第一種方法是起始地址加空格加終止地址,比如dd 0012fd9c 0012fda8命令以雙字格式顯示從0012fd9c開始到0012fda8結束的16字節內存數據
- 第二種方法是起始地址加空格加L(或者1)和對象個數,比如上面的命令可以等價的寫為:dd 0012fd9c L4
- 第三種方式是結束地址加空格加L(或者1)加負號和對象個數。使用這種方式可以把上面的命令寫為:dd 0012fdac L-4
顯示字符串
可以以0結尾的簡單字符串,可以使用da或者du命令來顯它的內容,前者用于使用單字節字符集的字符串,后者用于采用UNICODE字符集的字符串。當遇到字符串末尾的0時,會自動停止顯示。例如:du 003a2e9c
顯示數據類型
WinDBG的dt命令用來顯示數據類型以及按照類型來顯示數據。Dt的含義是Dump symbolic Type information。Dt是個比較復雜的命令,下面我們按照用法分別來介紹
首先,可以使用dt來顯示一個數據類型(數據結構)。這種用法的典型格式是:
dt[模塊名!]類型名
其中模塊名部分可以省略,如果省略,那么調試器會自動搜索所有模塊。類型名即程序中定義數據結構或者通過typedef定義的類姓名。類型名中可以包含通配符,比如以下命令會列出NTDLL模塊中的所有類型:
dt ntdll!*
如果類型名是確定的類型,那么dt便會顯示這個類型的定義,如果類型中還包含子類型,那么可以用-b開關來遞歸式顯示所有子類型,也可以使用-r開關來指定顯示深度。-r0表示不顯示子類型,-r1表示顯示1級子類型,依此類推,例如:
dt -r1 _TEB
如果不想顯示整個結構,而只顯示某些字段,那么可以在類型名后使用-ny開關附加字段搜索選項,比如以下命令只顯示TEB結構的LastError開始的字段:
dt _TEB -ny LastError
Dt命令的第二種用法是在上一種方法的基礎上增加內存地址,讓dt 按照類型顯示指定地址的變量。例如,以下命令使用_PEB結構來顯示內存地址7ffdd000出的數據:
dt _PEB 7ffdd000
Dt命令的第三種用法是顯示類型的實例,包括全局變量、靜態變量和函數。比如以下命令顯示dbgee程序的g_szGlobal全局變量:
dt dbgee!*wmain*
搜索內存
S命令用于搜索內存,有三種使用方法。
-
第一種用法是在指定的內存范圍內搜索任何ASCⅡ字符或者UNICODE字符串,其格式如下:
- s-[[Flags]]sa|su Range
- Range用來指定內存范圍
- sa用來搜索ASCII字符串 su用來搜索unicode字符串
- Flag可以指定搜索選項
例如,以下命令搜索nt!PsInitialSystemProcess變量所指向地址開始的512個字節范圍內任何長度不小于5的ASCIⅡ字符串:
s-[l5]sa poi(nt!)PsInitialSystemProcessl200
-
第二種用法是在指定內存地址范圍內搜索與指定對象相同類型的對象,這里的對象是指包含虛擬函數表的使用面向對象語言(如C++)編寫的類(Class)對象。其格式為:
- s[[Flags]]v Range Object
-
S命令的第三種用法是在指定范圍內搜索某一內容模式,其語法格式為:
- s[-[[Flags]]Type] Range Pattern
- 其中類型表示搜索的內容的數據類型(寬度),可以為b(字節) d(雙字)
- Pattern參數用來指定要搜索的內容
如果你覺得上面一段話理解起來有點費勁,不妨直接看下面的例子
s 0012ff40 L20 'H' 'e' 'l' 'l' 'o' s 0012ff40 L20 48 65 6c 6c 6f s -a 0012ff40 L20 "Hello"它們都是等效的。意為在0012ff40到0012ff60之間搜索hello字符,-a參數指定以ACSII的方式搜索字符,類似的還有-u,它指定以UNICODE的方式搜索字符
修改內存
命令e用來修改指定內置地址或者區域的內容,
-
第一種是按字符串編輯,其命令格式為:
- e{a|u|za|zu}Address "String"
- 其中Address是要修改的內存的起始地址
- za代表以0結尾的ASCII字符 zu代表以0結尾的Unicode字符
- a和u分別代表不是以0結尾的ASCII和Unicode字符
-
第二種用法是以數值方式編輯,其格式為:
- e{b|d|D|f|p|q|w} Address [Values]
- 其中大括號中的字母用來表示要修改的數據類型,也決定要修改的內存方式
- Address用來指定要修改的內存起始地址
- Values用來指定新的值
例如:
Eb 00100000 01 02 03 04 數據類型為BYTE Ed 00100000 0201 0403 數據類型為DWORD Ea 00100000 ‘hello’ 數據類型為ASCII Eu 00100000 ‘你好’ 數據類型為UNICODE其他命令
下表記錄了我個人認為在調試時經常會用到的一些命令,記錄的不全,歡迎補充
| .attach | .attach PID 附加到指定ID的進程 |
| .restart | 重啟被調試應用 |
| .cls | 清理屏幕 |
| .formats | 顯示數字的各種格式信息 |
| lm | 顯示模塊 |
| .reboot | 重啟虛擬機 |
| r | 查看或修改寄存器 |
| u | 顯示匯編 |
| .help | 查看幫助 |
參考資料
《windbg用法詳解》
WinDbg命令三部曲
從Ollydbg說起-----WinDbg用戶態調試教程:https://bbs.pediy.com/thread-34379.htm
總結
以上是生活随笔為你收集整理的Windbg新手入坑指南的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 160个Crackme023
- 下一篇: 160个Crackme024之Opcod