TL;DR
더블 AES Meet-in-the-Middle 문제
johndoe.py에선 메시지를 두 번 암호화한 결과를 출력하고 server.py에서는 그 결과를 받아 다시 두 번 암호화해서 준다.
그렇게 패킷 캡쳐가 된 파일(pcap)을 제공하는데, 4중 암호문을 복호화하면 된다.
Analysis
client: johndoe.py
from Crypto.Cipher import AES
k1 = b'AA' # Obviously not the actual key
k2 = b'AA' # Obviously not the actual key
message = b'scriptCTF{testtesttesttesttest!_' # Obviously not the actual flag
keys = [k1,k2]
final_keys = []
for key in keys:
assert len(key) == 2 # 2 byte key into binary
final_keys.append(bin(key[0])[2:].zfill(8)+bin(key[1])[2:].zfill(8))
cipher = AES.new(final_keys[0].encode(), mode=AES.MODE_ECB)
cipher2 = AES.new(final_keys[1].encode(), mode=AES.MODE_ECB)
enc2 = cipher2.encrypt(cipher.encrypt(message)).hex()
print(enc2)
to_dec = bytes.fromhex(input("Dec: ").strip())
secret = cipher.decrypt(cipher2.decrypt(to_dec))
print(secret.hex())
AES-ECB로 두 번 암호화: $C_2 = E_{K_2}(E_{K_1}(M))$
server: server.py
import os
from Crypto.Cipher import AES
print("With the Secure Server 2, sharing secrets is safer than ever! We now support double encryption with AES!")
enc = bytes.fromhex(input("Enter the secret, encrypted twice with your keys (in hex): ").strip())
# Our proprietary key generation method, used by the server and John Doe himself!
k3 = b'BB' # Obviously not the actual key
k4 = b'B}' # Obviously not the actual key
# flag = secret_message + k1 + k2 + k3 + k4 (where each key is 2 bytes)
# In this case: scriptCTF{testtesttesttesttest!_AAAABBB}
keys = [k3,k4]
final_keys = []
for key in keys:
assert len(key) == 2 # 2 byte key into binary
final_keys.append(bin(key[0])[2:].zfill(8)+bin(key[1])[2:].zfill(8))
cipher = AES.new(final_keys[0].encode(), mode=AES.MODE_ECB)
cipher2 = AES.new(final_keys[1].encode(), mode=AES.MODE_ECB)
enc2 = cipher2.encrypt(cipher.encrypt(enc)).hex()
print(f"Quadriple encrypted secret (in hex): {enc2}")
dec = bytes.fromhex(input("Decrypt the above with your keys again (in hex): ").strip())
secret = cipher.decrypt(cipher2.decrypt(dec))
print("Secret received!")
서버는 받은 $C_2$를 다시 두 번 암호화: $C_4 = E_{K_4}(E_{K_3}(C_2))$
클라이언트가 서버의 $C_4$를 받아 자신의 키로 역산한 값도 보낸다: $D=D_{K_1}(D_{K_2}(C_4))$
키는 모두 2바이트지만, 실제 키로 쓰일 땐
K_raw = [k[0], k[1]]
K_AES = (bin(k[0])[2:].zfill(8) + bin(k[1])[2:].zfill(8)).encode()
# 0/1로 구성된 16글자 ASCII == 16바이트 AES 키
AES-128-ECB가 된다.
pcap

