Skip to main content

Faultline

Challenge

Imported from local notes.md.

Solution

Original Notes

Faultline

Challenge Summary

  • Given: a single static ELF64 binary named faultline and a minimal description: good luck!.
  • Goal: recover the local acceptance pair and extract the flag in the format CIT{...}.
  • Constraints: the binary is statically linked, unstripped, and native execution on this host triggers SIGILL, so analysis and execution needed an emulated runner.

Initial Recon / Triage

  • File identification: starting_files/faultline is a statically linked, non-PIE, unstripped x86-64 ELF.
  • Runtime surface: running it showed subcommands notes, score, trace, token, compare, nudge, and submit.
  • Key observation: qemu-x86_64 could execute the scoring paths cleanly even though direct native execution faulted with an illegal instruction.
  • Built-in notes exposed the intended model: stress uses adjacent symbols on a 2:3 wheel, shear uses xor across distance two, grain folds positions i, i+1, and i+3, and the target score is exactly 2026.

Hypotheses & Approach

  • Hypothesis 1: the profile alphabet is a 16-symbol ring and the score is a sum of penalties/rewards against hidden target traces.
  • Hypothesis 2: because the binary is unstripped, the named routines parseProfile, computeFaultlineScoreVisible, and buildSurveyTokenVisible would be enough to reconstruct both the profile solver and token builder without relying on the hidden validate body.
  • Approach: extract the embedded target trace arrays from .rodata, rebuild the scoring and token math in Python, solve the profile, then use the binary's own local submit command under qemu-x86_64 to reveal the flag.

Execution Steps (Reproducible)

Stage 1

Commands:

cd /root/cit2026CTF/Faultline
file starting_files/faultline
checksec --file=starting_files/faultline
qemu-x86_64 ./starting_files/faultline
qemu-x86_64 ./starting_files/faultline notes

Results:

  • The binary reported the usable command surface and the restricted alphabet BCDFGHJKLMNPQRST with profile length 12.
  • The notes command described the stress, shear, grain, load, and seal scoring families.

Stage 2

Commands:

cd /root/cit2026CTF/Faultline
python3 - <<'PY'
from pathlib import Path
p = Path('starting_files/faultline').read_bytes()
ro_vaddr = 0x579000
ro_off = 0x179000
for name, addr, count in [('OBS_STRESS', 0x579600, 11), ('OBS_SHEAR', 0x579640, 10), ('OBS_GRAIN', 0x579680, 9)]:
off = ro_off + (addr - ro_vaddr)
values = [int.from_bytes(p[off + i * 4: off + i * 4 + 4], 'little', signed=True) for i in range(count)]
print(name, values)
PY

Results:

  • Extracted target arrays:
    • OBS_STRESS = [2, 5, 11, 10, 5, 1, 13, 4, 3, 3, 14]
    • OBS_SHEAR = [5, 5, 15, 8, 5, 6, 7, 4, 5, 5]
    • OBS_GRAIN = [3, 11, 3, 4, 14, 4, 5, 6, 1]
  • Disassembly showed that stress alone induces a recurrence over a 16-element ring, so the search space collapses to just 16 candidate seeds.

Stage 3

Commands:

cd /root/cit2026CTF/Faultline
python3 artifacts/solve_faultline.py
python3 artifacts/solve_faultline.py --submit

Results:

  • The solver found a unique valid profile: SDPKGTCMJRFL.
  • The reconstructed token builder produced Z2L-2F5-BUBP.
  • Running the local submit path with that pair revealed the flag CIT{12z4PXVTa3x3}.

Artifacts Produced

  • artifacts/solve_faultline.py: local reimplementation of the recovered scoring and token-generation logic.
  • artifacts/solution.txt: recovered profile, token, score, and final flag.

Flag

CIT{12z4PXVTa3x3}