用 set follow-fork-mode child即可。這是一個 gdb 命令,其目的是告訴 gdb 在目標應用調用fork之后接着調試子進程而不是父進程,因為在 Linux 中fork系統調用成功會返回兩次,一次在父進程,一次在子進程


GDB的那些奇淫技巧

gdb也用了好幾年了,雖然稱不上骨灰級玩家,但也有一些自己的經驗,因此分享出來給大家,順便也作為一個存檔記錄。

多進程調試

最近在調試一個漏洞的exploit時遇到一個問題。目標漏洞程序是一個 CGI 程序,由主進程調起,而且運行只有一瞬的時間;我的需求是想要在在該程序中下斷點,在內存布局之后可以調試我的 shellcode,該如何實現?當然目標程序是沒有符號的,而且我希望下的斷點是一個動態地址。在 lldb 中有--wait-for,gdb 里卻沒有對應的命令,經過多次摸索,終於總結出一個比較完美的解決方案。

這里構建一個簡單的示例來進行實際演示。首先是父進程:

 
#include <stdio.h> #include <unistd.h> #include <sys/wait.h>  int main(int argc, char **argv, char **env) { printf("parent started, pid=%d\n", getpid()); char *line = NULL; size_t len = 0; ssize_t read; while ((read = getline(&line, &len, stdin)) != -1) { pid_t pid = fork(); if (pid == -1) { perror("fork"); break; } if (pid == 0) { printf("1 fork return in child, pid=%d\n", getpid()); char *const av[] = {"child", line, NULL}; if (-1 == execve("./child", av, env)) { perror("execve"); break; } } else { printf("2 fork return in parent, child pid=%d\n", pid); int status = 0; wait(&status); } } return 0; } 

子進程很簡單:

 
#include <stdio.h> #include <string.h>  void vuln(char *str) { char buf[4]; strcpy(buf, str); printf("child buf: %s", buf); } int main(int argc, char **argv) { puts("child started"); vuln(argv[1]); return 0; } 

這里編譯子進程時候指定-no-pie,並且strip掉符號。我們的調試目標是斷點在子進程的strcpy中,拓展來說是希望能斷點在子進程的任意地址上。

通過搜索可以找到一個 stackoverflow 的回答: gdb break when entering child process。根據其說法,使用 set follow-fork-mode child即可。這是一個 gdb 命令,其目的是告訴 gdb 在目標應用調用fork之后接着調試子進程而不是父進程,因為在 Linux 中fork系統調用成功會返回兩次,一次在父進程,一次在子進程。我們來試一下,直接斷點在 strcpy 符號中:

 
gdb child --pid $parent_pid (gdb) set follow-fork-mode child (gdb) b strcpy Breakpoint 1 at 0x4004c0 (gdb) c Continuing. Warning: Cannot insert breakpoint 1. Cannot access memory at address 0x4004c0 Command aborted. 

噢,斷點都打不上,理由很簡單,因為不同進程之間的虛擬地址空間都不一樣。

另外一個回答中說了,雖然不能斷在指定地址,但我們可以break main,告訴 gdb 把斷點設置在 main 函數。不過我們的子進程是沒有符號的,所以break main並沒有卵用。

現在已經有了讓 gdb 跟着子進程的方法,只不過問題是無法把斷點打到子進程上,因為子進程還沒有啟動,那么用硬件斷點可不可以?

 
gdb child --pid $parent_pid (gdb) set follow-fork-mode child (gdb) hb *0x4004c0 Hardware assisted breakpoint 1 at 0x4004c0 (gdb) c Continuing. [New process 309] process 309 is executing new program: /pwn/child Thread 2.1 "child" received signal SIGABRT, Aborted. [Switching to process 309] 

可以是可以,但是斷點壓根沒有觸發,子進程直接拷貝溢出崩潰了都沒有停下來!所以硬件斷點在這里並沒有用。

那么把斷點設置在一些起始函數的上呢?根據之前對 ELF 以及動態鏈接的學習,我們可以斷在比如_start或者__libc_start_main上面:

 
gdb child --pid $parent_pid (gdb) set follow-fork-mode child (gdb) b _start Breakpoint 1 at 0x7fbfb4c30090 

