문제 설명
드림이가 AES 서버를 운영하고 있어요. 무엇이든 암호화하고 복호화 할 수 있다던데... 플래그를 획득해주세요!
소스코드
#challenge.py
from Crypto.Util.Padding import pad, unpad
from random import choices, randint
from Crypto.Cipher import AES
BLOCK_SIZE = 16
flag = open("flag", "rb").read()
key = bytes(randint(0, 255) for i in range(BLOCK_SIZE))
encrypt = lambda pt: AES.new(key, AES.MODE_CBC, key).encrypt(pad(pt, BLOCK_SIZE))
decrypt = lambda ct: unpad(AES.new(key, AES.MODE_CBC, key).decrypt(ct), BLOCK_SIZE)
print("Welcome to dream's AES server")
while True:
print("[1] Encrypt")
print("[2] Decrypt")
print("[3] Get Flag")
choice = input()
if choice == "1":
print("Input plaintext (hex): ", end="")
pt = bytes.fromhex(input())
print(encrypt(pt).hex())
elif choice == "2":
print("Input ciphertext (hex): ", end="")
ct = bytes.fromhex(input())
print(decrypt(ct).hex())
elif choice == "3":
print(f"flag = {encrypt(flag).hex()}")
exit()
else:
print("Nope")
소스코드 분석
이 코드는 AES-CBC 모드를 사용해서 데이터를 암호화하고 복호화하는 간단한 서비스를 제공한다.
기본설정
- BLOCK_SIZE = 16 : AES는 16바이트 단위로 데이터를 처리한다.
- key : 0~255 사이의 무작위 숫자로 16바이트 키를 생성한다.
암호화 함수
encrypt = lambda pt: AES.new(key, AES.MODE_CBC, key).encrypt(pad(pt, BLOCK_SIZE))
- AES-CBC 모드는 Key와 초기 벡터(IV)가 필요하다.
- AES.new(key, AES.MODE_CBC, key)에서 첫 번째 인자 key는 암호화 키이고, 세 번째 인자 key는 IV이다.
- 즉, IV를 무작위로 생성하지 않고 암호화 키와 동일하게 사용하고 있다.
복호화 함수
decrypt = lambda ct: unpad(AES.new(key, AES.MODE_CBC, key).decrypt(ct), BLOCK_SIZE)
- 암호화와 동일하게 key와 IV를 모두 key 변수로 사용한다.
- 복호화 후 unpad를 통해 패딩을 제거한다.
메뉴
if choice == "1":
print("Input plaintext (hex): ", end="")
pt = bytes.fromhex(input())
print(encrypt(pt).hex())
elif choice == "2":
print("Input ciphertext (hex): ", end="")
ct = bytes.fromhex(input())
print(decrypt(ct).hex())
elif choice == "3":
print(f"flag = {encrypt(flag).hex()}")
exit()
else:
print("Nope")
[1] Encrypt
- 사용자가 평문을 hex 문자열로 입력하면 이를 바이트로 바꾸고 encrypt()를 호출한다.
- 암호문을 hex로 출력한다.
- 공격자는 임의의 평문에 대한 암호문을 얻을 수 있는 encryption oracle을 가지게 된다.
[2] Decrypt
- 사용자가 암호문을 hex 문자열로 입력하면 decrypt()를 호출해서 복호화한다.
- 결과를 hex로 출력한다.
- 패딩이 올바르지 않으면 unpad 예외가 발생해서 프로그램이 죽는다.
- 공격자는 임의의 암호문에 대한 복호화 결과를 얻을 수 있는 decryption oracle을 가지게 된다.
[3] Get Flag
- encrypt(flag)를 호출해서 플래그의 암호문을 hex로 출력한다.
- 그리고 exit()로 프로그램을 종료한다.
취약점 분석
이 코드의 가장 핵심적인 취약점은 AES 암호화 초기 벡터 IV와 비밀 키를 동일하게 설정했다는 점이다.
- IV는 매번 무작위여야하며 공개되어도 안전하지만, 비밀 키와는 달라야한다.
- 이러한 구성은 복호화 오라클이 있을 때 비밀 키를 쉽게 복구할 수 있게 만든다.
문제 풀이
IV를 다양한 방법으로 알아낼 수 있다.
Key = IV이기 때문에 IV만 알아낸다면 키를 찾는 것과 동일하다.


