패딩
평문에 데이터를 붙여서 평문의 크기가 블록 크기의 배수가 되도록 만드는 과정을 패딩(Padding)이라고 한다. 패딩된 암호문을 복호화할 때는 복호화된 평문에서 패딩을 제거해야 원래의 평문을 얻을 수 있다. 따라서 패딩된 암호문의 수신자는 어떤 패딩이 적용됐는지 알아야 암호문을 제대로 복호화할 수 있다.
비트 패딩
비트 패딩(Bit padding)은 마지막 블록에서 평문이 채우지 못하는 비트 중 최상위 비트를 1로 설정하고, 나머지는 모두 0으로 채우는 패딩 기법이다.
수신자는 평문의 마지막 비트부터 처음으로 값이 1인 비트가 나올 때까지를 패딩으로 인식할 수 있으며 이를 제거하여 평문을 복구할 수 있다. 이를 막기 위해 비트 패딩을 적용할 때는 평문의 크기가 블록 크기의 배수이면, 패딩으로 한 블록을 추가한다.
바이트 패딩: ANSI X.923
바이트 패딩: PKCS#7
PKCS(Public-Key Cryptography Standard)는 공개키 암호의 표준 문서로서, 그중 7번째 문서인 PKCS#7은 AES와 같은 블록 암호의 패딩 기법을 제시하고 있다. PKCS#7에 소개된 패딩 기법은 추가할 패딩의 바이트 크기로 마지막 블록을 채우는 패딩 기법이다. 아래 예시는 PKCS#7으로 마지막 블록에 4바이트를 패딩한 것다.
ECB 모드
ECB(Electronic Codebook) 모드는 가장 간단한 운영모드로, 각 블록에 별도의 처리 과정을 거치지 않고 같은 키로 암호화 된다. ECB 모드에서는 각 블록이 독립적으로 암호화되므로 여러 블록을 병렬적으로 암호화할 수 있다는 장점이 있습니다. 그러나 ECB는 다른 운영모드에 비해 암호학적 안전성이 부족하다는 단점이 있다.
ECB 모드의 취약점
리플레이 공격(Replay attack)을 수행하는 것이 가능해집니다
약한 혼돈 성질
이미지의 한 픽셀을 한 블록으로 하여 ECB 암호화하면 같은 픽셀들은 모두 같은 픽셀로 암호화되므로, 원본 이미지의 패턴이 암호화된 이미지에도 그대로 나타난다.
재전송 공격
어떤 데이터를 재전송하여 시스템이 의도치 않은 행동을 하게 하는 것을 재전송 공격(Replay attack)이라고 한다. ECB 모드는 다음과 같은 방식으로 재전송 공격을 당할 수 있다. Alice가 'Alice sends 10000 USD to Bob'라는 28바이트 평문을 4바이트 블록 암호로 암호화한다.
CBC 모드
CBC(Cipher Block Chaining) 모드는 어떤 블록을 암호화하기 전에, 이 블록을 직전 블록의 암호문과 XOR합니다. 평문의 첫 번째 블록은 이전 블록이 존재하지 않으므로, 초기 벡터(Initialization Vector, IV)라고 불리는 임의의 데이터와 XOR합니다. 이 모드를 사용하면 각 블록이 서로의 암호화에 영향을 주므로 같은 블록도 전체 평문 및 IV에 따라 암호화 결과가 달라집니다.
송신자는 초기 벡터를 암호문의 0번째 블록 로 전송합니다. CBC 모드의 암, 복호화를 수학적으로 표현하면 다음과 같습니다.
초기 벡터는 일반적으로 논스(Nonce, number used only once)라는 무작위 값을 사용하며, 이는 공격자가 알아도 안전성에 영향을 끼치지 않습니다. 그러나 만약 공격자가 암호문을 중간에 가로채서 이를 조작할 수 있으면, 복호화 결과의 첫 번째 블록을 조작할 수 있습니다. 따라서 CBC 모드를 사용할 때는 초기 벡터의 무결성을 함께 보장해야 합니다.
CBC Bit-flipping attack
공격자가 초기 벡터를 원하는 값으로 조작하여 복호화된 평문의 첫 번째 블록을 조작하는 공격을 CBC Bit-flipping attack이라고 한다. 첫 번째 암호문 블록의 복호화 과정을 수식으로 표현하면 다음과 같다.
공격자는 초기 벡터를 로 조작하여 첫 번째 평문 블록을 으로 조작할 수 있다. 이는 아래의 수식으로 보일 수 있다.
CTR 모드
CTR(Counter) 모드는 블록 암호에 nonce와 평문 블록의 인덱스(counter)를 결합한 값을 입력한다. 암호문은 블록 암호의 출력과 평문 블록을 XOR 연산하여 생성한다. 평문에 정해진 값을 XOR 연산하여 암호문을 생성하는 일종의 스트림 암호다. CTR 모드의 암, 복호화를 수학적으로 나타내면 아래와 같습니다. 는 두 바이트 배열을 결합하는 기호입니다.
GCM 모드
GCM 운영 모드는 Authenticated encryption의 형식을 가진다. 태그를 생성하고, 태그가 있어여 복호화 진행이 가능하다. 아래 그림에서는 Auth Tag에 해당한다. 또한 Associated data라고 하는 추가 정보를 메시지 암호화 시 포함할 수 있다. 이 두 특징들을 합한 표현으로, GCM 운영 모드는 AEAD(Authenticated encryption with associated data)의 형식을 가집니다.
CBC 모드 복호화
CBC 운영 모드에서 복호화 기능이 주어지지만 복호화 결과는 확인할 수 없는 경우, 패딩을 제거하는 과정에서 발생하는 오류 여부를 통해 암호문을 복호화하는 공격을 Padding oracle attack이라고 한다. 먼저 특별히 CBC 모드만 가지는 특징을 살펴본 후, 공격에 대해 알아보겠다. 의 두 블록을 CBC 모드에 따라 복호화하면 다음의 두 블록을 얻을 수 있다. 여기서 는 CBC 모드에서 사용되는 논스, 는 키 를 이용하는 대칭키 암호에서의 한 블록을 복호화하는 함수다.
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모드의 암, 복호화 기능이 구현되어 있으며, AES.new(key, AES.MODE_CBC, key)에서 IV의 값과 키의 값이 동일한 것을 확인할 수 있다.
- 사용자에게 암호화 기능과 복호화 기능이 모두 주어져 있고, [3] Get Flag을 선택할 경우 플래그의 암호화 결과를 얻은 후 종료한다.
- 암, 복호화 기능으로 키를 알아낸 후 플래그의 암호문을 전송받아 로컬에서 복호화를 진행해야 한다
익스플로잇 설계
IV값을 도출하는 법은 아래와 같다.
IV를 암호화하여 다시 복호화 하는 방법은, 암호화 기능을 사용하여 평문으로 을 넣게 되면 과 를 XOR 연산한 결과를 암호화하게 된다. 첫 블록은 를 암호화한 결과인
임을 알 수 있다. 둘째 블록부터는 암호문의 이전 블록을 기반으로 복호화가 이루어지기 때문에 원하는 연산을 진행할 수 있다.
이것울 복호화하면 둘째 블록은
가 되어 IV와 동일한 값을 가진다. 패딩이 올바르지 않을 시 unpad 에러로 프로세스가 종료되니 을 암호화할 때 얻은 둘째 블록도 암호문의 셋째 블록으로 추가해야 한다.
IV의 XOR 연산을 이용하여 추출하는 방법은, 을 복호화할 경우 첫 블록과 둘째 블록은 어떤 값을 가지는지 계산해보는 걸로 보겠다. 첫 블록은
이러한 값을 가지고, 둘째 블록은
값을 가진다.
솔브 코드
from pwn import *
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
io = remote("host3.dreamhack.games", 9090)
def encrypt(pt):
io.sendline(b"1")
io.sendlineafter(b"(hex): ", bytes.hex(pt).encode())
return bytes.fromhex(io.recvline().decode())
def decrypt(pt):
io.sendline(b"2")
io.sendlineafter(b"(hex): ", bytes.hex(pt).encode())
return bytes.fromhex(io.recvline().decode())
def getflag():
io.sendline(b"3")
io.recvuntil(b"flag = ")
return bytes.fromhex(io.recvline().decode())
zeroblock = bytes(16)
METHOD = 1
# METHOD = 2
if METHOD == 1:
enc = encrypt(zeroblock)
key = decrypt(zeroblock + enc)[16:32]
else:
enc = encrypt(os.urandom(16))
dec = decrypt(zeroblock * 2 + enc)
key = xor(dec[:16], dec[16:32])
enc_flag = getflag()
flag = AES.new(key, AES.MODE_CBC, key).decrypt(enc_flag)
flag = unpad(flag, 16).decode()
print(flag)
chall.py
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()
- 암호화, 복호화 기능이 주어져 있고, secret의 암호화 결과를 알 수 있다. 암호문으로부터 secret의 값을 알아내면 플래그를 획득할 수 있다.
- 복호화 과정을 살펴면 try문을 사용하여 복호화 과정에서 에러가 발생할 시 "Invalid ciphertext."가 암호문에 저장된다. 올바르게 진행되어도 패딩 성공 여부만을 알 수 있다.
익스플로잇 설계
Padding Oracle Attack을 사용하면 임의의 16바이트 암호문을 복호화할 수 있다.
from pwn import *
from tqdm import trange
from Crypto.Util.Padding import unpad
# io = process(["python3", "chall.py"])
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
zeroblock = bytes(16)
enc_secret = encrypt("secret")[:16]
# TODO 2, Recovering Secret
submit(secret)
io.interactive()
enc_secret에 저장되어 있는 값은 아래와 같다.
0불록울 암호화한 결과의 첫 번째 블록은 다음과 같다.
이를 복호화하면 IV의 값을 얻을 수 있다.
솔브 코
from pwn import *
from tqdm import trange
from Crypto.Util.Padding import unpad
# io = process(["python3", "chall.py"])
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()