DELPHI
팀명 : SecurityFact
점수 : 600
2번 오라클 출력의 결과는 패딩판정 ⊕ RNG 1비트(LFSR XOR) 이고
1. 블록 배수 아닌(17바이트) CT를 보내면 패딩판정이 항상 False이므로 출력이 RNG 비트.
2. 이렇게 모은 비트열로 두 개 64-bit LFSR 초기상태를 복구하고 예측 RNG 비트를 XOR해 패딩을 얻은 후
3. 표준 CBC 패딩 오라클 복호화 해서 msg를 얻는다.
from pwn import *
import os, random
HOST, PORT = "[Link]", 13372
context.log_level = "info"
# ---------------- LFSR / GF(2) ----------------
def build_T_matrix_64(fb):
T = [[0]*64 for _ in range(64)]
# shift-right: bit[i] <- old[i+1]
for i in range(63):
T[i][i+1] = 1
# MSB <- XOR of taps
for k in range(64):
if (fb >> k) & 1:
T[63][k] = 1
return T
def matmul_rowvec_mat(row, M):
N = len(row)
out = [0]*N
for j in range(N):
s=0
for i in range(N):
if row[i] and M[i][j]:
s ^= 1
out[j] = s
return out
def build_A_rows(T, M):
e0 = [0]*64
e0[0] = 1 # LSB 선택
rows = []
a = e0[:]
for _ in range(M):
[Link](a[:])
a = matmul_rowvec_mat(a, T)
return rows
def gf2_gauss_solve(A, b):
M = len(A); N = len(A[0])
aug = [A[i][:] + [b[i]] for i in range(M)]
r=c=0
where = [-1]*N
DELPHI 1
while r < M and c < N:
piv = r
while piv < M and aug[piv][c] == 0:
piv += 1
if piv == M:
c += 1
continue
aug[r], aug[piv] = aug[piv], aug[r]
where[c] = r
for i in range(M):
if i != r and aug[i][c]:
aug[i] = [aug[i][j] ^ aug[r][j] for j in range(N+1)]
r += 1; c += 1
for i in range(r, M):
if aug[i][N]:
raise ValueError("No solution (inconsistent)")
x = [0]*N
for j in range(N):
if where[j] != -1:
x[j] = aug[where[j]][N]
else:
x[j] = 0
return x
def bits_to_int_le(bits):
v=0
for i, b in enumerate(bits):
if b & 1:
v |= (1 << i)
return v
class LFSR:
def __init__(self, fb, state):
[Link] = fb & ((1<<64)-1)
[Link] = state & ((1<<64)-1)
@staticmethod
def parity64(x):
return x.bit_count() & 1
def clock(self):
out = [Link] & 1
new_msb = LFSR.parity64([Link] & [Link])
[Link] = ((new_msb << 63) | ([Link] >> 1)) & ((1<<64)-1)
return out
class RNG:
def __init__(self, s1, s2):
self.L1 = LFSR(11277095078203943143, s1)
self.L2 = LFSR(12022921549183311343, s2)
def bit(self):
return [Link]() ^ [Link]()
# ---------------- Remote IO ----------------
class OracleClient:
def __init__(self, host, port):
[Link] = remote(host, port)
self.q2_count = 0
[Link] = None
DELPHI 2
def prompt(self):
[Link](b"> ")
def menu1_get_ct(self):
[Link]()
[Link](b"1")
line = [Link]().strip()
return [Link]([Link]())
def menu2_query_raw(self, ct_bytes):
[Link]()
[Link](b"2")
[Link](b"(hex) > ")
[Link](ct_bytes.hex().encode())
line = [Link]().strip()
if line == b"No repeat":
return None
try:
val = int(line)
except Exception:
raise RuntimeError(f"Unexpected oracle output: {line!r}")
self.q2_count += 1
return val
def menu3_submit(self, pt_bytes):
[Link]()
[Link](b"3")
[Link](b"(hex) > ")
[Link](pt_bytes.hex().encode())
return [Link]().strip()
# ---------------- Attack helpers ----------------
def collect_rng_bits(cli, M):
bits = []
bar = [Link](f"[1/5] Collect RNG bits ({M})")
for i in range(M):
# 길이 바이트17 → check_padding=False → o = [Link]()
ct = bytearray([Link](16) + b"\x00")
ct[-1] = i & 0xFF
o = cli.menu2_query_raw(bytes(ct))
if o is None:
ct[0] ^= 1
o = cli.menu2_query_raw(bytes(ct))
assert o is not None
[Link](o & 1)
if (i+1) % 16 == 0:
[Link](f"{i+1}/{M}")
[Link]("done")
return bits
def recover_states(r_stream):
M = len(r_stream)
T1 = build_T_matrix_64(11277095078203943143)
T2 = build_T_matrix_64(12022921549183311343)
A1 = build_A_rows(T1, M)
A2 = build_A_rows(T2, M)
DELPHI 3
A = [A1[i] + A2[i] for i in range(M)]
b = r_stream[:]
[Link]("[2/5] Solving GF(2) linear system")
x = gf2_gauss_solve(A, b)
s1_bits = x[:64]; s2_bits = x[64:]
s1 = bits_to_int_le(s1_bits)
s2 = bits_to_int_le(s2_bits)
[Link](f"Recovered states: s1=0x{s1:016x}, s2=0x{s2:016x}")
return s1, s2
def verify_rng_sync(cli, trials=32):
if [Link] is None:
raise RuntimeError("rng not set")
ok = 0
for i in range(trials):
ct = bytearray([Link](16) + b"\x01")
ct[-1] ^= i & 0xFF
o = cli.menu2_query_raw(bytes(ct))
if o is None:
ct[0] ^= 1
o = cli.menu2_query_raw(bytes(ct))
r = [Link]()
if (o ^ 0) == r:
ok += 1
[Link](f"[check] RNG sync: {ok}/{trials} matched")
return ok == trials
def oracle_true(cli, ct_bytes):
o = cli.menu2_query_raw(ct_bytes)
assert o is not None
r = [Link]()
return o ^ r
def decrypt_block(cli, Cprev, C):
I = [0]*16
prog = [Link](" decrypt_block")
for pad in range(1, 17):
idx = 16 - pad
# 꼬리(k>idx)는 pad로 맞추기: P[k] = I[k] ^ Cprev'[k] = pad
# → Cprev'[k] = I[k] ^ pad
base_prev = bytearray([Link](16)) # 앞부분은 아무 값이어도 됨
for k in range(idx+1, 16):
base_prev[k] = I[k] ^ pad
found = False
order = list(range(256))
[Link](order)
for g in order:
prev = bytearray(base_prev)
prev[idx] = g
if pad == 1:
prev[0] ^= 0x01 # 1- 패딩 우연 매치 방지용 더미
iv_rand = [Link](16)
pkt = iv_rand + bytes(prev) + C
res = oracle_true(cli, pkt)
DELPHI 4
if res == 1:
I[idx] = g ^ pad
found = True
break
if not found:
[Link](f"pad={pad} failed")
raise RuntimeError(f"Pad search failed at pad={pad}")
[Link](f"bytes recovered: {17-pad}/16")
[Link]("ok")
P = bytes([I[i] ^ Cprev[i] for i in range(16)])
return P
def solve():
cli = OracleClient(HOST, PORT)
# 1) RNG 비트 수집
M = 512
r_stream = collect_rng_bits(cli, M)
# 2) 상태 복구
s1, s2 = recover_states(r_stream)
# 3) RNG 동기화
[Link] = RNG(s1, s2)
for _ in range(M):
[Link]()
# 3.5) 동기화 검증
if not verify_rng_sync(cli, trials=32):
[Link]("RNG mismatch. Increase M or re-run.")
[Link]()
return
# 4) 암호문 획득
[Link]("[3/5] Fetching ciphertext (menu 1)")
ct = cli.menu1_get_ct()
assert len(ct) == 48
iv, C1, C2 = ct[:16], ct[16:32], ct[32:48]
[Link](f"IV={[Link]()} C1={[Link]()} C2={[Link]()}")
# 5) P2, P1 복호 마지막 블록부터
( )
[Link]("[4/5] Decrypting P2")
P2 = decrypt_block(cli, C1, C2)
[Link](f"P2={[Link]()}")
[Link]("[4/5] Decrypting P1")
P1 = decrypt_block(cli, iv, C1)
[Link](f"P1={[Link]()}")
msg = P1 + P2
[Link](f"[5/5] MSG={[Link]()} (len={len(msg)})")
# 6) 제출
out = cli.menu3_submit(msg)
print([Link](errors="ignore"))
DELPHI 5
[Link]()
if __name__ == "__main__":
solve()
DELPHI 6