Post

Pwnable.kr 「input2 (Toddler's Bottle)」Writeup

linuxの基礎知識を確かめる問題

Pwnable.kr 「input2 (Toddler's Bottle)」Writeup

pwnable_kr-input2

Summary

本問は,linuxの基礎知識を確かめる問題です.

  • Category: Pwn
  • Description: Mom? how can I pass my input to a computer program?
  • Tools & TechStack:
    • C
  • Release: N/A

階層構造

1
2
3
4
5
6
7
8
9
10
11
.
├── Dockerfile
├── flag
├── input2
├── input2.c
├── loader.sh
├── readme
├── run.sh
└── super.pl

1 directory, 8 files

ソースコードの解析

Stage 1 ~ Stage 5 まで,Linuxの基礎を確認する問題になっています.

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int main(int argc, char *argv[], char *envp[])
{
	printf("Welcome to pwnable.kr\n");
	printf("Let's see if you know how to give input to program\n");
	printf("Just give me correct inputs then you will get the flag :)\n");

	// argv
	if (argc != 100)
		return 0;
	if (strcmp(argv['A'], "\x00"))
		return 0;
	if (strcmp(argv['B'], "\x20\x0a\x0d"))
		return 0;
	printf("Stage 1 clear!\n");

	// stdio
	char buf[4];
	read(0, buf, 4);
	if (memcmp(buf, "\x00\x0a\x00\xff", 4))
		return 0;
	read(2, buf, 4);
	if (memcmp(buf, "\x00\x0a\x02\xff", 4))
		return 0;
	printf("Stage 2 clear!\n");

	// env
	if (strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef")))
		return 0;
	printf("Stage 3 clear!\n");

	// file
	FILE *fp = fopen("\x0a", "r");
	if (!fp)
		return 0;
	if (fread(buf, 4, 1, fp) != 1)
		return 0;
	if (memcmp(buf, "\x00\x00\x00\x00", 4))
		return 0;
	fclose(fp);
	printf("Stage 4 clear!\n");

	// network
	int sd, cd;
	struct sockaddr_in saddr, caddr;
	sd = socket(AF_INET, SOCK_STREAM, 0);
	if (sd == -1)
	{
		printf("socket error, tell admin\n");
		return 0;
	}
	saddr.sin_family = AF_INET;
	saddr.sin_addr.s_addr = INADDR_ANY;
	saddr.sin_port = htons(atoi(argv['C']));
	if (bind(sd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
	{
		printf("bind error, use another port\n");
		return 1;
	}
	listen(sd, 1);
	int c = sizeof(struct sockaddr_in);
	cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t *)&c);
	if (cd < 0)
	{
		printf("accept error, tell admin\n");
		return 0;
	}
	if (recv(cd, buf, 4, 0) != 4)
		return 0;
	if (memcmp(buf, "\xde\xad\xbe\xef", 4))
		return 0;
	printf("Stage 5 clear!\n");

	// here's your flag
	setregid(getegid(), getegid());
	system("/bin/cat flag");
	return 0;
}

Stage 1

1
2
3
4
5
6
7
8
9
10
11
12
13
int main(int argc, char *argv[], char *envp[])
{
#...
	// argv
	if (argc != 100)
		return 0;
	if (strcmp(argv['A'], "\x00"))
		return 0;
	if (strcmp(argv['B'], "\x20\x0a\x0d"))
		return 0;
	printf("Stage 1 clear!\n");
#...
}

argc (コマンド引数の数) が 100 個であることを確認しています.
'A' は ASCII コードで表すと 65 , 'B' は 66 で表されます.
そのため,argv['A'] は argv[65], argv['B'] は argv[66] として処理されることになります.

よって,以下を満たせば Stage 1 はパスできます.

  • argv[65] == "\x00"
  • argv[66] == "\x20\x0a\x0d"

Stage 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(int argc, char *argv[], char *envp[])
{
#...
	// stdio
	char buf[4];
	read(0, buf, 4);
	if (memcmp(buf, "\x00\x0a\x00\xff", 4))
		return 0;
	read(2, buf, 4);
	if (memcmp(buf, "\x00\x0a\x02\xff", 4))
		return 0;
	printf("Stage 2 clear!\n");
#...
}
  • 1つ目の if: FD0 (stdin) で,標準入力から渡されたバイト列が \x00\x0a\x00\xff であること.
  • 2つ目の if: FD2 (stderr) で,標準エラー出力から渡されたバイト列が \x00\x0a\x02\xff であること.

Stage 3

1
2
3
4
5
6
7
8
9
int main(int argc, char *argv[], char *envp[])
{
#...
	// env
	if (strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef")))
		return 0;
	printf("Stage 3 clear!\n");
#...
}

このステージでは,環境変数へのポインタ char *envp[] に渡す値を調整すればよさそうです.

  • \xde\xad\xbe\xef という名前(バイト列)の環境変数の値を,\xca\xfe\xba\xbe に変更する

Stage 4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main(int argc, char *argv[], char *envp[])
{
#...
	// file
	FILE *fp = fopen("\x0a", "r");
	if (!fp)
		return 0;
	if (fread(buf, 4, 1, fp) != 1)
		return 0;
	if (memcmp(buf, "\x00\x00\x00\x00", 4))
		return 0;
	fclose(fp);
	printf("Stage 4 clear!\n");
#...
}

ファイル名が \x0a (1byteの改行文字) かつ,ファイルの中身が4バイトの \x00\x00\x00\x00 であればパスできます.

Stage 5

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
int main(int argc, char *argv[], char *envp[])
{
#...
	// network
	int sd, cd;
	struct sockaddr_in saddr, caddr;
	sd = socket(AF_INET, SOCK_STREAM, 0);
	if (sd == -1)
	{
		printf("socket error, tell admin\n");
		return 0;
	}
	saddr.sin_family = AF_INET;
	saddr.sin_addr.s_addr = INADDR_ANY;
	saddr.sin_port = htons(atoi(argv['C']));
	if (bind(sd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
	{
		printf("bind error, use another port\n");
		return 1;
	}
	listen(sd, 1);
	int c = sizeof(struct sockaddr_in);
	cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t *)&c);
	if (cd < 0)
	{
		printf("accept error, tell admin\n");
		return 0;
	}
	if (recv(cd, buf, 4, 0) != 4)
		return 0;
	if (memcmp(buf, "\xde\xad\xbe\xef", 4))
		return 0;
	printf("Stage 5 clear!\n");
#...
}

最後のステージでは,ソケットプログラムが実装されています.
重要な部分は待ち受けを行う IP, PORT と送信すべきバイト列です.

  • IP: saddr.sin_addr.s_addr = INADDR_ANY; より,INADDR_ANY は 0.0.0.0 で全ネットワークインタフェースで待ち受けるものなので,ローカルループバックアドレス 127.0.0.1 でよさそうです.
  • PORT: saddr.sin_port = htons(atoi(argv['C'])); より,Stage 1 と同じで,argv[67] == 任意ポート が指定できます.
  • バイト列: 127.0.0.1: PORT に対して,\xde\xad\xbe\xef を送ればいいようです.

Exploit を書く

これらのすべてをpwntoolsとsocketを用いて書きます.

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 pwn import *
import socket, time

argv = [b"./input2"]              # argv[0]

# Stage 1
argv += [b"filler"] * 64          # argv[1] ~ argv[64]
argv += [b""]                     # argv[65] = '\x00' (空文字列)
argv += [b" \n\r"]                # argv[66] = ' \n\r'
argv += [b"8192"]                 # Stage 5, argv[67] = PORT
argv += [b"filler"] * 32          # argv[68] ~ argv[99]
assert(len(argv) == 100) # argv が丁度100個であること

# Stage 4
# ファイル名が改行文字 1byte だけのファイルを,プロセス起動前に作成しておく
with open(b"\x0a", "wb") as f:
    f.write(b"\x00\x00\x00\x00")

# Stage 3
# \xde\xad\xbe\xef という名前の環境変数を,適切な値とともにプロセスに渡す
p = process(argv=argv, stderr=PTY, env={
    b"\xde\xad\xbe\xef": b"\xca\xfe\xba\xbe"
})

# Stage 2
# FD0: stdin
p.send(b"\x00\x0a\x00\xff")
# FD2: stderr
p.proc.stderr.write(b"\x00\x0a\x02\xff")
p.proc.stderr.flush()

# Stage 5
time.sleep(1)  # サーバがaccept()に到達するまで少し待つ
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("127.0.0.1", 8192))
s.send(b"\xde\xad\xbe\xef")
s.close()

data = p.recvall(timeout=3)
print(data.decode(errors='replace'))
1
2
3
4
5
6
7
8
9
10
11
12
13
input2@de00a95be9d1:/tmp/tmp.JInzLhdnuS$ python3 exploit.py 
[+] Starting local process './input2': pid 390
[+] Receiving all data: Done (251B)
[*] Process './input2' stopped with exit code 0 (pid 390)
Welcome to pwnable.kr
Let's see if you know how to give input to program
Just give me correct inputs then you will get the flag :)
Stage 1 clear!
Stage 2 clear!
Stage 3 clear!
Stage 4 clear!
Stage 5 clear!
<REDACTED>

Post-Mortem & Dead ends

レースコンディションのミス

Stage 4 がパスされない問題が起こりました.
コードの順序は以下のようになっていました.

  1. process() でバイナリを起動
  2. stdin/stderr を送信 (Stage 2)
  3. "\x0a" ファイルを作成 (Stage 4 用)

送信 (p.send / flush) は非同期なので,進む前にプロセスが Stage 4 を追い越していました.
そのため,Stage 4 のファイル作成を process() の前に移動することで,正常に動作しました.

References

N/A

This post is licensed under CC BY 4.0 by the author.