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.

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).

image

I managed to build images like the one above, and scrolling through them, the last two images contained the flag:

cd22{HIDDEN}

← Back to SecChallenge22

all tags