GDB_PWNDBG调试
简介
下载什么就不多说了,我们直接讲使用。
GDB算是这个世界上最强的动态调试器了,而PWNDBG是GDB的一个插件,可以更好地展示GDB得到的一些内容,让我们方便理解。
GDB可以使用的功能包括且不限于:
运行
步入、步过、步出、步止
断点(设置、删除、显示)
查看内存、寄存器、各种参数
设置内存、寄存器、各种参数(加载文件)
远程调试
其他辅助功能
当我们要调试某个程序的时候,要输入:
1 | >gdb [文件名] |
当我们忘记命令的时候,可以输入:
1 | (gdb)/(pwndbg) help |
以Intel方式查看汇编代码(似乎pwndbg默认Intel汇编代码格式):
1 | (gdb)/(pwndbg) set disassembly-flavor intel |
上述的查看反汇编格式,可以通过修改.gdbinit来完成。
1 | vim ~/.gdbinit |
接下来 我们一点一点开始讲。
程序执行与控制
执行程序及设置管理断点
当我们要执行一个程序的时候,我们需要输入run(简写r)或者start
1 | (gdb)/(pwndbg) run #程序直接跑一边,如果有断点进入断点处 |
在这之后,我们一般而言会设置断点(break,简写b)来方便调试程序:
1 | (gdb)/(pwndbg) b main |
b是用来断点的,或者说是标记断点且默认处于启动状态。我们也可以用disable或者dis、disa来禁用全部断点或者部分断点。启用断点则是enable或者en。而当某个断点不需要的时候,我们可以使用delete或者d来删除断点。其中每个断点都会有一个b_id号码。不过删除和禁用分别有两种语法需要注意:
1 | (gdb)/(pwndbg) d id #删除某id的断点 |
其实pwn方向一般不会设置太多断点啥的。不过逆向倒是要学这些。
如果想是一些开有PIE的保护的话,我们则需要一个*$rebase(偏移量)函数了。例如:
1 | b *$rebase(0x0f0) |
步操作
1 | int sum(int a,int b){ |
在上面这个代码里面,我们可以用GDB做一些基础的程序运行操作:
步入(**step**** 简写为****s****):**用于单步执行程序,当遇到函数调用时,会进入该函数内部,逐行执行函数的实现代码。当我运行到第 6 行的 printf 的时候,步入会进入 printf 函数中,一步步走过 printf 函数的实现代码。
步过(**next**** 简写为****n****):**同样是单步执行,但遇到函数调用时,不会进入函数内部,而是直接执行完该函数后,进入下一行代码等待执行。当我运行到第 7 行的 printf 的时候,步过会不管 printf 函数里面如何如何,直接运行完 printf 函数进入到第 8 行 sum 函数待执行。
s和n都可以在后面加一个<font style="color:rgba(0, 0, 0, 0.85);">instruction</font>参数,大概使用方法就是<font style="color:rgba(0, 0, 0, 0.85);">si</font>和<font style="color:rgba(0, 0, 0, 0.85);">ni</font>:
<font style="color:rgba(0, 0, 0, 0.85);">si</font>(step instruction):执行下一步汇编指令,会进入汇编或 C 函数内部,无论函数是否有调试信息。
<font style="color:rgba(0, 0, 0, 0.85);">ni</font>(next instruction):执行下一步汇编指令,不会进入任何函数内部,直接执行完当前汇编指令并停在下一条。
简单来说,<font style="color:rgba(0, 0, 0, 0.85);">s/n</font>适用于 C 代码层面的调试,<font style="color:rgba(0, 0, 0, 0.85);">si/ni</font>适用于更底层的汇编层面调试,核心区别在于是否进入函数、以及进入后的定位级别(C 级 vs 汇编级)。上面两个指令,一般情况下都会用n和ni。
步出(**finish**** 简写为****fin****):**适用于已进入某个函数内部,希望快速结束该函数的执行,直接返回到调用该函数的下一行代码。当我从第八行步入到 sum 函数之后,我感觉 sum 函数不需要看,想要直接从 sum 函数回到 main 函数的时候,步出可以快速运行完 sum 函数回到 main 函数的第九行待执行。
步止(**continue**/**until**** 简写为****c**/**u 行号****):**用于让程序持续运行,执行到断点停止。如果我想运行到第 10 行等待,那么我可以设置一个断点在第十行,这样执行步止操作的时候我就可以直接运行到第十行。
数据查看与修改
寄存器数据与汇编代码查看
在指令里面,分为_操作符号_和_被操作的对象_。例如下面的:
查看(info 简写为i):用于查看对象的值。例如查看断点则是i b
寄存器(registers 简写为r):一种被查看的对象。例如查看寄存器的值可以是i r
例如查看寄存器的值可以写成(gdb)(pwndbg) i r
反汇编(disassemble 函数名/$寄存器名(eip/rip)):将某一段函数或者rip/eip指针附近的机器码反编译为汇编。也是一种操作。
1 | (gdb)/(pwndbg) disassemble main |
内存数据查看
除了上面的i r,我还可以通过print(缩写为p)来查看寄存器的位置(也就是此寄存器的地址),例如p eax
如果我要看地址的话,就要用x指令来查询。
例如有一个movzx eax,BYTE PTR[rbp-0x20]的操作,我现在想要查询rbp-0x20这个地址里面的值,就可以用:x/20b $rbp-0x20的方式。这样就是一个byte一个byte的查询20个byte。其中语法是这样的:
1 | x/[查询行数][以多少位输出][显示格式] [查询的地址] |
如此这般的有:
x/20gx(64 位十六进制,显示 20 项)x/20wx(32 位)x/20bx(8 位)x/20bx(字符串显示)x/20ix(反汇编指令显示)
内存数据修改
如果我要修改这里的值,那么就用set指令,其中语法为:
1 | set ([地址值])=[修改值] #这个地址值外面的的括号必须有 |
例如set (rbp - 0x10) = 41
如此这般的有:
set *0x7ffffffe050=19(改写内存值)set $rax=19(改写寄存器值)set(char [4])0x80477a4 = "Ace"(改写内存字符串)








