A deep dive into digital signals and embedded memory. In this challenge, we analyze a captured SPI communication stream between a microcontroller and a microSD card. What begins as a simple .sal file quickly unfolds into a forensic decoding of SPI transactions — isolating the moment a secret flag is transmitted. With nothing but waveforms, configuration tinkering, and Python at our disposal, we transform bits into bytes and bytes into a story.

The challenge description includes - "We accessed the embedded device's asynchronous serial debugging interface while it was operational and captured some messages that were being transmitted over it. Can you decode them?"

An image to describe post

Challenge: Secure Digital
Category: Hardware Exploitation
Difficulty: Very Easy
Points: 0
Link: Challenge Link

Initial Foothold

We began by downloading the provided archive: Secure Digital.zip. Inside was a familiar friend — a .sal file captured via the Saleae Logic Analyzer.

unzip "Secure Digital.zip"

The archive extracted a folder called secure_digital containing
trace_captured.sal.
You can checkout another similar challenge related to .sal file here.

We fired up Logic 2, Saleae’s analysis software, and loaded it up.

Analyzing the Capture

Importing the file instantly populated waveform channels and metadata. But what protocol was used?
We know from our research that microSD cards use Serial Peripheral Interface (SPI) as a communication protocol. While they are capable of operating in both SD (native) and SPI modes, SPI is commonly used in embedded systems where simplicity and fewer pin requirements are priorities. It’s a go-to for interfacing microcontrollers with SD cards.

An image to describe post

So, we decided to add a SPI Analyzer. To decode SPI correctly, you need to match the configuration settings.

An image to describe post

There's multiple ways to determine each parameter:

You must know which physical lines correspond to:

Analyzer Setting What to Assign
Clock The channel with repeating square wave during transmission
MOSI Line toggling before slave responds (data to slave)
MISO Line toggling after command (data from slave)
Enable (CS/SS) Channel that goes low before and high after communication (can be disabled if unavailable)

For Clock Polarity (CPOL):

Zoom into the wave before transmission starts (i.e., before CS goes active):

  • If clock is LOW before data, CPOL = 0
  • If clock is HIGH before data, CPOL = 1

Tip: Saleae will give a “idle state mismatch” error if this is wrong. That’s your clue.

Now, for Clock Phase (CPHA):

Once CS goes low and clock pulses start:

  • Leading edge = the first edge after CS goes active
  • If CPOL = 0 → leading edge = rising
  • If CPOL = 1 → leading edge = falling

Now check:

  • Does data change on leading edge and settle on trailing?
    → Then CPHA = 1
  • Does data settle on leading edge?
    → Then CPHA = 0

Our Configuration

After a bit of tinkering, we figured out the combination.

An image to describe post

Now we can see our final analysis result in the terminal.

An image to describe post

Decoding information

First, I tried to decode it from the Logic 2 software by itself turning the output format to ASCII but that didn't help. So, we exported the data as CSV which looks like this -

Time [s],Packet ID,MOSI,MISO
2.481394520000000,0,\xFF,\xFF
2.481428680000000,0,@,\xFF
2.481472660000000,0,\0,\xFF
2.481513460000000,0,\0,\xFF
2.481550740000000,0,\0,\xFF
2.481584520000000,0,\0,\xFF
2.481618060000000,0,\x95,\xFF
2.481651280000000,0,\xFF,\xFF
2.481685000000000,0,\xFF,\x01
2.481729560000000,0,\xFF,\xFF
2.481763720000000,0,H,\xFF
...

While reviewing the CSV, a pattern emerged — many lines were noise: \xFF,\xFF and \xFF,\0.

We cleaned them up with this script:

def clean_spi_data(input_file="data.csv", output_file="cleaned_data.csv"):
    with open(input_file, 'r', encoding='utf-8') as infile, \
         open(output_file, 'w', encoding='utf-8') as outfile:
        
        for line in infile:
            if line.strip().startswith("Time"):
                outfile.write(line)
                continue

            parts = line.strip().split(',')
            if len(parts) == 4:
                mosi, miso = parts[2].strip(), parts[3].strip()
                # Skip if both fields are exactly these values
                if (mosi == r'\xFF' and miso in {r'\xFF', r'\0'}):
                    continue

            outfile.write(line)

    print(f"{output_file}")

clean_spi_data()

output: **cleaned_data.csv"

Time [s],Packet ID,MOSI,MISO
2.481428680000000,0,@,\xFF
2.481472660000000,0,\0,\xFF
2.481513460000000,0,\0,\xFF
2.481550740000000,0,\0,\xFF
2.481584520000000,0,\0,\xFF
2.481618060000000,0,\x95,\xFF
2.481685000000000,0,\xFF,\x01
2.481763720000000,0,H,\xFF
2.481807700000000,0,\0,\xFF
...

This time we searched for the char "H" to find the expression "HTB{" and fortunately, we found it.

An image to describe post

Upon finding the exact timestamp, we used the following script to pull the flag out of the cleaned file:

def extract_flag_from_csv(input_file):
    flag_started = False
    flag_chars = []

    with open(input_file, 'r', encoding='utf-8') as f:
        for line in f:
            if line.startswith("Time"):
                continue

            parts = line.strip().split(',', 3)
            if len(parts) < 4:
                continue

            timestamp, packet_id, mosi, miso = parts
            char = miso.strip()

            if not flag_started:
                if timestamp.strip() == "2.493672220000000" and char == 'H':
                    flag_started = True
                else:
                    continue

            try:
                decoded_char = bytes(char, 'utf-8').decode('unicode_escape')
                flag_chars.append(decoded_char)
                if decoded_char == '}':
                    break  # End of flag
            except Exception:
                continue

    flag = ''.join(flag_chars)
    print("Extracted flag:", flag)
    return flag

extract_flag_from_csv("cleaned_data.csv")

and thus, we found our flag.

Found flag:

Extracted flag: HTB{unp2073c73d_532141_p2070c015_0n_53cu23_d3v1c35}

Conclusion

Secure Digital is a brilliant example of how embedded interfaces, if captured at the right moment, can leak everything. Through decoding SPI and understanding timing relationships, we peeled apart the signal structure and reconstructed hidden transmissions byte by byte.

This challenge reinforces how understanding low-level digital communication is crucial — especially in devices that seem quiet on the surface. From logic probes to protocol analyzers, it’s all about listening to the wires.