前言
这个题目是一个c++的堆题,而我自己对于c++的一些内存分配不太了解,同时也不太会c++的逆向,硬看是没有办法了,所以就想能不能通过fuzz的角度去进行利用
fuzz
大概思路
函数选择
可以看到有add delete switch read listuser message6个操作,而凭感觉来说,listuser一般没什么效果,所以我这里主要fuzz其他五个函数
交互
简单写一下交互
1 | def add(name): |
参数
这里可以注意到,参数总共有3种,1 username,2 message的size,3 message的data
username这里我就简单一个random.choice(“abcdefgh”) 随机取值,大概范围就是8个值,样本范围不适合太大
size 这里我简单看了一下程序,里面有个分配0x60大小的堆,保存了用户的信息,然后我就想把message的size和这个用户的size构造一样,有可能会有uaf的问题
data 这里我就随便写了短的内容,最开始也没想过溢出,还是想fuzz double free这种问题
初步fuzz结构
下面是一个fuzz的例子,可以看到思路就很简单,随机执行操作,然后记录到log里面
1 | def fuzz(): |
但是在做这个题目的时候遇到了其他的一些问题,不像之前fuzz其他题目一样,这个题目fuzz很容易报错,那么我们必须要进行一个异常的捕获
处理异常
处理异常这里坑还是比较多的,我就分别描述
无法获取程序异常返回结果
在交互里面,我们比较习惯写成sla,或者ru这种,他报错的时候不会返回给我们程序的报错内容,所以这里我通过阅读ru的实现,修改了一下代码,把程序的返回值放到Exception里面,然后我们就可以知道程序到底报了什么错,是double free呢,还是invalid pointer
log记录问题
log里面少记录
按照前面提到的fuzz结构,增加异常处理后类似于下面的代码
1 | def fuzz(): |
这种写法,如果在执行流程里面比如说add里报了错,因为抛了异常导致这个操作会无法记录下来
log里面多记录
针对上面少记录的情况,可能我们会这样改
1 | def fuzz(): |
但是上面这种修改又会带来新的问题
我们add操作输入完username后程序抛了异常,但是这个时候不会在add这里结束,他还会执行到下一个流程,然后会在下一个流程的sla(b”listuser, exit”)这里抛异常,然后我们就会多记录一个操作
最终解决方法
在这里,我们需要定义一个流程完整的生命周期
交互开始,从收到):代表我们流程的开始
交互结束,收到Choose action为止
这样定义后我们可以保证下一个流程能够正常的开始,就不会出上面提到的多记录的问题了,也就能够保证,当前流程抛出的异常一定是当前流程里执行某些操作引起的,而不是上一个流程遗留的异常
总共就是下面三个情况
- 如果add中间出错了,那么flag=0,我们会在excpet里面记录
- 如果ru报错,那么证明add完之后出现了问题,flag=0,也会记上
- 如果本次add操作没有触发任何异常,那么也会记录在log里,同时也能够成功执行到下一次的操作,按照上面的结构fuzz,一方面我们可以获取到程序的异常原因,另一个方面也可以不多不少的记录下来执行的操作
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
33
34def fuzz():
global io
io = process("./chatting")
f=open("log.txt","w")
try:
for i in range(0,0x1000):
flag=0#防止在执行函数的时候报错
tmp=""
if i % 19 == 0:
name=random.choice("abcdefgh")
tmp=f'add("{name}")\n'
add(name)
ru('Choose action')
flag=1
f.write(tmp)
elif i%4==0:
name=random.choice("abcdefgh")
tmp=f'free("{name}")\n'
free(name)
ru('Choose action')
flag=1
f.write(tmp)
except Exception as e:
if flag==0:
f.write(tmp)
if b"double free or corruption" not in e.args[0]:
return 0
else:
print(e.args[0])
return 1
finally:
f.close()
io.close()
开始fuzz
保留所有结果的fuzz
这个fuzz里面保留了所有的结果
1 | #!/usr/bin/python3 |
方便的验证脚本
1 | #!/usr/bin/python3 |
去掉一些无用操作的fuzz
fuzz里会有一些无用操作,比如说我们free一个不存在的index的结构体,那么他会提示not found,同时实际上他也没有影响堆布局等,这些可以忽略的
先调整一下delete
1 | name=random.choice("abcdefgh") |
调整一下switch
1 | name=random.choice("abcdefgh") |
经过测试发现,虽然message会报Recipient not found!的错误,但是我们不能把他过滤,实际上他还是做了对应的操作的,下面是代码部分
那从纯fuzz的角度来说呢,我们就是一个个试,看看哪些是可以忽略,哪些不能忽略
如果我们保存log的时候忽略了一些看起来没有效果的操作,但是实际上这些操作可能影响了堆布局,我们在复现log里面保存的payload的时候就会无法触发异常
所以本次经过测试,show delete switch操作可以忽略一些无效的操作,但是message不行
1 | #!/usr/bin/python3 |
结果
1 | add("b") |
修改payload
当我们去掉最后一个的时候可以发现这里已经double free了,那么我们只需要按照正常操作,去写free_hook即可
1 | message("c",0x58,p64(libc_base+libc.sym["__free_hook"])) |
最后发现add会free message,直接rce
all payload
1 | #!/usr/bin/python3 |
总结
- 注意异常处理部分
- 注意每一个流程完整的生命周期,防止当前流程触发的异常实际是上一个操作的
- 不要随意忽视看起来没有效果的操作,除非能保证去除那些操作之后不影响fuzz的结果,或者说除非我们从代码层面能够保证实际是没有效果的