参考资料
- Guide to Faster, Less Frustrating Debugging
什么情况下会使用gbd
- 需要逆向ELF文件时(掌握gdb的使用,是二进制安全的基本功)
- 开发程序时,程序执行结果不符合预期
动态调试ELF文件可以使用另外一种方法:IDA的远程linux动态调试。个人觉得使用ida调试更为方便,因为ida是图形化界面,那么可以使用鼠标交互,比如通过鼠标光标确定打断点的位置等等。而且IDA具有反编译功能,对初学者较为友好。
为什么使用gbd呢
调试程序时,我们可能习惯使用printf
, cout
函数直接将变量打印在控制台上。如果用printf
作为调试的主要方式,好处是这种做法确实方便,但以下缺点也会大大影响调试的进度(以及个人心情)
printf
一个变量后,发现那个变量的值是正确的,需要继续printf
其他变量- 大量的
printf
会将编写的代码变得惨不忍睹,在测试完后,还需要将编写的printf
删除掉 - 如果我们要检查一个结构体变量有没有符合预期,使用
printf
将这个变量的所有属性打印出来十分繁琐。(虽然我更喜欢用IDE自带的调试功能观察2333)
所以我个人认为,在编写程序时,可以适当使用printf
作为调试的手段,但如果太过依赖它,反而会事倍功半
接下来说一下gbd的好处:
- 需要掌握的命令少,和markdown一样,熟练使用了就能转换成自己的肌肉记忆。
- 具有一个调试器应该有的功能,如可以显示运行程序的寄存器,运行时栈堆,支持反汇编等等
(虽然使用体验确实比不上支持图形化的调试器2333)
常用命令
知乎:GDB使用详解
介绍一下打断点的方式
b functionName
: 在函数的入口处添加断点b line
: 在文件的第line行添加断点b *adderess
在某一个地址添加断点
解下来以csapp的第二个lab为例,讲解pwngdb的使用
这篇博客假设大家已经有汇编语言基础,因此我不会花很多篇幅在汇编语句讲解上 😃
我之所以使用pwngdb,是因为之前做ctf pwn题目时配置好了。pwngdb比起原版的gdb新增了一些独有的指令,尤其是在堆的方面。
csapp lab的网页: https://csapp.cs.cmu.edu/3e/labs.html
分析整个程序的流程如下:
输入disassemble main
观察main函数反汇编的结果
0x0000000000400e2d <+141>: call 0x400b10 <puts@plt>
0x0000000000400e32 <+146>: call 0x40149e <read_line>
0x0000000000400e37 <+151>: mov rdi,rax
0x0000000000400e3a <+154>: call 0x400ee0 <phase_1>
0x0000000000400e3f <+159>: call 0x4015c4 <phase_defused>
0x0000000000400e44 <+164>: mov edi,0x4023a8
0x0000000000400e49 <+169>: call 0x400b10 <puts@plt>
0x0000000000400e4e <+174>: call 0x40149e <read_line>
0x0000000000400e53 <+179>: mov rdi,rax
0x0000000000400e56 <+182>: call 0x400efc <phase_2>
0x0000000000400e5b <+187>: call 0x4015c4 <phase_defused>
0x0000000000400e60 <+192>: mov edi,0x4022ed
0x0000000000400e65 <+197>: call 0x400b10 <puts@plt>
0x0000000000400e6a <+202>: call 0x40149e <read_line>
0x0000000000400e6f <+207>: mov rdi,rax
0x0000000000400e72 <+210>: call 0x400f43 <phase_3>
0x0000000000400e77 <+215>: call 0x4015c4 <phase_defused>
0x0000000000400e7c <+220>: mov edi,0x40230b
0x0000000000400e81 <+225>: call 0x400b10 <puts@plt>
0x0000000000400e86 <+230>: call 0x40149e <read_line>
0x0000000000400e8b <+235>: mov rdi,rax
0x0000000000400e8e <+238>: call 0x40100c <phase_4>
0x0000000000400e93 <+243>: call 0x4015c4 <phase_defused>
0x0000000000400e98 <+248>: mov edi,0x4023d8
0x0000000000400e9d <+253>: call 0x400b10 <puts@plt>
0x0000000000400ea2 <+258>: call 0x40149e <read_line>
0x0000000000400ea7 <+263>: mov rdi,rax
0x0000000000400eaa <+266>: call 0x401062 <phase_5>
0x0000000000400eaf <+271>: call 0x4015c4 <phase_defused>
0x0000000000400eb4 <+276>: mov edi,0x40231a
0x0000000000400eb9 <+281>: call 0x400b10 <puts@plt>
0x0000000000400ebe <+286>: call 0x40149e <read_line>
0x0000000000400ec3 <+291>: mov rdi,rax
0x0000000000400ec6 <+294>: call 0x4010f4 <phase_6>
0x0000000000400ecb <+299>: call 0x4015c4 <phase_defused>
一般来说,函数的返回值会放在eax寄存器中。所以以下的代码
0x0000000000400e32 <+146>: call 0x40149e <read_line>
0x0000000000400e37 <+151>: mov rdi,rax
0x0000000000400e3a <+154>: call 0x400ee0 <phase_1>
read_line
读取的字符串先放在rax寄存器中,再经过mov rdi,rax
放在rdi寄存器中。
接下来分析第一关卡:
b phase_1
打下断点,r
运行程序。接着输入一串字符串,为了表示方便,我称其为input
。
按下s
程序单步运行,结果如下图。
在上面的截图中,程序被蓝色的横线分为了三个区域
- 最上面的是寄存器区域,用于显示各个寄存器的值。寄存器有很多作用:比如数据可以存储在寄存器里,数据也可以通过寄存器在函数之间传递。高级语言中的
if
,while
等等功能都可以通过汇编语言实现,这需要使用到寄存器:比如将数据与寄存器中的值进行比较,如果小于就进行跳转命令等等。- 中间区域是反汇编区域,这是程序的运行代码。绿色箭头表示程序当前位置。
- 最下面的区域是栈区。栈区存放函数的参数,返回地址,局部变量等等内容
如果使用gdb,按下
s
的时候可能不会跳出这么多内容
可以使用以下命令打印寄存器的值info registers
或者是
i r
要打印栈的内容和栈帧信息,可以执行以下命令:
info frame
或者是
i f
继续单步执行。
这段代码告诉我们传入strings_not_equal
的参数是什么
一个参数是我们输入的input
字符串 。 另一个参数是程序自带的字符串Border relations with Canada have never been better.
解释一下:rdi: 0x003780 <- 0x333231
0x003780 表示rdi寄存器指向内存地址
0x333231 表示这段地址里存放的值
123 是0x333231 ascii码对应的字符
按下s
,分析strings_not_equal
函数的代码
0x401338 <strings_not_equal> push r12
0x40133a <strings_not_equal+2> push rbp
0x40133b <strings_not_equal+3> push rbx
0x40133c <strings_not_equal+4> mov rbx, rdi
0x40133f <strings_not_equal+7> mov rbp, rsi
0x401342 <strings_not_equal+10> call string_length <string_length>
0x401347 <strings_not_equal+15> mov r12d, eax
0x40134a <strings_not_equal+18> mov rdi, rbp
0x40134d <strings_not_equal+21> call string_length <string_length>
0x401352 <strings_not_equal+26> mov edx, 1
0x401357 <strings_not_equal+31> cmp r12d, eax
这段代码调用string_length
函数,用于取得字符串的长度。接着使用cmp r12d , eax
比较两个传入字符串的长度。如果长度一样,那么第一关就过了。
接下来验证我们的想法:
分别在0x401342 和0x40134d添加临时断点,tb *0x401342
tb *0x40134d
。然后按下c
运行到断点。按下n
让函数执行完毕,观察rax
寄存器的值
第一个string_length
函数执行完毕的寄存器页面
第二个string_lenth
函数执行完毕的寄存器页面
按下r
重新运行。输入字符串Border relations with Canada have never been better.
这样关卡1就通过了。
使用gdb是熟能生巧的过程,只要多练就能掌握这个软件的使用😃
照例meme时间
当导师发现你没有在规定时间内完成任务时 be like↓
摸了一周鱼的我 be like↓
不多说了,要赶紧完成java web的管理系统和qt的文件编辑器了 😭