前言

学长推荐了个有趣的github项目,就此来回顾学习一下gdb的调试技巧:100-gdb-tips/src/index.md at master · hellogcc/100-gdb-tips

信息显示

显示gdb版本信息

1
2
3
4
gdb --version
gdb -v
# 显示正在运行的 GDB 版本。您应该将此信息包含在 GDB 错误报告中。如果您的站点使用多个版本的 GDB,您可能需要确定正在运行的 GDB 版本;随着 GDB 的发展,新的命令被引入,旧的命令可能会消失。此外,许多系统供应商提供了 GDB 的变体版本,并且 GNU/Linux 发行版中也有 GDB 的变体版本。版本号与启动GDB时公布的版本号相同。
# 目前只接触到gdb版本会和python版本存在一定兼容性问题 而一些插件又依赖于python版本,所以会导致gdb版本低会安装不了插件

image-20241117201712754

显示gdb版权相关信息

1
2
pwndbg> show/info copying   #gdb权限信息
pwndbg> show/info warranty #保修信息

调用gdb时不显示提示信息

Invoking GDB (Debugging with GDB)

1
2
gdb -q(--quiet)
gdb --silent

image-20241117203010329

为了每次使用gdb时不显示提示信息

可以在.bashrc里面设置gdb

1
2
alias gdb="gdb -q"  #~/.bashrc 添加
source ~/.bashrc #root 和普通用户区分

image-20241117203535064

输出信息多时不会暂停输出

Screen Size (Debugging with GDB)

还没遇到输出多的调试信息而暂停输出的情况,不过记下来

1
2
set pagination off
set height 0

pwndbg设置输出重定向

pwndbg使用链接:https://github.com/pwndbg/pwndbg/blob/dev/FEATURES.md#context

我们可以通过 set context-output /path/to/file 输出重定向到文件(包括其它tty),同时保留其它输出。

1
set context-output /dev/pts/x

image-20241117205032329

快捷更新~/.gdbinit

image-20241117205242373

1
2
3
4
5
6
7
8
9
#!/bin/bash

current_terminal=$(tty)

replacement="set context-output $current_terminal"

sed -i '$s|.*|'"$replacement"'|' ~/.gdbinit

echo "Updated ~/.gdbinit with $replacement"

函数

列出函数名

符号(使用 GDB 进行调试)

demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#include <pthread.h>
void *thread_func(void *p_arg)
{
while (1)
{
sleep(10);
}
}

void func(int x, int y){
int sum;
sum = x+y;
printf("x + y = %d\n", sum); //不添加换行将导致输出被缓存在缓冲区中,不会及时显示
}

int main(void)
{
pthread_t t1, t2;
int a,b;
a = 1;
b = 3;

pthread_create(&t1, NULL, thread_func, "Thread 1");
pthread_create(&t2, NULL, thread_func, "Thread 2");
func(a, b);

sleep(1000);
return;
}
// gcc demo0.c -o demo0 -pthread
// gcc demo0.c -o demo0 -pthread -g

usage

1
2
info functions
info functions thre* #匹配正则

image-20241117213222927

调试技巧

  • start:程序从头开始重新启动
  • run(r):执行程序,遇到断点将停止,等待用户输入。
  • continue (c ):继续运行程序,直到遇到下一个断点或错误。
  • next( n):单步跟踪程序,当遇到函数调用时,也不进入此函数体;此命令同 step 的主要区别是,step 遇到用户自定义的函数,将步进到函数中去运行,而 next 则直接调用函数,不会进入到函数体内。
  • nexti(ni):同next,单步一条机器指令,不进入函数。
  • step (s):单步调试如果有函数调用,则进入函数;与命令n不同,n是不进入调用的函数的
  • stepi(si):同step,单步一条机器指令。
  • until:用于退出循环体了。
  • until N: 执行运行程序,直到当前行前面已经运行了N行代码。
  • finish(fini): 退出正在执行的函数。
  • quit(q):退出gdb。
  • info frame(i frame/f):打印函数堆栈帧信息。
  • info registers(i registers/r):查看函数寄存器信息。
  • disasseble func:查看函数反汇编代码。
  • up N:向上切换函数栈帧。
  • down N:向下切换函数栈帧。
  • set debug entry-values 1:打印尾调用堆栈帧信息。
  • frame N:选择函数堆栈帧(N为层数),也可以使用frame addr

n & ni 的区别

前者用于源代码级别的调试,后者用于机器码级别的调试。

(个人调试时喜欢使用ni/si指令😍)

image-20241117221258876

执行n后,跳过了sum = x+y对应的汇编语句

image-20241117221326966

