TL;DR The Binary prints Flag: , then enforces a 30s input timeout and a strict length check of 37 bytes.
The real verifier is JIT-generated x86-64 code (Xbyak) called via call [rsp+0xc8]
JIT checks a 37 byte input against a 37*uint32_t table at VA 0x21EDA0 using a rolling 32bit state and 4byte key 42 19 66 99.
Analysis
Binary
- Validate the process filename via /proc/self/exe (anti-rename gate).
- Compute a runtime seed from several environment probes.
- Print Flag: , enforce a timeout, read a string, require length 37.
- Build an executable JIT function and call it as f(input_ptr, len, table_ptr).
- Print Correct! if the JIT returns 1, else Wrong.
Filename gate (“Bad filename”)
sub_251F10:
- Reads /proc/self/exe into a buffer.
- Takes the basename (searches for ’/’ and uses the tail).
- Computes a CRC32-like value over that basename using polynomial 0xEDB88320.
- Compares the result against 0xC50018F7.
The important detail: the code initializes the CRC accumulator as 0xFFFFFFFF and does not XOR with 0xFFFFFFFF at the end. That means the computed value is the bitwise complement of the standard CRC32 output
= =0xffffffff, xorout=0(s) = ~crc32(s)
For s = "crown_flash"
- crc32(“crown_flash”)=0x3affe708
- ~0x3affe708 = 0xc50018f7
Anti-debugging via IFUNC ptrace
There is an IFUNC resolver (ifunc_2450F0)
- ptrace(PTRACE_TRACEME, 0, 0, 0) via syscall
- Writes a global dword_381810
- 0 if ptrace succeeds
- 0xDEAD if ptrace fails
Later, the program computes a value that will be passed into the JIT generator, and it explicitly forces/clears the sign bit depending on this anti-debug state. The net effect
- Normal: sign bit cleared, JIT uses a simple check
- Debugging detected: sign bit set, JIT adds an extra per-index perturbation, making the verification mismatch unless
Prompt, timeout, and length check
Inside the main check function (the large sub_24EE10 region):
- Prints Flag: .
- Uses poll() with timeout = 0x7530 = 30000 milliseconds.
- If timeout occurs, prints Too slow!.
- Reads input into a std::string and checks:
- len == 0x25 (37 bytes), else prints Wrong.
Only then it runs the JIT verifier.
IDA shows a global string: ourhardworkbythesewordsguardedpleasedontsteal
This is referenced from inside the big function (sub_24EE10), where it is used to construct a std::string object (via a routine that matches std::basic_string::_S_construct-style behavior).
It is not the flag and it is not used as the flag input. It appears to be:
- a flavor string / easter egg,
- and/or a value used during internal setup (allocator / exception / JIT scaffolding), but it is not part of the final byte-by-byte verifier that decides Correct!.
Locating the real verifier (JIT call site)
After the input passes length checks, the code does:
- Build a callable at runtime.
- Then calls it via an indirect call:
- call qword ptr [rsp + 0xc8]
Right before this call, registers are set up as:
- rdi = input_ptr
- rsi = input_len (must be 37)
- rdx = 0x21EDA0 (pointer to a 37 * uint32_t table)
The JIT returns eax, and the program checks:
-
return value == 1: Correct!
-
else: Wrong
-
Then reconstructing the JIT algorithm
static inline uint32_t rol32(uint32_t x, int r) { return (x << r) | (x >> (32 - r));}int verify(const uint8_t *in, size_t n, const uint32_t *T) { if (n != 0x25) return 0; const uint8_t key[4] = { 0x42, 0x19, 0x66, 0x99 }; uint32_t acc = 0x72616e64; // 'dnar'
for (uint32_t i = 0; i < 0x25; i++) { uint32_t x = (uint8_t)(in[i] ^ key[i & 3]); x = x + i * 0x9e3779b9; acc = acc + x; acc = acc * 0x045d9f3b; acc = acc + rol32(acc, 7); uint32_t mixed = acc ^ (acc >> 16); uint32_t rconst = (i + 1) * 0x7ed55d16 + 0xc761c23c;
if (mixed != (T[i] ^ rconst)) return 0; } return 1;}Recurrence with wraparound
- = 0x72616e64
If the sign bit is set Normal execution, the code forces the sign bit clear, so this branch is skipped
Exploit
- Compute the target
- Try all 256 bytes , simulate one step, compute mixed
- The byte that makes mixed == is the correct
- Commit the new acc and move to the next index.
This is only 37256 iterations, i.e. trivial
FLAG_LEN = 0x25TABLE_VA = 0x21EDA0KEY = bytes([0x42, 0x19, 0x66, 0x99])ELF64_EHDR = struct.Struct("<16sHHIQQQIHHHHHH")ELF64_PHDR = struct.Struct("<IIQQQQQQ")PT_LOAD = 1
def rol32(x, r): x &= 0xffffffff return ((x << r) | (x >> (32 - r))) & 0xffffffff
def va_to_off(f, va): f.seek(0) eh = ELF64_EHDR.unpack(f.read(ELF64_EHDR.size)) e_phoff = eh[5] e_phentsize = eh[9] e_phnum = eh[10] f.seek(e_phoff) for _ in range(e_phnum): ph = ELF64_PHDR.unpack(f.read(e_phentsize)) p_type, p_flags, p_offset, p_vaddr, p_paddr, p_filesz, p_memsz, p_align = ph if p_type != PT_LOAD: continue if p_vaddr <= va < p_vaddr + p_memsz: return p_offset + (va - p_vaddr) raise ValueError("VA not in any PT_LOAD segment")
def main(): with open("crown_flash", "rb") as f: off = va_to_off(f, TABLE_VA) f.seek(off) raw = f.read(FLAG_LEN * 4) T = list(struct.unpack("<" + "I" * FLAG_LEN, raw)) acc = 0x72616e64 out = bytearray() for i in range(FLAG_LEN): r14 = (((i + 1) * 0x7ed55d16) + 0xc761c23c) & 0xffffffff target = (T[i] ^ r14) & 0xffffffff found = None for b in range(256): x = (b ^ KEY[i & 3]) & 0xff x = (x + ((i * 0x9e3779b9) & 0xffffffff)) & 0xffffffff acc2 = (acc + x) & 0xffffffff acc2 = (acc2 * 0x045d9f3b) & 0xffffffff acc2 = (acc2 + rol32(acc2, 7)) & 0xffffffff mixed = (acc2 ^ (acc2 >> 16)) & 0xffffffff if mixed == target: found = b acc = acc2 break if found is None: raise RuntimeError(f"no solution at i={i}") out.append(found) print(out.decode())
if __name__ == "__main__": main()