WannaFly
(forensics, 100)
We have an image of an ext4 filesystem. It contains some pictures that have been encrypted and blurred by the ransomware WannaFly. Apart from the blurry pictures, there is a Python script called ...
and a .bash_history
file which shows that ...
was executed with the argument Hb8jnSKzaNQr5f7p
.
Looking at the Python script, we see that it transforms each input file in the following way:
- Load it as an image.
- Encrypt the original image using AES-CFB, using the key passed as an argument, and with an IV which is derived from the time.
- Blur the image and overlay a bird logo and some text.
- Overwrite the original file with the blurred image, a newline and the encrypted image in base64 format.
So to recover the original images, we need to extract the part of each file after the last newline, decode it as base64, and decrypt it with the known key and the IV.
The seed for deriving the IV is the current time when the script starts encrypting each file, rounded down to a whole second. That's going to be the timestamp of the file or a little before. The timestamps on the files are less than one second apart on average, so the time seed is likely to be exactly the file's timestamp or one second before.
#!/usr/bin/env python
import random, string, sys, os
from time import time
from Crypto.Cipher import AES
import base64
import textwrap
def get_iv(seed):
iv = ""
random.seed(seed)
for i in range(0,16):
iv += random.choice(string.letters + string.digits)
return iv
def encrypt(m, p):
iv=get_iv()
aes = AES.new(p, AES.MODE_CFB, iv)
return base64.b64encode(aes.encrypt(m))
def decrypt(m, p, i):
aes = AES.new(p, AES.MODE_CFB, i)
return aes.decrypt(base64.b64decode(m))
def encrypt_image(img):
data = open(img, 'r').read()
encrypted_img = encrypt(data, sys.argv[1])
blurred_img = open('/tmp/sha.png', 'r').read()
stat = os.stat(img)
with open(img, 'r+') as of:
of.write('\0' * stat.st_size)
of.flush()
open(img, 'w').write(blurred_img + "\n" + encrypted_img)
def load_file(file_name):
with open(file_name) as f:
data = f.read()
return data[data.rfind('\n')+1:]
def decrypt_file(password, encrypted_file_name):
basename = os.path.basename(encrypted_file_name)
mtime = os.stat(encrypted_file_name).st_mtime
ciphertext = load_file(encrypted_file_name)
for delta in [0,1]:
iv = get_iv(mtime - delta)
print iv
output_file_name = 'recovered-%d-%s' % (delta, basename)
out = open(output_file_name, 'wb')
out.write(decrypt(ciphertext, password, iv))
out.close()
if __name__ == '__main__':
if len(sys.argv) <= 1:
print "Usage: %s PASSWORD FILE..." % sys.argv[0]
else:
for encrypted_file in sys.argv[2:]:
decrypt_file(sys.argv[1], encrypted_file)
The seed time delta turns out to be 0 for all files except one. Looking at the decrypted images, in recovered-0-f09086061f03f080d0851d9154e11653.png
, we see some text over the pony image which is the flag: flag{ed70550afe72e2a8fed444c5850d6f9b}
.
Stack Overflow
(crypto, 100)
We have some broken encryption code in Python with PyCrypto. (Source)
from Crypto.Cipher import AES
...
crypto = AES.new(os.urandom(32), AES.MODE_CTR, counter=lambda: secret)
What a badly-designed API — writing broken crypto code shouldn't be that easy! This code is using CTR with a constant counter. This means that there is a 16-byte string m = AES_enc(key, secret)
such that each 16-byte block b[i]
of the file is encrypted as b[i] xor m
. All we have to do to decrypt the file is guess 16 consecutive bytes of it, which will give us the mask to xor with.
We start by guessing that the file begins with %%PDF-1.
, which gives us the first 8 bytes of the 16-byte mask. Then look at the partially-decrypted output, find partially-corrupted PDF operators such as /Contents
, /MediaBox
, etc. and work out the remaining bytes in the mask to rectify those operators.
#!/usr/bin/env perl
use warnings;
use strict;
my $guess1 = "%PDF-1.3 \n1 0 ob";
sysread STDIN, $_, 16;
my $mask = $_ ^ $guess1;
do {
print STDOUT ($_ ^ $mask)
} while (sysread STDIN, $_, 16);
ONE DOES NOT SIMPLY COPY CRYPTO CODE FROM STACK OVERFLOW FLAG{15AD69452103C5DF1CF61E9D98893492}
Secure Login
(crypto, 200)
There is a server which responds to signed commands. The command we want is 2: “Collect flag”. This command requires a signed ticket whose plaintext starts with ticket:admin|root|
.
A signed ticket is generated by encoding the ticket string as an integer $m$ and calculating $m^d \bmod n$ where $d$ is a secret. In other words, this is textbook RSA, and we have the public key only. It's a 2048-bit key with $e=65537$.
We can obtain the signature of messages in two forms:
- Messages that start with the string
ticket:user|
and contain another |
, using command 1 “Register yourself as a user”.
- Messages that start with the byte 0xff, using command 3 “Sign a message”.
We want to obtain the signature of a message that starts with ticket:admin|root|
, i.e. a message of the form $0x7469636b65743a61646d696e7c726f6f747c \times 2^{8b} + a$ where $a < 2^{8b}$.
Let $u = $ticket:user||
and $v = 0xffffffffffffffd3e3bae71d30e6eac5d3ad$. Then $u \times v = $ticket:admin|root|l\xb3\xf0m\xca1\x16I\xe4\x0edS\xcc
.
Use command 1 on an empty user name and full name to get $u^d$. Use command 2 on ffffffffffffd3e3bae71d30e6eac5d3ad
($v$ without the leading 0xff) to get $v^d$. Multiply them; the result is
0x6e08962dabfd99df74dcff5f0685be2950fa030a2f3ea59f8f836f80eca71aee19027bdee3e0d7828f3b651efc1b21878254d6d69aa8776405d4bd3f88d9cdda3b538dc2fec25bbd3974287187adde132e389ec7a3b468d499a768967fa390e9b0b1712112d131940032b32c31c19c095f36277fd1b18b21ec04869f526f6eb405262d4395f24c374a4450bb095c9f764d2c582627feb2da6576e6652afe8018533c414276303fb50881d848ce73f404907e00db220e040ff81b658a4cdc9233ea379c2538c6fa88f95643b32dbf8af6fc54feaa25bddf4c76361af2d2fbd8fb3895f08bd6c177b79c702c24631c206dbee19b5f924b521ff937708355e4041abc2f0f73b80e6187a5b1cf68cc9b4b06cfb9caeb1cb03823e0f409a751f5ef68a5978c32522bafd6d7b9751c7e4d63faa608847dc461def7078d889ee02a92271d21fbb9cf602ee15937b1df87e36ad7f36e38af5ddb63ec074b33293d9ee6f1356083da7143149a3f49a1798f146c2d22a390f337ffe24d191ea5ac4b8107651a9d2af6ba2d37092b40be0d51842a5a6c91ca74997b995c40955480fce657a352186a8b98027c6e7bf67441ccf62f10fd9ea2569c4a823b43462f7bdaa1c83a561e0152a584dd8b737c69ee9f2d3ef0fa480e40a06555bb89ae60bd266395369f5c4aa5ae5aee936e4312a53bb33ff944e01c35078bea27b3a28f38a76be7c
Enter that at command 2 without the leading 0x
to get the flag
Here you go!
flag{8f898e19de410591acbcdbfae798d603}