← ~/notes · 2 min read

ECB 모드 패턴 유출 — 펭귄 말고 HTTP/DNS 페이로드 버전

ECB 위험성을 보여주는 그 펭귄 이미지 너무 많이 봤다. 실제 트래픽 페이로드로 시연해보면 더 와닿는다.

목차
  1. ECB가 새는 이유
  2. 시연 1 — HTTP 응답 헤더
  3. 시연 2 — DNS 쿼리
  4. 시연 3 — JSON 객체
  5. CBC와 비교
  6. 그럼 ECB는 어디 쓰나
  7. 만들면서 알게 된 것들

암호 강의에서 ECB 모드 위험성을 설명할 때 항상 그 펭귄 이미지가 나온다. 평문 펭귄 → ECB로 암호화하면 펭귄 윤곽이 그대로 보임. 익숙해서 더 안 와닿는다.

진짜 트래픽인 HTTP 헤더, DNS 쿼리, 반복 구조 페이로드로 같은 효과를 시연하면 더 실감 난다.


ECB가 새는 이유

ECB(Electronic Codebook)는 블록마다 독립적으로 동일 함수로 암호화한다.

P_1 → AES_K → C_1
P_2 → AES_K → C_2  (P_1 == P_2 이면 C_1 == C_2)
P_3 → AES_K → C_3
...

같은 평문 블록 = 같은 암호문 블록. 평문에 반복 패턴이 있으면 암호문에서도 동일 패턴이 보인다.


시연 1 — HTTP 응답 헤더

HTTP 응답은 다음 패턴이 자주 반복된다:

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 12345
Connection: keep-alive
Date: ...

위 텍스트를 16바이트 블록 단위로 끊어보자:

Block 0: "HTTP/1.1 200 OK\n"   ← 16 byte
Block 1: "Content-Type: te"
Block 2: "xt/html\nContent-"
Block 3: "Length: 12345\nCo"
...

이걸 ECB로 암호화하고 16바이트씩 잘라서 hex로 표시:

from Crypto.Cipher import AES
KEY = b'A'*16
data = (b"HTTP/1.1 200 OK\n"
        b"Content-Type: text/html\n"
        b"Content-Length: 0\n"
        b"Connection: keep-alive\n"
        b"\n") * 5  # 5번 반복

cipher = AES.new(KEY, AES.MODE_ECB)
# 16-byte 패딩
padded = data + b'\x00' * ((16 - len(data) % 16) % 16)
ct = cipher.encrypt(padded)

for i in range(0, len(ct), 16):
    print(ct[i:i+16].hex())

출력:

3a4f...   ← Block: "HTTP/1.1 200 OK\n"
8b21...   ← "Content-Type: te"
c4e7...   ← "xt/html\nContent-"
...
3a4f...   ← 또 "HTTP/1.1 200 OK\n" (같은 ciphertext!)
8b21...   ← 또 "Content-Type: te"
...

5번 반복된 응답 헤더가 ciphertext에서 5번 동일 패턴으로 보인다. 키를 모르고도 “이건 HTTP다”가 추론 가능.


시연 2 — DNS 쿼리

DNS 쿼리는 더 짧고 더 반복적이다.

queries = [
    b'\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00'  # 헤더
    b'\x06google\x03com\x00'                              # google.com
    b'\x00\x01\x00\x01'                                   # A IN
    for _ in range(10)
]

10개 동일 쿼리를 ECB로 암호화:

all_data = b''.join(queries)
ct = cipher.encrypt(all_data + b'\x00' * (16 - len(all_data) % 16))

ciphertext에서 같은 16-byte 블록이 정확히 10번 등장. 패킷 길이 + 반복 구조만 봐도 “같은 쿼리 10번 보낸 트래픽”이 즉시 보인다.


시연 3 — JSON 객체

API 응답이 JSON 배열일 때:

[
  {"user": "alice", "role": "admin"},
  {"user": "bob",   "role": "user" },
  {"user": "carol", "role": "user" }
]

"role": "user"가 두 번 등장. ECB로 암호화하면 그 16-byte 블록이 두 번 동일 ciphertext.

공격자가 적극적으로 chosen-plaintext attack을 시도하면 (자기 데이터를 응답에 끼워 넣을 수 있는 환경에서) 알려진 블록 = 알려진 ciphertext의 사전을 만들 수 있다.


CBC와 비교

같은 키, 같은 IV로 같은 데이터를 CBC로 암호화하면 패턴이 완전히 사라진다.

cipher_cbc = AES.new(KEY, AES.MODE_CBC, iv=b'\x00'*16)
ct_cbc = cipher_cbc.encrypt(padded)
# 어떤 블록도 다른 블록과 같지 않음

CBC는 이전 블록의 ciphertext를 다음 블록 평문에 XOR하므로, 같은 평문이라도 위치가 다르면 다른 ciphertext가 된다.

이게 CBC의 본질적 가치. ECB와의 차이가 그 한 줄(이전 블록 XOR).


그럼 ECB는 어디 쓰나

이론적으론 단일 블록(<= 16byte) 암호화엔 ECB라도 안전하다. 같은 데이터가 두 번 안 나오니까.

실무에선 거의 안 쓰는 게 맞다:

  • 키 무결성 확인용 (단일 블록 평문)
  • 암호 키 자체를 다른 키로 wrap (KEK 시나리오)
  • 길이가 정확히 16 byte 한 블록인 토큰

이외엔 무조건 GCM 또는 CBC + HMAC.


만들면서 알게 된 것들

평문의 패턴이 ciphertext의 패턴이다. ECB는 그 사실을 그대로 노출한다. 펭귄 그림이 강력한 시연인 이유는 시각 패턴이라서. 텍스트 데이터에서도 똑같이 일어나지만 사람 눈엔 안 보일 뿐.

16 byte 블록 정렬을 의식하면 패턴 더 잘 보인다. 보고 싶은 패턴이 정확히 16 byte 정렬에 맞으면 ECB가 그대로 노출. 정렬에서 어긋나면 일부만 노출. 내가 구성하기 나름.

ECB로 암호화된 데이터 = 키만 빼고 다 새는 평문. 키를 못 알아내도 분석에 필요한 정보가 ciphertext에 가득.


펭귄은 잊고, 자기가 자주 보는 트래픽으로 직접 ECB 시연해보면 “절대 ECB 쓰지 말 것”이 본능에 박힌다.