Crown_Flash — Reverse Challenge Writeup
-
Author: Team/You
-
Category: Reverse
-
Target: crown_flash (ELF x86-64, statically linked, stripped)
-
Flag Format: SECCON{...}
Overview
This challenge hides its core input verification inside a dynamically-generated (JIT-like) code block. The binary expects a single input and prints Flag: , followed by Correct! or Wrong. The objective was to reverse the validation algorithm and generate a correct payload (flag).
Preparation & Initial Observations
-
The file is an ELF 64-bit static stripped binary. Running it prints
Flag:and expects input from stdin. -
Strings analysis showed
Wrong,Correct!,Flag:and many runtime library messages.
Static Analysis
-
Looked up the
Flag:string and traced where it’s printed; we found a function that validates input and then indirectly calls a function pointer to dispatch the final validator. -
The code contains a length check comparing the input length to
0x25(37), so the flag is 37 bytes long. -
There’s an apparent CRC check earlier which compares a computed value to
0xc50018f7, but in our runs it always passed the program path (so it’s not input-dependent in our environment).
Dynamic Analysis
-
Running the program under
gdb, we set breakpoints around the validation logic and found a match/generic algorithm is performed via an indirect call:call *0xc8(%rsp). -
We set a watchpoint on that stack slot (
rsp+0xc8) and found it is written at a setup function (around0x2438XX) to a dynamically-allocated/copy-patched code buffer. -
The pointer at
rsp+0xc8pointed to a code buffer in memory (we saw it around0x7ffff7ff8000), suggesting that the binary generates a function at runtime used to check the input (a JIT). We dumped the code around this buffer.
Reverse-engineering the JIT
-
The dynamic code takes the input buffer and its length as arguments in
rdi/rsiand initializesh = 0x72616e64(ASCII content suggestsrand-like seed). -
Looping over input bytes
i = 0..36it computes per-byte steps roughly as:
- mask = [0x42, 0x19, 0x66, 0x99][i % 4]
- x = (input[i] ^ mask) + i * 0x9e3779b9 (mod 2^32)
- H1 = H + x (mod 2^32)
- H2 = H1 * 0x045d9f3b (mod 2^32)
- H3 = H2 + ROTL(H2, 7) (mod 2^32)
- r11 = H3 ^ (H3 >> 16)
- There’s a conditional term where if a certain sign bit in r13 is set, an extra term ((i * 0x632be5c9) ^ c1) was added to r11 – this depends on a random constant determined at runtime. In our test environment, that extra term was not applied (the chosen runtime c1 had its sign bit clear or the condition resulted in not adding it).
- Finally the per-iteration r11 is compared with an entry from a table located at .rodata (e.g., 0x21eda0). The table holds 37 32-bit values which must match r11 after masking by a small r14 XOR modification.
Reconstructing the Algorithm
- Combining the above means the per-iteration check is:
- expected = table[i] ^ ((i+1)*0x7ed55d16 + 0xc761c23c)
- We must find input[i] such that make_r11(H3) == expected where H3 is the updated hash state for that iteration and H is the running state.
Solving Approach
-
Because the per-iteration update is small and only depends on the previous state
H, the iteration can be solved sequentially (left to right): at iterationiwe try every possible bytebin0..255, compute the resultingH3andr11, and check whether it equalsexpected. -
If a byte
bmatches, it is the correct byte, updateHtoH3and proceed with the next iteration. Since there are only 256 possibilities per byte, this brute force approach is fast and deterministic. The algorithm is purely arithmetic and uses 32-bit unsigned arithmetic.
Deploying the Solver
I wrote a Python script solver.py that implements the state update and sequential brute force. Important details:
-
Initial
H = 0x72616e64 -
Constant masks
M = [0x42, 0x19, 0x66, 0x99] -
Table is taken from the
.rodatadump (37 32-bit values) -
Per-iteration
targetistable[i] ^ ((i+1)*0x7ed55d16 + 0xc761c23c)
Because the extra c1 term (mixed when a sign bit is set) was not active during execution, the solver ignores that term. The solver script includes code to try the simpler path (expected) and returns the byte sequence.
Running the Solver
Commands used to run the solver:
python3 solver.py
#!/usr/bin/env python3
"""
Solver for Crown_Flash: Reconstructs the 37-byte input checked by the JIT routine.
"""
from struct import pack, unpack
M = [0x42, 0x19, 0x66, 0x99]
EXPECTED_TABLE = [
1519851539, -1512952224, 929313778, -759091023,
1361408542, 1872277834, -1961930340, 1891994566,
1573528577, 681844225, -1320400951, -722713855,
2026543873, -756568311, 1516269594, -1870331073,
1821176531, -943196058, 814639659, -712779758,
-1954975390, -1263047893, -1501088074, 1578053261,
-1593891559, 1498387334, 1392083243, 1417685259,
-676225942, 599239661, -367477116, -988880472,
-1511847233, 1005389262, -2113591192, -1550646734, 1777904443
]
# Convert values into unsigned 32-bit integers
EXPECTED_TABLE = [v & 0xffffffff for v in EXPECTED_TABLE]
def rotl(x, r):
x &= 0xffffffff
return ((x << r) & 0xffffffff) | (x >> (32 - r))
def step_h(H, idx, byte):
# Per JIT: x = (byte ^ M[idx % 4]) + idx * 0x9e3779b9
x = (byte ^ M[idx % 4]) & 0xff
x = (x + (idx * 0x9e3779b9)) & 0xffffffff
H1 = (H + x) & 0xffffffff
H2 = (H1 * 0x045d9f3b) & 0xffffffff
H3 = (H2 + rotl(H2, 7)) & 0xffffffff
return H3
def make_r11(H3, idx, c1=0):
r11 = (H3 ^ (H3 >> 16)) & 0xffffffff
# JIT adds ((idx * 0x632be5c9) ^ c1) only when high bit of c1 is set
# For correct path observed, that extra term was not used
# r11 = (r11 + (((idx * 0x632be5c9) ^ c1) & 0xffffffff)) & 0xffffffff
return r11
def compute_target(idx, table_val):
r14 = ((idx + 1) * 0x7ed55d16 + 0xc761c23c) & 0xffffffff
return table_val ^ r14
def brute_solve():
H = 0x72616e64 # initial H seen in the JIT
res = []
for i, table_val in enumerate(EXPECTED_TABLE):
target = compute_target(i, table_val)
found = None
for b in range(256):
H3 = step_h(H, i, b)
r11 = make_r11(H3, i)
if r11 == target:
found = b
H = H3
res.append(b)
break
if found is None:
raise RuntimeError(f"Could not find byte for index {i}")
return bytes(res)
if __name__ == '__main__':
flag_bytes = brute_solve()
print('Flag:', flag_bytes.decode())This produced the correct input which decodes to the final flag:
Flag: SECCON{good->sPLqsLsooJY,EFwBU8Std7Y}
Notes and checks
-
The solver can be adapted easily to handle the
c1term: add a loop over candidatec1values (or inspect runtime to detect its value) and re-run reconstruction accordingly. -
The brute-force approach works thanks to the small, linear search space per byte (256), and the sequential dependence of the state
Hlets us recover the bytes incrementally. -
Debugging with
gdband watchpoints is key to finding the stack slot where the dynamic function pointer is placed (rsp+0xc8) and tracing how the code gets generated and used.
Appendix: Tools & Commands Used
-
file crown_flash– identify binary type -
strings crown_flash– inspect strings -
objdump -d crown_flash | grep <addr>– inspect disassembly around interesting addresses -
gdb -q ./crown_flash– breakpointing -
watch *(void**)($spval+0xc8)– watch pointer slot -
LD_PRELOAD=./fakerand.soto control dynamic random constants (used to check consistent JIT constants in testing environment)
Appendix: Useful Function Addresses
-
The main validation code we examined sits around the
0x251e18–0x251efcregion. -
The JIT code buffer pointer is set up in code near
0x243800.
Conclusion
This challenge mixes typical tasks for reverse: static analysis to find input constraints, dynamic analysis to find JIT code generation and write sites, and final algorithm reconstruction and brute-forcing the small per-byte search space. After reversing the JIT algorithm and implementing a solver, we recovered the flag.
Good luck, and happy reversing!
Solver Implementation
The solver is included as solver.py alongside this writeup. It implements a direct reverse of the JIT check:
-
It uses the mask list
M = [0x42, 0x19, 0x66, 0x99]used to XOR each input byte depending on the position modulo 4. -
It computes the per-iteration
xand updates the stateHusing the same multiplication and rotate operations the JIT used. -
The solver sequentially brute-forces each byte (0..255) and checks whether the corresponding
r11equals thetargetcomputed from the static.rodatatable. -
If found, that byte is appended to the solution and the state
His updated to continue to the next byte.
Key constants from the JIT code are embedded in solver.py so it’s simple to run and reproduce the solution.
Example usage and verification (also included in the solver):
python3 solver.py
# The script prints: Flag: SECCON{good->sPLqsLsooJY,EFwBU8Std7Y}
# To verify with the original binary:
printf 'SECCON{good->sPLqsLsooJY,EFwBU8Std7Y}\n' | ./crown_flash
# The program prints: Flag: Correct!
Brief Solver Walkthrough
-
rotl(x,7)— rotation used by JIT to mix bits. -
Hinitial value0x72616e64is set to match the JIT initial seed. -
For each byte, the solver tries
bfrom0..255, computes:
- x = (b ^ M[i%4]) + i * 0x9e3779b9
- H1 = H + x
- H2 = H1 * 0x045d9f3b
- H3 = H2 + rotl(H2, 7)
- r11 = H3 ^ (H3 >> 16) (without the extra c1 term used in alt paths)
- If r11 == expected_target then b is correct; set H = H3 and continue.
This brute-force, incremental method is fast enough to recover the keyword with a single run (37 * 256 checks max).