site_vineetk

Source for vineetk.net website
Log | Files | Refs | LICENSE

crewctf24_sniff.md (13447B)


      1 title: CrewCTF2024 misc/Sniff Writeup
      2 date: 2024-08-04 12:00
      3 ---
      4 ## Challenge
      5 ### Description
      6 > I came across this mysterious device. So I hooked up my logic analyzer
      7 and recorded somebody using it. (`capture.sol`)
      8 This 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
     30 Instead of sleeping, I made the mistake^Wwise decision of looking at my
     31 Discord notification that said there was a hardware challenge in this CTF. It
     32 just so happened that there it was using an ATmega-powered keyboard and an
     33 e-paper screen, and it almost seemed like a coincidence since I was designing
     34 my own keyboard and wanted to interface with an e-paper screen in the near
     35 future. This seemed like a great learning opportunity so I started working on
     36 the two-part challenge.
     37 
     38 There were a few files inside the `dist.zip`, with the most
     39 interesting one being capture.sal which was technically a zip but actually
     40 the analyzer file for Salae. Sadly it seemed to need their proprietary
     41 program 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 
     45 The first thing I did was figuring out what each channel was connected to
     46 and what it meant. It seemed that the keyboard and display were controlled by
     47 the Raspberry Pi which then seemed to go to the logic analyzer. So I looked
     48 at what each channel was connected to and based on its connected pin on the
     49 Pi, I found its function via pinout.xyz. I ended up with this:
     50 
     51 ```
     52 Channel 0: P03  I2C SDA
     53 Channel 1: P05  I2C SCL
     54 Channel 2: P11  GPIO 17 (busy)
     55 Channel 3: P13  GPIO 27 (reset)
     56 Channel 4: P15  GPIO 22 (data/command)
     57 Channel 5: P21  MOSI
     58 Channel 6: P23  SPI0 SCLK
     59 Channel 7: P24  SPI0 CE0
     60 ```
     61 
     62 ![Raspberry Pi Pinout](https://vineetk.net/images/crewctf24_sniff/rpi_pinout.png)
     63 
     64 ## Part 1
     65 In Logic 2, I opened the I2C analyzer and outputted the dump in the
     66 terminal 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)
     70 It seemed there was a lot of NUL bytes being sent, probably indicating
     71 that there wasn’t anything during that cycle, and a few seconds later there
     72 were also some other different bytes with NUL and some `0x01`
     73 bytes in between, and these seemed to be printable ASCII.
     74 
     75 ```
     76 read to 0x5F ack data: 0x01
     77 read to 0x5F ack data: 0x01
     78 read to 0x5F ack data: 0x66
     79 read to 0x5F ack data: 0x6c
     80 read to 0x5F ack data: 0x61
     81 read to 0x5F ack data: 0x67
     82 read to 0x5F ack data: 0x7b
     83 read to 0x5F ack data: 0x37
     84 read to 0x5F ack data: 0x01
     85 read to 0x5F ack data: 0x31
     86 read to 0x5F ack data: 0x37
     87 read to 0x5F ack data: 0x66
     88 read to 0x5F ack data: 0x37
     89 read to 0x5F ack data: 0x35
     90 read to 0x5F ack data: 0x01
     91 read to 0x5F ack data: 0x33
     92 read to 0x5F ack data: 0x32
     93 read to 0x5F ack data: 0x7d
     94 read to 0x5F ack data: 0x01
     95 read to 0x5F ack data: 0x0d
     96 ```
     97 
     98 Filtering out the `0x00` and `0x01` data bytes and
     99 converting 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 
    104 I first outputted the SPI dump from the analyzer into a file and kept only
    105 the `MOSI` and `MISO` columns.
    106 
    107 ```
    108 Time [s],Packet ID,MOSI,MISO
    109 4.108880200000000,0,0x12,0x00
    110 5.109988000000000,0,0x01,0x00
    111 5.110044320000000,0,0xF9,0xFF
    112 5.110062800000000,0,0x00,0xFF
    113 5.110081280000000,0,0x00,0xFF
    114 5.110125600000000,0,0x3A,0x00
    115 5.110167440000000,0,0x1B,0xFF
    116 5.110210120000000,0,0x3B,0x00
    117 5.110251760000000,0,0x0B,0xFF
    118 ```
    119 
    120 To actually understand what’s going on, I couldn’t find any proper
    121 documentation 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 
    126 Then I found the [Python library
    127 source from Pimoroni](https://github.com/pimoroni/inky) of their epaper screens, which is probably what was
    128 used to make this challenge.
    129 
    130 ```python
    131 def 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 
    164 def _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 
    218 It was also communicating over SPI which seemed to indicate that this was
    219 the 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
    222 sent in the analyzed dump log matched exactly, including each byte of the LUT
    223 table.
    224 
    225 All the SPI commands used in the library are used with named constants
    226 that are defined `library/inky/ssd1608.py`:
    227 
    228 ```python
    229 """Constants for SSD1608 driver IC."""
    230 DRIVER_CONTROL = 0x01
    231 GATE_VOLTAGE = 0x03
    232 SOURCE_VOLTAGE = 0x04
    233 DISPLAY_CONTROL = 0x07
    234 NON_OVERLAP = 0x0B
    235 BOOSTER_SOFT_START = 0x0C
    236 GATE_SCAN_START = 0x0F
    237 DEEP_SLEEP = 0x10
    238 DATA_MODE = 0x11
    239 SW_RESET = 0x12
    240 TEMP_WRITE = 0x1A
    241 TEMP_READ = 0x1B
    242 TEMP_CONTROL = 0x1C
    243 TEMP_LOAD = 0x1D
    244 MASTER_ACTIVATE = 0x20
    245 DISP_CTRL1 = 0x21
    246 DISP_CTRL2 = 0x22
    247 WRITE_RAM = 0x24
    248 WRITE_ALTRAM = 0x26
    249 READ_RAM = 0x25
    250 VCOM_SENSE = 0x28
    251 VCOM_DURATION = 0x29
    252 WRITE_VCOM = 0x2C
    253 READ_OTP = 0x2D
    254 WRITE_LUT = 0x32
    255 WRITE_DUMMY = 0x3A
    256 WRITE_GATELINE = 0x3B
    257 WRITE_BORDER = 0x3C
    258 SET_RAMXPOS = 0x44
    259 SET_RAMYPOS = 0x45
    260 SET_RAMXCOUNT = 0x4E
    261 SET_RAMYCOUNT = 0x4F
    262 NOP = 0xFF
    263 ```
    264 
    265 I 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
    267 buffer 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 ...
    274 0x19,0xFF
    275 0x01,0xFF
    276 0x00,0xFF
    277 0x3C,0x00
    278 0x01,0xFF
    279 0x4E,0x00
    280 0x00,0xFF
    281 0x4F,0x00
    282 0x00,0xFF
    283 0x00,0xFF
    284 0x24,0x00
    285 0xFF,0xFF
    286 0xFF,0xFF
    287 0xFF,0xFF
    288 0xFF,0xFF
    289 0xFF,0xFF
    290 0xFF,0xFF
    291 0xFF,0xFF
    292 0xFF,0xFF
    293 0xFF,0xFF
    294 0xFF,0xFF
    295 ...
    296 ```
    297 
    298 There also seemed to be two different updates at around 5 seconds and 70
    299 seconds.
    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 
    303 However, the number of bytes written was 4250, which wasn’t the 3812.5 or
    304 2756 bytes I was expecting. This wasn’t divisible by 250 nor 122 and so I was
    305 stuck for a long time. Looking through the library source for more than an
    306 hour with my tired self didn’t help much either. As a last ditch attempt, I
    307 tried converting the raw bytes into an image via Pillow, I used the
    308 `L` mode (`8bpp`) and just got an uninteresting garbled
    309 image.
    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 
    317 After waking up and working on the CTF after it ended, my partner asked on
    318 the Discord and found some interesting very helpful information. It turned
    319 out the image was a packed 1bpp image. This meant that each byte in the
    320 memory framebuffer contained 8 pixels (8 bits / 1 bits per pixel = 8
    321 pixels).
    322 
    323 ```python
    324 # under show()
    325 buf_a = numpy.packbits(numpy.where(region == BLACK, 0, 1)).tolist()
    326 buf_b = numpy.packbits(numpy.where(region == RED, 1, 0)).tolist()
    327 ```
    328 
    329 In 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
    331 about NumPy, so this was a skill issue as I initially assumed it was a
    332 complicated way of saving all the black and red pixels into lists. This is a
    333 good reminder that the documentation should be checked for all unfamiliar
    334 functions instead of naively assuming what they seem to do.
    335 
    336 Also in addition to the screen being rotated by 90 degrees, the vertical
    337 resolution is actually 136 pixels and not 120 according to the driver.
    338 
    339 Knowing all this solved all my problems as 4250 * 8 was indeed divisible
    340 by 250 and the actual vertical resolution 136.
    341 
    342 All I had to do was change the Pillow mode when converting the bytes to an
    343 image from `L` (8bpp) to `1` (1bpp) and the (rotated)
    344 resolution from `(250, 16)` to `(136, 250)` and got an
    345 actual 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 
    349 I used the first updated bytes which was the screen shown in the
    350 challenge’s screenshots. Using the second update’s bytes gave half of the
    351 flag in the black/white channel and the other half in the yellow/red
    352 channel.
    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 
    357 Each character index in both channels seemed to alternate, so the actual
    358 flag was `flag{ec9cf2b7}`. After I finished writing this writeup
    359 and seeing the two images side-by-side, they probably could’ve been overlayed
    360 after one’s colours are inverted, and is probably what was meant by
    361 “stitching” the channels together.
    362 
    363 ## Conclusion
    364 This was my most favourite CTF challenge by far and I learned a lot,
    365 especially about stuff I wanted to learn like how SPI e-paper screens work
    366 and not be lost with I2C. I am personally now curious whether the SPI screens
    367 can be interfaced directly with the MCU instead of going through an
    368 intermediate daughterboard/HAT and how different the protocol for parallel
    369 screens are since they’re much faster and use more pins.