Summary: Standard byte-by-byte ECB oracle decryption.

Challenge Prompt

totallyfoolproofcrypto
Cryptography

Solves (7) - 884 Points

In hindsight, rolling my own crypto was a rather stupendous stroke of stupidity. I'll be switching to a well-known, verified library to fix this.

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

with open("flag", 'rb') as f: flag = f.read().strip()
key = os.urandom(16)

while 1:
    pt = input('> ').encode()
    padded = pad(pt+flag, AES.block_size)
    cipher = AES.new(key, AES.MODE_ECB)
    print(cipher.encrypt(padded).hex())
nc challs.sieberrsec.tech 31311

A first blood prize of one (1) month of Discord Nitro is available for this challenge.

Some amount of "bruteforce" will be necessary -- and hence legal -- for this challenge.

Solution

This challenge is a pretty standard byte-by-byte ECB oracle challenge. For an illustrated writeup, please see this excellent article by c0nrad.

First, let’s identify the maximum possible number of blocks comprising the flag. We can do this by sending an empty prefix and getting a sample encrypted output.

nc challs.sieberrsec.tech 31311
>
d4037cd10db2222f01cf737f8f08353c7879f5d8932dfea5916d8ea68e943681b5eed8d24350b43ae5c1be9f26e58b90
>

Some quick math tells us that the likely number of blocks is 3.

In [243]: len('d4037cd10db2222f01cf737f8f08353c7879f5d8932dfea5916d8ea68e943681b5eed8d24350b43ae5c1be9f26e58b90')/2/16
Out[243]: 3.0

We can start off with an initial trial of A characters of length 3 * 16 and iterating through possible candidates (restricted to just a subset of printable bytes) to obtain the flag byte-by-byte with the following script:

#!/usr/bin/env python

import string
from pwn import *

# context.log_level = 'debug'

max_blocks = 3
block_size = 16

def main():
    p = remote('challs.sieberrsec.tech', 31311)

    max_size_secret = max_blocks * block_size
    secret = b''

    for size in range((max_size_secret - 1), -1, -1):
        # Original
        origin = b"A" * size
        p.recvuntil(b'> ')
        p.sendline(origin)
        original_blocks = p.recvline().strip()

        tmp = b"A" * size + secret
        for character in string.printable[:95]:
            cur_line = tmp + character.encode()
            p.recvuntil(b'> ')
            p.sendline(cur_line)
            candidate_blocks = p.recvline().strip()
            if original_blocks[:max_size_secret * 2] == candidate_blocks[:max_size_secret * 2]:
                secret += character.encode()
                log.info('{}'.format(secret.decode()))
                break

        if len(secret) + size <= max_size_secret - 1:
            log.success('Flag: {}'.format(secret.decode()))
            return


if __name__ == '__main__':
    main()

Running the script gives us the flag:

$ python solve.py
[+] Opening connection to challs.sieberrsec.tech on port 31311: Done
[*] I
[*] IR
[*] IRS
[*] IRS{
[*] IRS{w
[*] IRS{w0
[*] IRS{w0w
[*] IRS{w0w_
[*] IRS{w0w_w
[*] IRS{w0w_wh
[*] IRS{w0w_wh@
[*] IRS{w0w_wh@t
[*] IRS{w0w_wh@t_
[*] IRS{w0w_wh@t_a
[*] IRS{w0w_wh@t_an
[*] IRS{w0w_wh@t_an_
[*] IRS{w0w_wh@t_an_0
[*] IRS{w0w_wh@t_an_0r
[*] IRS{w0w_wh@t_an_0ri
[*] IRS{w0w_wh@t_an_0rig
[*] IRS{w0w_wh@t_an_0rig1
[*] IRS{w0w_wh@t_an_0rig1n
[*] IRS{w0w_wh@t_an_0rig1na
[*] IRS{w0w_wh@t_an_0rig1nal
[*] IRS{w0w_wh@t_an_0rig1nal_
[*] IRS{w0w_wh@t_an_0rig1nal_p
[*] IRS{w0w_wh@t_an_0rig1nal_pr
[*] IRS{w0w_wh@t_an_0rig1nal_pr0
[*] IRS{w0w_wh@t_an_0rig1nal_pr0b
[*] IRS{w0w_wh@t_an_0rig1nal_pr0bl
[*] IRS{w0w_wh@t_an_0rig1nal_pr0bl3
[*] IRS{w0w_wh@t_an_0rig1nal_pr0bl3m
[*] IRS{w0w_wh@t_an_0rig1nal_pr0bl3m}
[+] Flag: IRS{w0w_wh@t_an_0rig1nal_pr0bl3m}

Flag: IRS{w0w_wh@t_an_0rig1nal_pr0bl3m}

Leave a Comment