而执行ni后,会执行下一条机器码

image-20241117221454149

s & si 的区别

s:在n的基础上能够步入函数

si:在ni的基础上能够步入函数

直接执行函数

前提是需要加载调试符号!!!

如果未编译程序时没有包含调试符号(-g 选项),GDB 无法识别函数的签名和类型信息。

image-20241117221905888

没有设置-g选项时

func 函数是以非调试符号(Non-debugging symbols)的形式存在

image-20241117222511230

手动加载调试信息

指明类型进行调试

1
p (void)func(1, 5)

image-20241117222705213

断点

调试技巧

  • 匿名空间设置断点 b Foo::foo b (anonymous namespace)::bar
  • 程序地址上打断点 b *address
  • 在程序入口处打断点 去除符号表后可以 先start 执行到__libc_start_main找main函数的调用地址
  • 在文件行号上打断点 文件编译时-g添加了调试信息,那么可以直接 b N进行源代码级别的行号设置断点
  • 保存已经设置的断点 save breakpoints file-name-to-save
  • 查看断点 info breakpoints (i b)
  • 设置临时断点(生效一次)tbreak(tbr) –> enable once 断点编号
  • 设置条件断点 b 10 if i==101
  • 禁用断点 disable 断点编号(批量禁止disable num1-num2)
  • 启用断点 enable 断点编号
  • 忽略前N次命中:ignore 断点编号 次数
  • 删除断点 delete [断点] (单delete删除全部)

gcc -s 和 strip命令区别

gcc作为编译器/链接器,-s选项是在链接时完成的。用来删除符号表和重定向信息。

strip则是对已经编译生成的目标文件进行删减。有对应的命令选项来进行删除,比如-g选项能删除对应gcc -g添加的调试信息而保留符号表。

一般在实际开发中,经常需要对ELF格式二进制文件进行strip操作,删除部分section:减少size,节省空间;去掉symbol table,增加逆向工程难度😭

(nm 命令是 Linux 下的一个强大的文件分析工具,用于检查和分析二进制文件、库文件、可执行文件中的符号表。)

image-20241118130529914

提取调试信息:objcopy –only-keep-debug program program.debug

gdb加载调试信息:symbol-file program.debug

去除符号表后调试程序时调用__libc_start_main的原因

去除符号表后,调试器不能找到main的入口地址,只能依赖程序启动的默认流程。

在 Linux 程序中,程序启动通常经过以下步骤:

  1. **程序入口点 (_start)**:
    • 这是 ELF 文件的默认入口,由操作系统加载程序后首先执行。
    • _start 通常初始化运行时环境(如栈、堆等),并调用 __libc_start_main
  2. **__libc_start_main**:
    • 一个标准的 C 程序入口函数,它负责:
      • 初始化全局变量。
      • 执行构造函数(.init_array 中的函数)。
      • 最终调用 main 函数。
  3. 调用 main 函数
    • __libc_start_main 完成初始化后,它会跳转到 main 函数,开始程序的主要逻辑。

image-20241118133501181

直接调试去除符号表的程序将调用libc_start_main

image-20241118133103781

加载符号表后再启动断在main函数

image-20241118133155920

strip使用方法

选项 描述
-I --input-target=<bfdname> 假设输入文件的格式为 <bfdname>
-O --output-target=<bfdname> 创建格式为 <bfdname> 的输出文件
-F --target=<bfdname> 将输入和输出格式都设置为 <bfdname>
-p --preserve-dates 将修改/访问时间戳复制到输出文件
-D --enable-deterministic-archives 生成确定性输出时剥离归档文件(默认行为)
-U --disable-deterministic-archives 禁用 -D 行为
-R --remove-section=<name> 从输出中移除名称为 <name> 的段
--remove-relocations <name> <name> 段移除重定位信息
-s --strip-all 移除所有符号和重定位信息
-g -S -d --strip-debug 移除所有调试符号和段
--strip-dwo 移除所有 DWO 段
--strip-unneeded 移除重定位信息不需要的符号
--only-keep-debug 仅保留调试信息,移除其他信息
-M --merge-notes 移除注释段中冗余的条目(默认行为)
--no-merge-notes 不尝试移除冗余注释
-N --strip-symbol=<name> 不复制名称为 <name> 的符号
--keep-section=<name> 不剥离名称为 <name> 的段
-K --keep-symbol=<name> 不剥离名称为 <name> 的符号
--keep-file-symbols 不剥离文件符号
-w --wildcard 允许符号匹配中使用通配符
-x --discard-all 移除所有非全局符号
-X --discard-locals 移除所有编译器生成的符号
-v --verbose 列出所有被修改的目标文件
-V --version 显示此程序的版本号
-h --help 显示帮助信息