< 방법 1. IV를 암호화해서 다시 복호화하기 >
- 이 방법은 서버가 스스로 IV를 출력하도록 유도하는 방법이다.
[1단계]
- [1] Encrypt 기능에 평문 0을 넣는다.
- 원래는 0을 암호화해야 하지만, CBC 모드 특성상 IV와 XOR 연산되어 섞인다.
- 0 XOR IV = IV이므로, 결국 IV를 암호화한 값이 나온다.
[2단계]
- [2] Decrypt 기능에 0 + IV를 암호화한 값을 붙여서 넣는다.
- 서버가 두 번째 블록 (IV를 암호화한 값)을 복호화해 IV 값이 나온다.
- CBC 모드는 해독한 값에 앞 블록의 암호문을 XOR한다.
- 따라서 0 XOR IV = IV가 되어서, 서버가 IV 값을 반환한다.
from pwn import *
from Crypto.Util.number import long_to_bytes
p = remote('host', 12345)
def get_menu(choice):
p.recvuntil(b'[3] Get Flag\n')
p.sendline(choice.encode())
# ---------------------------------------------------------
# [단계 1] 평문 0을 암호화 (Enc(IV) 획득)
# ---------------------------------------------------------
get_menu('1') # Encrypt
p.recvuntil(b'(hex): ')
# 평문 0을 보냄 -> 첫 블록은 Enc(0 ^ IV) = Enc(IV)가 됨
p.sendline(b'00' * 16)
enc_result = bytes.fromhex(p.recvline().strip().decode())
# 암호화 결과에서 블록 추출
# enc_block_1: Enc(IV)
# enc_rest: 나머지 블록들 (이 안에 정상적인 패딩이 포함되어 있음)
enc_block_1 = enc_result[0:16]
enc_rest = enc_result[16:]
# ---------------------------------------------------------
# [단계 2] 조작된 암호문 복호화
# ---------------------------------------------------------
# 공격 페이로드 구성:
# 1. 0으로 채운 블록 (나중에 XOR 되어 사라질 용도)
# 2. 위에서 얻은 Enc(IV) 블록
# 3. 패딩 에러 방지용 나머지 블록들
zero_block = b'\x00' * 16
payload = zero_block + enc_block_1 + enc_rest
get_menu('2') # Decrypt
p.recvuntil(b'(hex): ')
p.sendline(payload.hex().encode())
decrypted_hex = p.recvline().strip().decode()
decrypted_bytes = bytes.fromhex(decrypted_hex)
# 두 번째 블록이 Key가 됨
# 설명:
# 서버는 2번째 블록(Enc(IV))을 복호화 -> IV 생성
# CBC 모드이므로 이전 암호문 블록(1번블록: 00..00)과 XOR 함
# 결과 = IV ^ 0 = IV
key = decrypted_bytes[16:32]
print(f"[+] Found Key: {key.hex()}")
# ---------------------------------------------------------
# [단계 3] Flag 복호화 (1번 코드와 동일)
# ---------------------------------------------------------
get_menu('3')
p.recvuntil(b'flag = ')
flag_enc = bytes.fromhex(p.recvline().strip().decode())
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
cipher = AES.new(key, AES.MODE_CBC, key)
flag = unpad(cipher.decrypt(flag_enc), 16)
print(f"[+] Flag: {flag.decode()}")
p.close()

< 방법 2. IV의 XOR 연산을 이용해 추출하기 >
- 이 방법은 Trash값을 이용해 IV만 남기는 원리이다.
서버의 [2] Decrypt 기능에 0으로 채워진 블록 2개를 보낸다.
- 첫 번째 블록 : (어떤 값) XOR IV가 나온다.
- 두 번째 블록 : (어떤 값) XOR 0이 나온다.
두 번째 블록의 결과는 그냥 (어떤 값)이다.
첫 번째 블록 결과인 (어떤 값) XOR IV에서 (어떤 값)을 XOR하면 IV만 남게 된다.
from pwn import *
from Crypto.Util.number import long_to_bytes, bytes_to_long
p = remote('------------', 12345)
def get_menu(choice):
p.recvuntil(b'[3] Get Flag\n')
p.sendline(choice.encode())
# ---------------------------------------------------------
# [단계 1] 패딩 에러 방지용 '정상 암호문' 확보
# ---------------------------------------------------------
get_menu('1') # Encrypt 선택
p.recvuntil(b'(hex): ')
p.sendline(b'00' * 16) # 아무 평문이나 입력 (16바이트)
valid_ciphertext = bytes.fromhex(p.recvline().strip().decode())
# ---------------------------------------------------------
# [단계 2] 00 || 00 을 복호화하여 Key 추출
# ---------------------------------------------------------
# 공격 페이로드: 0 (16바이트) + 0 (16바이트) + 정상 암호문(패딩 우회용)
zero_block = b'\x00' * 16
payload = zero_block + zero_block + valid_ciphertext
get_menu('2') # Decrypt 선택
p.recvuntil(b'(hex): ')
p.sendline(payload.hex().encode())
# 결과 수신
decrypted_hex = p.recvline().strip().decode()
decrypted_bytes = bytes.fromhex(decrypted_hex)
# 앞의 두 블록 가져오기
block_1 = decrypted_bytes[0:16] # Dec(0) ^ IV
block_2 = decrypted_bytes[16:32] # Dec(0) ^ 0 = Dec(0)
# XOR 연산으로 Key(IV) 복구: (Dec(0) ^ IV) ^ Dec(0) = IV
key = xor(block_1, block_2)
print(f"[+] Found Key: {key.hex()}")
# ---------------------------------------------------------
# [단계 3] Flag 획득 및 복호화
# ---------------------------------------------------------
get_menu('3') # Get Flag
p.recvuntil(b'flag = ')
flag_enc = bytes.fromhex(p.recvline().strip().decode())
# 로컬에서 복호화 (IV = Key)
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
cipher = AES.new(key, AES.MODE_CBC, key) # IV와 Key가 동일
flag = unpad(cipher.decrypt(flag_enc), 16)
print(f"[+] Flag: {flag.decode()}")
p.close()

'Cryptography > Cryptography CTF' 카테고리의 다른 글
| [Dreamhack] safeprime (0) | 2026.01.18 |
|---|---|
| [Dreamhack] Padding Miracle Attack (0) | 2025.11.17 |
| [Dreamhack] No shift please! (0) | 2025.11.07 |
| [Dreamhack] No sub please! (0) | 2025.11.06 |
| [Dreamhack] chinese what? (0) | 2025.10.09 |