Skip to content

SECURE_CONNECTION//PRESS[CTRL+J]FOR ROOT ACCESS

BACK TO INTEL
ReverseEasy

Crown Flash

CTF writeup for Crown Flash from SECCON

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 (around 0x2438XX) to a dynamically-allocated/copy-patched code buffer.

  • The pointer at rsp+0xc8 pointed to a code buffer in memory (we saw it around 0x7ffff7ff8000), 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 / rsi and initializes h = 0x72616e64 (ASCII content suggests rand-like seed).

  • Looping over input bytes i = 0..36 it 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 iteration i we try every possible byte b in 0..255, compute the resulting H3 and r11, and check whether it equals expected.

  • If a byte b matches, it is the correct byte, update H to H3 and 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 .rodata dump (37 32-bit values)

  • Per-iteration target is table[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:

bash

python3 solver.py
python
#!/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 c1 term: add a loop over candidate c1 values (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 H lets us recover the bytes incrementally.

  • Debugging with gdb and 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.so to 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 0x251e180x251efc region.

  • 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 x and updates the state H using the same multiplication and rotate operations the JIT used.

  • The solver sequentially brute-forces each byte (0..255) and checks whether the corresponding r11 equals the target computed from the static .rodata table.

  • If found, that byte is appended to the solution and the state H is 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):

bash

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.

  • H initial value 0x72616e64 is set to match the JIT initial seed.

  • For each byte, the solver tries b from 0..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).