文章目录
前言
之前学栈时gdb调试太少了,这次通过学堆把动态调试过程都再了掌握一遍。刚开始学堆,可能存在错误或不足,之后会不定时修改补充。
一 :gdb动态调式时常见的基本命令
1、基本的调试命令
s step,si单步步进
n 执行下一条指令 ni步入
b 在某处下断点,可以用 b * adrress(地址) 与 b function_name(函数名字)
info b 查看断点信息
delete 删除所有断点
c 继续
r 重新执行
3、P命令的使用
p system/main 显示某个函数地址
p $esp 显示寄存器
p/x p/a p/b p/s。。。
p 0xff - 0xea 计算器
print &VarName 查看变量地址
p * 0xffffebac 查看某个地址处的值
3、X命令的使用
命令格式:x/<n/f/u> <addr>
n是一个正整数,表示需要显示的内存单元的个数
f 表示显示的格式(可取如下值: x 按十六进制格式显示变量。d 按十进制格式显示变量。u 按十进制格式显示无符号整型。o 按八进制格式显示变量。t 按二进制格式显示变量。a 按十六进制格式显示变量。i 指令地址格式c 按字符格式显示变量。f 按浮点数格式显示变量。)
u 表示从当前地址往后请求的字节数 默认4byte,u参数可以用下面的字符来代替,b表示单字节,h表示双字节,w表示四字 节,g表示八字节。
<addr>表示一个内存地址
x/xw addr 显示某个地址处开始的16进制内容,如果有符号表会加载符号表
x/x $esp 查看esp寄存器中的值
x/s addr 查看addr处的字符串
x/b addr 查看addr处的字符
x/i addr 查看addr处的反汇编结果
4、堆中常见命令的使用
heap:查看堆中的chunk情况,之后结合x命令查看每一个chunk的内部情况
bins:查看程序运行过程中bin链表的分布情况。
二 :手动输入调试过程
(1)打开gdb,运行程序
首先打开gdb,r运行一下程序。先执行两次add命令,来查看chunk的分布。
程序链接:https://buuoj.cn/challenges#hitcontraining_uaf
(2)heap命令查看chunk个数
ctrl + c断掉,然后再heap命令查看程序中的堆分布
存在6个chunk,根据其大小可以确定其中 ②~⑤ 号chunk是由我们分配的,
(其中第一个分配的chunk具体是什么我也不是很清楚,但是每一个程序都有。最后一个chunk是top chunk,我们分配的chunk就是从这里面来的。)
这些chunk如何分布的,大小如何确定,内容是如何存放的,这个我们打开chunk来具体分析一下。
## (3) X命令查看chunk里内容 查看chunk里面的内容主要靠一个x命令(前面有讲),其中x命令的地址是从第②个chunk开始(32位用wx,64位用gx。这样的目的是为了方便查看,至于输出大小自己决定)
②~⑤这些chunk是由我们自己来决定的,我们就重点讲一下
-
首先 ②和③ chunk是输入第一次add时开辟的,执行一次add开辟两个chunk,说明add函数当中存在两次malloc,具体的情况需要结合ida来分析。
-
②的是0x11,首先chunk存在一个head头,size与pre_size占了8个字节,然后后面还有一个打印的函数以及它的参数(打印的位置),至于为什么知道是一个puts函数,打开ida点击跳转到地址==》然后输入地址就会跳转到该函数。这就是0x10,还有一字节是因为size字段的低三位是一个状态标识,当前面的chunk不为空时表示为1,
-
③的是0x21,这个chunk才是由用户自己决定的,包括其大小与内容。0x21 = 0x10(你自己输入的大小) + 0x8(32位的head的size与pre_size占8个字节)+ 0x1(状态标识) + 0x8(此处留坑)。可以看到我们输入的内容aaaa存在这个chunk里面,而且puts函数的参数指向的也是这里。
-
接下来看一下整体流程,我们add创建一个堆空间,它开辟了两个chunk,第一个chunk大小固定 是puts函数,它的参数是一个地址,指向我们输入的内存处。第二个chunk是我们自己决定的,可以输入大小以及内容,
-
④和⑤和前面的②和③一样,只不过我们自己在前面开辟的大小是0x20罢了。
查看完以后要想再继续调试,就按c继续执行,然后输入命令
手动调试肯定是比较慢的,真的做题需要使用脚本进行调试,前期像我这样的不会做,就一行一行代码的去调试查看,查看差别,来弄懂程序的实现过程。
三 :写脚本调试
(1)首先需要一个基本的脚本代码(像libc一样),先编写一个脚本保存,之后遇到题目以后根据题目的输入输出格式进行修改。这里我的一个脚本大家可以参考
from pwn import *
r = process("./pwn")
context(log_level = 'debug')
def add(size, content):
r.recvuntil(":")
r.sendline("1")
r.recvuntil(":")
r.sendline(str(size))
r.recvuntil(":")
r.sendline(content)
def delete(index):
r.recvuntil(":")
r.sendline("2")
r.recvuntil(":")
r.sendline(str(index))
def show(index):
r.recvuntil(":")
r.sendline("3")
r.recvuntil("Index :")
r.sendline(str(index))
gdb.attach(r)
pause()
r.interactive()
主要就是三个函数,有些程序有四个程序多了一个fill函数,这些函数的构造就是按照这个程序的流程进行的。比如add函数:首先要执行add函数,看目录需要先按1,然后add函数里面需要输入大小以及内容。具体题目具体分析修改。
然后就是下面两行代码是进行动态调式的两行关键代码,还有其他代码也可以请自行百度
gdb.attach®
pause()
(2)具体如何调试
首先先加两个add函数,然后python运行脚本。这样的效果和我们前面自己手动输入的效果是一样的
前面讲了add函数,接下来讲讲释放以后的内存布局
可以发现第一个chunk已经被清空,但是需要注意的是第二个chunk没有清空,我觉得这儿说明这个程序存在问题,再查看chunk内部,发现puts地址和内容已经被清空
之后就是自己根据题目,一步一步的去调试了。前期先看别人的脚本,一行一行的去调试,然后看懂每一行的代码的作用含义。真正弄懂一道题以后就发现自己已经不一样了。
总结
学会了动态调试,就看着别人的脚本一步一步去调试吧。