HTB: Secret Treasures Challenge
Secret Treasures - HackTheBox Challenge Writeup
Challenge Information
| Field | Value |
|---|---|
| Name | Secret Treasures |
| Category | Hardware |
| Difficulty | Medium |
| Author | d3vn0mi |
Description
Our years undercover in the art dealing world have paid off, and we have compromised the inner circle. The coordinates of the stolen treasures are contained within a mysterious device that we now have clearance to access. We managed to dump its flash memory and extract the embedded software running on it, and tap its input channel with a trigger-based capturing device in order to monitor passcode input. Its secrets must be retrieved!
The challenge ships three artifacts: a Saleae Logic 2 capture (input_channel_trace.sal) of a passcode-entry line, a 16 MB SPI flash dump (flash_memory_dump.bin), and the ARM32 firmware binary that runs on the device (embedded_software). The goal is to reverse the firmware’s flag-extraction logic, replay it against the captured passcode traffic, and pull the flag out byte-by-byte from the flash dump.
Solution
This is a pure hardware/RE challenge, not a forensics or web task — all three artifacts have to be worked together:
- Decode the raw Saleae
.saldigital capture down to bit transitions. - Recover the UART bytes carried on that line — 1001 separate 8-digit passcode attempts.
- Disassemble the ARM firmware to understand how an 8-digit passcode is turned into a PRNG seed, and how that PRNG is used to pull single bytes out of SPI flash across 49 iterations.
- Replay the recovered LCG against the flash dump for every captured passcode until the extracted bytes spell out a valid flag.
Key Steps
1. Recovering artifacts
The staged trace file was a 0-byte placeholder; the real archive was password-protected and had to be extracted directly:
# Restore the real artifact set (trace, flash dump, firmware ELF)unzip -o -P hackthebox /out/<challenge-uuid>.zip# -> input_channel_trace.sal# -> flash_memory_dump.bin (16 MB SPI flash image)# -> embedded_software (ARM32 ELF, statically linked against wiringPi)2. Parsing the Saleae .sal digital trace
Saleae Logic 2 .sal files are zip containers; the digital channel data (digital-0.bin) is an undocumented run-length/delta encoded stream. Reverse-engineering the varint format was the first real hurdle:
# digital-0.bin varint format (reverse engineered):# byte0: bit6 = continuation flag, bits[5:0] = high 6 data bits# byte1+: bit7 = continuation flag, bits[6:0] = 7 data bits (little style tail)# delta = decoded_value + 1## Sanity check: summing per-block deltas reproduces the block's exact# sample-length field, confirming the encoding.
import struct
def read_varint(buf, off): b0 = buf[off] cont = b0 & 0x40 val = b0 & 0x3f off += 1 shift = 6 while cont: b = buf[off] cont = b & 0x80 val |= (b & 0x7f) << shift shift += 7 off += 1 return val + 1, off
# Walking the stream yields 46,838 signal transitions# sampled at 50 MS/s.3. Decoding UART traffic off the trace
With absolute transition timestamps in hand, the line was decoded as standard 8N1 UART:
# 38400 baud @ 50 MS/s -> 1301.5 samples per bitBAUD_SAMPLES = 1301.5
def level_at(t, transitions): # binary-search the transition list to find the line level # at sample time t ...
def decode_uart_byte(start_bit_time, transitions): # sample the 8 data bits at the middle of each bit period # following the start bit, LSB first, then check the stop bit bits = [] for i in range(8): t = start_bit_time + BAUD_SAMPLES * (1.5 + i) bits.append(level_at(t, transitions)) return sum(b << i for i, b in enumerate(bits))This produced 8,008 ASCII digit characters, cleanly splitting into 1001 distinct 8-digit passcode attempts captured on the device’s input channel — every guess the operator (or a brute-forcer) sent at the keypad/UART.
4. Reversing the firmware
objdump on the host couldn’t touch the ARM32 target, so the ELF was disassembled with Capstone:
import structfrom capstone import *
data = open('embedded_software', 'rb').read()# ... parse ELF32 headers/sections to locate .text and entry points ...
md = Cs(CS_ARCH_ARM, CS_MODE_ARM)for insn in md.disasm(text_bytes, text_vaddr): print(f"0x{insn.address:x}:\t{insn.mnemonic}\t{insn.op_str}")Key findings from the disassembly:
-
main()reads 8 ASCII characters from/dev/ttyS0and converts them withstrtoul()into a 32-bit seed,next_in_seq. -
random_generator()is a classic Linear Congruential Generator:// x = seed after strtoul() of the 8-digit passcodeuint32_t random_generator(uint32_t x) {x = x * 0x41C64E6D + 0x8042A;return x % 0xFFFFFF;} -
The firmware then loops 49 times, each iteration drawing three successive LCG outputs
r1, r2, r3and building a flash address:addr = (r1 & 0xff0000) | (r2 & 0xff00) | (r3 & 0xff);byte = spi_read(cmd=0x03, addr); // standard SPI flash READ commandflag[i] = byte; // one flag byte extracted per iterationIn other words, a correct 8-digit passcode seeds the LCG, and the LCG’s output stream is used to scatter-read the flag one byte at a time out of the 16 MB flash image — a correct passcode is required to land on the right 49 addresses.
5. Replaying against every captured passcode
With the LCG and address-derivation logic known, every one of the 1001 captured 8-digit attempts was replayed offline against the flash dump until one produced a printable flag:
flash = open('flash_memory_dump.bin', 'rb').read()
def try_passcode(seed): x = seed out = bytearray() for _ in range(49): x = (x * 0x41C64E6D + 0x8042A) % 0xFFFFFF; r1 = x x = (x * 0x41C64E6D + 0x8042A) % 0xFFFFFF; r2 = x x = (x * 0x41C64E6D + 0x8042A) % 0xFFFFFF; r3 = x addr = (r1 & 0xff0000) | (r2 & 0xff00) | (r3 & 0xff) out.append(flash[addr]) return bytes(out)
for passcode in captured_passcodes: # all 1001 UART-decoded attempts candidate = try_passcode(int(passcode)) if candidate.startswith(b'HTB{REDACTED} and candidate.rstrip(b'\x00').endswith(b'}'): print(candidate) breakOne of the captured attempts reproduced a clean HTB{REDACTED} string, which was submitted and accepted by the HTB grader.
HTB{REDACTED}Tools Used
- Python 3 — custom
.salcontainer parser, varint delta decoder, UART bit-timing decoder, LCG replay/brute-force script - Capstone — ARM32 disassembly of the firmware ELF (host
objdumpcannot target ARM) unzip— password-protected artifact extraction- Manual ELF32 header/section parsing to locate
.text, symbols, and cross-references
Key Learnings
- Saleae
.salcaptures are just zip containers around an undocumented run-delta binary format — worth reverse engineering by hand when no public decoder exists; the internal varint (continuation-bit + 6/7 data bits,delta = value + 1) can be validated by checking that summed deltas equal the declared block length. - UART can be recovered purely from transition timestamps once the baud rate is known — no vendor tooling required, just bit-center sampling against a binary-searchable transition list.
- A “flash memory dump” + “firmware” + “captured input” triple is meant to be correlated, not solved independently — the firmware defines how the passcode becomes flash addresses, the flash dump holds the data, and the captured traffic supplies the candidate keys. Treat all three as one system.
- Weak, hand-rolled LCGs (
x = x*A + C) are trivially replayable once the multiplier/increment/modulus are recovered from disassembly — no need to brute the passcode space blindly when the traffic capture already contains real login attempts to test. - When an environment’s staged artifact looks suspiciously empty (0-byte trace file) or an injected write-up references an unrelated box, trust the actual binary artifacts over any accompanying narrative and re-derive the real target from what’s on disk.