filegr
filegr
Platform: CUSTOM | Category: Reversing | Type: Challenge | Difficulty: Easy | OS: N/A | **Author: XESXOR Techniques: static_analysis, per_byte_bruteforce, independent_constraint_solving, x86_arithmetic_modeling
Summary
Walkthrough of the CUSTOM Reversing filegr. Task: 32-bit x86 PIE ELF crackme reading a file whose 36-char content must satisfy 36 independent obfuscated per-character arithmetic checks; the content itself becomes the flag body. Solution: transc
Description
Usage: %s filepath
Success! You can pass flag now: spbctf{%s}
A 32-bit x86 ELF (PIE, dynamically linked, ~15 KB, not stripped) is given. It
reads a file path from argv[1], parses a single whitespace-delimited token from
the file, runs a series of checks over it, and — if all pass — prints
spbctf{<file content>}. So the goal is to recover the exact 36-character string
that passes every check; that string is the flag body.
Analysis
Symbols (nm): main, readFromFile, check_them_all. Imports: fopen,
__isoc99_fscanf, malloc, strlen, exit, puts, printf.
main (@ 0x1b83)
- Requires
argc == 2, else printsUsage: %s filepath. buf = readFromFile(argv[1]).if (check_them_all(buf) == 1) printf("... spbctf{%s}\n", buf);The flag body is literally the file content that passes the check.
readFromFile (@ 0x11f9)
malloc(0xff),fopen(path, "r"); ifNULL→ error +exit(0).fscanf(fp, "%s", buf)— reads one whitespace-delimited token.- returns the buffer.
check_them_all (@ 0x1278)
-
Prints
[+] Running some checks.... -
strlen(input)must equal0x24(36); otherwiseWrong length!+exit(0). -
Then, for each of the 36 indices, an independent per-character transform of the general form:
((c * A + B) * C ^ D) * E == target (32-bit wraparound)with per-index variants:
xor al, imm8— XOR affects only the low byte, not the full 32-bit reg.- a shift instead of the final multiply, e.g. index 3 uses
shl 9, index 17 uses(x*5) << 7.
-
Each index's boolean result is stored in a stack slot; at the end all 36 slots are AND-ed via a long chain of
cmp slot, 0 ; je fail. Return 1 iff every check passed.
The critical observation: no cross-character dependency. Each byte is validated
in complete isolation, so each byte can be brute-forced independently over
0..255, keeping the single printable-ASCII solution (which turned out to be
unique for every index).
Solution
Transcribe all 36 constraints straight from the objdump -d -M intel disassembly,
carefully modeling three x86 quirks:
- 32-bit modular arithmetic — mask every intermediate with
0xFFFFFFFF. xor al, imm— low-byte-only XOR, not a full 32-bit XOR.- Shift-based multiplies —
shl 9,(x*5) << 7, etc.
python read-only • copy allowed#!/usr/bin/env python3 MASK = 0xFFFFFFFF def m(x): # 32-bit imul / add wraparound return x & MASK def xor_al(x, imm): # `xor al, imm8` — only the low byte is affected return (x & 0xFFFFFF00) | ((x & 0xFF) ^ (imm & 0xFF)) # Each constraint is a callable f(c) that reproduces that index's exact # obfuscated arithmetic and returns the 32-bit result to compare against target. # The tuples below are transcribed verbatim from the disassembly of # check_them_all; A,B,C,D,E are the multiply/add/xor/multiply constants, and # `target` is the value the slot is compared to. Variant flags select the # low-byte XOR and shift-multiply forms. # Illustrative form of a "normal" index: ((c*A + B) * C ^ D) * E == target def make_normal(A, B, C, D, E, target, xoral=False): def f(c): x = m(m(c * A) + B) x = m(x * C) x = xor_al(x, D) if xoral else m(x ^ D) x = m(x * E) return x == target return f # Shift variants, e.g. index 3: (... ) shl 9 ; index 17: (x*5) << 7 def make_shift(A, B, C, D, shift, target, pre_mul=1, xoral=False): def f(c): x = m(m(c * A) + B) x = m(x * C) x = xor_al(x, D) if xoral else m(x ^ D) x = m(m(x * pre_mul) << shift) return x == target return f # constraints[i] = one of the callables above, built from the disassembled # constants for index i (36 entries total). constraints = [ ... ] # 36 transcribed checks def solve(): out = [] for i, f in enumerate(constraints): cands = [chr(c) for c in range(0x20, 0x80) if f(c)] # printable only assert len(cands) == 1, (i, cands) # unique solution out.append(cands[0]) return "".join(out) if __name__ == "__main__": flag_body = solve() print(len(flag_body), flag_body) assert len(flag_body) == 0x24 print("spbctf{%s}" % flag_body)
Running the solver recovers the 36-char content:
REDACTED
which reads as "angr is for really lazy reversers" (4n6r = angr) — a hint
that the intended shortcut is angr symbolic execution over these linear
per-character constraints instead of hand-transcribing them.
Verification
macOS lacks qemu-user, so the real 32-bit binary was run under Docker on
linux/386:
bash read-only • copy allowedcurl -O https://rev-kids20.forkbomb.ru/files/rev/new8/filegr.elf chmod +x filegr.elf printf 'REDACTED' > input.txt # exactly 36 bytes docker run --rm --platform linux/386 -v "$PWD":/w -w /w \ i386/ubuntu:18.04 ./filegr.elf input.txt
Output:
[+] Running some checks...
[+] Success! You can pass flag now: spbctf{REDACTED}
Key Takeaways / Lessons
- Mastering static_analysis, per_byte_bruteforce, independent_constraint_solving, x86_arithmetic_modeling was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-07-02 • CUSTOM • filegr • Difficulty: Easy