Task 9 of NSA Codebreaker 2022
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
andutil.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
.
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.
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
.
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:
- The user enters a 32 character encryption key via the console
- The key is converted to hexcode (which doubles the size to 64)
- The last 32 characters are stripped
- The key is added to the environmental variables list
- Searches for any documents ending in
pdf
,doc
,docx
,xls
,xlsx
,ppt
, andpptx
- Generates a random 16 character hex value
- Adds the IV to the beginning of the output file
- Creates an encrypted file using the IV, key, and CBC AES 128 encryption
- 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.
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.
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.
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!
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!!!
When we type in the value of tC33i1RGlEBlSCJyZo3WafhEMGY2QQjM
from the PDF, we get a success banner for our final task!!!