roaris / ctf-log

0 stars 0 forks source link

RTACTF 2023 : Reused-AES #72

Open roaris opened 2 months ago

roaris commented 2 months ago

https://alpacahack.com/challenges/reused-aes

roaris commented 2 months ago

AESのCFBモードが使われている

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import os

iv = os.urandom(16)
key = os.urandom(16)
FLAG = os.getenv("FLAG", "RTACTF{**** REDACTED ****}").encode()

def encrypt(data):
    cipher = AES.new(key, AES.MODE_CFB, iv)
    return cipher.encrypt(pad(data, 16))

if __name__ == '__main__':
    print(encrypt(FLAG).hex())
    print(encrypt(input("> ").encode()).hex())

平文ブロックを $P_0, P_1, P_2, \ldots$ 暗号文ブロックを $C_0, C_1, C_2, \ldots$ 初期化ベクトルを $IV$ AESによる暗号化を $E$とすると $P_0 \oplus E(IV) = C_0$ $P_1 \oplus E(C_0) = C_1$ $P_2 \oplus E(C_1) =C_2$ となるのがCFBモードだと思っていたのだが、そうとは限らなかった

https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38a.pdf のp12

In CFB encryption, the first input block is the IV, and the forward cipher operation is applied to the IV to produce the first output block. The first ciphertext segment is produced by exclusiveORing the first plaintext segment with the s most significant bits of the first output block. (The remaining b-s bits of the first output block are discarded.) The b-s least significant bits of the IV are then concatenated with the s bits of the first ciphertext segment to form the second input block. An alternative description of the formation of the second input block is that the bits of the first input block circularly shift s positions to the left, and then the ciphertext segment replaces the s least significant bits of the result.

$b$ はブロックサイズ(単位はbit)であり、この問題の場合は128である $s = 128$ の場合だと、 $P_0 \oplus E(IV) = C_0$ $P_1 \oplus E(C_0) = C_1$ $P_2 \oplus E(C_1) =C_2$ となるが、pycryptodomeのCFBモードの説明 https://pycryptodome.readthedocs.io/en/latest/src/cipher/classic.html#cfb-mode を読むと、 $s = 8$ になっていることが分かる

roaris commented 2 months ago

平文、暗号文を1バイトずつ区切って $P_0, P_1, P_2, \ldots$, $C_0, C_1, C_2, \ldots$ とする $P_0$ $\oplus$ ($E(IV)$ の先頭1バイト) $= C_0$ である 初期化ベクトルとAESの鍵が使いまわされているので、 $C_0$を与えれば $P_0$を得ることが出来る 同様に $P_0C_1$ を与えれば $C_0P_1$ を得ることが出来て $P_0P_1C_2$ を与えれば $C_0C_1P_2$ を得ることが出来る これを続けることで平文が分かる

roaris commented 2 months ago

しかし、この方針だとinput関数でUnicodeDecodeErrorが発生してしまう 与えるバイト列がutf-8として正しくないためである

なので $C_0$ ではなく 'a'を与えて、'a' $\oplus$ $C_0$ $\oplus$ ('a'を与えて得られる暗号文の先頭1バイト) で $P_0$を求めることにする

from pwn import *
from Crypto.Util.Padding import unpad

host, port = '34.170.146.252', 50604
byte_num = 32
flag = b''

for i in range(byte_num):
    io = remote(host, port)
    cipher1 = bytes.fromhex(io.recvline()[:-1].decode())
    io.sendline(flag + b'a')
    io.recvuntil(b'> ')
    cipher2 = bytes.fromhex(io.recvall()[:-1].decode())
    x = ord('a') ^ cipher1[i] ^ cipher2[i]
    flag += x.to_bytes()

flag = unpad(flag, 16).decode()
print(flag)