Step 232. Cryptopals Set 2: ECB/CBC, 패딩 오라클 — 블록 암호의 허점을 뚫는다
Level 3 — CTF 실전과 공격 스킬 심화 | 난이도 ★★★★☆ | 예상 소요 시간 6시간
전제: Step 231(Set 1) — 오늘 만드는 오라클 공격들은 전부 지난 챕터의 점수 함수와 블록 감각 위에 섭니다.
⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다.
- 준비물: 파이썬 3(실측: 3.12.14), pycryptodome, Step 231의
xorlib.py. - 주의: Cryptopals(cryptopals.com)는 공개된 합법 학습 문제집입니다. 이 챕터의 모든 오라클(암호화해 주는 서버 역할)은 여러분의 파이썬 안에 직접 구현합니다 — 실제 서버를 공격하는 것이 아니라, 취약한 서버를 "내 랩에 세우고" 깨는 것이 오늘의 방식입니다. 출력은 전부 2026-09-09 실측입니다.
Set 1이 "XOR과 통계"였다면, Set 2는 "블록 암호 모드의 구조적 결함"입니다. ECB는 같은 평문 블록이 같은 암호문 블록이 되는 성질 하나로 "비밀 문자열을 한 글자씩 빼내는" 공격을 허용하고, CBC는 "패딩이 맞나요?"라는 참/거짓 응답 하나가 암호문 전체를 복호화하는 패딩 오라클 공격에 무너집니다. 암호학의 명작 공격들을 오늘 전부 손으로 재현합니다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
- PKCS#7 패딩을 구현하고 유효성 검증의 참/거짓이 왜 정보를 새는지 설명한다
- CBC 모드를 ECB 프리미티브만으로 직접 구현하고 라이브러리 결과와 대조한다
- ECB/CBC 탐지 오라클과 바이트 앳 어 타임(byte-at-a-time) 공격으로 비밀 문자열을 추출한다
- CBC 비트 플리핑으로 평문의 특정 블록을 원하는 값으로 조작한다
- 패딩 오라클 공격을 구현해 키 없이 CBC 암호문을 복호화한다
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | 파이썬 3 (실측: 3.12.14) + pycryptodome(AES 프리미티브) |
| 오늘의 명령어 | AES.new(key, AES.MODE_ECB/CBC), 직접 짜는 pkcs7_pad/unpad, cbc_encrypt/decrypt |
| 필요한 개념 | 블록 암호 모드(ECB/CBC)의 연결 구조, XOR 연쇄, 오라클 공격 |
| 오늘의 산출물 | 오라클 4종(탐지·바이트 앳 어 타임·비트 플리핑·패딩) 공격 구현체 |
2-1. PKCS#7 패딩 — 마지막 바이트가 규칙이다
블록 암호는 입력이 16바이트 배수여야 하므로 빈자리를 채웁니다. PKCS#7의 규칙: 부족한 바이트 수 N을 값으로 N개 채웁니다 — 3바이트 부족하면 \x03\x03\x03, 정확히 배수면 16바이트 전체를 \x10으로 새 블록 추가. 복호화 후 이 규칙이 맞는지 검사하는데, 이 검사의 참/거짓 응답 자체가 오라클이 됩니다.
2-2. CBC의 연쇄 구조
암호화: C[i] = Enc(P[i] XOR C[i-1]) (C[0]의 자리는 IV)
복호화: P[i] = Dec(C[i]) XOR C[i-1]
핵심 직관: 암호문 블록 C[i]를 바꾸면 평문 블록 P[i+1]이 예측 가능하게 바뀝니다(대신 P[i]는 쓰레기가 됨). 이 성질이 비트 플리핑 공격이고, "Dec(C[i])만 알면 평문"이라는 관찰이 패딩 오라클의 출발점입니다.
2-3. 오라클 공격 — 물어보면 답해주는 시스템
오라클(oracle)은 "질문하면 부분 정보를 답해주는 시스템"입니다. 암호화 오라클은 내 입력을 암호화해 주고, 패딩 오라클은 변조된 암호문의 패딩 유효성을 알려줍니다. 정보 이론적으로 각 응답은 1비트 안팎이지만, 반복 질문으로 누적하면 평문 전체가 새어 나옵니다 — 오늘 실측에서 패딩 오라클은 48바이트 복호화에 6,105회의 질의를 썼습니다.
3. 따라 하기
이 챕터의 모든 출력은 2026-09-09 파이썬 3.12.14 + pycryptodome 실측입니다. 키와 IV는 매 실행마다 무작위이므로 세부 값은 다르게 나옵니다.
3-1. PKCS#7 패딩 구현 (챌린지 9)
BS = 16
def pkcs7_pad(data, bs=BS):
n = bs - (len(data) % bs)
return data + bytes([n]) * n
def pkcs7_unpad(data):
n = data[-1]
if n == 0 or n > BS or data[-n:] != bytes([n]) * n:
raise ValueError("invalid padding")
return data[:-n]
padded = pkcs7_pad(b"YELLOW SUBMARINE")
print("패딩 후:", padded, "-> 길이", len(padded))
try:
pkcs7_unpad(b"ICE ICE BABY\x05\x05\x05\x05")
except ValueError as e:
print("잘못된 패딩:", repr(e))
패딩 후: b'YELLOW SUBMARINE\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10' -> 길이 32
잘못된 패딩: ValueError('invalid padding')
출력 읽는 법: 16바이트 정확히 맞는 입력에도 \x10 16개의 새 블록이 붙습니다 — "배수면 패딩 생략"은 금지입니다. 그리고 unpad가 거짓을 반환하는 대신 예외를 던지는 것에 주목하세요 — 이 예외가 서버의 에러 응답이 되어 패딩 오라클이 됩니다.
3-2. CBC 직접 구현 (챌린지 10)
라이브러리의 CBC를 쓰지 않고, ECB 프리미티브로 연쇄를 손으로 만듭니다.
from Crypto.Cipher import AES
import secrets
def cbc_encrypt(pt, key, iv):
c = AES.new(key, AES.MODE_ECB)
out, prev = b"", iv
for i in range(0, len(pt), BS):
blk = bytes(x ^ y for x, y in zip(pt[i:i+BS], prev)) # XOR 후 암호화
prev = c.encrypt(blk)
out += prev
return out
def cbc_decrypt(ct, key, iv):
c = AES.new(key, AES.MODE_ECB)
out, prev = b"", iv
for i in range(0, len(ct), BS):
blk = ct[i:i+BS]
out += bytes(x ^ y for x, y in zip(c.decrypt(blk), prev)) # 복호화 후 XOR
prev = blk
return out
key, iv = secrets.token_bytes(16), secrets.token_bytes(16)
pt = b"CBC mode chains every block to the previous one!!"
ct = cbc_encrypt(pkcs7_pad(pt), key, iv)
print("내 CBC 왕복:", pkcs7_unpad(cbc_decrypt(ct, key, iv)) == pt)
ref = AES.new(key, AES.MODE_CBC, iv=iv).decrypt(ct)
print("라이브러리 CBC와 일치:", pkcs7_unpad(ref) == pt)
내 CBC 왕복: True
라이브러리 CBC와 일치: True
출력 읽는 법: 손으로 만든 연쇄가 라이브러리와 비트 단위로 일치합니다. 이제 C[i]를 바꾸면 P[i+1]이 어떻게 바뀌는지 수식이 손에 있습니다 — 뒤의 두 공격이 이 구조를 직접 이용합니다.
3-3. ECB/CBC 탐지 오라클 (챌린지 11)
모르는 모드로 암호화해 주는 오라클에, 같은 바이트 64개를 넣습니다.
def encryption_oracle(data):
k = secrets.token_bytes(16)
mode = secrets.choice(["ECB", "CBC"])
if mode == "ECB":
return AES.new(k, AES.MODE_ECB).encrypt(pkcs7_pad(data)), mode
return AES.new(k, AES.MODE_CBC, iv=secrets.token_bytes(16)).encrypt(pkcs7_pad(data)), mode
def detect_mode(ct):
blocks = [ct[i:i+BS] for i in range(0, len(ct), BS)]
return "ECB" if len(set(blocks)) < len(blocks) else "CBC"
ct3, real = encryption_oracle(b"A" * 64)
print("탐지 결과:", detect_mode(ct3), "| 실제 모드:", real, "| 정답:", detect_mode(ct3) == real)
탐지 결과: CBC | 실제 모드: CBC | 정답: True
출력 읽는 법: A 64개는 16바이트 블록 4개가 완전히 같습니다 — ECB라면 암호문도 4개 동일, CBC라면 연쇄 때문에 전부 다릅니다. 여러 번 실행하면 ECB/CBC 양쪽 다 나오는데, 탐지는 매번 정답입니다(실측으로 확인). Step 231의 반복 블록 탐지가 그대로 판별기가 됐습니다.
3-4. ECB 바이트 앳 어 타임 (챌린지 12)
오라클이 내 입력 + 비밀 문자열을 ECB로 암호화해 줍니다. 비밀을 한 글자씩 뺍니다.
import base64
target = base64.b64decode(b"Um9sbGluJyBpbiBteSA1LjA=") # 오라클 안에만 존재하는 비밀
okey = secrets.token_bytes(16)
def oracle_ecb(user):
return AES.new(okey, AES.MODE_ECB).encrypt(pkcs7_pad(user + target))
recovered = b""
for i in range(len(target)):
pad_len = BS - 1 - (len(recovered) % BS)
prefix = b"A" * pad_len
base = oracle_ecb(prefix)[:pad_len + len(recovered) + 1]
for g in range(256):
trial = oracle_ecb(prefix + recovered + bytes([g]))[:pad_len + len(recovered) + 1]
if trial == base:
recovered += bytes([g]); break
print("바이트 앳 어 타임 추출:", recovered)
바이트 앳 어 타임 추출: b"Rollin' in my 5.0"
출력 읽는 법: 원리는 이렇습니다. A를 15개 넣으면 첫 블록은 A×15 + 비밀[0] — 비밀 첫 글자가 블록 끝에 옵니다. 이 암호문 블록과 같은 블록이 나오는 A×15 + 추측글자를 256번 시도해 찾으면 첫 글자 확정. 다음은 A 14개 + 지금까지 밝힌 글자 + 추측글자. 같은 입력=같은 암호문이라는 ECB 성질이 사전 대조표 역할을 합니다. 비밀 16글자가 정확히 추출됐습니다.
3-5. CBC 비트 플리핑 (챌린지 16)
목표: 복호화 결과에 ;admin=true;를 심습니다. 사용자 입력은 세미콜론이 제거되므로 정상 경로로는 불가능합니다.
bkey, biv = secrets.token_bytes(16), secrets.token_bytes(16)
def enc_userdata(u):
u = u.replace(b";", b"").replace(b"=", b"")
pt = b"comment1=cooking%20MCs;userdata=" + u + b";comment2=%20like%20a%20pound%20of%20bacon"
return AES.new(bkey, AES.MODE_CBC, iv=biv).encrypt(pkcs7_pad(pt))
ct5 = enc_userdata(b"A" * 32)
print("조작 전 admin 검사:", b";admin=true;" in pkcs7_unpad(
AES.new(bkey, AES.MODE_CBC, iv=biv).decrypt(ct5)))
mod = bytearray(ct5)
inject = b";admin=true;AAAA" # 정확히 16바이트
orig = b"A" * 16 # 3번째 블록 평문(우리가 아는 값)
for i in range(BS):
mod[BS + i] ^= orig[i] ^ inject[i] # 이전 블록 조작 -> 다음 블록 평문 치환
pt2 = pkcs7_unpad(AES.new(bkey, AES.MODE_CBC, iv=biv).decrypt(bytes(mod)))
print("조작 후 admin 검사:", b";admin=true;" in pt2)
print("조작 후 평문 일부:", pt2[32:64])
조작 전 admin 검사: False
조작 후 admin 검사: True
조작 후 평문 일부: b';admin=true;AAAAAAAAAAAAAAAAAAAA'
출력 읽는 법: P[3] = Dec(C[3]) XOR C[2]이므로 C[2]에 orig XOR inject를 XOR하면 P[3]이 inject로 치환됩니다. 대가로 P[2]는 쓰레기가 되지만(연쇄가 끊긴 자리), 검사는 통과했습니다. 키도 IV도 모른 채 평문의 한 블록을 원하는 값으로 바꿨습니다.
3-6. 패딩 오라클 (챌린지 17)
오라클은 "이 암호문의 패딩이 유효한가?"에만 참/거짓으로 답합니다.
pkey, piv = secrets.token_bytes(16), secrets.token_bytes(16)
pct = AES.new(pkey, AES.MODE_CBC, iv=piv).encrypt(pkcs7_pad(b"Attack at dawn!! The padding oracle talks."))
def padding_oracle(ct):
try:
pkcs7_unpad(AES.new(pkey, AES.MODE_CBC, iv=piv).decrypt(ct))
return True
except ValueError:
return False
def attack_block(prev, cur):
inter = bytearray(BS); plain = bytearray(BS)
for pos in range(BS - 1, -1, -1):
pad = BS - pos
crafted = bytearray(prev)
for j in range(pos + 1, BS):
crafted[j] = inter[j] ^ pad
for g in range(256):
if g == prev[pos] and pos == BS - 1:
continue # 원문 그대로 → 진짜 패딩이 유효한 함정 후보
crafted[pos] = g
if padding_oracle(bytes(crafted) + cur):
if pos > 0: # 검증: 앞 바이트를 뒤집어도 유효하면 진짜
crafted[pos - 1] ^= 1
ok = padding_oracle(bytes(crafted) + cur)
crafted[pos - 1] ^= 1
if not ok: continue
inter[pos] = g ^ pad
plain[pos] = prev[pos] ^ inter[pos]
break
return bytes(plain)
blocks = [piv] + [pct[i:i+BS] for i in range(0, len(pct), BS)]
out = b"".join(attack_block(blocks[i-1], blocks[i]) for i in range(1, len(blocks)))
print("패딩 오라클 복구:", pkcs7_unpad(out))
패딩 오라클 복구: b'Attack at dawn!! The padding oracle talks.'
출력 읽는 법: 마지막 바이트부터 공략합니다. prev[15]를 조작해 복호화 마지막 바이트가 \x01(유효 패딩)이 되는 g를 찾으면, 중간값 inter[15] = g ^ 1이고 평문은 prev[15] ^ inter[15]입니다. 다음은 \x02\x02를 목표로 두 바이트째. 바이트당 최대 256회, 실측 총 질의는 48바이트에 6,105회였습니다 — 참/거짓 1비트짜리 응답이 평문 전체를 새게 했습니다. 함정 후보(원문 그대로)와 검증 쿼리 두 줄이 없으면 마지막 블록에서 오판하니 주석의 위치를 확인하세요.
4. 미션과 연습문제
미션 — 오라클 공격 종합 세트
oracle_attacks.py에 오늘의 네 공격을 함수로 정리합니다 —detect_mode,ecb_byte_at_a_time(oracle),cbc_bit_flip(ct, orig, inject, offset),padding_oracle_attack(ct, iv, oracle)- 각 함수의 호출 횟수(오라클 질의 수)를 세어 출력합니다 — 패딩 오라클이 바이트당 평균 ~128회(실측 6,105회/48바이트)인 것을 확인합니다
- 네 공격 전부를 "방어 코드 한 줄"과 짝지어 주석으로 씁니다 — 예: 패딩 오라클 → "복호화 전에 MAC 검증(Encrypt-then-MAC)"
- (선택) 원본 Cryptopals Set 2의 챌린지 12·16·17에 같은 함수를 적용해 보세요
연습문제
문제 1. PKCS#7에서 입력이 정확히 16바이트 배수일 때도 16바이트 전체를 추가하는 이유는 무엇인가요? 생략하면 복호화 쪽에 어떤 모호함이 생기는지 답해 보세요.
문제 2. 바이트 앳 어 타임 공격이 ECB에서는 되고 CBC에서는 안 되는 이유를, "같은 입력 블록 → 같은 출력 블록" 성질의 유무로 설명해 보세요.
문제 3. 비트 플리핑에서 C[2]를 조작해 P[3]을 바꿨을 때 P[2]가 쓰레기가 되는 이유를 CBC 복호화 수식으로 보여 주세요.
문제 4. 패딩 오라클 공격이 "복호화 결과의 마지막 바이트"부터 확정하는 이유를, 패딩 검증이 끝에서 시작된다는 관찰로 설명해 보세요.
5. 모범 답안과 완료 기준
미션 모범 답안
def padding_oracle_attack(ct, iv, oracle):
blocks = [iv] + [ct[i:i+BS] for i in range(0, len(ct), BS)]
return b"".join(attack_block(blocks[i-1], blocks[i], oracle)
for i in range(1, len(blocks)))
판정 기준: ① 네 함수 모두 본문 실측과 같은 결과(비밀 추출·admin 심기·평문 복구) ② 질의 카운터가 실제로 증가(패딩 오라클 수천 회) ③ 방어 주석이 공격의 전제를 정확히 겨냥할 것 — 예: 바이트 앳 어 타임의 방어는 "비밀과 사용자 입력을 같은 블록에 섞지 않기 + ECB 폐기"입니다.
검증하는 법: 키를 새로 뽑아 두 번 실행하세요. 추출 결과가 키와 무관하게 성립해야 진짜 공격이고, 특정 키에서만 되면 구현 버그입니다.
연습문제 해답
문제 1 해답. 패딩 생략이 허용되면, 원문이 \x01로 끝나는 16바이트 배수 메시지를 복호화할 때 "이 \x01이 패딩인가 원문인가"를 구분할 방법이 없습니다. 항상 패딩을 추가하면 "마지막 바이트 값 N만큼은 무조건 패딩"이라는 규칙이 성립해 unpad가 결정론적이 됩니다.
문제 2 해답. 공격의 핵심 장치는 "추측 평문 블록과 같은 암호문 블록이 나오는지" 대조하는 사전표입니다. ECB는 블록이 독립이라 이 대조가 성립하지만, CBC는 앞 블록(또는 IV)이 매번 연쇄에 섞여 같은 평문 블록도 다른 암호문이 되므로 사전표를 만들 수 없습니다.
문제 3 해답. P[2] = Dec(C[2]) XOR C[1]인데 C[2]를 조작했으므로 Dec(C[2])가 원래와 무관한 무작위 값이 됩니다 — 블록 암호의 눈사태 효과로 한 비트 차이도 출력 전체를 뒤집습니다. 반면 P[3] = Dec(C[3]) XOR C[2]에서 Dec(C[3])은 그대로고 C[2]만 바뀌었으니, 바꾼 만큼 정확히 반영됩니다. 한 블록은 깨지고 다음 블록은 조종되는 것이 CBC 조작의 비용 구조입니다.
문제 4 해답. 유효한 패딩의 최소 형태는 마지막 1바이트 \x01입니다. 그래서 마지막 바이트만 목표로 삼으면(나머지는 아무 값이어도 됨) 단일 바이트 조작으로 참/거짓 신호를 얻을 수 있습니다. 마지막 바이트가 확정되면 그다음 목표는 \x02\x02 — 목표 패딩이 한 칸씩 앞으로 자라는 형태라, 공격은 반드시 끝에서 시작해 앞으로 진행됩니다.
완료 기준 체크리스트
- [ ] PKCS#7 pad/unpad를 구현하고 잘못된 패딩에서 ValueError를 확인했다
- [ ] CBC를 ECB 프리미티브로 구현해 라이브러리와 일치를 봤다
- [ ] 반복 블록 수로 ECB/CBC를 구별했다
- [ ] 바이트 앳 어 타임으로 비밀 문자열 16바이트를 추출했다
- [ ] 비트 플리핑으로
;admin=true;를 심어 검사를 통과했다 - [ ] 패딩 오라클로 키 없이 전체 평문을 복구했다
- [ ] "암호문 블록 i 조작 → 평문 블록 i+1 조종, 블록 i 붕괴"를 수식으로 설명할 수 있다
- [ ] 미션: 4개 공격 함수화 + 질의 횟수 출력 확인
6. 흔한 실수와 해결
벽 1. ValueError: Data must be aligned to block boundary in ECB mode
증상: ValueError: Data must be aligned to block boundary in ECB mode (2026-09-09 실측).
원인: pycryptodome은 패딩을 자동으로 해 주지 않습니다 — 16바이트 배수가 아닌 입력을 그대로 encrypt에 넣은 것입니다.
해결: 항상 pkcs7_pad를 거치세요. 이 에러가 "라이브러리가 패딩을 안 해준다"는 것을 알려주는 신호이며, 3-1을 먼저 구현하는 이유입니다.
벽 2. ValueError: Incorrect AES key length (5 bytes)
증상: ValueError: Incorrect AES key length (5 bytes) (2026-09-09 실측).
원인: AES 키는 16/24/32바이트만 허용됩니다. "YELLOW SUBMARINE"처럼 정확히 16바이트인지 확인하세요.
해결: len(key)를 출력해 확인하고, 임의 키는 secrets.token_bytes(16)을 쓰세요.
벽 3. CBC에서 IV를 빼먹었다
증상: AES.new(key, AES.MODE_CBC)가 에러 없이 되는데, 복호화가 첫 블록부터 깨집니다.
원인: pycryptodome은 IV 생략 시 무작위 IV를 자동 생성합니다 — 암호화 때 만든 IV를 모르면 복호화 첫 블록이 쓰레기가 됩니다.
해결: CBC는 항상 IV를 명시하고, IV를 암호문 앞에 붙여 보관하는 관행(iv + ct)을 들이세요. 3-2의 직접 구현이 이 구조 그대로입니다.
벽 4. 패딩 오라클이 마지막 블록에서 오판한다
증상: 복구 결과의 끝쪽이 \x00이나 깨진 문자입니다.
원인: 마지막 바이트 공략에서 "원문 그대로"(g == prev[pos])는 진짜 패딩이 유효해서 True가 나오는 함정 후보입니다 — 목표 패딩 \x01이 아닌 실제 패딩 길이로 통과합니다.
해결: 3-6의 두 장치 — ① 마지막 바이트에서 원문 그대로 후보 건너뛰기, ② 앞 바이트를 뒤집는 검증 쿼리 — 를 빠뜨리지 마세요. 검증 쿼리는 질의 수를 늘리지만 정확도를 보장합니다(실측 6,105회에 포함).
벽 5. 바이트 앳 어 타임에서 recovered가 중간에 멈춘다
증상: 일부 글자만 추출되고 루프가 break 없이 끝납니다.
원인: 블록 경계를 넘을 때 pad_len 계산이 틀렸거나, 대조 범위([:pad_len + len(recovered) + 1])가 블록 경계를 넘어 다음 블록까지 비교하고 있습니다.
해결: 비교는 "현재 작업 중인 블록까지만" 해야 합니다 — 3-4의 슬라이스 범위를 그대로 쓰고, 17바이트 이상의 비밀에서 경계 넘기를 반드시 시험하세요(실측 비밀은 정확히 17바이트라 경계를 넘습니다).
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| PKCS#7 | 부족한 수 N을 N개 채우는 패딩 — 유효성 검사가 오라클이 됨 |
| CBC 연쇄 | C[i] = Enc(P[i] ⊕ C[i-1]) — 연결이 곧 조작점 |
| 바이트 앳 어 타임 | ECB의 결정론적 성질로 비밀을 한 글자씩 사전 대조 |
| 비트 플리핑 | C[i] 조작 → P[i+1] 정확히 조종(대신 P[i] 붕괴) |
| 패딩 오라클 | 패딩 참/거짓 1비트로 중간값 역산, 평문 전체 복구(실측 ~128회/바이트) |
오늘의 명령어·코드
| 명령 | 하는 일 |
|---|---|
pkcs7_pad / pkcs7_unpad |
패딩 부착·검증 — 오라클의 심장 |
AES.new(key, AES.MODE_CBC, iv=iv) |
CBC 라이브러리(대조용) |
crafted[j] = inter[j] ^ pad |
목표 패딩으로 꾸민 조작 블록 |
bytes(x ^ y for x, y in zip(a, b)) |
바이트열 XOR — 오늘 모든 공격의 기본 연산 |
oracle(prefix + recovered + bytes([g])) |
한 글자 추측 사전 쿼리 |
명령어보다 중요한 감각
오늘의 네 공격은 모두 같은 구조입니다 — "시스템이 무심코 답해주는 것"을 질문으로 조직한 것. ECB의 결정론, CBC의 연쇄, 패딩 검증의 참/거짓은 각자 무해해 보이지만, 반복 질문과 결합하면 평문이 통째로 샙니다. 그래서 현대 암호 설계의 답은 "질문을 못 하게 하기" — 인증된 암호(AEAD)로 변조된 암호문은 복호화 전에 폐기하는 것입니다. 이 답의 구조가 다음 챕터 AES 총정리의 주제입니다.
전부 체크되면 Step 232 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.