Logo
Overview
[D-CTF] last for sus rngs

[D-CTF] last for sus rngs

November 13, 2025
7 min read

TL;DR

256비트 keystream, combiner z3 -> LFSR

Analysis

LFSR 구조

import os
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Random.random import getrandbits
flag = b"CTF{fake_flag}"
class LFSR:
def __init__(self, key, taps):
d = max(taps)
assert len(key) == d, "Error: key of wrong size."
self._s = key
self._t = [d - t for t in taps]

상태는 길이 d의 비트 리스트 self._s taps는 탭 위치인데, self._t = [d - t for t in taps]로 한 번 뒤집는다.

e.g. d=17, taps[17,14]이면 self._t=[0,3], 실제로는 s[0], s[3] XOR

def _sum(self, L):
s = 0
for x in L:
s ^= x
return s
def _clock(self):
b = self._s[0]
self._s = self._s[1:] + [self._sum(self._s[p] for p in self._t)]
return b

_clock은 맨 앞 비트를 출력하고, 탭 위치 비트를 XOR한 값을 뒤에 붙인다.

LFSR 길이가 dd, 탭 집합이 TT일 때

st+d=jTst+js_{t+d} = \bigoplus_{j \in T} s_{t+j}

이며, 모든 연산은 F2\mathbb{F}_2 에서 이뤄지기 때문에 초기 상태 비트들을 Boolean로 잡았을 때 전이식은 모두 선형 제약식으로 표현 가능

Jeff combiner

class Jeff:
def __init__(self, key):
assert key.bit_length() <= 102
key = [int(i) for i in list("{:0102b}".format(key))]
self.LFSR = [
LFSR(key[:17], [17, 14]),
LFSR(key[17:42], [25, 22]),
LFSR(key[42:73], [31, 28]),
LFSR(key[73:], [29, 27]),
]
def bit(self):
b = [lfsr.bit() for lfsr in self.LFSR]
return b[1] if b[0]==0 else (b[1] if b[2]==0 and b[3]==0 else (1 if b[1]== 0 else 0))

102비트 정수 키를 이진수로 변환한 뒤 17 25 31 29비트로 나누어 각 LFSR에 배정한다. combiner 삼항식: 각 라운드에서 LFSR 출력들을 b0,i,b1,i,b2,i,b3,ib_{0, i}, b_{1, i}, b_{2, i}, b_{3,i}로 두면

si=b1,i(b0,i(b2,ib3,i))s_i = b_{1,i} \oplus \left(b_{0,i} \land (b_{2,i} \lor b_{3,i})\right)

따라서 keystream sis_i가 총 256개 제공되므로 256개의 비선형 Boolean 방정식을 얻는다.

AES

def encrypt_flag(key):
md5 = hashlib.md5()
md5.update(str(key).encode('ascii'))
key = md5.digest()
cipher = AES.new(key, AES.MODE_ECB)
ciphertext = cipher.encrypt(pad(flag, 16))
data = {}
data['encrypted'] = ciphertext.hex()
return data
key = getrandbits(102)
J = Jeff(key)
stream = [J.bit() for _ in range(256)]
print(stream)
print(encrypt_flag(key))

output.txt 첫 줄은 256비트 keystream 배열, 두 번째 줄은 AES 결과 JSON이다.

AES 키는 102비트 정수 키를 알아내야 flag를 복호화할 수 있다. keystream이 이미 256비트로 102비트 seed보다 충분히 길기에 keystream 내부 상태를 역산하는 쪽으로 접근했다.

각 LFSR의 초기 상태 비트를 xk,jx_{k,j}라 두고 clock을 256번 전개하면 각 LFSR 출력 비트는 모두 초기 상태 비트들의 선형식으로 표현된다.

combiner를 적용하면

si=x1,i(x0,i(x2,ix3,i))s_i = x_{1,i} \oplus \left(x_{0,i} \land (x_{2,i} \lor x_{3,i})\right)

LFSR 전이식은

xk,t+dk=jTkxk,t+jx_{k,t+d_k} = \bigoplus_{j \in T_k} x_{k,t+j}

형태이며 이를 적용해 256 step 동안 전재하면 미지수는 102비트(17+25+31+29), 256개의 식이 생긴다. 즉, 과잉 결정된 비선형 Boolean 시스템이 된다.

Exploit

import ast
from hashlib import md5
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from z3 import *
with open('output.txt') as f:
stream = ast.literal_eval(f.readline())
enc = ast.literal_eval(f.readline())['encrypted']
cycles = len(stream)
solver = Solver()
def xor_all(bits):
out = bits[0]
for b in bits[1:]:
out = Xor(out, b)
return out

LFSR 전개 함수

def lfsr(prefix, length, taps):
init = [Bool(f'{prefix}_{i}') for i in range(length)]
idx = [length - t for t in taps]
state = init[:]
seq = []
for _ in range(cycles):
seq.append(state[0])
state = state[1:] + [xor_all([state[i] for i in idx])]
return init, seq
lfsrs = [
lfsr('l1', 17, [17, 14]),
lfsr('l2', 25, [25, 22]),
lfsr('l3', 31, [31, 28]),
lfsr('l4', 29, [29, 27]),
]

