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.
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 = 37642f4574474258656752564f3437555667442b6d71684d2b5a3052756147734b5674526264567a727466434f676a74794767556e765a6f6e756b4652484535So, 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 = ",cipherAfter looking at the script, It has 3 functions :
encrypt(plain, key): It’s just createAESinstanse and returnbase64of 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:
- 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.
- 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: thThe 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}