1. 클라이언트가 서버에 제출한 2중 암호문 $C_2$
19574ac010cc9866e733adc616065e6c019d85dd0b46e5c2190c31209fc57727
2. 서버가 출력한 4중 암호문 $C_4$
0239bcea627d0ff4285a9e114b660ec0e97f65042a8ad209c35a091319541837
3. 클라이언트가 되돌려 보낸 자기 키로의 복호 결과 $D$
4b3d1613610143db984be05ef6f37b31790ad420d28e562ad105c7992882ff34
Exploitation
1. $(K_3, K_4)$ 복구: server-side 더블 AES
$$ C_4 = E_{K_4}(E_{K_3}(C_2)) \leftrightarrow E_{K_3}(C_2)=D_{K_4}(C_4) $$
1. 모든 $K_3 \in [0,2^{16})$에 대해 $X=E_{K_3(C_2)$를 테이블
2. 모든 $K_4$에 대해 $Y=D_{K_4}(C_4)$를 구하고 $X=Y$가 되는 지점 히트
2. $(K_1, K_2)$ 복구: client-side 더블 AES
$$ D = D_{K_1}(D_{K_2}(C_4)) \leftrightarrow D_{K_2}(C_4)=E_{K_1}(D) $$
1. 모든 $K_2$에 대해 $U=D_{K_2}(C_4)$ 테이블
2. 모든 $K_1$에 대해 $V=E_{K_1}(D)$ 계산 $\rightarrow U=V$ 매치
[1-2] 단계 거쳐서 $ O(2 \times 2^{16}) $ 밖에 안 거친다. (이해하기 편하게 빅오 표기법을 무시했습니다)
3. 평문 복구
평문은 $W = D_{K_1}(D_{K_2}(C_2))$으로 복구한다.
from pwn import *
from Crypto.Cipher import AES
from collections import defaultdict
context.log_level = 'debug'
enc2_hex = "19574ac010cc9866e733adc616065e6c019d85dd0b46e5c2190c31209fc57727"
enc4_hex = "0239bcea627d0ff4285a9e114b660ec0e97f65042a8ad209c35a091319541837"
dec_hex = "4b3d1613610143db984be05ef6f37b31790ad420d28e562ad105c7992882ff34"
enc2 = bytes.fromhex(enc2_hex)
enc4 = bytes.fromhex(enc4_hex)
dec_resp = bytes.fromhex(dec_hex)
def u16_to_raw_bytes(u):
return bytes([(u >> 8) & 0xFF, u & 0xFF])
def u16_to_aes_key(u):
b0, b1 = (u >> 8) & 0xFF, u & 0xFF
return (format(b0, '08b') + format(b1, '08b')).encode()
def enc(key, data): return AES.new(key, AES.MODE_ECB).encrypt(data)
def dec(key, data): return AES.new(key, AES.MODE_ECB).decrypt(data)
log.info("MITM #2 (k3,k4) 탐색")
tbl = defaultdict(list)
for k3 in range(0x10000):
mid = enc(u16_to_aes_key(k3), enc2)
tbl[mid].append(k3)
cands34 = []
for k4 in range(0x10000):
mid2 = dec(u16_to_aes_key(k4), enc4)
if mid2 in tbl:
for k3 in tbl[mid2]:
cands34.append((k3, k4))
assert len(cands34) >= 1
k3, k4 = cands34[0]
log.success(f"(k3,k4) = {k3:#06x}, {k4:#06x} | raw={u16_to_raw_bytes(k3).hex()} {u16_to_raw_bytes(k4).hex()}")
log.info("MITM #1 (k1,k2) 탐색")
tbl2 = defaultdict(list)
for k2 in range(0x10000):
mid = dec(u16_to_aes_key(k2), enc4)
tbl2[mid].append(k2)
cands12 = []
for k1 in range(0x10000):
mid2 = enc(u16_to_aes_key(k1), dec_resp)
if mid2 in tbl2:
for k2 in tbl2[mid2]:
cands12.append((k1, k2))
assert len(cands12) >= 1
k1, k2 = cands12[0]
log.success(f"(k1,k2) = {k1:#06x}, {k2:#06x} | raw={u16_to_raw_bytes(k1).hex()} {u16_to_raw_bytes(k2).hex()}")
P = dec(u16_to_aes_key(k1), dec(u16_to_aes_key(k2), enc2))
log.info(f"Plain(hex) = {P.hex()}")
try:
log.success(f"Plain(str) = {P.decode()}")
except:
log.success("실패")
flag_bytes = P + u16_to_raw_bytes(k1) + u16_to_raw_bytes(k2) + u16_to_raw_bytes(k3) + u16_to_raw_bytes(k4)
flag = flag_bytes.decode('latin-1')
log.success(f"FLAG = {flag}")
print(flag)
그으러면
pwn@meow:~/ctftemp$ /usr/bin/env python3 "/home/pwn/ctftemp/crypt/secure-server-2/ex.py"
[*] MITM #2 (k3,k4) 탐색
[+] (k3,k4) = 0x6638, 0x647d | raw=6638 647d
[*] MITM #1 (k1,k2) 탐색
[+] (k1,k2) = 0x6534, 0x6233 | raw=6534 6233
[*] Plain(hex) = 7363726970744354467b7333637233375f6d3373733467335f31333337215f37
[+] Plain(str) = scriptCTF{s3cr37_m3ss4g3_1337!_7
[+] FLAG = scriptCTF{s3cr37_m3ss4g3_1337!_7e4b3f8d}
scriptCTF{s3cr37_m3ss4g3_1337!_7e4b3f8d}
쉽게 나옵니다
'CTF' 카테고리의 다른 글
| [PWN] scriptCTF 2025 Vault 3 write-up (0) | 2025.08.21 |
|---|---|
| [Rev] scriptCTF 2025 - plastic-shield 1, 2 write-up (3) | 2025.08.21 |
| [Crypto] scriptCTF 2025 - EaaS write-up (0) | 2025.08.21 |
| [Crypto] CCE 2025 Qual - jokes write-up (2) | 2025.08.21 |
| [Rev] CCE 2025 Qual - Directcalc write-up (4) | 2025.08.16 |