Lockpick 3.0 is a Hard-rated HackTheBox forensics challenge. You are given a disk image of a Windows system that has been hit by ransomware. Five flags are hidden across encrypted files, the registry, and memory artefacts. The encryption is not
as strong as it looks.
Initial image mounting and triage
# Mount the image read-only sudo mkdir /mnt/lockpick sudo mount -o ro,loop,offset=$((512*2048)) lockpick.img /mnt/lockpick # Check for the ransomware binary find /mnt/lockpick -name "*.exe" -newer /mnt/lockpick/Windows/System32/notepad.exe 2>/dev/null # Returns: /mnt/lockpick/Users/victim/AppData/Local/Temp/svchost32.exe
The binary name is a classic masquerading technique — naming malware after a legitimate Windows process. The real svchost.exe lives in System32 and is never in AppData.
Identifying the packer
$ die svchost32.exe PE32: compiler: Python 3.x (PyInstaller) PE32: packer: PyInstaller 5.x
PyInstaller packages Python scripts into standalone executables. The Python bytecode is extractable.
# Extract with pyinstxtractor python3 pyinstxtractor.py svchost32.exe # Creates: svchost32.exe_extracted/ # Find the main script ls svchost32.exe_extracted/ # svchost32.pyc PYZ-00.pyz ... # Decompile the bytecode pip install uncompyle6 uncompyle6 svchost32.exe_extracted/svchost32.pyc > svchost32.py
Analysing the encryption
The decompiled source reveals the key derivation:
import os, hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
VICTIM_ID = open("C:\ProgramData\victim_id.txt").read().strip()
def get_key_iv(victim_id):
key = hashlib.sha256(victim_id.encode()).digest() # 32 bytes
iv = hashlib.md5(victim_id.encode()).digest() # 16 bytes
return key, iv
def encrypt_file(filepath):
key, iv = get_key_iv(VICTIM_ID)
cipher = AES.new(key, AES.MODE_CBC, iv)
with open(filepath, "rb") as f:
data = f.read()
encrypted = cipher.encrypt(pad(data, AES.block_size))
with open(filepath + ".locked", "wb") as f:
f.write(encrypted)
os.remove(filepath)
The victim ID is stored in plaintext at C:\ProgramData\victim_id.txt and also in the ransom note. The key is fully deterministic from that value.
Recovering the victim ID and decrypting
cat /mnt/lockpick/ProgramData/victim_id.txt
# VIC-8821-KXZP
# Decrypt all .locked files
python3 << EOF
import hashlib, os, glob
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
victim_id = "VIC-8821-KXZP"
key = hashlib.sha256(victim_id.encode()).digest()
iv = hashlib.md5(victim_id.encode()).digest()
for locked in glob.glob("/mnt/lockpick/Users/victim/Documents/*.locked"):
with open(locked, "rb") as f:
data = f.read()
cipher = AES.new(key, AES.MODE_CBC, iv)
try:
plain = unpad(cipher.decrypt(data), AES.block_size)
outpath = locked.replace(".locked", "")
with open(outpath, "wb") as f:
f.write(plain)
print(f"Decrypted: {outpath}")
except Exception as e:
print(f"Failed {locked}: {e}")
EOF
Flags 1-4 are embedded in the decrypted document contents as HTB{…} strings.
Flag 5: registry persistence
# Extract the NTUSER.DAT hive cp /mnt/lockpick/Users/victim/NTUSER.DAT /tmp/ # Parse with regipy pip install regipy registry-explorer /tmp/NTUSER.DAT -p "Software\Microsoft\Windows\CurrentVersion\Run" # Output: # Key: Software\Microsoft\Windows\CurrentVersion\Run # Value: WindowsUpdate # Data: C:\ProgramData\svchost32.exe /silent # Modified: 2024-01-15 03:22:41
The persistence key data contained a base64-encoded string appended after the binary path. Decoding it gave Flag 5.
Forensic lesson
The critical flaw in this ransomware is using the victim ID as the sole source of entropy for key derivation. A proper implementation would generate a random symmetric key, encrypt it with an asymmetric public key (keeping the private key on the
attacker’s server), and discard the symmetric key after encryption. Without the attacker’s private key, decryption would be impossible. Deterministic key derivation from publicly visible material makes recovery trivial once you understand the
scheme.