]> git.somenet.org - pub/jan/ctf-seminar.git/blob - writeups/tortagel/36C3_hxp19.md
Add writeups
[pub/jan/ctf-seminar.git] / writeups / tortagel / 36C3_hxp19.md
1 # 36C3 hxp CTF 2019
2
3 ## Retrospective
4
5 I played on Saturday and Sunday, on both days about 7 hours, but I found the challenges either quit difficult or much guessing. So I lost a lot of time to find an interesting challenge for me. The `catch the flag` challenge was interesting, but I just started it a view hours before the end of the CTF and had not time to finish it.
6
7 ### catch the flag (not solved)
8
9 Given was a client (`client.py`) and a server (`game.py`) program which communicated over sockets. There was another python script (`world_generator.py`) which generated a random world for the game. This script was executed only once at the beginning, so the _world_ was every time the same. The `flag_char.py` script was only a helper script for the server (`game.py`).
10
11 A world looks something like that:
12
13 ```
14 00100000100000000000
15 00000000000000000000
16 00000101000000100000
17 00100000100000000000
18 00000000100000000000
19 00002222222222001000
20 01010000000000000000
21 11000000010000000000
22 00100010000000000000
23 00000010000000000000
24 ```
25
26 The real world was much bigger! We can navigate through this world with the directions **up**, **down**, **left** and **right**. We always start on the top left corner. If we _crash_ into a **1**, we lost. The **twos** are the flag which we have to _collect_. We cannot leave the game world.
27
28 Additionally we receive as response from the server the following _information_:
29
30 - `i\x00breeze`: horizontally or vertically next to us is a **1**.
31 - `i\x00smell`: horizontally or vertically next to us is a **2** (flag char).
32 - `iw`: we move onto a wall.
33 - `e\x00<some text>`: a message.
34 - `f\x00<some char>`: we found a flag char.
35 - `d:` we lost.
36
37 The client program is just a wrapper which does some fancy stuff on the different responses from the server - we do not really need it.
38
39 The tricky part now is that each character of the flag, initially in the center of the world, moves every second turn randomly one field up, down, left or right. So we cannot just easy navigate to the flag and collect all the characters from it.
40
41 I wrote the following script to explore the world a little bit:
42
43 ``` python
44 #!/usr/bin/env python3
45
46 import socket
47
48 HOST = "78.47.17.200"
49 PORT = 7888
50 WIDTH = 80
51 HEIGHT = 80
52
53 pos = (0, 0)
54
55 flag_chars = []
56
57 def read_map():
58     mapp = [[9 for i in range(WIDTH)] for j in range(HEIGHT)]
59     with open("exploit_map", "r") as f:
60         lines = f.read().strip().split('\n')
61         for y, line in enumerate(lines):
62             for x, c in enumerate(line):
63                 mapp[y][x] = int(c)
64     return mapp
65
66 def write_map(mapp):
67     with open("exploit_map", "w") as f:
68         for line in mapp:
69             f.write(''.join(map(str, line)) + "\n")
70
71 def receive_until_prompt(sock, prompt=b"\n"):
72     received = b""
73     buf_size = len(prompt)
74     while True:
75         new = sock.recv(buf_size)
76         received += new
77         for i in range(1, len(prompt) + 1):
78             if received.endswith(prompt[-i:]):
79                 if i == len(prompt):
80                     return received
81             else:
82                 buf_size = len(prompt) - i + 1
83         if not new:
84             raise Exception(f"Connection closed before {prompt} was found.")
85
86 def recv(sock):
87     data = receive_until_prompt(sock)
88     data = data.strip()
89     data = data.split(b"\x00")
90     return data
91
92 def set_curr_pos(mapp, val):
93     x, y = pos
94     mapp[y][x] = val
95     write_map(mapp)
96
97 def parse(data, mapp):
98     res = None
99     if data[0] == b"c":
100         print(f"c-text={data[1]}")
101     elif data[0] == b"e":
102         print(f"e-text={data[1]}")
103     elif data[0] == b"i":
104         last = data
105         set_curr_pos(mapp, 0)
106         if len(data) == 1:
107             #print("nothing")
108             None
109         elif len(data) == 2:
110             if data[1] == b'breeze':
111                 print("breeze")
112                 res = "breeze"
113             elif data[1] == b'smell':
114                 print("smell")
115         elif len(data) == 3:
116             print(f"breeze and smell: {data}")
117             res = "breeze"
118         else:
119             print(f"???? {data}")
120     elif data[0] == b"iw":
121         print("wall")
122     elif data[0] == b"d":
123         print("dead")
124         set_curr_pos(mapp, 1)
125         res = "dead"
126     elif data[0] == b"f":
127         set_curr_pos(mapp, 0)
128         print(f"flag char={data[1]}")
129         flag_chars.append(data[1])
130     write_map(mapp)
131     return res
132
133 def set_pos(direction):
134     global pos
135     x, y = pos
136     if direction == "s":
137         pos = (x, y + 1)
138     elif direction == "w":
139         pos = (x, y - 1)
140     elif direction == "a":
141         pos = (x - 1, y)
142     elif direction == "d":
143         pos = (x + 1, y)
144     x, y = pos
145     if x < 0:
146         pos = (0, y)
147     if x >= WIDTH:
148         pos = (WIDTH - 1, y)
149     if y < 0:
150         pos = (x, 0)
151     if y >= HEIGHT:
152         pos = (x, HEIGHT - 1)
153
154 def move(s, direction, mapp):
155     set_pos(direction)
156     s.sendall(f"{direction}\n".encode())
157     data = recv(s)
158     return parse(data, mapp)
159
160 def connect(mapp):
161     global pos
162     pos = (0, 0)
163     s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
164     s.connect((HOST, PORT))
165     parse(recv(s), mapp)
166     return s
167
168 mapp = read_map()
169
170 s = connect(mapp)
171
172 for d in range(WIDTH):
173     way = "d"*d  + "s"*HEIGHT
174     #way = "s"*d  + "d"*WIDTH
175     for direction in way:
176         res = move(s, direction, mapp)
177         if res == "dead":
178             s = connect(mapp)
179             break
180         elif res == "breeze":
181             res = move(s, "a", mapp)
182             if res == "dead":
183                 s = connect(mapp)
184                 break
185
186 print(f"flag_chars={flag_chars}")
187 ```
188
189 But obviously this does not really help us, because even if we find every flag character, we do not know the correct order. So I looked again to the scripts and found the following line in the `flag_char.py` file: `random.seed(int(time.time()))`. So we know the seed and can thus predict the position of the flag characters.
190
191 So a solution could be the following:
192
193 1. Explore the whole world to know where the **ones** are.
194 2. Move randomly around to find all the flag characters to know the length of the flag.
195 3. Predict the position of each character with the known seed.
196
197 As I said at the beginning, the time was over after I found out all this information.
198
199 ### flag concat (not solved)
200
201 Given was a program which took two flags as input and concatenated them. If a flag included the string `hxp{`, it ignored the characters before this part. So e.g. if flag 1 is `aaahxp{flag1}\n` and flag 2 is `bbbhxp{flag2}\n` the output is `hxp{flag1}\nhxp{flag2}\n`.
202
203 I looked for a while at the source code and to the man pages to find something for a buffer overflow. With gdb I found out, we have to change the return address from `0x4009cf` to `0x4007b6`. The behavior with the packed_strings struct was somehow weird to me. Everything I tried to overwrite the return address on the stack did not work.