문제 설명
AES에서 SubBytes 기능을 없애봤어요! 이 암호는 과연 안전할까요?
소스 코드
#chall.py
from AES import AES_implemented
import os
# For real AES without modification, this challenge is unsolvable with modern technology.
# But let's remove a step.
ret = lambda x: None
AES_implemented._sub_bytes = ret
AES_implemented._sub_bytes_inv = ret
# Will it make a difference?
secret = os.urandom(16)
key = os.urandom(16)
flag = open("flag.txt", "r").read()
cipher = AES_implemented(key)
secret_enc = cipher.encrypt(secret)
assert cipher.decrypt(secret_enc) == secret
print(f"enc(secret) = {bytes.hex(secret_enc)}")
while True:
option = int(input("[1] encrypt, [2] decrypt: "))
if option == 1: # Encryption
plaintext = bytes.fromhex(input("Input plaintext to encrypt in hex: "))
assert len(plaintext) == 16
ciphertext = cipher.encrypt(plaintext)
print(f"enc(plaintext) = {bytes.hex(ciphertext)}")
if plaintext == secret:
print(flag)
exit()
elif option == 2: # Decryption
ciphertext = bytes.fromhex(input("Input ciphertext to decrypt in hex: "))
assert len(ciphertext) == 16
if ciphertext == secret_enc:
print("No way!")
continue
plaintext = cipher.decrypt(ciphertext)
print(f"dec(ciphertext) = {bytes.hex(plaintext)}")
(AES.py는 코드가 길어서 생략)
소스 코드 설명
ret = lambda x: None
AES_implemented._sub_bytes = ret
AES_implemented._sub_bytes_inv = ret
- AES_implemented 클래스 안에 있는 _sub_bytes와 _sub_bytes_inv를 작동하지 않게 만듦.
- 즉, AES의 SubBytes 과정을 없앰.
secret = os.urandom(16)
key = os.urandom(16)
- secret과 key 값을 16바이트 랜덤 값으로 할당함.
cipher = AES_implemented(key)
secret_enc = cipher.encrypt(secret)
assert cipher.decrypt(secret_enc) == secret
print(f"enc(secret) = {bytes.hex(secret_enc)}")
- 생성된 키로 AES 암호화 객체를 생성함.
- secret 값을 AES로 암호화해서 secret_enc에 저장함.
- 암호화가 정상적으로 되었으면 secret 값을 출력함.
[ ⭐ 암호화 ]
if option == 1:
plaintext = bytes.fromhex(input("Input plaintext to encrypt in hex: "))
assert len(plaintext) == 16
ciphertext = cipher.encrypt(plaintext)
print(f"enc(plaintext) = {bytes.hex(ciphertext)}")
if plaintext == secret:
print(flag)
exit()
- 사용자가 16바이트의 평문을 입력하면 암호문을 생성해서 출력함.
- 만약 입력한 평문이 secret 값과 같으면 flag를 출력함.
- 즉, secret 값을 맞추면 flag를 알 수 있음.
[ 복호화 ]
elif option == 2:
ciphertext = bytes.fromhex(input("Input ciphertext to decrypt in hex: "))
assert len(ciphertext) == 16
if ciphertext == secret_enc:
print("No way!")
continue
plaintext = cipher.decrypt(ciphertext)
print(f"dec(ciphertext) = {bytes.hex(plaintext)}")
- 사용자가 암호문을 입력하면 복호화해서 평문을 알려줌.
- 단, secret_enc 값은 복호화 못하게 막아둠.
- secret_enc을 넣어서 바로 secret 값을 알아내는 꼼수를 차단함.
문제 풀이
이 코드의 존재하는 치명적인 취약점은 Subbytes 단계를 제거한 것이다.
AES 암호화 가정에서 SubBytes 단계는 S-Box라는 non-linear 치환 테이블을 사용한다.
이는 AES의 전체 암호화 과정을 non-linear로 만들어, 단순한 대수적 공격(ex. 연립방정식 풀이)를 불가능하게 만드는 장치이다
SubBytes가 제거되면 AES에 남는 연산은 ShiftRows, MixColumns, AddRoundKey이다.
이 연산들은 모두 linear 연산이다.
선형성의 의미: 암호화 함수 $E_k(P)$와 복호화 함수 $D_k(C)$가 선형 (정확히는 아핀 변환, $f(x) = Ax + b$)이 된다.
- $E_k(P_1 \oplus P_2) = E_k(P_1) \oplus E_k(P_2) \oplus E_k(0)$
- $D_k(C_1 \oplus C_2) = D_k(C_1) \oplus D_k(C_2) \oplus D_k(0)$
우리는 secret = cipher.decrypt(secret_enc)을 계산하고 싶지만, 복호화 함수가 이를 막고 있다.
하지만, 선형성의 성질을 이용하면 이 값을 알아낼 수 있다.
알고 있는 값 : secret_enc
원하는 값 : secret = cipher.decrypt(secret_enc)
1. cipher.decrypt(0) 값 구하기
- decrypt 옵션을 선택한다.
- 입력값으로 16바이트의 0을 전송한다.
- 서버가 반환하는 평문 값을 $P_0$으로 저장한다. ( $P_0$ = cipher.decrypt(0) )
2. 임의의 $C_1$에 대한 cipher.decrypt($C_1$) 값 구하기
- decrypt 옵션을 선택한다.
- 입력값으로 임의의 16바이트 값 $C_1$을 전송한다. ex) 11111111111111111111111111111111
- 서버가 반환하는 평문 값을 $P_1$으로 저장한다. ( $P_1$ = cipher.decrypt( $C_1$ ) )
3. cipher.decrypt( $secret\_enc \oplus C_1$ ) 값 구하기
- $C_1$ 값과 secret_enc 값을 XOR 연산한다.
- $C_x = secret\_enc \oplus C_1$
- decrypt 옵션을 선택한다.
- 계산된 $C_x$ 값을 서버에 전송한다.
- 서버가 반환하는 평문 값을 $P_x$으로 저장한다. ( $P_x$ = cipher.decrypt( $C_x$ ) = cipher.decrypt($ secret\_enc \oplus C_1$ ) )
4. secret 계산하기
- 이제 복호화 함수의 선형적 성질 $D_k(C_1 \oplus C_2) = D_k(C_1) \oplus D_k(C_2) \oplus D_k(0)$을 이횽한다.
- $P_x$ = cipher.decrypt( $ secret\_enc \oplus C_1$ ) = cipher.decrypt( $ secret\_enc$ ) ⊕ cipher.decrypt( $C_1$ ) ⊕ cipher.decrypt( $0$ )
- $P_x$ = $secret$ ⊕ $P_1$ ⊕ $P_0$
- 이 식을 secret에 대한 식으로 정리하면
- $secret$ = $P_x$ ⊕ $P_1$ ⊕ $P_0$
5. flag 획득
- encrypt 옵션을 선택한다.
- 4단계에서 계산한 secret의 16진수 값을 입력한다.
- 서버가 flag 값을 출력한다.
풀이 코드
from pwn import *
HOST = 'host8.dreamhack.games'
PORT = 20204
def xor(a, b):
return bytes([x ^ y for x, y in zip(a, b)])
r = remote(HOST, PORT)
r.recvuntil(b'enc(secret) = ')
secret_enc = bytes.fromhex(r.recvline().strip().decode())
def decrypt(payload_bytes):
r.sendlineafter(b"[1] encrypt, [2] decrypt: ", b'2')
r.sendlineafter(b"Input ciphertext to decrypt in hex: ", payload_bytes.hex().encode())
r.recvuntil(b'dec(ciphertext) = ')
plaintext = bytes.fromhex(r.recvline().strip().decode())
return plaintext
def encrypt(payload_bytes):
r.sendlineafter(b"[1] encrypt, [2] decrypt: ", b'1')
r.sendlineafter(b"Input plaintext to encrypt in hex: ", payload_bytes.hex().encode())
C0 = b'\x00' * 16
P0 = decrypt(C0)
C1 = b'\x11' * 16
P1 = decrypt(C1)
Cx = xor(secret_enc, C1)
Px = decrypt(Cx)
secret = xor(xor(Px, P1), P0)
encrypt(secret)
r.interactive()

'Cryptography > Cryptography CTF' 카테고리의 다른 글
| [Dreamhack] Textbook-CBC (0) | 2025.11.17 |
|---|---|
| [Dreamhack] No shift please! (0) | 2025.11.07 |
| [Dreamhack] chinese what? (0) | 2025.10.09 |
| [Dreamhack] Exploit Tech: Meet-in-the-middle Attack (0) | 2025.10.09 |
| [Dreamhack] flag-shop (0) | 2025.10.09 |