Broken
Description
A customer just called. They dropped their Token Locker 3000, shattering both of its 1.3" SH1106 OLED
displays. I have to go take care of some business, so I’m gonna need you to recover the data for them. You’ll be able to do it, right?
I’m not sure whether you’re familiar with the Token Locker 3000; it is a handheld crypto token protecting device. It has two screens on the two sides and two sets of keypads and buttons. When you start up the device, you have to enter two separate passwords, and if it checks out, it displays two QR codes containing the token.
Unfortunately, we are out of replacement displays (damn supply chain shortages); thus, replacing them is not an option. However, I recorded the signals of the displays for you while the user entered their password (correctly, I hope), so you’ll have to just work out how things work. The silkscreen labels of the display’s pins are GND
, VCC
, SDA
, SCL
.
Note: The SH1106 is a cheap copy of the SSD1306. Depending on the resources you manage to find, you may need to use both’s documentation, but keep in mind that they are not 100% identical.
- Author: csf3r3ncz1
- Attachments: challenge.sal
Solution
This challenge was seemingly comparable to last years Crystal from the past challenge. Should be easy. It was, but at what cost?
I quickly found out, that the provided data capture is an I2C
communication, transmitting the display data. Googling around I found this manual, which is ONLY 53 PAGES LONG.
Understanding how the data delivered via the SDA line is interpreted by the display was quite the reading, but slowly I gathered the required knowledge to implement something that can recover me the displayed data.
I exported the data from Salae’s Logic Analyzer to a .csv
file, and implemented part of the protocol using python:
import csv
import numpy as np
from PIL import Image
# saves an image of size width x height as name supplied
#TODO: adapt code to display the matrix instead
def newImg(name, width, height, display):
nextisBlack = True
img = Image.new('RGB', (width, height))
for y in range(height):
for x in range(width):
if display[x][y] > 0:
img.putpixel((x,y), (255,255,255))
else:
img.putpixel((x,y), (0,0,0))
img.save(name)
return img
# turns 0x00 to 00000000 e.g.
def hex_to_bin(h):
b = bin(int(h, 16))[2:]
padlen = 8 - len(b)
return "0"*padlen + b
# note keeping section
display_height = 64
display_width = 128
csvdata = []
# parse csv data from SalaeLogic2
with open('signals.csv') as csvfile:
reader = csv.DictReader(csvfile)
for row in reader:
csvdata.append(row)
display = np.zeros((display_width, display_height), dtype=int)
c0 = 0 # c0=0 last control, only data after; c0=1 following 2 is: data, cntrl
dc = 0 # dc=0 the data byte is for command operation, dc=1 RAM operation
current = 0 # 0 if control byte, 1 if data byte is coming, we start /w control
i = 0
currx = 0
curry = 0
freq_mode_set = 0
multiplex_ration_mode_set = 0
display_offset_mode_set = 0
dcdc_control_mode_set = 0
common_pads_config_mode_set = 0
contrast_control_mode_set = 0
dispre_mode_set = 0
vcom_deselect_mode_set = 0
dcdc_status = None
column_addr_low = 0
column_addr_high = 0
lower_set = 0
upper_set = 0
page_addr = 0
drawn = False
img_count = 0
ram_count = 0
for line in csvdata:
if line['type'] == 'start':
#print('----- START -----')
c0 = 0
dc = 0
current = 0
#reset everything here
continue
if line['type'] == 'stop':
#print('----- STOP -----')
continue
if line['type'] == 'data':
byte = line['data']
bits = hex_to_bin(byte)
byte = int(byte, 16)
# control byte is current
if current == 0:
c0 = int(bits[0])
dc = int(bits[1])
current = 1
continue
#current byte is data byte
if current == 1:
# if c0 was set, next is a control byte
if c0 == 1:
current = 0
# data byte for command op
if dc == 0:
if ram_count != 0:
print(f'Data RAM calls: {ram_count}')
ram_count = 0
if freq_mode_set:
print(f"Freq: {byte >> 4} , Div: {byte & 0xf} TODO")
freq_mode_set = 0
continue
# 2nd byte of multiplex ration mode
if multiplex_ration_mode_set:
print(f"Multiplex ration: {byte & 0x3f} TODO")
multiplex_ration_mode_set = 0
continue
# 2nd byte of display offset mode
if display_offset_mode_set:
print(f"Display offset: {byte & 0x3f} TODO")
display_offset_mode_set = 0
continue
# dcdc control mode
if dcdc_control_mode_set:
dcdc_status = byte & 0x1
print(f'DCDC-Control Mode set: {dcdc_status} ({hex(byte)}) TODO')
dcdc_control_mode_set = 0
continue
# common pads config control mode
if common_pads_config_mode_set:
print(f'Common Pads mode set: {bits[3]} TODO')
common_pads_config_mode_set = 0
continue
# contrast control mode
if contrast_control_mode_set:
print(f'Contrast mode set: {byte} TODO')
contrast_control_mode_set = 0
continue
# dis/precharge control mode
if dispre_mode_set:
print(f'Dis/Pre-charge mode set. Dis: {byte >> 4}, Pre: {byte & 0xf} TODO')
dispre_mode_set = 0
continue
# dis/precharge control mode
if vcom_deselect_mode_set:
print(f'VCOM Deselect set: {byte} TODO')
vcom_deselect_mode_set = 0
continue
if 0x00 <= byte and byte <= 0x0f:
column_addr_low = byte & 0xf
print(f"Setting column lower address to: {bin(column_addr_low)}")
lower_set = 1
elif 0x10 <= byte and byte <= 0x1f:
column_addr_high = byte >> 4
print(f"Setting column higher address to: {bin(column_addr_high)}")
upper_set = 1
elif 0xb0 <= byte and byte <= 0xb7:
page_addr = byte & 0xf
curry = page_addr * 8
print(f"Setting page address to: {page_addr}")
elif 0xae <= byte and byte <= 0xaf:
print("Display ON/OFF TODO")
elif byte == 0xd5:
print(f"Oscillator Frequency mode set TODO")
freq_mode_set = 1
elif byte == 0xa8:
print(f"Multiplex Ration Mode Set TODO")
multiplex_ration_mode_set = 1
elif byte == 0xd3:
print(f"Display Offset Mode Set TODO")
display_offset_mode_set = 1
elif 0x40 <= byte and byte <= 0x7f:
print(f"Set Display start line: {byte & 0x3f} TODO")
elif byte == 0xad:
print(f"DC-DC Control Mode Set TODO")
dcdc_control_mode_set = 1
elif 0xa0 <= byte and byte <= 0xa1:
print(f"Set segment re-map: {byte & 0x1} TODO")
elif 0xc0 <= byte and byte <= 0xc8:
print(f"Set common output scan direction: {bits[4]} TODO")
elif byte == 0xda:
print(f"Common Pads Config Mode Set TODO")
common_pads_config_mode_set = 1
elif byte == 0x81:
print(f"Contrast Control Mode Set TODO")
contrast_control_mode_set = 1
elif byte == 0xd9:
print(f"Dis/Pre-charge period mode set TODO")
dispre_mode_set = 1
elif byte == 0xdb:
print(f"VCOM Deselect Mode Set TODO")
vcom_deselect_mode_set = 1
elif 0x30 <= byte and byte <= 0x33:
print(f"Set pump voltage value: {byte & 0x3} TODO")
elif 0xa6 <= byte and byte <= 0xa7:
print(f"Set normal/reverse display: {byte & 0x1} TODO")
elif byte == 0x20:
print(f"CSONGI PLS TODO")
elif 0xa4 <= byte and byte <= 0xa5:
print(f"Set entire display ON/OFF: {byte & 0x1} TODO")
else:
print(f'Unknown command op: {hex(byte)}')
exit()
if lower_set and upper_set:
lower_set = 0
upper_set = 0
#currx = column_addr_high << 4 | column_addr_low
currx = 0
print(f'Set collumn to: {currx}')
continue
#data byte for RAM op
if dc == 1:
ram_count += 1
#print(f"RAM op {bits}")
drawn = True
offset = 0
for bit in bits[::-1]:
display[currx][curry + offset] = int(bit)
offset += 1
#increment the column
currx += 1
# if we reach the end of the screen, go to next page
if currx > 127:
currx = 0
#increment page modulo 8
prev_page_addr = page_addr
page_addr = (page_addr + 1) % 8
#set y cordinate
curry = page_addr * 8
if prev_page_addr > page_addr:
newImg(str(img_count) + ".png", display_width,display_height, display)
img_count += 1
display = np.zeros((display_width, display_height), dtype=int)
continue
It was quite buggy, and the “screenshots” I managed to build weren’t necessary correct, but were good enough where they needed to be (probably because Csongi is a good guy, and didn’t wanted to make it any harder).
I managed to build images like the one above, and scrolling through them, the last two images contained the flag:
cd22{HIDDEN}