LACTF ‘86 Flag Checker — Writeup
Challenge Overview
We’re given two files:
- CHALL.EXE — A 16-bit MS-DOS executable (9.8 KB)
- CHALL.IMG — A 1.44 MB FAT12 floppy disk image containing the same
CHALL.EXE
Running strings on the binary reveals it’s a flag checker:
| |
Reversing the Binary
MZ Header Analysis
The executable is a standard MZ DOS binary compiled with what appears to be Turbo C:
| Field | Value |
|---|---|
| Header size | 48 bytes (0x30) |
| Entry point (CS:IP) | 0000:02C2 |
| Code segment | 0x0000–0x238F (file offset 0x30) |
| Data segment | 0x2390–0x26D5 (file offset 0x23C0) |
Main Function (fcn.000000b0)
Using radare2 (r2 -a x86 -b 16), the main logic was disassembled. It performs three steps:
Step 1 — Prefix Validation
The checker reads user input and verifies the first 5 characters match lactf{:
| |
If any mismatch, it prints "Sorry, the flag must begin with "lactf{..."" and exits.
Step 2 — Hash the Input (Seed the PRNG)
The function at fcn.00000010 computes a 20-bit hash of the entire input string. Tracing the assembly:
| |
In Python:
| |
This produces a 20-bit seed value in DX:AX.
Step 3 — LFSR XOR Stream Cipher
The function at fcn.0000007b implements a 20-bit Linear Feedback Shift Register (LFSR). Each step:
- Compute feedback bit =
bit0 XOR bit3 - Right-shift the 20-bit state by 1
- Insert feedback bit at position 19 (MSB)
| |
The main loop iterates over each character of the input (up to 0x49 = 73 characters):
| |
Expected Ciphertext
The 73-byte expected ciphertext is stored in the data segment at DS:0x146 (file offset 0x2506):
| |
Solution Strategy
The cipher is: ciphertext[i] = input[i] XOR lfsr_keystream[i]
The LFSR keystream is fully determined by the 20-bit seed, which is the hash of the input. This creates a circular dependency — the seed depends on the plaintext, and the plaintext depends on the seed.
However, the seed is only 20 bits (1,048,576 possible values). We can brute-force all seeds:
- For each candidate seed (0 to 2²⁰ − 1):
- Generate the LFSR keystream
- Decrypt:
plaintext[i] = ciphertext[i] XOR keystream_low_byte[i] - Check if result starts with
lactf{and ends with} - Verify:
hash(plaintext) == seed
Solve Script
| |
The brute-force completes in under a minute and finds seed 0xf3fb5.
Flag
| |
Summary
| Component | Detail |
|---|---|
| Architecture | 16-bit x86 (MS-DOS MZ executable) |
| Cipher | LFSR-based XOR stream cipher |
| LFSR width | 20 bits |
| Feedback taps | bit 0 ⊕ bit 3 |
| Seed derivation | Polynomial hash: s = 67·s + c (mod 2²⁰) |
| Key weakness | 20-bit keyspace → brute-forceable (~1M) |
| Flag length | 73 characters |
Comments