burnt-out - full reverse solve writeup
This one looked like a game config repair task, but the core trick was a weak runtime reflection system that compares only string hashes. That design bug lets us feed arbitrary collision keys, build hidden action/predicate objects, and drive the player to the goal on both local and remote instances.
Provided artifacts: stripped PIE ELF (application/octet-stream) and remote service
nc 34.179.171.239 31095. Goal format is the standard DCTF token with braces.
- Quick Triage
- Understanding the Input Model
- Critical Bug: Hash-Only Field/Type Resolution
- Recovering Runtime Type Metadata
- Action/Predicate VM Reconstruction
- Building a Deterministic Win Strategy
- Generating Collision Keys
- Final Payload Structure
- Remote Solve
- Takeaways
1. Quick Triage
file ./game-release-1362197-jenkins-test-build_final2
checksec --file=./game-release-1362197-jenkins-test-build_final2
strings -n 5 ./game-release-1362197-jenkins-test-build_final2 | head -n 120
The binary is a stripped 64-bit PIE ELF. Strings show an in-binary decoy message that looks like a flag, but it is intentionally not the real remote value. So we need full behavioral solve, not just string extraction.
2. Understanding the Input Model
Running the program shows it expects JSON text terminated by a line containing END. The root object is not a typed
object itself; it must contain fields like on_start.
Minimal valid config:
{
"on_start": [
{
"$type": "log_action_t",
"message": "hi"
}
]
}
END
This executes 32 ticks, then prints madness/death text if no goal reached. That gave the first execution baseline.
3. Critical Bug: Hash-Only Field/Type Resolution
Reversing the lookup function (fcn.00004cb0) shows the parser computes:
h = sum((i + 1) * s[i])
Then it resolves type/field definitions by hash table lookup, without final string equality verification. This means any string that hashes to the expected value is accepted as that semantic field or type name.
This single flaw is the whole challenge pivot: we can synthesize valid objects using collision keys even when we do not know the original source names.
4. Recovering Runtime Type Metadata
The binary builds a reflection table at runtime (type entries + field descriptors + hash buckets).
I dumped these regions in gdb after initialization and parsed them offline.
# key snapshots dumped from process memory:
/tmp/types.bin
/tmp/ht.bin
/tmp/alloc_chunk.bin
Parsing these structures gave all required hashes and field layouts for the gameplay VM objects:
player_thash path and root field hashes (includingon_starthash0xf7c).log_action_ttype hash0x205e, message field hash0xb6e.move_actiontype hash0x572e, direction field hash0x28c.- Predicate/action container types used for conditional execution (
0x4ddd,0x31cc,0xaffd).
5. Action/Predicate VM Reconstruction
Dispatcher logic in fcn.00002120 maps type hashes to callbacks. Important callbacks:
0x1740: print message action.0x17e0: movement action.0x1770: conditional/compound action with predicate list + nested action list.0x1ba0/0x1c60: predicate combinators.0x1cc0: directional blocked/lava check predicate.
Movement callback details from disassembly:
- Direction enum is 1..4 (0 is invalid and asserts).
- World bounds are
0..29on each axis (30x30). - Checks occupancy and lava maps before committing move.
- If player position equals hidden goal coordinates (
map+0x1c28/0x1c2c), setsplayer[9]=1.
Main game loop checks that byte and prints success when it flips to 1.
6. Building a Deterministic Win Strategy
Direct scripted movement can randomly fail by stepping into lava. To make the solve robust across map RNG, I used predicate-guarded movement:
if (NOT blocked(dir)) then move(dir)
Then repeated this for all four directions in a cycle, many times, within the 32-tick window. Because each move is guarded by the runtime blocked/lava predicate, the script avoids fatal steps while still exploring.
In practice, this converged reliably and reached the goal consistently in local tests.
7. Generating Collision Keys
Since keys are hash-matched, we can use collision strings instead of true field/type names. I generated printable ASCII collisions for every needed hash (type names, field names, enum names).
def h(s):
return sum((i + 1) * ord(c) for i, c in enumerate(s))
def gen_collision(target):
# build printable string with h(s) == target
# (bounded search with arithmetic feasibility pruning)
...
Example collision targets solved this way: 0x572e (move type), 0x28c (direction field),
0x4ddd (if/compound action), 0x31cc/0xaffd (predicate nodes).
8. Final Payload Structure
Final JSON uses collision keys but semantic shape is:
{
"on_start": [
{
"$type": "if_action_like",
"conditions": [
{
"$type": "predicate_not_like",
"inner": {
"$type": "is_blocked_like",
"dir": "LEFT"
}
}
],
"actions": [
{ "$type": "move_action_like", "dir": "LEFT" }
]
},
{
"$type": "if_action_like",
"conditions": [ ... dir RIGHT ... ],
"actions": [ ... move RIGHT ... ]
},
{
"$type": "if_action_like",
"conditions": [ ... dir UP ... ],
"actions": [ ... move UP ... ]
},
{
"$type": "if_action_like",
"conditions": [ ... dir DOWN ... ],
"actions": [ ... move DOWN ... ]
}
// repeated several rounds
]
}
END
Local runs repeatedly reached success (instead of lava or madness output), confirming the chain is stable.
9. Remote Solve
cat payload.json | nc 34.179.171.239 31095
The remote service returned the real challenge flag line. Omitted here intentionally.
[flag intentionally omitted from public writeup]
10. Takeaways
- Reflection systems must never trust hashes alone; always verify canonical string identity after hash bucket match.
- CTF VM-style binaries are often easier solved by reconstructing object schemas than by brute-forcing gameplay.
- When map dynamics are random, predicate-guarded action plans are more reliable than hard-coded paths.
- Memory dumps plus small offline parsers are extremely effective against stripped binaries with runtime metadata.
Writeup by mitza (Tudor Mihai Alexandru).
More challenges: Cyber-EDU profile