← Back to Writeups
CUSTOMEasyReversing

filegr

XESXOR9/10/20264 min read
#reversing#custom#easy#static_analysis#per_byte_bruteforce#independent_constraint_solving#x86_arithmetic_modeling#linux

filegr

Platform: spbctf | Category: Reversing | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-07-02 | Status: Solved Techniques: static_analysis, per_byte_bruteforce, independent_constraint_solving, x86_arithmetic_modeling

Summary

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: transcribe each index's ((c*A+B)*C^D)*E==target formula from objdump (modeling 32-bit wraparound, xor al low-byte-only, shl variants) and brute-force each byte 0..255 for the unique printable solution.

filegr — SPB CTF (kids/rev)

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 prints Usage: %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"); if NULL → 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 equal 0x24 (36); otherwise Wrong 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:

  1. 32-bit modular arithmetic — mask every intermediate with 0xFFFFFFFF.
  2. xor al, imm — low-byte-only XOR, not a full 32-bit XOR.
  3. Shift-based multiplies — shl 9, (x*5) << 7, etc.
python
#!/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
curl -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}

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