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
固件的示例:
Xtensa指令集调试
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 |