Skip to main content

[Crypto] Multipage recycling

In this Crypto challenge, we are given 2 files, a python script and a text file, nammed output.text.

source.py

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

FLAG = b'HTB{??????????????????????}'


class CAES:

    def __init__(self):
        self.key = os.urandom(16)
        self.cipher = AES.new(self.key, AES.MODE_ECB)

    def blockify(self, message, size):
        return [message[i:i + size] for i in range(0, len(message), size)]

    def xor(self, a, b):
        return b''.join([bytes([_a ^ _b]) for _a, _b in zip(a, b)])

    def encrypt(self, message):
        iv = os.urandom(16)

        ciphertext = b''
        plaintext = iv

        blocks = self.blockify(message, 16)
        for block in blocks:
            ct = self.cipher.encrypt(plaintext)
            encrypted_block = self.xor(block, ct)
            ciphertext += encrypted_block
            plaintext = encrypted_block

        return ciphertext

    def leak(self, blocks):
        r = random.randint(0, len(blocks) - 2)
        leak = [self.cipher.encrypt(blocks[i]).hex() for i in [r, r + 1]]
        return r, leak


def main():
    aes = CAES()
    message = pad(FLAG * 4, 16)

    ciphertext = aes.encrypt(message)
    ciphertext_blocks = aes.blockify(ciphertext, 16)

    r, leak = aes.leak(ciphertext_blocks)

    with open('output.txt', 'w') as f:
        f.write(f'ct = {ciphertext.hex()}\nr = {r}\nphrases = {leak}\n')


if __name__ == "__main__":
    main()

output.txt

ct = bc9bc77a809b7f618522d36ef7765e1cad359eef39f0eaa5dc5d85f3ab249e788c9bc36e11d72eee281d1a645027bd96a363c0e24efc6b5caa552b2df4979a5ad41e405576d415a5272ba730e27c593eb2c725031a52b7aa92df4c4e26f116c631630b5d23f11775804a688e5e4d5624
r = 3
phrases = ['8b6973611d8b62941043f85cd1483244', 'cf8f71416111f1e8cdee791151c222ad']

Analysis

We see that the algorithm used for encryption is AES mode ECB, encryption will happen by 16 bytes block, and we also know that, if we run the script once, with the same key, the same block of byte will generate the same encrypted output.

First step, the flag is concatenated with itself several times, and padded to a multiple of 16 bytes. For the encryption, that message will be split in block of 16 bytes.

For analysing what’s happening, let’s assume the flag is HTB{absdefghujklmnopqrstuv}, the message, split by block of 16 bytes will be:

HTB{absdefghujkl
mnopqrstuv}HTB{a
bsdefghujklmnopq
rstuv}HTB{absdef
ghujklmnopqrstuv
}HTB{absdefghujk
lmnopqrstuv}\x04\x04\x04\x04

let’s name those block, block1, block 2, …

HTB{absdefghujkl <-- block1
mnopqrstuv}HTB{a <-- block2
bsdefghujklmnopq <-- block3
rstuv}HTB{absdef <-- block4
ghujklmnopqrstuv <-- block5
}HTB{absdefghujk <-- block6
lmnopqrstuv}\x04\x04\x04\x04 <-- block7

For the encryption, we start with an 16 bytes IV, and by looking at the algorithm we find the following:

encrypted_block1 :  block1 XOR aes_encrypt_ecb(IV)
encrypted_block2 :  block2 XOR aes_encrypt_ecb(encrypted_block1)
encrypted_block3 :  block3 XOR aes_encrypt_ecb(encrypted_block2)
encrypted_block4 :  block4 XOR aes_encrypt_ecb(encrypted_block3)
encrypted_block5 :  block5 XOR aes_encrypt_ecb(encrypted_block4)
encrypted_block6 :  block6 XOR aes_encrypt_ecb(encrypted_block5)
encrypted_block7 :  block7 XOR aes_encrypt_ecb(encrypted_block6)

Now if we look at the leak function, we see that it will take 2 consecutives 16 blocks of the encrypted message starting at a random index, and return us their encrypted value, with the same key as the message encryption.

Let’s pick r = 3, the leack function will return us:

aes_encrypt_ecb(encrypted_block3) <-- leak3
aes_encrypt_ecb(encrypted_block4) <-- leak4

If we look at both what we know of the encrypted message and the leaked blocks, we have

encrypted_block4 :  block4 XOR aes_encrypt_ecb(encrypted_block3)
encrypted_block5 :  block5 XOR aes_encrypt_ecb(encrypted_block4)

where encrypted_block4 and encrypted_block5 are known (from the cipher test)

and we also have access to the value of aes_encrypt_ecb(encrypted_block3) and aes_encrypt_ecb(encrypted_block4) (nammed leak3 and leak4 for convenience).

with some substition, we have:

encrypted_block4 = block4 XOR leak3

and

encrypted_block5 = block5 XOR leak4

Properties of XOR allow us to deduct that:

block4 = encrypted_block4 XOR leak3
block5 = encrypted_block5 XOR leak4

Solution code:

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

class CAES:

    def __init__(self):
        self.key = os.urandom(16)
        self.cipher = AES.new(self.key, AES.MODE_ECB)

    def blockify(self, message, size):
        return [message[i:i + size] for i in range(0, len(message), size)]

    def xor(self, a, b):
        return b''.join([bytes([_a ^ _b]) for _a, _b in zip(a, b)])

    def encrypt(self, message):
        iv = os.urandom(16)

        ciphertext = b''
        plaintext = iv

        blocks = self.blockify(message, 16)
        for block in blocks:
            ct = self.cipher.encrypt(plaintext)
            encrypted_block = self.xor(block, ct)
            ciphertext += encrypted_block
            plaintext = encrypted_block

        return ciphertext

    def leak(self, blocks):
        r = random.randint(0, len(blocks) - 2)
        leak = [self.cipher.encrypt(blocks[i]).hex() for i in [r, r + 1]]
        return r, leak

def byte_xor(ba1, ba2):
    return bytes([_a ^ _b for _a, _b in zip(ba1, ba2)])

def main():
    aes = CAES()
    ct = 'bc9bc77a809b7f618522d36ef7765e1cad359eef39f0eaa5dc5d85f3ab249e788c9bc36e11d72eee281d1a645027bd96a363c0e24efc6b5caa552b2df4979a5ad41e405576d415a5272ba730e27c593eb2c725031a52b7aa92df4c4e26f116c631630b5d23f11775804a688e5e4d5624'
    ciphertext = bytes.fromhex(ct)
    ciphertext_blocks = aes.blockify(ciphertext, 16)

    print(ciphertext_blocks)
    print(byte_xor(ciphertext_blocks[4], bytes.fromhex('8b6973611d8b62941043f85cd1483244')))
    print(byte_xor(ciphertext_blocks[5], bytes.fromhex('cf8f71416111f1e8cdee791151c222ad')))

if __name__ == "__main__":
    main()

From the execution, we have both halves of the flag:

» python solve.py
b'_w34k_w17h_l34kz'
b'}HTB{CFB_15_w34k'

Let’s concatenate to have the flag: HTB{CFB_15_w34k_w34k_w17h_l34kz}