GDB的那些奇淫技巧
gdb也用了好幾年了,雖然稱不上骨灰級玩家,但也有一些自己的經驗,因此分享出來給大家,順便也作為一個存檔記錄。
多進程調試
最近在調試一個漏洞的exploit時遇到一個問題。目標漏洞程序是一個 CGI 程序,由主進程調起,而且運行只有一瞬的時間;我的需求是想要在在該程序中下斷點,在內存布局之后可以調試我的 shellcode,該如何實現?當然目標程序是沒有符號的,而且我希望下的斷點是一個動態地址。在 lldb 中有--wait-for
,gdb 里卻沒有對應的命令,經過多次摸索,終於總結出一個比較完美的解決方案。
示例程序
這里構建一個簡單的示例來進行實際演示。首先是父進程:
|
子進程很簡單:
|
這里編譯子進程時候指定-no-pie
,並且strip
掉符號。我們的調試目標是斷點在子進程的strcpy
中,拓展來說是希望能斷點在子進程的任意地址上。
踩坑過程
通過搜索可以找到一個 stackoverflow 的回答: gdb break when entering child process。根據其說法,使用 set follow-fork-mode child
即可。這是一個 gdb 命令,其目的是告訴 gdb 在目標應用調用fork
之后接着調試子進程而不是父進程,因為在 Linux 中fork
系統調用成功會返回兩次,一次在父進程,一次在子進程。我們來試一下,直接斷點在 strcpy 符號中:
|
噢,斷點都打不上,理由很簡單,因為不同進程之間的虛擬地址空間都不一樣。
另外一個回答中說了,雖然不能斷在指定地址,但我們可以break main
,告訴 gdb 把斷點設置在 main 函數。不過我們的子進程是沒有符號的,所以break main
並沒有卵用。
現在已經有了讓 gdb 跟着子進程的方法,只不過問題是無法把斷點打到子進程上,因為子進程還沒有啟動,那么用硬件斷點可不可以?
|
可以是可以,但是斷點壓根沒有觸發,子進程直接拷貝溢出崩潰了都沒有停下來!所以硬件斷點在這里並沒有用。
那么把斷點設置在一些起始函數的上呢?根據之前對 ELF 以及動態鏈接的學習,我們可以斷在比如_start
或者__libc_start_main
上面:
|
實際上該斷點也不會觸發,因為這個地址是是父進程的地址空間。
不過到現在答案已經呼之欲出了,總結一下,gdb 支持:
- fork 之后跟蹤到子進程
- 可以設置軟斷點
- 子進程有
_start
符號
所以,就有了一個最終方案。
最終方案
我的最終方案如下:
|
首先告訴 gdb 跟蹤子進程;然后設置set breakpoint pending on
是為了在設置斷點時讓 gdb 不強制在對符號下斷點時就需要固定地址,這樣在b _start
時就會 pending 而不是報錯;最后再連接到父進程以及加載子進程的符號。
detach-on-fork on
是為了在 fork 之后斷開父進程,避免 gdb 退出時把父進程殺死,並不是這節的重點。
其中的時序非常重要。如果先 attach 父進程再下斷點,那么斷點會直接下到父進程空間從而不會觸發;如果先讀取了子進程的符號再下斷點,可能會下在一個錯誤的虛擬地址上。
這也是我用了很久的一個方法,不過后來我知道了有更官方的解決方式:
|
囧,……
Catch Point真是個好東西,支持很多有用的事件:
- 常規的C++異常事件
- 系統調用事件(可直接指定系統調用號)
- 動態庫的加載/卸載事件
- exec/fork/vfork
- …
看來文檔搜索能力還有待提高啊。……
多線程調試
在調試大型程序的時候,經常會遇到這么一個問題,即涉及到的線程很多,少則十幾個多則上百個線程。在這些線程之間穿梭也是一個常見的困難。
首先最基本的是線程的切換命令:
info threads
: 查看當前所有的線程thread n
: 切換到 id 為n
的線程中
對於進程也有類似的命令
info inferiors
/inferior n
,在調試多進程交互的程序時會經常用到。
其次,在對某個線程進行單步調試時,會遇到 CPU 的迷之調度,突然一個next
或者nexti
就跑到其他線程去了,這個時候有個特殊的參數scheduler-locking
可以解決這個問題:
|
通常設置為step
模式可解決單步調試的問題。
程序運行
我經常用到的一個功能是需要使用 gdb 執行某個程序,並且能精確控制程序的參數,包括命令行、標准輸入和環境變量等。gdb 的 run 命令就是用來執行程序的。
這里還是先寫個示例測試程序:
|
參數
最基本的,通過 run 命令控制命令行參數:
|
或者在運行前設置args
參數:
|
標准輸入
在漏洞挖掘或者 CTF 比賽中經常遇到的情況是某些輸入觸發了進程崩潰,因此要掛 gdb 進行分析,這時候就需要gdb 掛載的程序能夠以指定的標准輸入運行。如果標准輸入是文件,那很簡單:
|
但更多時候為了方便調試,希望能以其他程序的輸出來運行,比如:
|
可惜 gdb 不支持這種管道,不過可以通過下面的方法實現:
|
或者:
|
后者實際上是 shell 命令 here string
的一種形式。這兩種方式是有區別的,注意示例程序中 read 調用會提前返回,所以如果我們想要第一次讀取3個字符,第二次讀取4個字符的話,就不能一次性全部輸入。比如下面這樣就不符合預期了:
|
正確的方式應該是這樣:
|
值得注意的是,這種情況下,使用here string
是沒用的,因為該字符串是計算完再一次性傳給命令:
|
而且這里是8字節,因為末尾還帶了個回車。 所以我更偏向於使用第一種方式。
環境變量
對於運行程序而言,還有個重要的參數來源是環境變量,比如在調試 CGI 程序的時候。這在 gdb 中可以使用environment
參數,不過需要注意的是該參數的設置是以空格為切分而不是傳統的以=
對環境變量賦值。
|
還有要注意的是這個參數要求變量是uninterpreted strings
,也就是說只能指定可打印字符。如果我們要傳輸一個的 payload 或者 shellcode 還要用 gdb 調試怎么辦呢?我一般使用的方式是在調用 gdb 時指定,比如:
|
后記
對於二進制研究人員來說,gdb 是一個鋒利的好工具,支持X86、ARM、MIPS、RISCV、Xtensa等各種常用和不常用的系統架構,對其熟練使用有時候可以達到事半功倍的效果,在文末的附錄中我也列舉了一些比較常用的命令。由於 gdb 本身支持 python 接口,因此現實中使用通常結合一些拓展使用,比如:
- gef: https://github.com/hugsy/gef
- pwndbg: https://github.com/pwndbg/pwndbg
- peda: https://github.com/longld/peda
這幾個我都用過,各有千秋。現在工作中使用更多的是gef
,因為安裝太方便了,一個文件搞定。
上面這幾個拓展可能大家可能都不陌生,但還有另外一個我比較常用的是 gdb-dashboard,其功能更為簡單,而且使用的是 gdb 原本的信息,所以支持的指令集更多。比如下面的截圖就是我曾經用 gdb + OpenOCD 來調試 ESP32
固件的示例:

