← ~/notes · 2 min read

내가 만든 DES로 Sweet32(CVE-2016-2183) 재현하기

2016년 TLS에서 3DES가 퇴출된 이유를 자체 구현으로 수치 증명. 키가 안 깨져도 암호문에서 평문이 새는 공격.

목차
  1. Sweet32의 핵심
  2. 직접 구현으로 시연
  3. 1) 같은 키로 많은 블록을 CBC 모드로 암호화
  4. 2) ciphertext 충돌 검색
  5. 3) 충돌에서 평문 정보 추출
  6. 실측 결과
  7. 실제 TLS는 어떻게 깨지나
  8. 결론 — 64bit 블록 시대는 끝났다
  9. 만들면서 알게 된 것들

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 환경에선 더 복잡하지만 본질은 같다.

  1. 공격자가 피해자가 자기 사이트(evil.com) 방문하게 유도 (XSS, 광고 등)
  2. JS로 피해자 브라우저가 합법 사이트(bank.com) 에 동일 세션 쿠키와 함께 반복 요청 보내게 함
  3. 약 32GB 트래픽 발생
  4. 공격자가 네트워크 가운데서 ciphertext 캡처
  5. 충돌 검색 → 쿠키 값 부분 복원

원논문 (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 미니어처에서 직접 잡아보면 즉시 보인다. 보안 학습엔 미니어처 시연이 정공.


작은 알고리즘으로 큰 공격을 재현하면 이론과 코드 사이의 다리가 생긴다.