2、GDB基础入门:启动GDB、加载程序、设置断点、运行、单步执行、查看变量与内存
GDB(GNU Debugger)是Linux/Unix环境下最经典的调试器,也是逆向工程与漏洞分析工作中不可或缺的基础工具。无论是分析一个崩溃的守护进程,还是定位缓冲区溢出漏洞的触发点,GDB都提供了从“程序外部观察”到“指令级控制”的完整能力。本章将围绕GDB最核心的操作闭环——启动、加载、断点、运行、单步、查看变量与内存——展开系统讲解,帮助读者建立扎实的调试基本功。
2.1 启动GDB与加载目标程序
核心原理:GDB通过ptrace系统调用与目标进程建立调试关系。启动时有两种典型方式:直接以gdb ./program方式启动,此时GDB会解析ELF文件中的符号表(.symtab)和调试信息(.debug_info),为后续的符号级调试(如按函数名设置断点)提供基础;另一种方式是gdb -p <pid>附加到已运行的进程,常用于调试服务型程序或分析正在运行的恶意样本。加载程序后,GDB并不会立即执行目标代码,而是停留在等待命令的状态,此时可以预先设置断点、修改环境变量或传递参数。
操作流程:
# 方式一:直接加载可执行文件
gdb ./vuln_program
# 方式二:带参数启动
gdb --args ./vuln_program arg1 arg2
# 方式三:附加到运行中的进程(需ptrace权限)
gdb -p 12345
# 在GDB内部加载文件(若已启动但未加载)
(gdb) file ./vuln_program
# 设置运行参数
(gdb) set args arg1 arg2
# 设置环境变量
(gdb) set env LD_PRELOAD=/path/to/lib.so
案例拆解:假设我们有一个存在栈溢出的程序stack_demo,需要分析其输入处理逻辑。启动时使用gdb -q ./stack_demo(-q表示安静模式,不显示版本信息)。此时GDB会显示类似Reading symbols from ./stack_demo...的提示。如果程序是strip过的(无符号表),GDB会提示(No debugging symbols found in ./stack_demo),此时只能使用地址级调试,这对后续漏洞分析是常见场景。
-g选项),但也要习惯在无符号环境下通过info file、info functions等命令获取ELF段信息和导出函数。使用checksec(pwndbg/gef插件)可快速查看防护机制(NX、PIE、RELRO、Canary),这决定了后续利用策略。
2.2 设置断点:从源码行到指令地址
核心原理:断点的本质是在目标地址处将指令替换为int3(0xCC)软中断指令。当CPU执行到该地址时触发SIGTRAP信号,GDB捕获该信号后暂停程序,并恢复原始指令以便继续执行。GDB支持多种断点类型:源码行断点、函数名断点、地址断点、条件断点、硬件断点(针对数据访问)。在漏洞分析中,地址断点(break *0x401234)和条件断点(break func if var == 0x10)最为常用。
断点管理命令:
# 常用断点设置
(gdb) break main # 在main函数入口
(gdb) break *0x401234 # 在地址处(反汇编后常用)
(gdb) break file.c:15 # 在源码第15行
(gdb) break func if len > 10 # 条件断点
# 查看与管理断点
(gdb) info breakpoints # 列出所有断点
(gdb) delete 1 # 删除编号为1的断点
(gdb) disable 2 # 禁用编号为2的断点
(gdb) enable 2 # 重新启用
# 临时断点(命中一次后自动删除)
(gdb) tbreak main
实战场景:在分析strcpy调用时,我们往往希望在调用前查看源和目标地址。反汇编后得到call 0x401030 <strcpy@plt>,此时设置break *0x401030,运行后使用info registers查看rdi(目标)和rsi(源)寄存器值,即可确认拷贝方向与缓冲区边界。
start命令(在main处暂停)后,通过info sharedlibrary查看库的实际加载基址,再计算目标函数地址。或者使用break *<function>@plt方式在PLT桩处下断。
2.3 运行控制:run、continue与单步执行
核心原理:GDB的运行控制基于信号与ptrace的暂停/恢复机制。run启动程序并运行至第一个断点或程序结束;continue从当前暂停点继续运行;单步执行则通过设置CPU的陷阱标志(TF)或使用ptrace(PTRACE_SINGLESTEP)实现指令级暂停。理解next与step的区别至关重要:next在源码级别执行一行,遇到函数调用时跳过(不进入函数体);step则会进入函数内部。对应的指令级命令是nexti(跳过调用指令)和stepi(进入调用指令)。
操作流程与对比:
| 命令 | 粒度 | 遇到函数调用时 | 典型用途 |
|---|---|---|---|
next |
源码行 | 不进入,整行执行 | 快速跳过库函数调用 |
step |
源码行 | 进入函数内部 | 跟踪自定义函数逻辑 |
nexti |
单条指令 | 不进入call指令 | 反汇编调试时跳过函数 |
stepi |
单条指令 | 进入call指令 | 分析函数序言/内部指令 |
until |
源码行 | 运行至指定位置 | 跳出循环 |
finish |
函数级 | 运行至当前函数返回 | 快速结束函数分析 |
案例拆解:在分析一个格式化字符串漏洞时,我们跟踪printf调用。使用stepi进入printf@plt,再使用x/10i $rip查看PLT桩指令,通过stepi跟踪跳转到libc中的实际实现。此时若使用nexti则会跳过整个printf执行,无法观察其内部对格式化字符串的解析过程。对于漏洞验证,通常使用next跳过printf,直接观察栈上数据变化。
until <line>可以运行至指定行,避免多次next。例如until 25表示运行至第25行。若需跳出当前函数,使用finish。对于无限循环,可先break循环条件判断处,再continue。
2.4 查看变量与内存:print、x与寄存器
核心原理:GDB通过读取目标进程的虚拟内存映射(/proc/pid/maps)和寄存器上下文来获取数据。print(简写p)按符号类型格式化输出变量值;x命令直接按指定格式查看内存内容;info registers显示所有通用寄存器。在漏洞分析中,查看内存布局(栈、堆、BSS段)和寄存器状态是判断溢出偏移、ROP链构建的基础。
常用查看命令:
# 查看变量(需有调试符号)
(gdb) print buf
(gdb) p &buf # 查看变量地址
(gdb) p/x var # 十六进制显示
# 查看内存(x/格式 地址)
(gdb) x/20gx $rsp # 查看栈顶20个8字节(g=giant,8字节)
(gdb) x/20wx $rsp # 以4字节十六进制查看
(gdb) x/10i $rip # 查看当前指令附近10条汇编
(gdb) x/s 0x402000 # 查看字符串
# 查看寄存器
(gdb) info registers # 所有通用寄存器
(gdb) p/x $rax # 单个寄存器
(gdb) p/x $rsp-0x20 # 寄存器运算
# 查看栈回溯
(gdb) bt # backtrace,查看调用栈
(gdb) frame 1 # 切换到第1层调用帧
实战场景:假设程序在strcpy后崩溃,我们执行bt查看调用栈,发现main调用vuln_func。使用frame 1切换到vuln_func帧,info locals查看局部变量,x/20gx $rsp-0x40查看栈上缓冲区内容。通过对比输入数据在栈上的位置与返回地址的偏移,可精确计算溢出长度。
print查看无符号表程序的全局变量会失败,此时需通过info proc mappings找到数据段基址,再结合反汇编中的偏移计算实际地址。另外,x命令的格式字符必须正确:g表示8字节,w表示4字节,b表示1字节,i表示指令。误用格式会导致数据解读错误,例如将8字节值拆成两个4字节查看。
2.5 综合案例:分析一个栈缓冲区溢出
案例背景:目标程序vuln存在一个简单的栈溢出漏洞,函数vuln_func使用gets()读取输入。程序编译时关闭了栈保护(-fno-stack-protector),但开启了NX(数据执行保护)。我们需要通过GDB确定溢出偏移,并验证能否控制返回地址。
初始分析:
# 启动GDB并加载
gdb -q ./vuln
(gdb) info functions # 查看函数列表
(gdb) disassemble vuln_func # 反汇编目标函数
反汇编结果显示gets调用位于0x4011a9,缓冲区起始地址为rbp-0x30(即48字节)。返回地址存储在rbp+8处,因此从缓冲区到返回地址的偏移为0x30 + 8 = 56字节。
验证过程:
# 在gets调用后设置断点
(gdb) break *0x4011a9
(gdb) run < input.txt # 输入56字节'A' + 8字节'B'
# 查看栈布局
(gdb) x/20gx $rsp
0x7fffffffe3a0: 0x4141414141414141 0x4141414141414141
0x7fffffffe3b0: 0x4141414141414141 0x4141414141414141
0x7fffffffe3c0: 0x4141414141414141 0x4141414141414141
0x7fffffffe3d0: 0x4141414141414141 0x4242424242424242 <- 返回地址被覆盖
# 继续运行,观察崩溃点
(gdb) continue
Program received signal SIGSEGV, Segmentation fault.
0x0000424242424242 in ?? ()
结果分析:程序崩溃在0x42424242424242处,证实了偏移为56字节,且返回地址可控。由于NX开启,不能直接执行栈上shellcode,后续需考虑ROP链。通过info proc mappings查看libc基址,结合ROPgadget工具寻找gadget,最终构造ROP链调用system("/bin/sh")。
复盘经验:GDB基础命令的熟练度直接决定漏洞分析效率。建议将disassemble、x/、info registers、bt这四个命令组合使用,形成“反汇编→看寄存器→查栈→回溯调用”的标准分析流程。同时,善用GDB脚本(source script.gdb)自动化重复操作,例如在断点处自动打印关键寄存器值。
checksec查看防护、vmmap(pwndbg)查看内存映射、cyclic模式生成偏移量。GDB是动态分析的基石,后续章节的反汇编与漏洞利用都将基于这些基础命令展开。