본문 바로가기

Cryptography/Cryptography CTF

[Dreamhack] Padding Miracle Attack

문제 설명

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