Skip to content

Commit 29272b6

Browse files
committed
off by one badges
1 parent e9950eb commit 29272b6

File tree

6 files changed

+642
-0
lines changed

6 files changed

+642
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,369 @@
1+
---
2+
title: Singapore Off-By-One Conference 2024 Hardware Badge
3+
description: writeup on hardware badge from off-by-one conference
4+
date: 2024-06-27 22:00:00 +0800
5+
categories: [Writeups]
6+
img_path: /assets/posts/2024-06-27-off-by-one-badge-2024/
7+
tags: [hardware]
8+
toc: True
9+
---
10+
11+
I attended the first ever Vulnerability Research based cybersecurity conference in Singapore -- [**Off By One**](https://offbyone.sg/) organized by __[Star Labs](https://starlabs.sg/), [Eugene Lim](https://spaceraccoon.dev) and [Sim Cher Boon](https://twitter.com/cherboon)__ -- and had an amazing time!
12+
13+
This conference featured a super cool hardware badge made by [Manzel](https://manzelseet.com/), which contained some hardware based challenges with a total of 6 flags.
14+
15+
This is my first time working on such hardware challenges so I thought I should document my experience :)
16+
17+
Also huge kudos for my teammates Gatari and Sunshinefactory for suffering with me, couldn't have done this without them!
18+
19+
![](powerpuff_girls.jpg)
20+
_my powerpuff friends are so cool!! i love this so much hehe_
21+
22+
## The Badge
23+
24+
![](badge_front.jpg)
25+
_front of badge_
26+
27+
![](badge_back.jpg)
28+
_back of badge_
29+
30+
This badge features 2 microcontrollers, the Arduino and ESP32S3, with 2 LCD screens alongside a DPAD and two buttons which can be used to interact with the badge functionality.
31+
32+
The functionality includes:
33+
34+
- Web Server
35+
- Bluetooth Spamming
36+
- Eyes _(basically just two PNG)_
37+
- Roulette _(some random number generator??)_
38+
39+
We can further interact with the badge through the serial port _(using either putty or [arduino labs](https://labs.arduino.cc/en/labs/micropython))_, which would greet us with a MicroPython repl.
40+
41+
> Micropython is a lightweight version of python with stripped functionality that is meant for embedded devices with limited storage space.
42+
{:.prompt-tip}
43+
44+
## Enumerating the Badge
45+
46+
### Extracting the Filesystem
47+
48+
We can look through the filesystem in micropython, and we notice that there are a few files and folders contained within the badge.
49+
50+
```py
51+
>>> import os
52+
>>> os.listdir()
53+
['boot.py', 'eyes', 'lib', 'starlabs']
54+
```
55+
56+
We can write a script to extract the filesystem for easier analysis, however there is no trivial way to extract the files through serial port.
57+
58+
Since `ESP32-S3` supports connecting to WiFi, we can simply connect to the internet and send the files back to ourselves via python sockets.
59+
60+
```py
61+
# script to connect to wifi
62+
import network
63+
sta_if = network.WLAN(network.STA_IF)
64+
sta_if.active(True)
65+
sta_if.scan() # Scan for available access points
66+
sta_if.connect("<AP_name>", "<password>") # Connect to an AP
67+
sta_if.isconnected() # Check for successful connection
68+
```
69+
70+
```py
71+
# script to run on the badge to send out the files
72+
def list_files_recursively(directory):
73+
files_list = []
74+
for item in os.listdir(directory):
75+
item_path = directory + '/' + item
76+
if os.stat(item_path)[0] & 0x4000: # Check if it's a directory
77+
files_list.extend(list_files_recursively(item_path))
78+
else:
79+
files_list.append(item_path)
80+
return files_list
81+
82+
import socket
83+
84+
s = socket.socket()
85+
s.connect(('X.X.X.X', 4444))
86+
87+
for file in list_files_recursively('./'):
88+
s.send(f"SPLIT{file}CONTENT")
89+
with open(file, "rb") as f:
90+
s.send(f.read())
91+
print(file)
92+
93+
s.close()
94+
```
95+
96+
### Extracting the Filesystem
97+
98+
In addition to the filesystem, we are also interested in the flash memory of the ESP32 _(essentially extracting the firmware)_.
99+
100+
We can do so using the `esp.flash_read` function that is exposed via the micropython. Afterwards, we can similarly send out the data back to ourselves over the internet.
101+
102+
```py
103+
# script to send out flash from badge
104+
import esp
105+
106+
flash_size = esp.flash_size()
107+
start_addr = 0x0
108+
# we need to segment our packets due to limited memory
109+
block_size = 1024*20
110+
buf = bytearray(block_size)
111+
112+
print("--------------------")
113+
print(f"flash size: {hex(flash_size)}")
114+
print(f"start addr: {hex(start_addr)}")
115+
print("--------------------")
116+
117+
import socket
118+
119+
s = socket.socket()
120+
s.connect(('X.X.X.X', 4444))
121+
122+
for i in range(flash_size//block_size):
123+
print(f'[*] {i/(flash_size/block_size)*100}%')
124+
esp.flash_read(start_addr+block_size*i, buf)
125+
s.send(bytes(buf))
126+
127+
s.close()
128+
```
129+
130+
If we `strings` the firmware, we can actually obtain 2 flags already XD.
131+
132+
For the sake of completeness, I'll go through each flag individually in the next sections.
133+
134+
## Flag 1: Welcome Flag
135+
136+
_During the CTF, I got this flag by dumping the firmware strings._
137+
138+
Essentially, we can list the device information using `lsusb` in linux, and the flag is available in the USB description.
139+
140+
```sh
141+
$ lsusb -v
142+
Bus 001 Device 005: ID 303a:4001 STAR LABS SG #BadgeLife
143+
Device Descriptor:
144+
bLength 18
145+
bDescriptorType 1
146+
bcdUSB 2.00
147+
bDeviceClass 239 Miscellaneous Device
148+
bDeviceSubClass 2
149+
bDeviceProtocol 1 Interface Association
150+
bMaxPacketSize0 64
151+
idVendor 0x303a
152+
idProduct 0x4001
153+
bcdDevice 1.00
154+
iManufacturer 1 STAR LABS SG
155+
iProduct 2 #BadgeLife
156+
iSerial 3 {Welcome_To_OffByOne_2024}
157+
bNumConfigurations 1
158+
159+
```
160+
161+
## Flag 2: Arduino I2C
162+
163+
After messing around with the micropython modules that expose the embedded devices, I came across the `arduino` module that seemed interesting.
164+
165+
```py
166+
>>> dir(arduino)
167+
['__class__', '__init__', '__module__', '__qualname__', '__dict__', 'off', 'on', 'i2c']
168+
>>> dir(arduino.i2c)
169+
['__class__', 'readinto', 'start', 'stop', 'write', 'init', 'readfrom', 'readfrom_into', 'readfrom_mem', 'readfrom_mem_into', 'scan', 'writeto', 'writeto_mem', 'writevto']
170+
```
171+
172+
I2C seems to be a protocol that allows data to be sent. We can use `arduino.i2c.scan()` to scan for open ports, then use `arduino.i2c.readfrom` to read the contents in these ports.
173+
```py
174+
>>> arduino.i2c.scan()
175+
[48, 49]
176+
>>> arduino.i2c.readfrom(48, 100)
177+
b'Welcome to STAR LABS CTF. Your first flag is starlabs{i2c_flag_1}' # truncated
178+
>>> arduino.i2c.readfrom(49, 100)
179+
b'The early bird catches the worm. System uptime: 20473. You are too late. Reboot the arduino and try again.'
180+
```
181+
182+
This gives us the flag, `starlabs{i2c_flag_1}`
183+
184+
## Flag 3: Arduino I2C Part 2
185+
186+
I had an oversight which caused me to not obtain this flag during the CTF.
187+
188+
If you noticed in the previous part, one of the I2C ports gave us the flag and the other one told us we were late.
189+
190+
The second I2C port asked us to reboot the arduino and try again. Apparently the I2C port prints out the flag one letter at a time when the arduino has just booted.
191+
192+
We can write a python script to extract the flag from the I2C.
193+
194+
```py
195+
arduino.off()
196+
arduino.on()
197+
time.sleep(3)
198+
for i in range(100):
199+
time.sleep_ms(2)
200+
x = arduino.i2c.readfrom(49, 100).rstrip(b'\xff')
201+
if b'flag' in x:
202+
print(x)
203+
```
204+
205+
```
206+
'The early bird catches the worm. System uptime: 200. You are an early bird, here is your flag: s'
207+
b'The early bird catches the worm. System uptime: 201. You are an early bird, here is your flag: t'
208+
b'The early bird catches the worm. System uptime: 202. You are an early bird, here is your flag: a'
209+
b'The early bird catches the worm. System uptime: 203. You are an early bird, here is your flag: r'
210+
b'The early bird catches the worm. System uptime: 204. You are an early bird, here is your flag: l'
211+
b'The early bird catches the worm. System uptime: 205. You are an early bird, here is your flag: a'
212+
b'The early bird catches the worm. System uptime: 206. You are an early bird, here is your flag: b'
213+
b'The early bird catches the worm. System uptime: 207. You are an early bird, here is your flag: s'
214+
b'The early bird catches the worm. System uptime: 208. You are an early bird, here is your flag: {'
215+
b'The early bird catches the worm. System uptime: 209. You are an early bird, here is your flag: i'
216+
b'The early bird catches the worm. System uptime: 210. You are an early bird, here is your flag: 2'
217+
b'The early bird catches the worm. System uptime: 211. You are an early bird, here is your flag: c'
218+
b'The early bird catches the worm. System uptime: 212. You are an early bird, here is your flag: _'
219+
b'The early bird catches the worm. System uptime: 213. You are an early bird, here is your flag: f'
220+
b'The early bird catches the worm. System uptime: 214. You are an early bird, here is your flag: l'
221+
b'The early bird catches the worm. System uptime: 215. You are an early bird, here is your flag: a'
222+
b'The early bird catches the worm. System uptime: 216. You are an early bird, here is your flag: g'
223+
b'The early bird catches the worm. System uptime: 217. You are an early bird, here is your flag: _'
224+
b'The early bird catches the worm. System uptime: 218. You are an early bird, here is your flag: 3'
225+
b'The early bird catches the worm. System uptime: 219. You are an early bird, here is your flag: }'
226+
b'The early bird catches the worm. System uptime: 220. You are an early bird, here is your flag: '
227+
```
228+
229+
The flag is `starlabs{i2c_flag_3}`.
230+
231+
## Flag 4: flaglib
232+
233+
I initially solved this by dumping the flag from the firmware, but afterwards I found the intended solution.
234+
235+
By enumerating the micropython REPL, we find that there is a builtin module called **flaglib**.
236+
237+
```py
238+
>>> help('modules')
239+
__main__ btree hashlib select
240+
_asyncio builtins heapq socket
241+
_boot cmath inisetup ssl
242+
_espnow collections io struct
243+
_onewire cryptolib json sys
244+
_thread deflate machine time
245+
_webrepl dht math uasyncio
246+
apa106 ds18x20 micropython uctypes
247+
array errno mip/__init__ umqtt/robust
248+
asyncio/__init__ esp neopixel umqtt/simple
249+
asyncio/core esp32 network upysh
250+
asyncio/event espnow ntptime urequests
251+
asyncio/funcs flaglib onewire webrepl
252+
asyncio/lock flashbdev os webrepl_setup
253+
asyncio/stream framebuf platform websocket
254+
binascii gc random
255+
bluetooth gc9a01 re
256+
Plus any modules on the filesystem
257+
>>> import flaglib
258+
>>> dir(flaglib)
259+
['__class__', '__name__', '__dict__', 'getflag']
260+
>>> flaglib.getflag("TESTING")
261+
'???????'
262+
```
263+
264+
flaglib exposes a `getflag` function which takes in a string and returns a bunch of question marks.
265+
266+
By doing some intelligent guessing, we can realize that the function returns question mark if our corresponding flag character is wrong.
267+
268+
```py
269+
>>> flaglib.getflag("{")
270+
'{'
271+
```
272+
273+
In that case, we can brute force the flag with this script.
274+
275+
```python
276+
import flaglib
277+
printable = r"{}abcdefghijklmnopqrstuvwxyz_1234567890"
278+
flag = "{"
279+
280+
while flag[-1] != '}':
281+
for x in printable:
282+
check = flaglib.getflag(flag+x)[-1]
283+
if check[-1] != '?':
284+
flag += x
285+
print(flag)
286+
```
287+
288+
The flag is `{my_compiled_python_library}`.
289+
290+
## Flag 5: The Roulette
291+
292+
By reading the `boot.py` file that details the functionality of the program, we realize that it imports a library `roulette` from a micropython compiled file `roulette.mpy`.
293+
294+
If all the numbers outputted by the roulette is **7**, the flag will be returned by the `roulette.roulette` function.
295+
296+
### My Solution
297+
298+
Without any information, the natural instinct for me is to reverse engineer this module.
299+
300+
My teammate managed to disassemble it into python bytecode, which you can find [here](/assets/posts/2024-06-27-off-by-one-badge-2024/decompiled.txt).
301+
302+
From the following two functions, we can tell that there is some `reversed()` and `zlib.decompress()` going on.
303+
304+
```
305+
simple_name: r
306+
raw bytecode: 16 19:08:10:18:80:1d:12:19:12:1a:b0:34:01:34:01:63
307+
prelude: (4, 0, 0, 1, 0, 0)
308+
args: ['s']
309+
line info: 80:1d
310+
12:19 LOAD_GLOBAL bytes
311+
12:1a LOAD_GLOBAL reversed
312+
b0 LOAD_FAST 0
313+
34:01 CALL_FUNCTION 1
314+
34:01 CALL_FUNCTION 1
315+
63 RETURN_VALUE
316+
children: []
317+
simple_name: <lambda>
318+
raw bytecode: 18 19:08:11:1b:80:22:12:1c:10:12:34:01:14:13:b0:36:01:63
319+
prelude: (4, 0, 0, 1, 0, 0)
320+
args: ['__']
321+
line info: 80:22
322+
12:1c LOAD_GLOBAL __import__
323+
10:12 LOAD_CONST_STRING zlib
324+
34:01 CALL_FUNCTION 1
325+
14:13 LOAD_METHOD decompress
326+
b0 LOAD_FAST 0
327+
36:01 CALL_METHOD 1
328+
63 RETURN_VALUE
329+
children: []
330+
```
331+
332+
We also notice the construction of this byte array within the code.
333+
334+
```
335+
22:81:65 LOAD_CONST_SMALL_INT 229
336+
8b LOAD_CONST_SMALL_INT 11
337+
94 LOAD_CONST_SMALL_INT 20
338+
22:81:7b LOAD_CONST_SMALL_INT 251
339+
...
340+
...
341+
...
342+
...
343+
22:37 LOAD_CONST_SMALL_INT 55
344+
22:81:2b LOAD_CONST_SMALL_INT 171
345+
22:81:1c LOAD_CONST_SMALL_INT 156
346+
22:80:78 LOAD_CONST_SMALL_INT 120
347+
```
348+
349+
If we make an intelligent guess, and `zlib.decompress(array[::-1])`, we will get the flag!
350+
351+
### Intended Solution
352+
353+
The solution was in reality, much cooler than what I've done.
354+
355+
The challenge author, Manzel, came over to me and took a spare wire to poke two things in the ESP32 chip and the roulette suddenly spinned to all **7s**!!
356+
357+
![](roulette.jpg)
358+
359+
Essentially, there was also the string `pin = 1 adc = machine.ADC(pin)` within the decompiled code which hinted that the generated roulette number was based off the value of Pin 1 of the ESP32 microcontroller.
360+
361+
I am still not certain how to correctly manipulate this value, which I believe will also require some reversing to be done on the code, but it was really cool poking a wire at the microcontroller to get the flag!
362+
363+
## Conclusion
364+
365+
This was an eye-opening experience, and I'm really greatful for the chance to try it out.
366+
367+
I hope to learn how to do glitching _(flag 6)_ one day so I can write about it...
368+
369+
Huge thanks to my friendos <3
Loading
Loading

0 commit comments

Comments
 (0)