观察点

gdb可以使用“watch”命令设置观察点,也就是当一个变量值发生变化时,程序会停下来。

调试技巧

  • 设置观察点watch a ,wacth (type)adress, info watchpoints disable、enable、delete
  • 设置观察点只针对特定线程生效 watch expr thread threadnum, wa a thread 2
  • 设置读观察点 rwatch 当发生读取变量行为时,程序就会暂停住。
  • 设置读写观察点 awacth 发生读取变量或改变变量值的行为时,程序就会暂停住。

Catchpoint

Breakpoints能让程序执行到暂停流程,包括Breakpoints、Watchpoints、Catchpoints。

Catchpoints是一种特殊的Breakpoints,当某种特殊的事件产生后停止程序执行。

除了设置Catchpoints,其他对Catchpoints的管理方式类似于Breakpoints。

调试技巧

  • catch 事件 (事件发生后暂停)
  • 设置命令触发一次 tcatch
  • 为系统调用设置catchpoint catch syscall [name | number] 当系统调用发生后,gdb会暂停程序的运行。

image-20241118143357295

通过为ptrace调用设置catchpoint破解anti-debugging的程序

ptrace系统调用

Linux沙箱入门——ptrace从0到1-安全客 - 安全资讯平台

提供父进程观察和控制另一个进程执行的机制,同时提供查询和修改另一进程的核心影像与寄存器的能力。主要用于执行断点调试和系统调用跟踪。

1
2
3
4
5
6
7
8
9
10
11
12
#include <sys/ptrace.h>
#include <stdio.h>

int main()
{
if (ptrace(PTRACE_TRACEME, 0, 0, 0) < 0 ) {
printf("Gdb is debugging me, exit.\n");
return 1;
}
printf("No debugger, continuing\n");
return 0;
}

可以使用 catch syscall ptrace来对系统调用进行监测,调用ptrace后来修改返回值

不过也可以直接nop掉或者是修改其操作码实现破解,也能使用LD_PRELOAD来劫持ptrace函数的调用

通过catchpoint实现破解是基于调试技术实现,后面三种方法则是对文件或文件执行时的patch

1
2
3
4
5
6
long ptrace(int request, int pid, int addr, int data)
{
return 0;
}
//gcc ptrace.c -o ptrace.so -fPIC -shared -ldl -D_GNU_SOURCE
//export LD_PRELOAD="./ptrace.so"

image-20241118145404180

LD_PRELOAD用于动态库的加载,动态库加载的优先级最高,一般情况下,其加载顺序为LD_PRELOAD>LD_LIBRARY_PATH>/etc/ld.so.cache>/lib>/usr/lib

LD_PRELOAD 是一个环境变量,用于在运行 Linux 程序时指定一个或多个共享库,这些库会在其他库之前加载。这使得开发者可以替换或增强现有库的功能。以下是对 LD_PRELOAD 的详细解释:

  • 函数重载:通过提供自定义实现,重载某些库函数。例如,可以替换标准库函数以添加日志、监控或调试功能。
  • 调试和分析:用于调试程序,允许开发者在不修改源代码的情况下跟踪函数调用。
  • 安全性:可以用来实现安全检查,例如在访问敏感资源时进行权限验证。

要使用 LD_PRELOAD,可以在终端中设置这个环境变量,然后运行程序。例如:

1
2
export LD_PRELOAD=/path/to/your/library.so
./your_program

工作原理:

  • 当程序启动时,动态链接器(ld.so)会检查 LD_PRELOAD 环境变量,并加载指定的共享库。
  • 这些库的函数实现会优先于系统库的相应函数,允许开发者插入自定义代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 gcc -fpic -c -ldl hack.c

gcc -shared -lc -o hack.so hack.o

export LD_PRELOAD=./hack.so #加载库

export LD_PRELOAD=NULL; #卸载库

也可以
LD_PRELOAD=/lib/evil.so #LD_PRELOAD的值设置为要预加载的动态链接库
export LD_PRELOAD #导出环境变量使环境变量生效
unset LD_PRELOAD #解除设置的LD_PRELOAD环境变量

LD_PRELOAD=/lib/evil.so #设置要预加载的恶意动态链接库地址

echo $LD_PRELOAD #查看环境变量

lsattr 查看文件的隐藏属性
chattr 更改文件的隐藏属性

image-20241118145733554

打印

参考:GDB调试指令 - noahze - 博客园 这位师傅写得很详细

常用的一些打印命令