ESP32是比較少見的Xtensa
指令集架構,上面的拓展都不支持,不過 gdb 本身支持,因此配合使用的效果絕佳。
附錄: gdb命令表
gdb 還有其他一些小技巧,可以參考awesome-cheatsheets/tools/gdb.txt中的列表。該列表最初由韋神創建,我時不時也會添加一些上去。當然為了方便大家的查閱,這里直接給出匯總表格附錄:
啟動 GDB
命令 | 含義 | 備注 |
---|---|---|
gdb object |
正常啟動,加載可執行 | |
gdb object core |
對可執行 + core 文件進行調試 | |
gdb object pid |
對正在執行的進程進行調試 | |
gdb |
正常啟動,啟動后需要 file 命令手動加載 | |
gdb -tui |
啟用 gdb 的文本界面(或 ctrl-x ctrl-a 更換 CLI/TUI) |
幫助信息
命令 | 含義 | 備注 |
---|---|---|
help |
列出命令分類 | |
help running |
查看某個類別的幫助信息 | |
help run |
查看命令 run 的幫助 | |
help info |
列出查看程序運行狀態相關的命令 | |
help info line |
列出具體的一個運行狀態命令的幫助 | |
help show |
列出 GDB 狀態相關的命令 | |
help show commands |
列出 show 命令的幫助 |
斷點
命令 | 含義 | 備注 |
---|---|---|
break main |
對函數 main 設置一個斷點,可簡寫為 b main | |
break 101 |
對源代碼的行號設置斷點,可簡寫為 b 101 | |
break basic.c:101 |
對源代碼和行號設置斷點 | |
break basic.c:foo |
對源代碼和函數名設置斷點 | |
break *0x00400448 |
對內存地址 0x00400448 設置斷點 | |
info breakpoints |
列出當前的所有斷點信息,可簡寫為 info break | |
delete 1 |
按編號刪除一個斷點 | |
delete |
刪除所有斷點 | |
clear |
刪除在當前行的斷點 | |
clear function |
刪除函數斷點 | |
clear line |
刪除行號斷點 | |
clear basic.c:101 |
刪除文件名和行號的斷點 | |
clear basic.c:main |
刪除文件名和函數名的斷點 | |
clear *0x00400448 |
刪除內存地址的斷點 | |
disable 2 |
禁用某斷點,但是部刪除 | |
enable 2 |
允許某個之前被禁用的斷點,讓它生效 | |
rbreak {regexpr} |
匹配正則的函數前斷點,如 ex_* 將斷點 ex_ 開頭的函數 |
|
tbreak function/line |
臨時斷點 | |
hbreak function/line |
硬件斷點 | |
ignore {id} {count} |
忽略某斷點 N-1 次 | |
condition {id} {expr} |
條件斷點,只有在條件生效時才發生 | |
condition 2 i == 20 |
2號斷點只有在 i == 20 條件為真時才生效 | |
watch {expr} |
對變量設置監視點 | |
info watchpoints |
顯示所有觀察點 | |
catch exec |
斷點在exec事件,即子進程的入口地址 |
運行程序
命令 | 含義 | 備注 |
---|---|---|
run |
運行程序 | |
run {args} |
以某參數運行程序 | |
run < file |
以某文件為標准輸入運行程序 | |
run < <(cmd) |
以某命令的輸出作為標准輸入運行程序 | |
run <<< $(cmd) |
以某命令的輸出作為標准輸入運行程序 | Here-String |
set args {args} ... |
設置運行的參數 | |
show args |
顯示當前的運行參數 | |
cont |
繼續運行,可簡寫為 c | |
step |
單步進入,碰到函數會進去 | |
step {count} |
單步多少次 | |
next |
單步跳過,碰到函數不會進入 | |
next {count} |
單步多少次 | |
CTRL+C |
發送 SIGINT 信號,中止當前運行的程序 | |
attach {process-id} |
鏈接上當前正在運行的進程,開始調試 | |
detach |
斷開進程鏈接 | |
finish |
結束當前函數的運行 | |
until |
持續執行直到代碼行號大於當前行號(跳出循環) | |
until {line} |
持續執行直到執行到某行 | |
kill |
殺死當前運行的函數 |
棧幀
命令 | 含義 | 備注 |
---|---|---|
bt |
打印 backtrace | |
frame |
顯示當前運行的棧幀 | |
up |
向上移動棧幀(向着 main 函數) | |
down |
向下移動棧幀(遠離 main 函數) | |
info locals |
打印幀內的相關變量 | |
info args |
打印函數的參數 |
代碼瀏覽
命令 | 含義 | 備注 |
---|---|---|
list 101 |
顯示第 101 行周圍 10行代碼 | |
list 1,10 |
顯示 1 到 10 行代碼 | |
list main |
顯示函數周圍代碼 | |
list basic.c:main |
顯示另外一個源代碼文件的函數周圍代碼 | |
list - |
重復之前 10 行代碼 | |
list *0x22e4 |
顯示特定地址的代碼 | |
cd dir |
切換當前目錄 | |
pwd |
顯示當前目錄 | |
search {regexpr} |
向前進行正則搜索 | |
reverse-search {regexp} |
向后進行正則搜索 | |
dir {dirname} |
增加源代碼搜索路徑 | |
dir |
復位源代碼搜索路徑(清空) | |
show directories |
顯示源代碼路徑 |
瀏覽數據
命令 | 含義 | 備注 |
---|---|---|
print {expression} |
打印表達式,並且增加到打印歷史 | |
print /x {expression} |
十六進制輸出,print 可以簡寫為 p | |
print array[i]@count |
打印數組范圍 | |
print $ |
打印之前的變量 | |
print *$->next |
打印 list | |
print $1 |
輸出打印歷史里第一條 | |
print ::gx |
將變量可視范圍(scope)設置為全局 | |
print 'basic.c'::gx |
打印某源代碼里的全局變量,(gdb 4.6) | |
print /x &main |
打印函數地址 | |
x *0x11223344 |
顯示給定地址的內存數據 | |
x /nfu {address} |
打印內存數據,n是多少個,f是格式,u是單位大小 | |
x /10xb *0x11223344 |
按十六進制打印內存地址 0x11223344 處的十個字節 | |
x/x &gx |
按十六進制打印變量 gx,x和斜桿后參數可以連寫 | |
x/4wx &main |
按十六進制打印位於 main 函數開頭的四個 long | |
x/gf &gd1 |
打印 double 類型 | |
help x |
查看關於 x 命令的幫助 | |
info locals |
打印本地局部變量 | |
info functions {regexp} |
打印函數名稱 | |
info variables {regexp} |
打印全局變量名稱 | |
ptype name |
查看類型定義,比如 ptype FILE,查看 FILE 結構體定義 | |
whatis {expression} |
查看表達式的類型 | |
set var = {expression} |
變量賦值 | |
display {expression} |
在單步指令后查看某表達式的值 | |
undisplay |
刪除單步后對某些值的監控 | |
info display |
顯示監視的表達式 | |
show values |
查看記錄到打印歷史中的變量的值 (gdb 4.0) | |
info history |
查看打印歷史的幫助 (gdb 3.5) |
文件操作
命令 | 含義 | 備注 |
---|---|---|
file {object} |
加載新的可執行文件供調試 | |
file |
放棄可執行和符號表信息 | |
symbol-file {object} |
僅加載符號表 | |
exec-file {object} |
指定用於調試的可執行文件(非符號表) | |
core-file {core} |
加載 core 用於分析 |
信號控制
命令 | 含義 | 備注 |
---|---|---|
info signals |
打印信號設置 | |
handle {signo} {actions} |
設置信號的調試行為 | |
handle INT print |
信號發生時打印信息 | |
handle INT noprint |
信號發生時不打印信息 | |
handle INT stop |
信號發生時中止被調試程序 | |
handle INT nostop |
信號發生時不中止被調試程序 | |
handle INT pass |
調試器接獲信號,不讓程序知道 | |
handle INT nopass |
調試起不接獲信號 | |
signal signo |
繼續並將信號轉移給程序 | |
signal 0 |
繼續但不把信號給程序 |
線程調試
命令 | 含義 | 備注 |
---|---|---|
info threads |
查看當前線程和 id | |
thread {id} |
切換當前調試線程為指定 id 的線程 | |
break {line} thread all |
所有線程在指定行號處設置斷點 | |
thread apply {id..} cmd |
指定多個線程共同執行 gdb 命令 | |
thread apply all cmd |
所有線程共同執行 gdb 命令 | |
set schedule-locking ? |
調試一個線程時,其他線程是否執行 | |
set non-stop on/off |
調試一個線程時,其他線程是否運行 | |
set pagination on/off |
調試一個線程時,分頁是否停止 | |
set target-async on/off |
同步或者異步調試,是否等待線程中止的信息 |
進程調試
命令 | 含義 | 備注 |
---|---|---|
info inferiors |
查看當前進程和 id | |
inferior {id} |
切換某個進程 | |
kill inferior {id...} |
殺死某個進程 | |
set detach-on-fork on/off |
設置當進程調用fork時gdb是否同時調試父子進程 | |
set follow-fork-mode parent/child |
設置當進程調用fork時是否進入子進程 |
匯編調試
命令 | 含義 | 備注 |
---|---|---|
info registers |
打印普通寄存器 | |
info all-registers |
打印所有寄存器 | |
print/x $pc |
打印單個寄存器 | |
stepi |
指令級別單步進入 | si |
nexti |
指令級別單步跳過 | ni |
display/i $pc |
監控寄存器(每條單步完以后會自動打印值) | |
x/x &gx |
十六進制打印變量 | |
info line 22 |
打印行號為 22 的內存地址信息 | |
info line *0x2c4e |
打印給定內存地址對應的源代碼和行號信息 | |
disassemble {addr} |
對地址進行反匯編,比如 disassemble 0x2c4e |
其他命令
命令 | 含義 | 備注 |
---|---|---|
show commands |
顯示歷史命令 (gdb 4.0) | |
info editing |
顯示歷史命令 (gdb 3.5) | |
ESC-CTRL-J |
切換到 Vi 命令行編輯模式 | |
set history expansion on |
允許類 c-shell 的歷史 | |
break class::member |
在類成員處設置斷點 | |
list class:member |
顯示類成員代碼 | |
ptype class |
查看類包含的成員 | /o可以看成員偏移,類似pahole |
print *this |
查看 this 指針 | |
define command ... end |
定義用戶命令 | |
<return> |
直接按回車執行上一條指令 | |
shell {command} [args] |
執行 shell 命令 | |
source {file} |
從文件加載 gdb 命令 | |
quit |
退出 gdb |