ASC - Wargames 2020 Filtration Phase

This is my writeup and walkthrough for the Challenge2 crypto from the competition

August 15, 2020 - 7 minute read -
Security ASC-Wargames CTF Infosec Python Hacking Cryptography cracking

This is the Qualification phase for ASC War Games at Egypt. There was 5 crypto challenges in the competition and I’ll go through the Challenge2, the most interesting one for me.

ASC

Analyzing the challenge

The challenge give us 2 files challenge.py script contains the encryption algorithm and output.txt. the output.txt file contains temp and cipher variables.

temp = 37575548727135643944546138552f432b4748447a6d465a3954304c642b6d71455968774b4649674950704868714552547767682b503036776b4d6e475a615036494539503166796470536744312f53477978625030667a553373306874313478306e49324c7453375851424a5162646b33544e4f776c6737704148664e7a4b7a6859797550436b70634f7566554b485976787667386b506f526374457747493556786d624c43753631525072326a456f613054346c4c494661614b4252663773454a6674546f37496...
cipher = 37642f4574474258656752564f3437555667442b6d71684d2b5a3052756147734b5674526264567a727466434f676a74794767556e765a6f6e756b4652484535

So, let’s look at the script file that was given.

from Crypto.Cipher import AES
import base64
from string import printable
import random

BLOCK_SIZE = 16
pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * chr(BLOCK_SIZE - len(s) % BLOCK_SIZE)
unpad = lambda s: s[:-ord(s[len(s) - 1:])]

def encrypt(plain, key):
    plain = pad(plain)
    cipher = AES.new(key, AES.MODE_ECB)
    return base64.b64encode(cipher.encrypt(plain))

def generate_random():
    letters = printable
    temp = []
    for i in range(4):
        temp.append(''.join(random.choice(letters) for i in range(1)))
    return "".join(temp)

def choose_random(length=1):
    letters = printable
    a = ''.join(random.choice(letters) for i in range(length))
    b = ''.join(random.choice(letters) for i in range(length))
    b = a+b
    return b

all_keys = []
for i in range(8):
    all_keys.append(choose_random(1))

plain = base64.b64encode(open("temp.txt").read()).encode("hex")
c1 = encrypt(plain, all_keys[0]*8).encode("hex")
c2 = encrypt(c1, all_keys[1]*8).encode("hex")
c3 = encrypt(c2, all_keys[2]*8).encode("hex")
c4 = encrypt(c3, all_keys[3]*8).encode("hex")
c5 = encrypt(c4, all_keys[4]*8).encode("hex")
c6 = encrypt(c5, all_keys[5]*8).encode("hex")
c7 = encrypt(c6, all_keys[6]*8).encode("hex")
c8 = encrypt(c7, all_keys[7]*8).encode("hex")

random = generate_random()
c9 = encrypt(c8,random*4).encode("hex")
print "temp = ", c9

key = ""
for i in all_keys:
    key = key + i

flag = open("flag.txt").read()

cipher = encrypt(flag, key).encode("hex")
print "cipher = ",cipher

After looking at the script, It has 3 functions :

  • encrypt(plain, key): It’s just create AES instanse and return base64 of the encrypted text with a given key.
  • choose_random(length=1): This function return 2 bytes random string given a specified length or 1 random byte as a default value.
  • generate_random(): Same as the above but it just returns 4 bytes random string.

Then, the script generates 2 bytes keys, repeat that 8 times, and put them into a list of all_keys. After that, it performs 9 encryption operations on some data in temp.txt file.

So, the vulnerability here is it encrypts the hex value of the data not on the raw bytes itself and we can take advantage of that by checking the decryption upon every possible subkey of length 2 is hex values if so, then we got the correct subkey.

Note that:

  1. The last operation 9 it performs the same process but on the key of length 4, so let’s first get the 4 bytes key.
  2. All the keys that is being used are repeated to be 16 bytes long.

After analyzing the bruteforce search space we got len(printable) = 100, therefore 4 bytes string has 100*100*100*100 = 10^8 possible key values and 2 bytes string has 100*100 = 10000 possible values. So, it’s feasible to bruteforce the keys with the itertools library in python.

Cracking the 4 bytes key

I wrote some python script to get all the possible combination of the 4 bytes key and try them.

#!/usr/bin/env python3
from itertools import product
from Crypto.Cipher import AES
from string import printable
from output import temp, flag_enc
from base64 import b64decode

BLOCK_SIZE = 16
pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * bytes(BLOCK_SIZE - len(s) % BLOCK_SIZE)
unpad = lambda s: s[:-ord(s[len(s) - 1:])]

def is_hex(s):
    try:
        int(s, 16)
        return True
    except ValueError:
        return False

def decrypt(c, key):
    cipher = AES.new(key, AES.MODE_ECB)
    return unpad(cipher.decrypt(c))

def run(temp, rep):
    for p in product(printable,repeat=rep):
        key = ''.join(p)*(BLOCK_SIZE//rep)
        dec = decrypt(temp, key.encode())
        if is_hex(dec):
            dec = bytes.fromhex(dec.decode())
            print(f'[+] Key: {key[:rep]}')
            return key[:rep], dec

temp = b64decode(bytes.fromhex(temp))
key, C8 = run(temp, 4)

I got the first big key 099!099!099!099! and now the value C8 can be used to get Ci where 0 <= i <= 7 and C0 is the original temp plain text.

Cracking all the 2 bytes keys

Before running the following script I got a one byte value problem from the dec variable while decrypting the Ci. So let’s check if the decryption string is greater than 1 by editing the run function and put if is_hex(dec) and len(dec) > 1:

Ci = b64decode(C8)
all_keys = []
for i in range(8):
    subkey, C7 = run(Ci, 2)
    all_keys.append(subkey)
    Ci = b64decode(C7)

And now we got all the keys.

[+] Key: 7!
[+] Key: 33
[+] Key: _1
[+] Key: 1s
[+] Key: y_
[+] Key: ke
[+] Key: e_
[+] Key: th

The flag

Because we’ve been decrypting from C9, let’s reverse them and put them together as the final key to get the flag.

flag_key = ''.join(reversed(all_keys))
print(f'[+] Final Key: {flag_key}')
flag_enc = b64decode(bytes.fromhex(flag_enc))
flag = decrypt(flag_enc, flag_key.encode()).decode()
print(f'[+] Flag: {flag}')

Eventually, the flag is here.

[+] Final Key: the_key_1s_1337!
[+] Flag: ASCWG{Th1s_A3S_1s_N0T_S3cure_1337}