down-the-stream-1 — gaslightCTF 2026 writeup
Category: crypto (500 points)
Author: william_etotheipi
Solves: 77
Series: down-the-stream (part 1 of 2)
Flag: gaslightCTF{d0nt-r3veal-y0ur-co3ff-v3ct0r}
"A synchronous stream cipher sounds like a very secure cipher!"
Table of contents
- Challenge overview
- The encryption script
- How the LFSR works
- Why the cipher is broken
- Attack — recover the IV from the known prefix
- Attack — replay the keystream
- Solver
- Flag
- Takeaways
1. Challenge overview
We are given:
chall.py— an 8-bit LFSR-based stream cipher (full source).output.txt— the ciphertext, as hex:
e944b3a55e47c5d8f3af3c93e2f7f2b1892094001e95a16b779b907bd374e2327a2dace45d222f69138bThe goal is to recover the plaintext flag.
2. The encryption script
def next_register(register: int) -> int:
for _ in range(8):
feedback = (((register >> 7) & 1) ^
((register >> 5) & 1) ^
((register >> 4) & 1) ^
((register >> 3) & 1))
register = ((register << 1) | feedback) & 0xff
return register
def LFSR(pt_bytes: bytes, IV: int) -> bytes:
register = IV
ct = bytearray()
ct.append(pt_bytes[0] ^ register) # ← first byte XORed with raw IV
for i in range(1, len(pt_bytes)):
register = next_register(register) # ← advance 8 clocks per byte
ct.append(pt_bytes[i] ^ register)
return bytes(ct)
if __name__ == class="hljs-string">'__main__':
with open(class="hljs-string">'gaslight-down-the-stream-1/flag_and_IV.txt', class="hljs-string">'r') as file:
flag = file.readline().strip()
IV = file.readline().strip()
flag = flag.encode()
IV = int(IV, 16)
print(LFSR(flag, IV).hex())Key structure:
- The keystream generator is an 8-bit Linear Feedback Shift Register (LFSR) seeded
with an unknown
IV. - The first plaintext byte is XORed with the raw IV itself (
ct[0] = p[0] ^ IV). - Each subsequent byte is XORed with the register state after advancing it 8 clock
steps (
next_register).
3. How the LFSR works
An LFSR is a shift register: on each clock, the register is shifted left by one, and the new bit pushed into the low bit is a linear (XOR) function of selected bits — the taps.
Here, the feedback bit is:
feedback = bit7 ⊕ bit5 ⊕ bit4 ⊕ bit3i.e. the XOR of the register's bits 7, 5, 4 and 3 (0-indexed from the LSB).
clock step:
bit7 → bit6 → … → bit0 → discards
↑
feedback = b7^b5^b4^b3 (becomes new bit0)next_register just applies this single step 8 times, so each byte of the message
consumes exactly 8 clock cycles. Because everything is linear, the whole system — including
the initial state IV — behaves like a deterministic generator producing a periodic
keystream:
k_0 = IV
k_i = next_register(k_{i-1}) (i ≥ 1)
c_i = p_i ⊕ k_i4. Why the cipher is broken
A synchronous stream cipher is only secure if the keystream is never reused and the initial state stays secret. This construction leaks the initial state immediately:
c_0 = p_0 ⊕ IV.p_0is the first byte of the flag, which begins with the known prefixgaslightCTF{→p_0 = ord('g') = 0x67.- Therefore:
IV = c_0 ⊕ 0x67Once IV is known, the entire keystream can be regenerated deterministically (the taps
are in the provided source), and the whole flag decrypts.
(It doesn't even matter whether we "should" know IV — the known plaintext prefix
recovery gives it to us. This is exactly the point of the flag text: "d0nt r3veal y0ur
co3ff v3ct0r".)
5. Attack — recover the IV from the known prefix
ct = bytes.fromhex(class="hljs-string">"e944b3a55e47c5d8f3af3c93e2f7f2b1892094001e95a16b779b907bd374e2327a2dace45d222f69138b")
IV = ct[0] ^ ord(class="hljs-string">'g')
print(hex(IV)) # 0x8eThe ciphertext byte e9 XOR 0x67 gives 0x8e.
6. Attack — replay the keystream
def next_register(register):
for _ in range(8):
feedback = (((register >> 7) & 1) ^
((register >> 5) & 1) ^
((register >> 4) & 1) ^
((register >> 3) & 1))
register = ((register << 1) | feedback) & 0xff
return register
reg = IV
pt = [ct[0] ^ reg]
for i in range(1, len(ct)):
reg = next_register(reg)
pt.append(ct[i] ^ reg)
print(bytes(pt))Output:
gaslightCTF{d0nt-r3veal-y0ur-co3ff-v3ct0r}7. Solver
python3 solver.py (in this directory) — recovers IV = 0x8e, regenerates the keystream,
and prints the plaintext.
8. Flag
gaslightCTF{d0nt-r3veal-y0ur-co3ff-v3ct0r}9. Takeaways
- Known-plaintext prefix is catastrophic for stream ciphers: the first keystream byte
is
IV, and one known plaintext byte reveals it. - With the keystream state and the (public) LFSR taps, the entire keystream replays.
- The flag text narrates the flaw: "don't reveal your coefficient vector" — here the IV (initial vector) is the leaked secret.
- This challenge seeds part 2: the
IV = 0x8ebecomes the palindrome IV of down-the-stream-2.