/fmt 功 能
/x 以十六进制的形式打印出整数。
/d 以有符号、十进制的形式打印出整数。
/u 以无符号、十进制的形式打印出整数。
/o 以八进制的形式打印出整数。
/t 以二进制的形式打印出整数。
/f 以浮点数的形式打印变量或表达式的值。
/c 以字符形式打印变量或表达式的值。

hexdump

1
hex/hexdump addr N -->打印从addr开始的N字节长度内容

打印内存地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# n:为正整数,表示需要打印的内存单元个数
#
# f:打印格式, 如下
# - x: 十六进制
# - d: 十进制
# - u: 十六进制
# - o: 八进制
# - t: 二进制
# - a: 十六进制
# - c: 字符格式
# - f: 浮点数
#
# u: 内存单元大小,如下:
# - b: 单字节
# - h: 双字节
# - w: 四字节
# - g: 八字节
#
x/<n/f/u> <addr>
例如 x/10xg addr

打印长字符串

1
2
show print elements     # 显示字符串最大打印长度
set print elements 0 # 取消字符串最大打印长度

打印堆相关信息

含义 命令
查看分配的堆 heap [-h] [-v] [-s] [addr] (chunk头开始)
查看各种bins bins [addr]
16进制查看 hexdump 地址 显示的字节数
查看各个bins的情况 heapinfo (常用)
查看堆的基地址 heapbase
查看用户使用的堆的使用情况 parseheap (chunk头开始)(par简写)(常用)
查看chunks内存 vis_heap_chunks
查找fake_chunks find_fake_fast &main_arena

打印结构体相关信息

含义 命令
输出变量类型 ptype
格式化输出显示结构体 p

set print pretty on 如果打开了这个选项,那么当显示字符串时,遇到结束符则停止显示。这个选项默认为off。

set print pretty on 如果打开printf pretty这个选项,那么当GDB显示结构体时会比较漂亮

set print union 设置显示结构体时,是否显式其内的联合体数据。

**也可以将一片内存自行转义成为结构体p (struct _IO_FILE_plus )addr <– 用于查看伪造的_IO_FILE_plus结构体

image-20241118154257484

打印其它信息

含义 命令
查看区段 vmmap
搜索字符串 searchmem 字符串
查看子命令帮助 help x
转换为intel格式 set disassembly-flavor intel
转换为att格式 set disassembly-flavor att
查看符号地址 info address 符号
查看puts的got表地址 info address puts@got.plt
查看libc中system函数地址 info address system
查看内存映射 info proc mappings
用栈的方式查看 telescope 地址
显示当前gdb断点信息 info breakpoints (i b)

修改内存

1
2
3
修改寄存器 set $rax=0x111
修改内存值 set *addr=info(默认最多修改四字节)
set *(unsigned long *)0x7fffffffe060 = 0x8888888888888888
无符号 修改字节数
set {unsigned char}addr =value 1字节
set {unsigned short}addr =value 2字节
set {unsigned int}addr =value 4字节
set {unsigned long long}addr =value 8字节
有符号 修改字节数
set {char}addr =value 1字节
set {short}addr =value 2字节
set {int}addr = value 4字节
set {long long}addr =value 8字节

多进程/线程调试

这位师傅写的很详细 –> GDB 调试多进程或者多线程应用-CSDN博客

follow-fork-mode

默认设置下, 在调试多进程程序时 GDB 只会调试主进程. 但是 GDB > V7.0 支持多进程的分别以及同时调试, 换句话说, GDB 可以同时调试多个程序. 只需要设置 follow-fork-mode (默认值 parent) 和 detach-on-fork (默认值 on )即可.

follow-fork-mode detach-on-fork 说明
parent on 只调试主进程( GDB 默认)
child on 只调试子进程
parent off 同时调试两个进程, gdb 跟主进程, 子进程 blockfork 位置
child off 同时调试两个进程, gdb 跟子进程, 主进程 blockfork 位置

常用命令

1
2
3
4
5
6
7
8
9
10
11
12
set follow-fork-mode [parent|child]

set detach-on-fork [on|off]

查看正在调试进程
info inferiors

切换调试的进程
inferior <infer number>

添加新的调试进程
add-inferior [-copies n] [-exec executable]

GDB默认支持调试多线程, 跟主线程, 子线程blockcreate thread

1
2
3
4
5
6
7
8
9
10
查询线程
info threads
切换调试线程
thread <thread number>
锁定当前线程
set scheduler-locking on
<=>解锁所有线程
set scheduler-locking off
查看调用栈
bt

Attach进程

  • gdb attach pid
  • gdb demo3 pid
  • detach命令脱离进程

image-20241118161213436