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 がパスされない問題が起こりました.
コードの順序は以下のようになっていました.
process()でバイナリを起動- stdin/stderr を送信 (Stage 2)
"\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.
