문제 설명
unpad는 정말 까다로운 친구에요... Padding oracle attack을 공부해봅시다!
소스 코드
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import os
flag = open("flag.txt", "r").read()
secret, key, iv = [os.urandom(16) for _ in range(3)]
while True:
mode = int(input("[1] Encrypt [2] Decrypt [3] Submit: "))
if mode == 1:
pt = input("Input plaintext: ")
if pt == "secret":
pt = secret
else:
pt = bytes.fromhex(pt)
pt = pad(pt, 16)
cipher = AES.new(key, AES.MODE_CBC, iv)
ct = bytes.hex(cipher.encrypt(pt))
print("pt ==Encryption=>", ct)
elif mode == 2:
ct = bytes.fromhex(input("Input ciphertext: "))
try:
cipher = AES.new(key, AES.MODE_CBC, iv)
pt = unpad(cipher.decrypt(ct), 16)
pt = "Don't steal my secret!"
except:
pt = "Invalid ciphertext."
print("ct ==Decryption=>", pt)
elif mode == 3:
if bytes.fromhex(input("Enter secret: ")) == secret:
print("You are a human decryptor!!", flag)
exit()
else:
print("Try again.. T.T")
else:
exit()
소스 코드 분석
이 코드는 AES-CBC 모드를 사용해 암호화 및 복호화 기능을 제공하는 서비스이다.
기본설정
- secret : 알아내야하는 비밀 데이터 (16바이트)
- key, iv : AES 암호화에 사용되는 키와 초기화 벡터, 각각 무작위로 생성됨, 16바이트
Mode 1 : Encrypt
- 사용자가 평문을 입력한다.
- 입력값이 "secret"이라는 문자열이면, 실제 secret 변수를 암호화한다.
- 입력값을 16바이트 단위로 패딩하고, AES-CBC 모드로 암호화하여 16진수 문자열을 반환한다.
Mode 2 : Decrypt
- 사용자가 16진수 암호문을 입력한다.
- 서버는 이를 복호화한 뒤 패딩을 제거한다.
- 패딩이 정상이면 Don't steal my secret을 출력하고 복호화된 평문을 보여주지 않는다.
- 패딩이 잘못되었으면, except 블록으로 넘어가 Invalid ciphertext를 출력한다.
- 이러한 에러 메세지의 차이가 공격자에게 힌트를 준다.
Mode 3 : Submit
- 공격자가 알아낸 secret 값을 16진수로 입력하면 flag를 반환한다.
취약점 분석: Padding Oracle Attack
이 문제의 핵심 취약점은 Mode 2에서 발생한다.
AES-CBC 모드에서 블록 암호화는 고정된 크기 단위로 데이터를 처리한다.
데이터가 블록 크기에 딱 맞지 않으면 PKCS#7과 같은 방식으로 빈공간을 채운다(padding)
서버는 복호화 후 패딩 값을 확인하는데, 패딩이 올바른지 아닌지에 따라서 서로 다른 에러 메시지를 보인다.
- "Don't steal my secret!" -> 패딩 정상
- "Invalid ciphertext." -> 패딩 비정상
공격자는 암호문을 조금씩 조작해서 서버에 보내고, 서버의 반응을 통해서 평문 내용을 한 바이트씩 역추적할 수 있다.
이러한 공격을 Padding Oracle Attack이라고 한다.
이 공격에 대한 자세한 설명은 다음 블로그에서 확인할 수 있다.
2025.11.15 - [Cryptography] - Padding Oracle Attack
Padding Oracle Attack
Padding Oracle Attack CBC 모드 암호화에서 padding 오류 메시지의 "다른 반응"을 이용해 복호화 키를 몰라도 평문을 역추론하는 공격이다. CBC 복호화$C_0 = IV$$P_i = D_k(C_i) \oplus C_{i-1}$ XOR 연산 때문에 이전
hundq.tistory.com
문제 풀이
flag를 얻기 위해서는 secret 값을 얻어야한다.
먼저 secret을 구하는 코드를 작성해보자.
enc_secret = encrypt("secret")[:16]
enc_secret에 저장된 값은 $\text{enc}_K(secret \oplus IV)$이다
따라서, enc_secret을 복호화하더라도 IV를 알아야 secret 값을 얻을 수 있다.
IV를 암호화하기 위해서 0으로 이루어진 블록을 암호화한다.
$\text{enc}_K(0\oplus IV) = \text{enc}_K(IV)$
이것을 복호화하면 IV 값을 얻을 수 있다.
이렇게 IV를 얻고나면 enc_secret을 복호화한 값과 XOR해서 secret 값을 얻는다.
enc_iv = encrypt(zeroblock)[:16]
iv = decrypt_block(enc_iv)
secret = xor(decrypt_block(enc_secret), iv)
이제, Padding oracle attack을 수행하는 함수를 구해보자.
def decrypt_block(msg):
pt = [0] * 16
for i in trange(16):
p = pt[:]
for j in range(i):
p[15 - j] ^= (i + 1)
for b in range(256):
p[15 - i] = b
if decrypt_oracle(bytes(p) + msg):
if i == 0:
p[14] = 1
if decrypt_oracle(bytes(p) + msg):
break
else:
continue
break
pt[15 - i] = b ^ (i + 1)
return bytes(pt)
라인별로 자세히 알아보자.
pt = [0] * 16
- 최종적으로 구해질 평문 블록을 저장하는 변수를 선언한다.
for i in trange(16):
- 마지막 바이트부터 첫 바이트까지 역순으로 복구한다.
p = pt[:]
for j in range(i):
p[15 - j] ^= (i + 1)
- 이전에 복구한 값들을 패딩에 맞게 조정한다.
예를 들어, 지금 i=2 라고 가정해보자.
우리가 서버에서 얻고 싶은 패딩 모양은 03 03 03이다.
즉, 마지막 3바이트가 3으로 이루어진 패딩이 되는 순간 서버는 패딩 OK 신호를 보낸다.
또한 이미 아래 두 바이트는 이미 복구 해놓은 상태이다.
[ ? ? ? ? ? ? ? P13 P14 P15 ]
에서 P14와 P15는 이미 찾은 상태인 것이고 지금 P13을 찾고 있는 것이다.
따라서, 마지막 두 바이트에 3을 XOR 연산을 해서 고정해둔다.
P14 xor 3, P15 xor 3을 한다고 생각하면 된다.
for b in range(256):
p[15 - i] = b
if decrypt_oracle(bytes(p) + msg):
- 가능한 바이트 0~255 brute force
- 현재 찾고 있는 바이트에 b = 0~255를 넣어서 계산해본다.
- 서버에서 padding 정상이 나오면 정답 후보 b를 찾은 것이다.
if i == 0:
p[14] = 1
if decrypt_oracle(bytes(p) + msg):
break
else:
continue
- padding oracle attack에서 마지막 바이트 i=0은 false positive 문제가 자주 발생한다.
- 예를 들어 b가 우연히 유효한 패딩을 만들어서 성공한 것처럼 보인다.
- 그래서 두 번째 바이트를 1로 고정해 다시 확인하는 과정이다.
pt[15 - i] = b ^ (i + 1)
- 평문의 실제 값을 계산한다.
따라서 최종적인 솔루션 코드는 다음과 같다.
from pwn import *
from tqdm import trange
from Crypto.Util.Padding import unpad
io = remote("host3.dreamhack.games", 23839)
def encrypt(msg):
if msg != "secret":
msg = bytes.hex(msg)
io.sendline(b"1")
io.sendline(msg.encode())
io.recvuntil(b"=> ")
return bytes.fromhex(io.recvline().decode())
def decrypt_oracle(msg):
msg = bytes.hex(msg)
io.sendline(b"2")
io.sendline(msg.encode())
io.recvuntil(b"=> ")
return io.recvline() == b"Don't steal my secret!\n"
def submit(msg):
msg = bytes.hex(msg)
io.sendline(b"3")
io.sendline(msg.encode())
io.recvuntil(b"secret: ")
def decrypt_block(msg):
# TODO 1, Padding Oracle Attack
pt = [0] * 16
for i in trange(16):
p = pt[:]
for j in range(i):
p[15 - j] ^= (i + 1)
for b in range(256):
p[15 - i] = b
if decrypt_oracle(bytes(p) + msg):
# i = 0, the first step
if i == 0:
# Second last byte is fixed to 0 for 256 iterations
# But setting it 1 here removes multiple(>1) byte padding success.
p[14] = 1
if decrypt_oracle(bytes(p) + msg):
break
else:
continue
break
pt[15 - i] = b ^ (i + 1)
return bytes(pt)
zeroblock = bytes(16)
enc_secret = encrypt("secret")[:16]
# TODO 2, Recovering Secret
enc_iv = encrypt(zeroblock)[:16]
iv = decrypt_block(enc_iv)
secret = xor(decrypt_block(enc_secret), iv)
submit(secret)
io.interactive()

'Cryptography > Cryptography CTF' 카테고리의 다른 글
| [Dreamhack] Textbook-RSA (0) | 2026.01.22 |
|---|---|
| [Dreamhack] safeprime (0) | 2026.01.18 |
| [Dreamhack] Textbook-CBC (0) | 2025.11.17 |
| [Dreamhack] No shift please! (0) | 2025.11.07 |
| [Dreamhack] No sub please! (0) | 2025.11.06 |