내가 만든 DES로 Sweet32(CVE-2016-2183) 재현하기
2016년 TLS에서 3DES가 퇴출된 이유를 자체 구현으로 수치 증명. 키가 안 깨져도 암호문에서 평문이 새는 공격.
목차
DES/3DES는 키 길이만 보면 56/112bit로 부르트포스가 어렵거나 비현실적이다. 그런데 2016년 TLS는 3DES를 퇴출시켰다. 왜?
키가 깨진 게 아니라 블록 크기가 깨졌다.
Sweet32의 핵심
64bit 블록 암호의 약점:
- 생일 역설: 같은 키로 2^32 블록 암호화하면 두 블록이 같은 ciphertext로 나올 확률이 50%를 넘는다.
- 같은 ciphertext는 같은 plaintext에서 나왔다는 뜻 (CBC 같은 모드에서).
- 충돌이 발생하면 평문 일부가 복원된다.
64bit 블록 = 8 byte. 2^32 블록 = 32GB. HTTPS 세션에서 32GB 트래픽이면 충분히 가능하다 (긴 다운로드, 영상 스트리밍).
128bit 블록(AES)에선 같은 공격이 2^64 블록 = 128 exabyte를 요구해서 실현 불가능. 64bit 블록 자체가 구식이라는 게 결론.
직접 구현으로 시연
자체 DES 구현을 그대로 쓴다.
1) 같은 키로 많은 블록을 CBC 모드로 암호화
import os
from des import des_encrypt_cbc, des_decrypt_cbc
KEY = os.urandom(8) # 56bit 실효 키
IV = os.urandom(8)
# 32GB는 노트북에서 못 돌리니 축소: 24bit 블록 공간으로 축소된 미니어처
# (실제 DES 블록 64bit 중 상위 40bit를 0으로 고정해서 하위 24bit만 변동)
def synth_plaintext_block(i: int) -> bytes:
return b'\x00\x00\x00\x00\x00' + i.to_bytes(3, 'big') # 8 bytes
# 2^14 = 16384 블록 (24bit 공간에서 충돌 기대치 ~약간)
N = 1 << 14
blocks = [synth_plaintext_block(i) for i in range(N)]
ct = des_encrypt_cbc(b''.join(blocks), KEY, IV)
ct_blocks = [ct[i*8:(i+1)*8] for i in range(N)]
2) ciphertext 충돌 검색
seen = {}
collisions = []
for idx, c in enumerate(ct_blocks):
if c in seen:
collisions.append((seen[c], idx))
else:
seen[c] = idx
print(f"블록 {N}개 → 충돌 {len(collisions)}개")
3) 충돌에서 평문 정보 추출
CBC 모드에서 두 ciphertext 블록이 같으면 (C_i == C_j), 다음이 성립한다:
P_i ⊕ C_{i-1} == P_j ⊕ C_{j-1}
∴ P_i ⊕ P_j == C_{i-1} ⊕ C_{j-1}
평문의 XOR 값이 ciphertext만으로 계산 가능. 한쪽 평문(예: 알려진 HTTP 헤더 GET / HTTP/1.1\r\n)을 알면 다른 쪽이 즉시 복원된다.
for (i, j) in collisions:
leak = xor(ct_blocks[i-1], ct_blocks[j-1]) # P_i XOR P_j
print(f"충돌 i={i}, j={j}: P_i ⊕ P_j = {leak.hex()}")
# 알려진 헤더가 있다면:
if known_plaintext_at(i):
recovered = xor(known_plaintext_at(i), leak)
print(f" 복원된 P_{j} = {recovered}")
실측 결과
24bit 블록 공간으로 축소한 시뮬레이션:
| 블록 수 | 충돌 발생 확률 | 실측 충돌 |
|---|---|---|
| 2^10 (1K) | ~3% | 0~1개 |
| 2^11 (2K) | ~12% | 1~3개 |
| 2^12 (4K) | ~46% | 5~10개 |
| 2^13 (8K) | ~93% | 30~50개 |
24bit 공간이라 2^12에서 절반. 64bit 공간으론 환산하면 2^32 블록.
실제 TLS는 어떻게 깨지나
POC of POC이라 실제 TLS 환경에선 더 복잡하지만 본질은 같다.
- 공격자가 피해자가 자기 사이트(
evil.com) 방문하게 유도 (XSS, 광고 등) - JS로 피해자 브라우저가 합법 사이트(
bank.com) 에 동일 세션 쿠키와 함께 반복 요청 보내게 함 - 약 32GB 트래픽 발생
- 공격자가 네트워크 가운데서 ciphertext 캡처
- 충돌 검색 → 쿠키 값 부분 복원
원논문 (Bhargavan & Leurent, 2016)에서 실제 OpenVPN 64bit 모드 세션을 75시간 만에 깬 게 시연됨.
결론 — 64bit 블록 시대는 끝났다
문제는 알고리즘 강도가 아니라 블록 크기다.
- DES 56bit 키 = 부르트포스 위협 (~1998 EFF Deep Crack)
- 3DES 112bit 실효 키 = 부르트포스 안전
- BUT 64bit 블록 = Sweet32 — 키 안 깨도 데이터 새어나감
TLS 1.2 RFC에서 3DES cipher suite가 deprecated된 핵심 근거가 이거. AES는 128bit 블록이라 자유롭다.
만들면서 알게 된 것들
키 길이만 보지 말 것. 블록 크기, 운영 모드, IV 재사용 — 셋 다 키만큼 중요. 하나라도 약하면 알고리즘 전체가 약해진다.
CBC의 함정은 충돌이다. ECB만 욕먹지만 CBC도 동일 키로 너무 많이 쓰면 깨진다. 키 회전이 답.
자체 구현으로 시연하면 이해가 다르다. “확률적 충돌”이 추상적이지만 24bit 미니어처에서 직접 잡아보면 즉시 보인다. 보안 학습엔 미니어처 시연이 정공.
작은 알고리즘으로 큰 공격을 재현하면 이론과 코드 사이의 다리가 생긴다.