HTB: Secret Treasures Challenge

Secret Treasures - HackTheBox Challenge Writeup

Challenge Information

FieldValue
NameSecret Treasures
CategoryHardware
DifficultyMedium
Authord3vn0mi

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:

  1. Decode the raw Saleae .sal digital capture down to bit transitions.
  2. Recover the UART bytes carried on that line — 1001 separate 8-digit passcode attempts.
  3. 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.
  4. 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:

Terminal window
# 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 bit
BAUD_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 struct
from 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/ttyS0 and converts them with strtoul() into a 32-bit seed, next_in_seq.

  • random_generator() is a classic Linear Congruential Generator:

    // x = seed after strtoul() of the 8-digit passcode
    uint32_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, r3 and building a flash address:

    addr = (r1 & 0xff0000) | (r2 & 0xff00) | (r3 & 0xff);
    byte = spi_read(cmd=0x03, addr); // standard SPI flash READ command
    flag[i] = byte; // one flag byte extracted per iteration

    In 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)
break

One 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 .sal container parser, varint delta decoder, UART bit-timing decoder, LCG replay/brute-force script
  • Capstone — ARM32 disassembly of the firmware ELF (host objdump cannot target ARM)
  • unzip — password-protected artifact extraction
  • Manual ELF32 header/section parsing to locate .text, symbols, and cross-references

Key Learnings

  • Saleae .sal captures 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.