實際上該斷點也不會觸發,因為這個地址是是父進程的地址空間。

不過到現在答案已經呼之欲出了,總結一下,gdb 支持:

  • fork 之后跟蹤到子進程
  • 可以設置軟斷點
  • 子進程有 _start 符號

所以,就有了一個最終方案。

我的最終方案如下:

 
set detach-on-fork on set follow-fork-mode child set breakpoint pending on b _start attach $parent_pid file child continue 

首先告訴 gdb 跟蹤子進程;然后設置set breakpoint pending on是為了在設置斷點時讓 gdb 不強制在對符號下斷點時就需要固定地址,這樣在b _start時就會 pending 而不是報錯;最后再連接到父進程以及加載子進程的符號。

detach-on-fork on是為了在 fork 之后斷開父進程,避免 gdb 退出時把父進程殺死,並不是這節的重點。

其中的時序非常重要。如果先 attach 父進程再下斷點,那么斷點會直接下到父進程空間從而不會觸發;如果先讀取了子進程的符號再下斷點,可能會下在一個錯誤的虛擬地址上。

這也是我用了很久的一個方法,不過后來我知道了有更官方的解決方式:

 
set follow-fork-mode child catch exec 

囧,……

Catch Point真是個好東西,支持很多有用的事件:

  • 常規的C++異常事件
  • 系統調用事件(可直接指定系統調用號)
  • 動態庫的加載/卸載事件
  • exec/fork/vfork

看來文檔搜索能力還有待提高啊。……

多線程調試

在調試大型程序的時候,經常會遇到這么一個問題,即涉及到的線程很多,少則十幾個多則上百個線程。在這些線程之間穿梭也是一個常見的困難。

首先最基本的是線程的切換命令:

  • info threads: 查看當前所有的線程
  • thread n: 切換到 id 為n的線程中

對於進程也有類似的命令info inferiors/inferior n,在調試多進程交互的程序時會經常用到。

其次,在對某個線程進行單步調試時,會遇到 CPU 的迷之調度,突然一個next或者nexti就跑到其他線程去了,這個時候有個特殊的參數scheduler-locking可以解決這個問題:

 
(gdb) help set scheduler-locking
Set mode for locking scheduler during execution.
off    == no locking (threads may preempt at any time)
on     == full locking (no thread except the current thread may run)
          This applies to both normal execution and replay mode.
step   == scheduler locked during stepping commands (step, next, stepi, nexti).
          In this mode, other threads may run during other commands.
          This applies to both normal execution and replay mode.
replay == scheduler locked in replay mode and unlocked during normal execution.

通常設置為step模式可解決單步調試的問題。

程序運行

我經常用到的一個功能是需要使用 gdb 執行某個程序,並且能精確控制程序的參數,包括命令行、標准輸入和環境變量等。gdb 的 run 命令就是用來執行程序的。

這里還是先寫個示例測試程序:

 
// demo.c #include <stdio.h> #include <unistd.h>  int main(int argc, char **argv) { int n; char buf[10]; for (int i = 0; i < argc; i++) printf("argv[%d] = %s\n", i, argv[i]); int nread; nread = read(STDIN_FILENO, buf, 10); printf("first read: %d\n", nread); nread = read(STDIN_FILENO, buf, 10); printf("second read: %d\n", nread); return 0; } 

最基本的,通過 run 命令控制命令行參數:

 
$ gdb demo
(gdb) run hello world Starting program: /pwn/demo hello world argv[0] = /pwn/demo argv[1] = hello argv[2] = world 

或者在運行前設置args參數:

 
(gdb) set args hello world (gdb) run Starting program: /pwn/demo hello world argv[0] = /pwn/demo argv[1] = hello argv[2] = world 

在漏洞挖掘或者 CTF 比賽中經常遇到的情況是某些輸入觸發了進程崩潰,因此要掛 gdb 進行分析,這時候就需要gdb 掛載的程序能夠以指定的標准輸入運行。如果標准輸入是文件,那很簡單:

 
$ gdb demo
(gdb) run <file 