LFSR마다 초기 상태 Bool 변수 배열과 clock 출력 시퀀스를 동시에 생성한다. 탭 인덱스를 length - t로 계산해 문제 코드와 동일하게 구현했다

combiner

for i, bit in enumerate(stream):
b0, b1, b2, b3 = [seq[i] for _, seq in lfsrs]
solver.add(Xor(b1, And(b0, Or(b2, b3))) == BoolVal(bool(bit)))

각 라운드마다 combiner 식을 그대로 제약으로 넣는다. 관측 keystream이 0이면 BoolVal(False), 1이면 BoolVal(True)로 비교하며 매칭한다.

Key Recovery, AES Decrypti0on

solver.check()
model = solver.model()
key_bits = ''.join('1' if model.eval(b) else '0' for init, _ in lfsrs for b in init)
key_int = int(key_bits, 2)
key_md5 = md5(str(key_int).encode()).digest()
plaintext = AES.new(key_md5, AES.MODE_ECB).decrypt(bytes.fromhex(enc))
print('key =', key_int)
print('flag =', unpad(plaintext, 16))

각 모델에서 LFSR 초기 상태를 읽어 102비트 이진 문자열을 만든다. str(key_int)를 MD5 해시로 얻은 AES 키를 사용하여 ciphertext를 복호화하고 unpad로 PKCS#7 패딩을 제거해 flag를 확인한다.

python3 solve.py
key = 2998029425760833875497006037011
flag = b'CTF{LFSR_means_Losing_Faith_in_Stream_Randomness}'
import ast
from hashlib import md5
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from z3 import Solver, Bool, BoolVal, Xor, And, Or
with open('output.txt') as f:
stream = ast.literal_eval(f.readline())
enc = ast.literal_eval(f.readline())['encrypted']
cycles = len(stream)
solver = Solver()
def xor_all(bits):
out = bits[0]
for b in bits[1:]:
out = Xor(out, b)
return out
def lfsr(prefix, length, taps):
init = [Bool(f'{prefix}_{i}') for i in range(length)]
idx = [length - t for t in taps]
state = init[:]
seq = []
for _ in range(cycles):
seq.append(state[0])
state = state[1:] + [xor_all([state[i] for i in idx])]
return init, seq
lfsrs = [
lfsr('l1', 17, [17, 14]),
lfsr('l2', 25, [25, 22]),
lfsr('l3', 31, [31, 28]),
lfsr('l4', 29, [29, 27]),
]
for i, bit in enumerate(stream):
b0, b1, b2, b3 = [seq[i] for _, seq in lfsrs]
solver.add(Xor(b1, And(b0, Or(b2, b3))) == BoolVal(bool(bit)))
solver.check()
model = solver.model()
key_bits = ''.join('1' if model.eval(b) else '0' for init, _ in lfsrs for b in init)
key_int = int(key_bits, 2)
key_md5 = md5(str(key_int).encode()).digest()
plaintext = AES.new(key_md5, AES.MODE_ECB).decrypt(bytes.fromhex(enc))
print('key =', key_int)
print('flag =', unpad(plaintext, 16))

Appendix

import os
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Random.random import getrandbits
flag = b"CTF{fake_flag}"
class LFSR:
def __init__(self, key, taps):
d = max(taps)
assert len(key) == d, "Error: key of wrong size."
self._s = key
self._t = [d - t for t in taps]
def _sum(self, L):
s = 0
for x in L:
s ^= x
return s
def _clock(self):
b = self._s[0]
self._s = self._s[1:] + [self._sum(self._s[p] for p in self._t)]
return b
def bit(self):
return self._clock()
class Jeff:
def __init__(self, key):
assert key.bit_length() <= 102
key = [int(i) for i in list("{:0102b}".format(key))]
self.LFSR = [
LFSR(key[:17], [17, 14]),
LFSR(key[17:42], [25, 22]),
LFSR(key[42:73], [31, 28]),
LFSR(key[73:], [29, 27]),
]
def bit(self):
b = [lfsr.bit() for lfsr in self.LFSR]
return b[1] if b[0]==0 else (b[1] if b[2]==0 and b[3]==0 else (1 if b[1]== 0 else 0))
def encrypt_flag(key):
md5 = hashlib.md5()
md5.update(str(key).encode('ascii'))
key = md5.digest()
cipher = AES.new(key, AES.MODE_ECB)
ciphertext = cipher.encrypt(pad(flag, 16))
data = {}
data['encrypted'] = ciphertext.hex()
return data
key = getrandbits(102)
J = Jeff(key)
stream = [J.bit() for _ in range(256)]
print(stream)
print(encrypt_flag(key))
[0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0]
{'encrypted': 'fd37825f5f21795a4f6fbb7ce812864808396198cb158e9b5ae7b44c80c6e1371a315a6f90d1fef7b3bfc64b2674db1e692f5c84b3222853351f2048a7ffe65d'}