Writeup by leraton for Soupe MISO

hardware

April 15, 2024

Introduction

Signals are given from a communication between a ILI9341 screen and a microcontroller (µC). The goal is to recreate the picture sent from the µC to the screen. By taking a look at the ILI9341 datasheet, it can be noticed that the communication is made using the SPI protocol, the name of the challenge is also an indication toward this. The signals can be visualized using PulseView. By zooming on them, it can quickly be seen that:

  • D1 is the clock line
  • D2 is the D/C (data or command) line
  • D5 is the data line

Decoding the signals

The SPI communication can then be decoded using PulseView:

  • Click on “add protocol decoder”
  • Select SPI
  • On the new SPI line, set D1 as the Clock, D2 as MISO and D5 as MOSI

The data can then be exported by right-clicking on MISO data and “export all annotations for this row” save as a file called ‘DC.txt’ and then do the same for MOSI saved as ‘data.txt’.

As it can be seen below, the data aren’t saved in a very clean and usable way.

0-2061350 SPI: MOSI transfer:
2623275-2691150 SPI: MOSI transfer: 01
151897975-152021225 SPI: MOSI transfer: EF 03 80 02
152046225-152169475 SPI: MOSI transfer: CF 00 C1 30
152194475-152341850 SPI: MOSI transfer: ED 64 03 12 81
152366850-152490100 SPI: MOSI transfer: E8 85 00 78
152515100-152686600 SPI: MOSI transfer: CB 39 2C 00 34 02
152711600-152786600 SPI: MOSI transfer: F7 20
152811600-152914825 SPI: MOSI transfer: EA 00 00
152939825-153014825 SPI: MOSI transfer: C0 23
153039825-153114825 SPI: MOSI transfer: C1 10
153139825-153238950 SPI: MOSI transfer: C5 3E 28
153263950-153338950 SPI: MOSI transfer: C7 86
153363950-153438950 SPI: MOSI transfer: 36 48
...

To clean this a bit, the following python script was written:

file_name = "data.txt"

output_file = "cleaned_data.txt"


def main():

    data = ""
    with open(file_name, "r") as file:
        lines = file.readlines()

    for line in lines:
        data += line.split(": ")[-1].replace("\n", " ").replace("  ", " ")

    with open(output_file, "w") as file:
        file.write(data.replace("  ", " "))
    print(data)


if __name__ == "__main__":
    main()

The data now look like this

01 EF 03 80 02 CF 00 C1 30 ED 64 03 12 81 E8 85 00 78 CB 39 2C 00 34 02 F7 20 EA 00 00 C0 23 C1 10 C5 3E 28 C7 86 36 48 ...

The DC signals is also cleaned in the same way.

00 00 FF FF FF 00 FF FF FF 00 FF FF FF FF 00 FF FF FF 00 FF FF FF FF FF 00 FF 00 FF FF 00 FF 00 FF 00 FF FF 00 FF 00 FF ...

Exploiting the commands

By opening both signals at the same time, the commands sent from the µC can be reconstructed:

  • If DC = 00, the data value corresponds to the command number
  • If DC = FF, the data value corresponds to the parameters associated with the previous command number

From this, some parameters can be estimated, for example, the following signals:

3A 55 B1 00 18
00 FF 00 FF FF

will correspond to:

  • Command: 3A, parameters: 55
  • Command: B1, parameters: 00 18

By taking a look at the datasheet, it can be seen that the command 3A set the Pixel format.
The parameter 55 will correspond to a RGB-565 format (the DBI register is set to ‘101’).

Three command are then useful in order to recreate the picture:

  • 2A that set the minimum and maximum column with 4 bytes parameters
  • 2B that set the minimum and maximum page (=line) with 4 bytes parameters
  • 2C that write the pixels value (in the RGB-565 format) in the memory

To summarize 2A and 2B define a square or a rectangle on which the pixels values transferred using 2C will be written.
The picture can be reconstructed with a state machine that analyze the commands 2A, 2B or 2C.
The following python script do that:

import cv2 as cv
import numpy as np

data_file = "cleaned_data.txt"
dc_file = "cleaned_DC.txt"

HEIGHT = 240
WIDTH = 320
picture = [[[255, 255, 255] for i in range(HEIGHT)] for j in range(WIDTH)]


def open_files(data_file: str, dc_file: str):
    with open(data_file, "r") as file:
        data = file.readline().split(" ")
    with open(dc_file, "r") as file:
        dc = file.readline().split(" ")
    return data, dc


def data_to_comm_dat(data: str, DC: str):
    i = 1
    cmd_dat = []
    current_cmd = [data[0], ""]
    while i < len(data):
        if DC[i] != "00":
            current_cmd[1] += data[i]
        else:
            print(current_cmd)
            cmd_dat.append(current_cmd)
            current_cmd = ["", ""]
            current_cmd[0] = data[i]
        i += 1

    cmd_dat.append(current_cmd)
    return cmd_dat


def bytes_to_RGB256(byte: str):
    val = int(byte, 16)

    red = (val >> 11) & 0x1F
    green = (val >> 5) & 0x3F
    blue = val & 0x1F

    red <<= 3
    green <<= 2
    blue <<= 3

    return [red, blue, green]


def main():
    data, dc = open_files(data_file=data_file, dc_file=dc_file)
    cmd_dat_list = data_to_comm_dat(data=data, DC=dc)
    x_min, x_max, y_min, y_max = 0, 0, 0, 0
    for cmd, dat in cmd_dat_list:
        if cmd == "2A":  # HEIGHT
            x_min, x_max = int(dat[:4], 16), int(dat[4:], 16)
        if cmd == "2B":  # WIDTH
            y_min, y_max = int(dat[:4], 16), int(dat[4:], 16)
        if cmd == "2C":  # Pixel data
            color_list = []
            for i in range(0, len(dat), 4):
                color_list.append(bytes_to_RGB256(dat[i : i + 4]))
            for i in range(len(color_list)):
                picture[y_min + i % (x_max - x_min + 1)][
                    x_min + i // (x_max - x_min + 1)
                ] = color_list[i].copy()

    cv.imshow("flag", np.array(picture, dtype=np.uint8))
    cv.waitKey(0)


if __name__ == "__main__":
    main()

Final picture

By running the script above, the picture is generated with the flag on it:

flag

Therefore the flag is :
FCSC{173814AF4312233A21EFC568911501233AC836193749943}