LUN4R-MU Timing Leak
The moonchace hardware challenge exposes a tiny VM ("LUN4R-MU") over TCP (103.49.131.90:31337) whose debugger accepts load $rN , load $rN 0($r0), batch … end, reset, and quit commands,…
Summary
The moonchace hardware challenge exposes a tiny VM ("LUN4R-MU") over TCP (103.49.131.90:31337) whose debugger accepts load $rN <imm>, load $rN 0($r0), batch … end, reset, and quit commands, echoing per-operation execution times in nanoseconds. Loading an address inside the protected range 0xA1–0xFF silently speculatively caches a value; a subsequent load of a byte candidate then runs fast (≈20 ns) if the candidate equals the cached byte and slow (≈40 ns) otherwise. Two solver scripts implement a Flush+Reload-style byte oracle against that protected memory and recover its ASCII contents NUL-terminated into flag.txt.
Files
sol.py— compact single-shot oraper, 4 attempts per byte.solve_lunar.py— hardened version with a candidate alphabet, vote counting, and rerun hints.flag.txt— the recovered secret, captured output of the runs below.
Analysis
The oracle
sol.py's leak_byte issues one connection containing, per candidate c:
reset
batch
load $r0 0xA1 # protected victim address ( enumeration starts at 0xA1)
load $r1 0($r0)
end
batch
load $r2 <c>
endfor every c in [0, 32..126], then quit. Every candidate therefore has two echoed timings; [index*2+1] is the probe timing. Since a cached probe completes in ~20 ns and an uncached one in ~40 ns, the script flags each candidate whose probe timing is < 32 ns. Exactly one candidate hits, because only the value that maps to the speculatively-cached byte finds its location warm. Sibling value loads with a shared line make false positives possible, so leak_byte retries up to 4 times and requires that at most one candidate matched, raising otherwise for manual investigation.
Hardening in solve_lunar.py
The second solver trims and stabilizes the attack:
- The candidate set is reordered to a realistic flag alphabet,
Lun4C{}abcdefghijklmnopqrstuvwxyz_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZplus printable ASCII, deduped withdict.fromkeys. - Per address it runs up to 9 independent
--votesrounds, tuples(candidate, timing), and accepts a hardtiming < 32single-hit; on ambiguity, it falls back to the global minimum if that is< 35. Counter(...).most_common(1)majority-votes the byte, and anything weaker than--min-majority 3prints a rerun suggestion (--start 0x<addr> --votes 15).- The outer loop scans from
--start 0xA1to0x100, appending bytes until a0terminator or the closing}(which also stops early viaord("}")).
Exploit
Both drivers stream the same protocol shape; a full run is a single connection of hundreds of command lines, well under the 15-second response timeout. The recovered secret in flag.txt is:
Lun4R{c0sm1c_b4rr13rs_br0k3n_4nd_h4v0k_s3cur3d}Flag
Lun4R{c0sm1c_b4rr13rs_br0k3n_4nd_h4v0k_s3cur3d}Note this is the byte-level secret leaked from addresses 0xA1–0xFF, which the solver wrote to flag.txt on success.