DownUnderCTF 2020 | Exra Cool Block Chaining

#!/usr/bin/env python3
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from Crypto.Util.strxor import strxor
from os import urandom

flag = open('./flag.txt', 'rb').read().strip()
KEY = urandom(16)
IV = urandom(16)

def encrypt(msg, key, iv):
    msg = pad(msg, 16)
    blocks = [msg[i:i+16] for i in range(0, len(msg), 16)]
    out = b''
    for i, block in enumerate(blocks):
        cipher = AES.new(key, AES.MODE_ECB)
        enc = cipher.encrypt(block)
        if i > 0:
            enc = strxor(enc, out[-16:])
        out += enc
    return strxor(out, iv*(i+1))

def decrypt(ct, key, iv):
    blocks = [ct[i:i+16] for i in range(0, len(ct), 16)]
    out = b''
    for i, block in enumerate(blocks):
        dec = strxor(block, iv)
        if i > 0:
            dec = strxor(dec, ct[(i-1)*16:i*16])
        cipher = AES.new(key, AES.MODE_ECB)
        dec = cipher.decrypt(dec)
        out += dec
    return out

flag_enc = encrypt(flag, KEY, IV).hex()

print('Welcome! You get 1 block of encryption and 1 block of decryption.')
print('Here is the ciphertext for some message you might like to read:', flag_enc)

try:
    pt = bytes.fromhex(input('Enter plaintext to encrypt (hex): '))
    pt = pt[:16] # only allow one block of encryption
    enc = encrypt(pt, KEY, IV)
    print(enc.hex())
except:
    print('Invalid plaintext! :(')
    exit()

try:
    ct = bytes.fromhex(input('Enter ciphertext to decrypt (hex): '))
    ct = ct[:16] # only allow one block of decryption
    dec = decrypt(ct, KEY, IV)
    print(dec.hex())
except:
    print('Invalid ciphertext! :(')
    exit()

print('Goodbye! :)')

AES 。なにやら独自の暗号化モードを実装している。

encryptは E(m_i) \oplus E(m_{i-1}) \oplus IV

decryptは  D(c_i \oplus c_{i-1} \oplus IV)

暗号化、復号は1ブロックのみ、1度ずつ許可されている状態で、暗号化したフラグが渡されるのでどうにかしようという問題。KEY, IVが接続毎に変わるので大変だぁ

ここで暗号化時にPKCS#7パディングが用いられていることを利用して\x10 * 16を暗号化してみる

すると暗号化される平文は m_0 = m_1 m_1の部分はパディングで追加された \x00 *16)になるので

 C_0 = E(m_0) \oplus IV

 C_1 = E_(m_1) \oplus E(m_0) \oplus IV = IV

として IVを求めることができる

 IVがわかっているならフラグの暗号文の C_0は簡単に復号できるし、 E(m_i) \oplus E(m_{i-1}) \oplus IVを送れば m_iが復号できる

from ptrlib import Socket, xor

cnt = 0
flag = b""
while True:
    sock = Socket("chal.duc.tf", 30201)
    ciphertext = bytes.fromhex(sock.recvlineafter(":").decode())

    sock.sendlineafter(": ", (b"\x10"*16).hex())
    iv = bytes.fromhex(sock.recvline().decode())[16:]

    if cnt == 0:
        c = ciphertext[:16]
    else:
        c = xor(ciphertext[cnt*16:(cnt+1)*16], ciphertext[(cnt-1)*16:cnt*16])
        c = xor(c, iv)

    sock.sendlineafter(": ", c.hex())
    flag += bytes.fromhex(sock.recvline().decode())

    print(flag)


    if 16*(cnt+1) == len(ciphertext):
        break

    cnt+=1
    sock.close()