summaryrefslogtreecommitdiff
path: root/posts/crewctf24_sniff.md
diff options
context:
space:
mode:
Diffstat (limited to 'posts/crewctf24_sniff.md')
-rw-r--r--posts/crewctf24_sniff.md369
1 files changed, 369 insertions, 0 deletions
diff --git a/posts/crewctf24_sniff.md b/posts/crewctf24_sniff.md
new file mode 100644
index 0000000..32d1adb
--- /dev/null
+++ b/posts/crewctf24_sniff.md
@@ -0,0 +1,369 @@
1title: CrewCTF2024 misc/Sniff Writeup
2date: 2024-08-04 12:00
3---
4## Challenge
5### Description
6> I came across this mysterious device. So I hooked up my logic analyzer
7and recorded somebody using it. (`capture.sol`)
8This challenge has two flags in the `flag{}` format
9> * The first (easier) is the password that was typed on the keyboard.
10> * The second (significantly harder) is what was display on the screen after the password was entered.
11
12### Images
13![Device](https://vineetk.net/images/crewctf24_sniff/device.jpg)
14
15![Everything](https://vineetk.net/images/crewctf24_sniff/everything.jpg)
16
17![Raspberry Pi first](https://vineetk.net/images/crewctf24_sniff/pi1.jpg)
18
19![Raspberry Pi second](https://vineetk.net/images/crewctf24_sniff/pi2.jpg)
20
21![Logic Analyzer first](https://vineetk.net/images/crewctf24_sniff/logic1.jpg)
22
23![Logic Analyzer second](https://vineetk.net/images/crewctf24_sniff/logic2.jpg)
24
25![Display first](https://vineetk.net/images/crewctf24_sniff/display1.jpg)
26
27![Display second](https://vineetk.net/images/crewctf24_sniff/display2.jpg)
28
29## Intro
30Instead of sleeping, I made the mistake^Wwise decision of looking at my
31Discord notification that said there was a hardware challenge in this CTF. It
32just so happened that there it was using an ATmega-powered keyboard and an
33e-paper screen, and it almost seemed like a coincidence since I was designing
34my own keyboard and wanted to interface with an e-paper screen in the near
35future. This seemed like a great learning opportunity so I started working on
36the two-part challenge.
37
38There were a few files inside the `dist.zip`, with the most
39interesting one being capture.sal which was technically a zip but actually
40the analyzer file for Salae. Sadly it seemed to need their proprietary
41program to open.
42
43![Screenshot of Saleae Logic 2, the program used for viewing the dump.](https://vineetk.net/images/crewctf24_sniff/logic2_main.png)
44
45The first thing I did was figuring out what each channel was connected to
46and what it meant. It seemed that the keyboard and display were controlled by
47the Raspberry Pi which then seemed to go to the logic analyzer. So I looked
48at what each channel was connected to and based on its connected pin on the
49Pi, I found its function via pinout.xyz. I ended up with this:
50
51```
52Channel 0: P03 I2C SDA
53Channel 1: P05 I2C SCL
54Channel 2: P11 GPIO 17 (busy)
55Channel 3: P13 GPIO 27 (reset)
56Channel 4: P15 GPIO 22 (data/command)
57Channel 5: P21 MOSI
58Channel 6: P23 SPI0 SCLK
59Channel 7: P24 SPI0 CE0
60```
61
62![Raspberry Pi Pinout](https://vineetk.net/images/crewctf24_sniff/rpi_pinout.png)
63
64## Part 1
65In Logic 2, I opened the I2C analyzer and outputted the dump in the
66terminal tab into a file
67
68![I2C analyzer screenshot in Logic 2 in the Terminal view.](https://vineetk.net/images/crewctf24_sniff/logic2_i2c_1.png)
69![I2C analyzer screenshot in Logic 2 in the Data Table view.](https://vineetk.net/images/crewctf24_sniff/logic2_i2c_2.png)
70It seemed there was a lot of NUL bytes being sent, probably indicating
71that there wasn’t anything during that cycle, and a few seconds later there
72were also some other different bytes with NUL and some `0x01`
73bytes in between, and these seemed to be printable ASCII.
74
75```
76read to 0x5F ack data: 0x01
77read to 0x5F ack data: 0x01
78read to 0x5F ack data: 0x66
79read to 0x5F ack data: 0x6c
80read to 0x5F ack data: 0x61
81read to 0x5F ack data: 0x67
82read to 0x5F ack data: 0x7b
83read to 0x5F ack data: 0x37
84read to 0x5F ack data: 0x01
85read to 0x5F ack data: 0x31
86read to 0x5F ack data: 0x37
87read to 0x5F ack data: 0x66
88read to 0x5F ack data: 0x37
89read to 0x5F ack data: 0x35
90read to 0x5F ack data: 0x01
91read to 0x5F ack data: 0x33
92read to 0x5F ack data: 0x32
93read to 0x5F ack data: 0x7d
94read to 0x5F ack data: 0x01
95read to 0x5F ack data: 0x0d
96```
97
98Filtering out the `0x00` and `0x01` data bytes and
99converting to ASCII results in `flag{717f7532}`.
100
101## Part 2
102![Logic 2 SPI analyzer output.](https://vineetk.net/images/crewctf24_sniff/logic2_spi.png)
103
104I first outputted the SPI dump from the analyzer into a file and kept only
105the `MOSI` and `MISO` columns.
106
107```
108Time [s],Packet ID,MOSI,MISO
1094.108880200000000,0,0x12,0x00
1105.109988000000000,0,0x01,0x00
1115.110044320000000,0,0xF9,0xFF
1125.110062800000000,0,0x00,0xFF
1135.110081280000000,0,0x00,0xFF
1145.110125600000000,0,0x3A,0x00
1155.110167440000000,0,0x1B,0xFF
1165.110210120000000,0,0x3B,0x00
1175.110251760000000,0,0x0B,0xFF
118```
119
120To actually understand what’s going on, I couldn’t find any proper
121documentation initially. There wasn’t even a proper datasheet on DigiKey; the
122“datasheet” was just a summary of the product.
123
124![DigiKey “datasheet”](https://vineetk.net/images/crewctf24_sniff/digikey_datasheet.png)
125
126Then I found the [Python library
127source from Pimoroni](https://github.com/pimoroni/inky) of their epaper screens, which is probably what was
128used to make this challenge.
129
130```python
131def setup(self):
132 """Set up Inky GPIO and reset display."""
133 if not self._gpio_setup:
134 if self._gpio is None:
135 try:
136 import RPi.GPIO as GPIO
137 self._gpio = GPIO
138 except ImportError:
139 raise ImportError('This library requires the RPi.GPIO module\nInstall with: sudo apt install python-rpi.gpio')
140 self._gpio.setmode(self._gpio.BCM)
141 self._gpio.setwarnings(False)
142 self._gpio.setup(self.dc_pin, self._gpio.OUT, initial=self._gpio.LOW, pull_up_down=self._gpio.PUD_OFF)
143 self._gpio.setup(self.reset_pin, self._gpio.OUT, initial=self._gpio.HIGH, pull_up_down=self._gpio.PUD_OFF)
144 self._gpio.setup(self.busy_pin, self._gpio.IN, pull_up_down=self._gpio.PUD_OFF)
145
146 if self._spi_bus is None:
147 import spidev
148 self._spi_bus = spidev.SpiDev()
149
150 self._spi_bus.open(0, self.cs_pin)
151 self._spi_bus.max_speed_hz = 488000
152
153 self._gpio_setup = True
154
155 self._gpio.output(self.reset_pin, self._gpio.LOW)
156 time.sleep(0.5)
157 self._gpio.output(self.reset_pin, self._gpio.HIGH)
158 time.sleep(0.5)
159
160 self._send_command(0x12) # Soft Reset
161 time.sleep(1.0)
162 self._busy_wait()
163
164def _update(self, buf_a, buf_b, busy_wait=True):
165 """Update display.
166
167 Dispatches display update to correct driver.
168
169 :param buf_a: Black/White pixels
170 :param buf_b: Yellow/Red pixels
171
172 """
173 self.setup()
174
175 self._send_command(ssd1608.DRIVER_CONTROL, [self.rows - 1, (self.rows - 1) >> 8, 0x00])
176 # Set dummy line period
177 self._send_command(ssd1608.WRITE_DUMMY, [0x1B])
178 # Set Line Width
179 self._send_command(ssd1608.WRITE_GATELINE, [0x0B])
180 # Data entry squence (scan direction leftward and downward)
181 self._send_command(ssd1608.DATA_MODE, [0x03])
182 # Set ram X start and end position
183 xposBuf = [0x00, self.cols // 8 - 1]
184 self._send_command(ssd1608.SET_RAMXPOS, xposBuf)
185 # Set ram Y start and end position
186 yposBuf = [0x00, 0x00, (self.rows - 1) & 0xFF, (self.rows - 1) >> 8]
187 self._send_command(ssd1608.SET_RAMYPOS, yposBuf)
188 # VCOM Voltage
189 self._send_command(ssd1608.WRITE_VCOM, [0x70])
190 # Write LUT DATA
191 self._send_command(ssd1608.WRITE_LUT, self._luts[self.lut])
192
193 if self.border_colour == self.BLACK:
194 self._send_command(ssd1608.WRITE_BORDER, 0b00000000)
195 # GS Transition + Waveform 00 + GSA 0 + GSB 0
196 elif self.border_colour == self.RED and self.colour == 'red':
197 self._send_command(ssd1608.WRITE_BORDER, 0b00000110)
198 # GS Transition + Waveform 01 + GSA 1 + GSB 0
199 elif self.border_colour == self.YELLOW and self.colour == 'yellow':
200 self._send_command(ssd1608.WRITE_BORDER, 0b00001111)
201 # GS Transition + Waveform 11 + GSA 1 + GSB 1
202 elif self.border_colour == self.WHITE:
203 self._send_command(ssd1608.WRITE_BORDER, 0b00000001)
204 # GS Transition + Waveform 00 + GSA 0 + GSB 1
205
206 # Set RAM address to 0, 0
207 self._send_command(ssd1608.SET_RAMXCOUNT, [0x00])
208 self._send_command(ssd1608.SET_RAMYCOUNT, [0x00, 0x00])
209
210 for data in ((ssd1608.WRITE_RAM, buf_a), (ssd1608.WRITE_ALTRAM, buf_b)):
211 cmd, buf = data
212 self._send_command(cmd, buf)
213
214 self._busy_wait()
215 self._send_command(ssd1608.MASTER_ACTIVATE)
216```
217
218It was also communicating over SPI which seemed to indicate that this was
219the proper library. Then I looked at the `setup()` and
220`_update()` functions in
221`library/inky/inky_ssd1608.py`, and the SPI commands that were
222sent in the analyzed dump log matched exactly, including each byte of the LUT
223table.
224
225All the SPI commands used in the library are used with named constants
226that are defined `library/inky/ssd1608.py`:
227
228```python
229"""Constants for SSD1608 driver IC."""
230DRIVER_CONTROL = 0x01
231GATE_VOLTAGE = 0x03
232SOURCE_VOLTAGE = 0x04
233DISPLAY_CONTROL = 0x07
234NON_OVERLAP = 0x0B
235BOOSTER_SOFT_START = 0x0C
236GATE_SCAN_START = 0x0F
237DEEP_SLEEP = 0x10
238DATA_MODE = 0x11
239SW_RESET = 0x12
240TEMP_WRITE = 0x1A
241TEMP_READ = 0x1B
242TEMP_CONTROL = 0x1C
243TEMP_LOAD = 0x1D
244MASTER_ACTIVATE = 0x20
245DISP_CTRL1 = 0x21
246DISP_CTRL2 = 0x22
247WRITE_RAM = 0x24
248WRITE_ALTRAM = 0x26
249READ_RAM = 0x25
250VCOM_SENSE = 0x28
251VCOM_DURATION = 0x29
252WRITE_VCOM = 0x2C
253READ_OTP = 0x2D
254WRITE_LUT = 0x32
255WRITE_DUMMY = 0x3A
256WRITE_GATELINE = 0x3B
257WRITE_BORDER = 0x3C
258SET_RAMXPOS = 0x44
259SET_RAMYPOS = 0x45
260SET_RAMXCOUNT = 0x4E
261SET_RAMYCOUNT = 0x4F
262NOP = 0xFF
263```
264
265I then noticed that there was a long string of bytes being sent after a
266`0x24` which in the library indicated that it was the memory
267buffer for the black/white channel ending with a `0x00`
268`MISO`, with the yellow/red channel afterward with a
269`0x26` `MOSI` and also ended with `0x00`
270`MISO`
271
272```
273...
2740x19,0xFF
2750x01,0xFF
2760x00,0xFF
2770x3C,0x00
2780x01,0xFF
2790x4E,0x00
2800x00,0xFF
2810x4F,0x00
2820x00,0xFF
2830x00,0xFF
2840x24,0x00
2850xFF,0xFF
2860xFF,0xFF
2870xFF,0xFF
2880xFF,0xFF
2890xFF,0xFF
2900xFF,0xFF
2910xFF,0xFF
2920xFF,0xFF
2930xFF,0xFF
2940xFF,0xFF
295...
296```
297
298There also seemed to be two different updates at around 5 seconds and 70
299seconds.
300
301![Logic 2 SPI analyzer with 0x24 searched to show when each screen update started.](https://vineetk.net/images/crewctf24_sniff/logic2_spi_update_times.png)
302
303However, the number of bytes written was 4250, which wasn’t the 3812.5 or
3042756 bytes I was expecting. This wasn’t divisible by 250 nor 122 and so I was
305stuck for a long time. Looking through the library source for more than an
306hour with my tired self didn’t help much either. As a last ditch attempt, I
307tried converting the raw bytes into an image via Pillow, I used the
308`L` mode (`8bpp`) and just got an uninteresting garbled
309image.
310
311![Garbled image reflecting my sadness at being unable to get the flag.](https://vineetk.net/images/crewctf24_sniff/failed_flag.png)
312
313## Part 2 Part 2: Electric Boogaloo
314![Joey asking in the CTF’s Discord about the challenge.](https://vineetk.net/images/crewctf24_sniff/discord1.png)
315![Me being surprised in the Discord for my stupidity that I blame on being tired.](https://vineetk.net/images/crewctf24_sniff/discord2.png)
316
317After waking up and working on the CTF after it ended, my partner asked on
318the Discord and found some interesting very helpful information. It turned
319out the image was a packed 1bpp image. This meant that each byte in the
320memory framebuffer contained 8 pixels (8 bits / 1 bits per pixel = 8
321pixels).
322
323```python
324# under show()
325buf_a = numpy.packbits(numpy.where(region == BLACK, 0, 1)).tolist()
326buf_b = numpy.packbits(numpy.where(region == RED, 1, 0)).tolist()
327```
328
329In the Python source, this was shown by `buf_a` and
330`buf_b` being packed bits of 1bpp via NumPy. I don’t know much
331about NumPy, so this was a skill issue as I initially assumed it was a
332complicated way of saving all the black and red pixels into lists. This is a
333good reminder that the documentation should be checked for all unfamiliar
334functions instead of naively assuming what they seem to do.
335
336Also in addition to the screen being rotated by 90 degrees, the vertical
337resolution is actually 136 pixels and not 120 according to the driver.
338
339Knowing all this solved all my problems as 4250 * 8 was indeed divisible
340by 250 and the actual vertical resolution 136.
341
342All I had to do was change the Pillow mode when converting the bytes to an
343image from `L` (8bpp) to `1` (1bpp) and the (rotated)
344resolution from `(250, 16)` to `(136, 250)` and got an
345actual image.
346
347![Extracted image of the display showing the initial message shown in the challenge description.](https://vineetk.net/images/crewctf24_sniff/converted_eink_image.png)
348
349I used the first updated bytes which was the screen shown in the
350challenge’s screenshots. Using the second update’s bytes gave half of the
351flag in the black/white channel and the other half in the yellow/red
352channel.
353
354![The flag in the black/white channel.](https://vineetk.net/images/crewctf24_sniff/flag1.png)
355![The flag in the yellow/red channel.](https://vineetk.net/images/crewctf24_sniff/flag2.png)
356
357Each character index in both channels seemed to alternate, so the actual
358flag was `flag{ec9cf2b7}`. After I finished writing this writeup
359and seeing the two images side-by-side, they probably could’ve been overlayed
360after one’s colours are inverted, and is probably what was meant by
361“stitching” the channels together.
362
363## Conclusion
364This was my most favourite CTF challenge by far and I learned a lot,
365especially about stuff I wanted to learn like how SPI e-paper screens work
366and not be lost with I2C. I am personally now curious whether the SPI screens
367can be interfaced directly with the MCU instead of going through an
368intermediate daughterboard/HAT and how different the protocol for parallel
369screens are since they’re much faster and use more pins.