TL; DR
Tree Parity Machine(TPM)을 이용해 AES 키를 공개 채널에서 학습 동기화로 생성한 뒤, 암호화된 플래그를 제공하는 문제다. 로그(log.json)를 기반으로 TPM weight를 재현하고, SHA256으로 키를 유도해 enc_flag.txt를 복호화하면 된다. naive한 단일 TPM imitation으로는 풀리지 않으며, population attack, mutation, selective unit flipping을 활용해 풀이 가능하다.
문제 구조
- log.json: TPM 학습 로그. 각 step의 입력 벡터(x: 3x100)와 출력 bit(tau: {-1, +1})가 주어짐. 단, Alice와 Bob이 tau^A = tau^B일 때만 기록됨.
- enc_flag.txt: AES-ECB로 암호화된 플래그.
- implementation.py : 구조체
- README.md : 걍 시나리오, 목표
출제의도
TPM은 neural cryptography에서 양측이 키를 공유하는 방식이다. 학습 기반 키 교환이 비동기적으로 이루어지고, 공개된 채널에서 이뤄진다는 점이 매력 적이지만, 공격자의 입장에서 동기화된 키를 재현가능 함.
해당 문제는 4가지의 근복적 취약점이 있다
- 출력 공개
- 동기식 업데이트 (weight 경로의 결정성)
- 상태공간의 유한성
- 학습 규칙의 공개
취약점 분석
1. Tree Parity Machine(TPM)의 형식적 정의
-
구조
- 은닉층 뉴런 K=3
- 각 뉴런 입력 차원 N=100
- 가중치 범위 $w_{k,j}\in{-L,\dots,L},;L=3$
-
국소장(local field)
$h_k(t)=\sum_{j=1}^{N}w_{k,j}(t),x_{k,j}(t),\qquad \sigma_k(t)=\operatorname{sgn}!\bigl(h_k(t)\bigr)$
(0은 +1로 치환)
-
출력 비트
$\tau(t)=\prod_{k=1}^{K}\sigma_k(t)\in{-1,+1}$
이 값만이 공개 채널을 통해 누설된다.
-
Hebbian 학습 규칙
$w_{k,j}(t+1)=g!\Bigl(w_{k,j}(t)+\sigma_k(t),x_{k,j}(t),\Theta!\bigl(\sigma_k(t)\tau(t)\bigr)\Bigr)$
여기서 $g(\cdot)$는 범위를 자르는 함수이고 $\Theta$는 부호 일치 여부(0/1) 지시자다 .
2. 출력 공개
공격자가 시퀀스
$\bigl{x(t),\tau(t)\bigr}{t=1}^{T}$
를 얻을 때, 한 스텝에서 숨겨진 $\sigma_k$ 3비트 중 알려지는 정보량은 1 bit뿐이다.
엔트로피 관점에서
$H\bigl(\sigma_1,\sigma_2,\sigma_3\bigr)=3 ,\qquad H\bigl(\sigma_1,\sigma_2,\sigma_3\mid\tau)=2$
즉 한 스텝당 1 bit가 그대로 남는다. 스텝의 로그는
$I{\text{leak}} = T\text{ bit}$
을 노출하며, 이는 가중치 공간
$(2L+1)^{KN}=7^{300}$
에 비하면 작지만 selection-filtering 과 결합될 때 치명적이다.
3. Population Attack
-
초기화 $n_0$개의 후보 가중치 벡터 ${W_i^{(0)}}$를 무작위로 뽑음
-
필터 단계
$P_{t+1}= \Bigl{,g\bigl(W_i^{(t)},x(t),\tau(t)\bigr);|;\tau\bigl(W_i^{(t)},x(t)\bigr)=\tau(t)\Bigr}$
여기서 g는 Hebbian update 연산.
-
크기 기대값
각 스텝의 생존 확률은
$p_{\text{match}}(t)=\Pr\bigl[\tau(W,x)=\tau(t)\bigr]\approx\frac12\bigl(1+\epsilon_t\bigr)$
(무작위 가중치면 $\epsilon_t\approx0$)
따라서
$\mathbb E!\bigl[|\mathcal P_{t}|\bigr]=n_0,\prod_{s=1}^{t-1}p_{\text{match}}(s)$
로그 길이가 충분하면 $|\mathcal P_{t}|\to0$ → 실패. 이를 선택적 플립과 돌연변이로 보완
4. Selective Unit Flipping
-
공격자가 $\tau_{\text{pred}}\neq\tau$인 스텝에서
$k^\ast = \arg\min_k \left| h_k \right|$
을 찾아 $\sigma_{k^\ast} \leftarrow -\sigma_{k^\ast}$ 로 강제 전환하면 새 출력 **$\tau’ = -\tau_{\text{pred}}$
즉 정확히 한 비트 반전으로 불일치 해소가 가능할 확률은
$P_{\text{flip}} = \Pr\left[ \operatorname{sgn}(h_{k^\ast}) \neq 0 \right] \approx 1$
(국소장이 0이면 이미 $\tau$ 일치였음).
-
필터 손실을 $O(1)$로 줄여 $|\mathcal P_t|$ 붕괴를 방지 가능함
5. Mutation Strategy
필터+플립에도 집단 다양성이 급감하면 탐색이 지수적으로 느려진다.
이를 해결하기 위해 남은 개체 W마다
$W’;=;W+\Delta W,\qquad \Delta W_{k,j}\sim{-1,0,+1}$
를 확률 $p_m$로 삽입, 전이 행렬 M을 두면
$\mathbf n_{t+1}= \mathbf n_t P_{\text{filter}} + \mathbf n_t M$
M이 가중치 랜덤 워크를 부여하여 마코프 체인의 미소 공간 접근성을 보장
6. 공격 성공 확률 & 시간 복잡도
$P_{\text{sync}}^{\text{Eve}}(T)\approx1-\exp!\bigl(-\alpha,T/n_0\bigr)$, $\alpha\propto\frac{1}{(2L+1)^N}$
동일 파라미터에서 50 k 개체, 10 k 스텝이면 $P_{\text{sync}}\to0.9$ 수준
연산량은 GPU 없이도 수 분–수십 분 내 실행 가능
$O(n_0 K N T)\approx 5!\times!10^4\cdot3\cdot100\cdot10^4\approx1.5!\times!10^{9}$
7. 키 파생 공식
가중치 행렬 W를 시프트 & 3진법 인코딩 → 비트열 $b$ →
$K_{\text{AES}}=\operatorname{Trunc}_{128}!\Bigl(\operatorname{SHA256}\bigl(\mathbf b\bigr)\Bigr)$
이는 가중치 1 bit라도 틀리면 해시가 전혀 달라지므로 공격자에게 완전체 동기화를 강제.
솔버
위 수식들 기반으로 솔버 작성하면
import json
import numpy as np
from Crypto.Cipher import AES
import hashlib
import random
K, N, L = 3, 100, 3
psize = 50000
b = [-1, 0, 1]
mthreshold = 100
mfactor = psize // 5
def tpm_output(w, x):
dot = np.einsum('ij,ij->i', w, x)
sig = np.sign(dot)
sig[sig == 0] = 1
tau = np.prod(sig)
return tau, sig, dot
def update_weights(w, x, sig, tau):
for i in range(K):
if sig[i] == tau:
w[i] += x[i]
w[i] = np.clip(w[i], -L, L)
return w
def weight_to_aes_key(w):
bits = ''.join(f'{(v + L):03b}' for v in w.flatten())
b = int(bits, 2).to_bytes((len(bits) + 7) // 8, byteorder='big')
return hashlib.sha256(b).digest()[:16]
def load():
with open("log.json") as f:
log = json.load(f)
with open("enc_flag.txt", "rb") as f:
ct = f.read()
return log, ct
def biased_init():
return np.random.choice(b, size=(K, N))
def mutate(w, strength=1):
mutation = np.random.randint(-strength, strength + 1, size=w.shape)
return np.clip(w + mutation, -L, L)
def population_attack():
log, ct = load()
population = [biased_init() for _ in range(psize)]
for t, entry in enumerate(log):
x = np.array(entry['x'])
tau_true = entry['tau']
new_pop = []
for w in population:
tau_pred, sig, dot = tpm_output(w, x)
if tau_pred == tau_true:
new_pop.append(update_weights(w.copy(), x, sig, tau_true))
else:
i_star = np.argmin(np.abs(dot))
sig[i_star] *= -1
tau_flip = np.prod(sig)
if tau_flip == tau_true:
new_pop.append(update_weights(w.copy(), x, sig, tau_true))
population = new_pop
print(f"[Step {t:03d}] Population: {len(population)}")
if len(population) == 0:
print("[-] nono ")
return
if len(population) < mthreshold:
print(f"[*] Low survivors ({len(population)}), mutating...")
survivors = population.copy()
population = []
for w in survivors:
for _ in range(mfactor // max(1, len(survivors))):
population.append(mutate(w, strength=1))
print(f"[*]New population after mutation: {len(population)}")
print(f"[*]{len(population)} survivors. Trying AES decryption...")
for w in population:
key = weight_to_aes_key(w)
pt = AES.new(key, AES.MODE_ECB).decrypt(ct)
if b"acsc{" in pt:
print(pt.decode(errors="ignore"))
return
if __name__ == "__main__":
population_attack()