Overview
We are presented with a custom stream cipher implementation involving a Linear Feedback Shift Register (LFSR) and a nonlinear filter function.
| Category | Difficulty | Flag |
|---|---|---|
| Crypto | Medium | uoftctf{l33ky_lfsr_w17h_n0n_l1n34r_fl4v0rrrr} |
Analysis
The system describes a classic Nonlinear Filter Generator.
- Source Files:
crypto.pyhandles key derivation and encryption, whilefilter_cipher.pyimplements the LFSR logic. - Parameters: The LFSR has a state size of 48 bits.
- Data: We are given 80 bits of generated
keystream, anonce, and thect(ciphertext).
System Properties
- State Size: The internal state is 48 bits ( possibilities). While brute-forcing this is computationally expensive, it is small enough for algebraic attacks.
- Overdefined System: We have 48 unknown variables (the initial state bits) but are given 80 bits of output (equations). This suggests the system is solvable.
- Vulnerability: The complexity of the filter function (WG-style ANF terms) is high, but the state transitions are linear. A SAT solver (like Z3) can easily model the linear shifts and the nonlinear constraints to recover the initial state essentially instantly.
Solution Strategy
We will perform an algebraic attack using Z3:
- Symbolic State: Define 48 symbolic boolean variables representing the initial LFSR state.
- Simulation: Replicate the
clock()function fromfilter_cipher.pyusing Z3 operations. For each of the 80 known keystream bits:- Calculate the symbolic output of the filter function.
- Add a constraint that this symbolic output must equal the actual keystream bit.
- Symbolically update the LFSR state (shift and XOR feedback).
- Solve & Decrypt: Ask Z3 to find a satisfying model (the initial bits). Use these bits to derive the key via HKDF and decrypt the flag using ChaCha20-Poly1305.
graph TD A[Symbolic State (48 vars)] --> B{Loop 80 times} B --> C[Calc Filter Output] C --> D[Add Constraint: out == keystream[i]] D --> E[Clock LFSR State] E --> B B -- Done --> F[Z3 Solve] F --> G[Recover Initial State] G --> H[Derive Key & Decrypt]Solver Script
import binasciifrom z3 import *from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305from cryptography.hazmat.primitives.kdf.hkdf import HKDFfrom cryptography.hazmat.primitives import hashes
# --- Challenge Parameters ---L = 48feedback_taps = [0, 1, 2, 3, 47]filter_taps = [0, 4, 7, 11, 16, 22, 29]keystream = [ 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1]nonce_hex = "8592d59a1c2c0491dab7ed39"ct_hex = "d2a9a529dfcb9b02ad497f9ed915285cc175edbd9c2e1c6f2144a7228fd00c2ba2db21470abb6f4de0bdec24c0eeddf13fa1a2935f735d5d429f932a21"
# ANF Terms (WG-style transformation)WG_ANF_TERMS = [ (1, 2, 3, 4, 5, 6), (0, 1, 2, 3, 5), (0, 1, 2, 4, 5), (0, 1, 3, 4, 5), (1, 2, 3, 4, 5), (0, 1, 2, 4, 6), (0, 2, 3, 4, 6), (1, 2, 3, 4, 6), (0, 1, 2, 5, 6), (0, 2, 3, 5, 6), (1, 2, 3, 5, 6), (0, 3, 4, 5, 6), (1, 3, 4, 5, 6), (0, 1, 2, 4), (0, 1, 2, 5), (1, 2, 3, 5), (0, 1, 4, 5), (1, 3, 4, 5), (2, 3, 4, 5), (1, 2, 3, 6), (0, 1, 4, 6), (1, 2, 4, 6), (0, 3, 4, 6), (1, 3, 4, 6), (1, 2, 5, 6), (0, 3, 5, 6), (1, 4, 5, 6), (2, 4, 5, 6), (3, 4, 5, 6), (0, 1, 2), (0, 1, 4), (0, 2, 4), (1, 2, 4), (0, 1, 5), (0, 3, 5), (1, 3, 5), (2, 3, 5), (3, 4, 5), (0, 1, 6), (0, 3, 6), (2, 3, 6), (3, 4, 6), (0, 5, 6), (2, 5, 6), (4, 5, 6), (2, 3), (3, 4), (0, 5), (3, 5), (1, 6), (3, 6), (4, 6), (0,), (3,), (5,), (6,)]
def solve(): print("[*] Setting up Z3 Solver...") solver = Solver()
# 1. Initialize Symbolic State (48 bits) initial_state = [BitVec(f's_{i}', 1) for i in range(L)] current_state = list(initial_state)
# 2. Simulate LFSR steps for k_bit in keystream: # Extract taps for filter function taps = [current_state[i] for i in filter_taps]
# Calculate symbolic output using ANF terms z_out = BitVecVal(0, 1) for term in WG_ANF_TERMS: term_val = BitVecVal(1, 1) for idx in term: term_val &= taps[idx] # Logical AND z_out ^= term_val # Logical XOR
# Constraint: Symbolic output == Known keystream bit solver.add(z_out == k_bit)
# Clock LFSR: Calculate feedback and shift fb = BitVecVal(0, 1) for idx in feedback_taps: fb ^= current_state[idx] current_state = [fb] + current_state[:-1]
# 3. Solve print("[*] Solving constraints...") if solver.check() == sat: print("[+] SAT! Model found.") model = solver.model() recovered_bits = [model[v].as_long() for v in initial_state] print(f"[+] Recovered Initial State: {recovered_bits}") return recovered_bits else: print("[-] UNSAT. Constraints are impossible.") return None
def decrypt_flag(state_bits): print("[*] Deriving key and decrypting...") # Helper from crypto.py data = bytes(state_bits) hkdf = HKDF(algorithm=hashes.SHA256(), length=32, salt=None, info=b"nlfg-ctf") key = hkdf.derive(data)
nonce = binascii.unhexlify(nonce_hex) ct = binascii.unhexlify(ct_hex) aead = ChaCha20Poly1305(key)
try: flag = aead.decrypt(nonce, ct, associated_data=None) print(f"\n[+] FLAG: {flag.decode()}") except Exception as e: print(f"[-] Decryption Error: {e}")
if __name__ == "__main__": state = solve() if state: decrypt_flag(state)Flag: uoftctf{l33ky_lfsr_w17h_n0n_l1n34r_fl4v0rrrr}