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?"
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.
So, we decided to add a SPI Analyzer. To decode SPI correctly, you need to match the configuration settings.
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.
Now we can see our final analysis result in the terminal.
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.
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.