但更多時候為了方便調試,希望能以其他程序的輸出來運行,比如:

 
$ python -c 'print "A"*100' | ./demo 

可惜 gdb 不支持這種管道,不過可以通過下面的方法實現:

 
$ gdb demo
(gdb) run < <(python -c 'print "A"*100') Starting program: /pwn/demo < <(python -c 'print "A"*100') argv[0] = /pwn/demo first read: 10 second read: 10 

或者:

 
$ gdb demo
(gdb) run <<<$(python -c 'print "A"*100') Starting program: /pwn/demo <<<$(python -c 'print "A"*100') argv[0] = /pwn/demo first read: 10 second read: 10 

后者實際上是 shell 命令 here string的一種形式。這兩種方式是有區別的,注意示例程序中 read 調用會提前返回,所以如果我們想要第一次讀取3個字符,第二次讀取4個字符的話,就不能一次性全部輸入。比如下面這樣就不符合預期了:

 
$ gdb demo
(gdb) run < <(echo -n 1112222) Starting program: /pwn/demo < <(echo -n 1112222) argv[0] = /pwn/demo first read: 7 second read: 0 

正確的方式應該是這樣:

 
$ gdb demo
(gdb) run < <(echo -n 111; sleep 1; echo -n 2222) Starting program: /pwn/demo < <(echo -n 111; sleep 1; echo -n 2222) argv[0] = /pwn/demo first read: 3 second read: 4 

值得注意的是,這種情況下,使用here string是沒用的,因為該字符串是計算完再一次性傳給命令:

 
(gdb) run <<<$(echo -n 111; sleep 1; echo -n 2222) Starting program: /pwn/demo <<<$(echo -n 111; sleep 1; echo -n 2222) argv[0] = /pwn/demo first read: 8 second read: 0 

而且這里是8字節,因為末尾還帶了個回車。 所以我更偏向於使用第一種方式。

對於運行程序而言,還有個重要的參數來源是環境變量,比如在調試 CGI 程序的時候。這在 gdb 中可以使用environment參數,不過需要注意的是該參數的設置是以空格為切分而不是傳統的以=對環境變量賦值。

 
(gdb) help set environment Set environment variable value to give the program. Arguments are VAR VALUE where VAR is variable name and VALUE is value. VALUES of environment variables are uninterpreted strings. This does not affect the program until the next "run" command. 

還有要注意的是這個參數要求變量是uninterpreted strings,也就是說只能指定可打印字符。如果我們要傳輸一個的 payload 或者 shellcode 還要用 gdb 調試怎么辦呢?我一般使用的方式是在調用 gdb 時指定,比如:

 
$ env CONTENT_TYPE="$(python -c "print 'A'*10 + '\x04\x03\x02\x01'")" gdb demo (gdb) run 

后記

對於二進制研究人員來說,gdb 是一個鋒利的好工具,支持X86、ARM、MIPS、RISCV、Xtensa等各種常用和不常用的系統架構,對其熟練使用有時候可以達到事半功倍的效果,在文末的附錄中我也列舉了一些比較常用的命令。由於 gdb 本身支持 python 接口,因此現實中使用通常結合一些拓展使用,比如:

這幾個我都用過,各有千秋。現在工作中使用更多的是gef,因為安裝太方便了,一個文件搞定。

上面這幾個拓展可能大家可能都不陌生,但還有另外一個我比較常用的是 gdb-dashboard,其功能更為簡單,而且使用的是 gdb 原本的信息,所以支持的指令集更多。比如下面的截圖就是我曾經用 gdb + OpenOCD 來調試 ESP32固件的示例:

 

https://img-blog.csdnimg.cn/20200913153257473.pngXtensa指令集調試

 

ESP32是比較少見的Xtensa指令集架構,上面的拓展都不支持,不過 gdb 本身支持,因此配合使用的效果絕佳。

附錄: gdb命令表

gdb 還有其他一些小技巧,可以參考awesome-cheatsheets/tools/gdb.txt中的列表。該列表最初由韋神創建,我時不時也會添加一些上去。當然為了方便大家的查閱,這里直接給出匯總表格附錄:

命令 含義 備注
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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM