0xnhl

Forge

/ Update
2 min read

Forge [RE] - Detailed Writeup#

Challenge#

  • Name: Forge
  • Category: Reverse Engineering
  • Given file: forge
  • Prompt hint: only a trinket/firmware can open the workshop

Expected flag format on platform: apoorvctf{...}.


1) Fast Triage#

I started with standard binary triage.

file forge
strings -n 6 forge
readelf -h forge
readelf -S forge
bash

Key observations:

  • forge is a stripped 64-bit PIE ELF.
  • Imports include OpenSSL primitives: EVP_sha256, EVP_aes_256_gcm, RAND_bytes, etc.
  • ptrace, fork, waitpid, prctl are present (anti-debug + sandbox-like behavior).
  • A suspicious encoded string appears in .rodata that decodes to payload.bin.

Running once:

./forge
echo $?
bash

It exits silently with code 1.


2) Why It Exits Immediately (Anti-Debug)#

I traced execution:

strace -o /tmp/forge.strace ./forge
bash

Important line in trace:

ptrace(PTRACE_TRACEME) = -1 EPERM
text

So the binary intentionally fails when it detects tracing/debug context.

In assembly, main starts with:

  • call ptrace@plt
  • compare result with -1
  • jump to an exit routine on failure

This explains the immediate exit during tracing.


3) Static RE of Main Logic#

With objdump -d -Mintel forge, the core flow is:

  1. Anti-debug check (ptrace).
  2. mmap executable + data regions.
  3. Build and solve a 56x56 system over a custom finite-field multiply table.
  4. Derive a 56-byte value from that solve.
  5. Hash/derive keys with SHA-256.
  6. Perform AES-256-GCM operations over 56-byte blocks.
  7. Decode a filename by XORing bytes with 0x5a.

That filename is recovered from .rodata bytes at 0x2020:

2a 3b 23 36 35 3b 3e 64 38 33 34
text

XOR with 0x5a gives:

payload.bin
text

The program tries to open/read this file and then uses seccomp-like prctl setup and child execution flow. That path is noisy and not needed to get the flag.


4) The Important Part: Embedded Math System#

Main solver uses three fixed .rodata objects:

  • b vector at virtual 0x2040, length 56
  • A matrix at virtual 0x2080, size 56*56
  • GF(256)-style multiply table at virtual 0x2cc0, size 256*256

In file offsets (because .rodata starts at file 0x2000):

  • b at 0x2040 (+0x40 in .rodata)
  • A at 0x2080 (+0x80)
  • mul at 0x2cc0 (+0xcc0)

The routine performs Gaussian elimination in that algebra:

  • pivot search for non-zero byte
  • row swaps
  • multiplicative inverse lookup using mul[(pivot<<8)+x] == 1
  • row normalization and elimination with table multiply and XOR

So this is a pure static solve: no need to emulate runtime anti-debug path.


5) Deterministic Solver Script#

I wrote a reproducible extractor:

  • Script: forge-solve.py
  • It reads forge directly and reconstructs the same augmented matrix logic.

Run:

python3 forge-solve.py
bash

Output:

APOORVCTF{Y0u_4ctually_brOught_Y0ur_owN_Firmw4re????!!!}
text

6) Final Flag#

Raw recovered string:

APOORVCTF{Y0u_4ctually_brOught_Y0ur_owN_Firmw4re????!!!}

If platform enforces lowercase prefix format, submit:

apoorvctf{Y0u_4ctually_brOught_Y0ur_owN_Firmw4re????!!!}


Why This Works (Short Reasoning)#

  • The binary contains all cryptographic/math material in .rodata.
  • Anti-debug and payload execution are defensive layers, not required for extraction.
  • The core secret is produced by a deterministic linear solve over the embedded GF table.
  • Re-implementing that solver statically reproduces the exact 56-byte flag string.
Forge
https://nahil.xyz/vault/writeups/apoorvctf2026/re/forge/
Author Nahil Rasheed
Published at March 24, 2026
Disclaimer This content is provided strictly for educational purposes only.