본문 바로가기

Cryptography/Cryptography CTF

[Dreamhack] Textbook-CBC

문제 설명

드림이가 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