Python Loader Evolution: Five Encryption Generations
Every RAT in this campaign -- nine families across six parallel attack chains -- entered through the same pipeline. A batch file downloads a ZIP from a disposable Cloudflare tunnel. The ZIP contains a Python runtime and one or more loader scripts. Each loader decrypts embedded shellcode, and that shellcode bootstraps the .NET Common Language Runtime (CLR) to load the actual payload. The RATs themselves barely changed over 176 days. The delivery pipeline evolved five times.
This post is the fifth and final entry in the SERPENTINE#CLOUD (tracked by Securonix, June 2025) breach analysis series. Prior posts covered the inner layers: PureLogs (the infostealer), PureCrypter (the .NET 3DES loader), Violet RAT (the 120-command dispatcher), and Remcos (AutoIt persistence and banking fraud). This one covers the outer layers -- batch download, Python loader, Donut shellcode, then .NET handoff.
Intelligence refresh (2026-02-26)
We re-validated the Dec17 and Feb1 stager URL mapping from decoded batch scripts and refreshed sandbox coverage for representative Python loaders from the Nov10, Nov19, and Nov24 sets.
| Artifact | SHA-256 | VT | Tria.ge |
|---|---|---|---|
| BKSNOLazyNov10_Viooooooo.py | c9dc14435857fd00e3f81e219c362c4dc235774dea08fb49fca874dfa108cdd2 | Not indexed | https://tria.ge/260225-syqahsb15d (score 3) |
| 1vio-obf.py (EMP Kramer) | 97e71a0347439a169a3278ca85c11f28fe2098c42f7513e2621c0a3f3258b7f2 | Not indexed | https://tria.ge/260225-syq7tab15f (score 3) |
| 1uunov24.py | ced8796ae647584824c9907d0513f2d2f4b63f9b5bb9cf713e5a59a777335824 | 1/76 | https://tria.ge/260225-syrtcab15g (score 3) |
The delivery stack
Batch stager (Cloudflare tunnel download)
-> Python loader (5 encryption generations)
-> Donut shellcode (Chaskey CTR, AMSI bypass)
-> PureCrypter (.NET 3DES loader) -- covered in post 2
-> Inner RAT -- covered in posts 1, 3, 4
Seven waves
| Wave | Date | Encryption | Execution | Obfuscation | Donut | Loaders |
|---|---|---|---|---|---|---|
| 1 | Oct 4--5 | Double-XOR (16B keys) + RC4 (8-char) | APC into notepad.exe / in-process CFUNCTYPE | Junk variables, readable | v0.9.2 (x86+x64) | 4 |
| 2 | Nov 10 | AES-256-CBC + dual XOR (16B keys) | Early Bird APC into explorer.exe | None (plaintext .py) | v0.9.2 (x64) | 7 |
| 3 | Nov 10 | AES-256-CBC + dual XOR (different keys) | Early Bird APC into explorer.exe | None (plaintext .py) | v0.9.2 (x64) | 7 |
| 4 | Nov 19 | RC4 (8-char key), Kramer .pyc | In-process CFUNCTYPE | Kramer bytecode obfuscator | v0.9.3 (x64) | 5 |
| 5 | Nov 19--20 | RC4 (8-char key), Kramer .pyc | In-process CFUNCTYPE | Kramer bytecode obfuscator | v0.9.3 (x64) | 10 |
| 6 | Dec 17 | Same as Wave 1 (redeployed) | Same as Wave 1 | Same as Wave 1 | v0.9.2 | 2 |
| 7 | Dec 16 | Mixed AES+XOR and RC4 | Mixed APC and in-process | Mixed plaintext and Kramer | Mixed | 13 |
Seven waves, 48 loader files, the same five RAT families repackaged over and over. Each wave either upgrades the encryption, changes the injection method, or both.
Wave 1 (October 5) -- XOR + RC4
The first deployment used two distinct loader architectures. All four files deliver the same .NET payload (Fviwknzr.exe, the PureCrypter loader analyzed in post 2). Python version: 3.14 (pre-release at time of deployment) -- the only wave to use it. All later waves drop to 3.12.
Type A: MOF loaders (double-XOR + process injection)
Two files (Oct05_MOF.py, Start_Oct05_MOF.py), each ~841 KB. Shellcode is XOR-encrypted with two 16-byte keys, then base64-encoded:
base64_decode -> XOR(key2) -> XOR(key1) -> raw shellcode
| File | XOR Key 1 | XOR Key 2 |
|---|---|---|
Oct05_MOF.py | 896a01e960d97a791133c51c7bf6629a | 2faa926e690a4fd4e7c5c1acbb910a28 |
Start_Oct05_MOF.py | d69d1f100b74ffa8bcc8eae2394e1bfd | 4079c58306a2e15568bb2c006fa65eb5 |
Injection technique: create a suspended notepad.exe, allocate RWX memory, write shellcode via WriteProcessMemory, queue an APC, resume the thread. The code is 151 lines, well-structured, with readable variable names and full ctypes structure definitions. A comment in the source reads: "CHANGE 2: Use the process argument from mof.py as the default here". This is development-stage code.
Type B: Python loaders (RC4 + junk code)
Two files (Oct05_pyt.py, Start_Oct05_Python.py), each ~820 KB. RC4 with 8-character keys:
| File | RC4 Key |
|---|---|
Oct05_pyt.py | xNuJ5Z75 |
Start_Oct05_Python.py | 0ADNeZBG |
Instead of process injection, these decrypt in-place and execute via ctypes.CFUNCTYPE -- shellcode runs directly in the Python process. Variable names are randomized 64-character strings with dead-code conditional blocks scattered throughout, but the RC4 implementation underneath is standard Key Scheduling Algorithm (KSA) + Pseudo-Random Generation Algorithm (PRGA).
The full chain (Wave 1)
Python (XOR or RC4)
-> Donut shellcode (Chaskey CTR, 16 rounds)
-> Fviwknzr.exe (.NET 3DES-CBC, 588,288 bytes)
-> Jaglt resource (AES-encrypted + GZip-compressed)
-> Qdjlj.dll (PureLogs, ConfuserEx-protected, protobuf C2)
Waves 2--3 (November 10) -- AES-256-CBC + dual XOR
14 plaintext Python loaders deployed across two staging directories (3D0bj for Wave 2/BKSNO, 3Dadu for Wave 3/BKNO). All use an identical 172-line template. Python version: 3.12.
The encryption scheme gets its first upgrade:
Encryption: XOR(key1) -> XOR(key2) -> AES-256-CBC(key, iv)
Decryption: base64_decode -> extract IV (first 16 bytes)
-> AES-256-CBC decrypt (PyCryptodome)
-> XOR(xor_key2) -> XOR(xor_key1) -> raw Donut shellcode
The injection method also changes. Instead of notepad.exe, the loaders now create a suspended explorer.exe and use Early Bird APC injection -- queuing the APC before the first instruction executes. Explorer blends in better than Notepad as a long-running process.
Seven RAT families deployed simultaneously per directory:
| Loader | RAT | Shellcode Size |
|---|---|---|
BKSNOLazyNov10_Anacrack.py | DcRat | 103,186 B |
BKSNOLazyNov10_Annorii.py | DcRat | 103,186 B |
BKSNOLazyNov10_Anoriihv.py | PureHVNC | 101,138 B |
BKSNOLazyNov10_Asyncccc.py | AsyncRAT | 84,754 B |
BKSNOLazyNov10_Ploggggggg.py | PureLogs | 629,522 B |
BKSNOLazyNov10_Venommm.py | VenomRAT | 103,186 B |
BKSNOLazyNov10_Viooooooo.py | Violet RAT | 1,464,594 B |
Wave 3 (BKNOLazy) carries the same seven payloads re-encrypted with different keys. Shellcodes are byte-identical to Wave 2 -- redundant deployment to two directories.
Each loader has unique AES-256 keys, unique XOR keys, and unique IVs. All Wave 2/3 payloads are delivered through Donut v0.9.2 directly to .NET assemblies in memory. The scripts are still plaintext and easy to read. This is the transition stage before the Kramer shift.
Waves 4--5 (November 19) -- Kramer obfuscator
The biggest architectural shift in the campaign. Files are named .py but are compiled Python 3.12 bytecode (magic cb0d0d0a). The encryption simplifies back to RC4 with 8-character keys. Everything else gets dramatically more complex.
The Kramer class
Each file contains a single class definition instantiated at module scope:
Kramer(_kramer=False, _rasputin=False, _sparkle=<encoded_blob>)
The __init__ method chains four lambda functions into a decode pipeline:
Lambda 1 (self._bit) -- hex decode. Imports binascii by reconstructing the module name from string indices ([1,8,13,0,18,2,8,8] into a _decode string). Calls unhexlify on each /-separated segment of input. Returns decoded UTF-8 characters.
Lambda 2 (self._bits) -- chain function. Connects decode output to execution: calls self._exit(Lambda3(input)).
Lambda 3 -- anti-debug + triple decode.
- Opens
__file__and checks if the stringsprintorinputappear anywhere in the source. Since the files are compiled bytecode, these strings are absent by default. If an analyst addsprint()statements for debugging, the check triggersexit(). - Layer 1: Hex unhexlify via Lambda 1 -- produces Unicode codepoints
- Layer 2: Character shift:
chr(ord(c) - SHIFT_CONSTANT)for non-zeta characters; Greek zeta (U+03B6) maps to newline - Layer 3: Alphabet rotation: +1 position in
abcdefghijklmnopqrstuvwxyz0123456789(a->b, z->0, 9->a)
Lambda 4 (self._exit) -- execution. Constructs the strings exec, globals, and utf8 from index operations, then runs eval(exec(globals(decoded_source))).
Polymorphic shift constants
Each sample uses a unique Unicode Private Use Area (PUA) shift constant, embedded deep in the bytecode constants of a nested generator expression. PUA codepoints (U+E000--U+F8FF) have no standard meaning and no displayable glyph -- they're invisible to signature-based detection that expects ASCII or common Unicode. Every file has a different constant, and the encoded blob is completely different even when the decoded payload is identical.
| Set | File | RAT | Shift Constant | RC4 Key |
|---|---|---|---|---|
| EMP | 1an-obf.py | VenomRAT | 808,394 | YMYpepLY |
| EMP | 1as-obf.py | AsyncRAT | 112,985 | SgfdbVz0 |
| EMP | 1hv-obf.py | PureHVNC | 196,742 | zLning3R |
| EMP | 1plog-obf.py | PureLogs | 774,771 | teSGI2Zb |
| EMP | 1vio-obf.py | Violet RAT | 966,348 | NOpzga4k |
| OBKS | OBKSLazyNov20_ancrack.py | VenomRAT | 933,876 | f9AibaSR |
| OBKS | OBKSLazyNov20_as.py | AsyncRAT | 749,837 | 9V00O0eh |
| OBKS | OBKSLazyNov20_hv.py | PureHVNC | 362,709 | 5vCtSuqf |
| OBKS | OBKSLazyNov20_plog.py | PureLogs | 671,200 | oxlFPpGn |
| OBKS | OBKSLazyNov20_vio.py | Violet RAT | 239,463 | NQzn2pMo |
| WBKS | WBKSLazyNov20_ancrack.py | VenomRAT | 529,419 | u04dVbwt |
| WBKS | WBKSLazyNov20_as.py | AsyncRAT | 556,625 | LgQpDep7 |
| WBKS | WBKSLazyNov20_hv.py | PureHVNC | 441,206 | 7HJ4oP5i |
| WBKS | WBKSLazyNov20_plog.py | PureLogs | 597,727 | gkT854GZ |
| WBKS | WBKSLazyNov20_vio.py | Violet RAT | 491,726 | Hs3TFDx1 |
All 15 samples decode to the same Python template:
import ctypes
import base64
def rc4_decrypt(key, data):
# Standard RC4 (KSA + PRGA)
...
def execute_shellcode():
encrypted_data = base64.b64decode('<base64_blob>')
key = '<RC4_KEY>'.encode('ascii')
shellcode = rc4_decrypt(key, encrypted_data)
shellcode_buffer = ctypes.create_string_buffer(shellcode)
ctypes.windll.kernel32.VirtualProtect(
ctypes.byref(shellcode_buffer),
ctypes.sizeof(shellcode_buffer),
0x40, # PAGE_EXECUTE_READWRITE
ctypes.byref(ctypes.c_ulong())
)
shellcode_func = ctypes.cast(
shellcode_buffer,
ctypes.CFUNCTYPE(ctypes.c_void_p)
)
shellcode_func()
execute_shellcode()
The WBKS delta
Three build profiles exist: EMP, OBKS, and WBKS. EMP and OBKS carry identical shellcode sizes per RAT family but use different shift constants and RC4 keys -- the build system repackages the same shellcode with fresh obfuscation parameters.
WBKS shellcodes are consistently +26,628 bytes larger than EMP/OBKS across all five RAT families:
| RAT | EMP/OBKS Shellcode | WBKS Shellcode | Delta |
|---|---|---|---|
| VenomRAT | 360,702 B | 387,330 B | +26,628 |
| AsyncRAT | 342,270 B | 368,898 B | +26,628 |
| PureHVNC | 884,478 B | 911,106 B | +26,628 |
| PureLogs | 887,038 B | 913,666 B | +26,628 |
| Violet RAT | 1,722,110 B | 1,748,738 B | +26,628 |
The constant delta across all five payloads strongly suggests WBKS adds an additional ~26 KB stage to the shellcode. We did not extract the WBKS-specific stub to confirm its purpose -- it could be an extra unpacking layer, an AMSI/ETW bypass, or a different Donut configuration -- but the exact +26,628 byte consistency across five unrelated RAT families rules out coincidence.
File size bloat
The obfuscation cost is significant. Nov10 plaintext loaders range from 119 KB to 1,959 KB. Nov19 Kramer loaders range from 4,126 KB to 20,684 KB. The Violet RAT loader -- the largest -- is 20.7 MB for a file that decodes to the same ~40-line RC4 template. That is a ~30x file size increase for the same functional payload.
The 7-layer chain (Nov19 only)
Wave 4/5 introduces the deepest nesting observed in the campaign. The Nov19 Donut instances deliver native x64 PE wrappers instead of .NET assemblies directly:
Layer 1: Python (.pyc disguised as .py)
Layer 2: Kramer decode (hex -> unicode shift -> rotation -> RC4 -> base64)
Layer 3: Donut #1 (Chaskey CTR, v0.9.3, 20,345-byte stub)
Layer 4: Native x64 AES crypter PE
Layer 5: AES-256-CBC + 16-byte XOR overlay
Layer 6: Donut #2 (Chaskey CTR)
Layer 7: .NET RAT assembly
The native AES crypter at Layer 4 uses per-sample keys stored in the .rdata section: 32-byte AES-256 key at offset +0x210, 16-byte CBC IV at +0x230, 16-byte XOR overlay key at +0x240. All Nov19 modules share an identical 176 KB .text section (the native loader stub) plus a _sysc section for direct syscalls.
Nov10 vs Nov19 comparison
| Feature | Nov10 (plaintext) | Nov19 (Kramer) |
|---|---|---|
| File format | Python source (.py) | Python bytecode (.pyc as .py) |
| Obfuscation | None | Hex/Unicode/Caesar + anti-debug |
| File size range | 119 KB -- 1,959 KB | 4,126 KB -- 20,684 KB |
| Payload encryption | AES-256-CBC + 2x XOR | RC4 with 8-char key |
| Execution method | Early Bird APC into explorer.exe | In-process CFUNCTYPE |
| Anti-debug | None | Source string checks (print/input) |
| Build variants | 1 set (7 payloads) | 3 sets (15 files) |
| Key management | Fixed per payload | Unique per file (polymorphic) |
| Donut version | v0.9.2 (.NET direct) | v0.9.3 (native PE wrapper) |
Donut v0.9.2 -- Chaskey CTR and AMSI bypass
Donut is the bridge between the Python shellcode and .NET. Every wave uses it. The framework packages .NET assemblies as position-independent shellcode that bootstraps the CLR from scratch.
Chaskey cipher
All Donut instances use the Chaskey block cipher -- a lightweight permutation-based MAC designed for microcontrollers (Mouha et al., 2015), repurposed by Donut as a stream cipher in CTR mode:
- Block size: 128 bits (16 bytes)
- Rounds: 16
- Mode: CTR with big-endian byte-array counter (incremented from byte[15] toward byte[0] with carry propagation)
- Pre-whitening: block XOR key
- Post-whitening: block XOR key
Instance layout
[+0x000] uint32 len -- total instance size
[+0x004] byte[16] Chaskey key -- 128-bit key
[+0x014] byte[16] Chaskey ctr -- 128-bit counter/nonce
[+0x024] byte[4] (zero pad)
[+0x028] uint64 API hash -- VirtualAlloc
[+0x048] uint64 API hash -- VirtualProtect
...
[+0x230] byte[] Encrypted module data (v0.9.2)
[+0xC08] byte[] Encrypted module data (v0.9.3)
AMSI/WLDP patching
Before loading the .NET assembly, every Donut instance runtime-patches five security scanning functions:
| Function | DLL | Purpose |
|---|---|---|
AmsiInitialize | amsi.dll | Disable AMSI startup |
AmsiScanBuffer | amsi.dll | Bypass buffer scanning |
AmsiScanString | amsi.dll | Bypass string scanning |
WldpQueryDynamicCodeTrust | wldp.dll | Bypass Windows Lockdown Policy |
WldpIsClassInApprovedList | wldp.dll | Bypass class approval checks |
After patching, Donut loads mscoree.dll, calls CLRCreateInstance to start the .NET CLR (v4.0.30319), and invokes ExecuteInDefaultAppDomain with the target class and method names stored in the instance.
v0.9.2 vs v0.9.3
| Feature | v0.9.2 (Waves 1--3) | v0.9.3 (Waves 4--5) |
|---|---|---|
| Module type | .NET assembly directly | Native x64 PE wrapper |
| Architectures | x86 + x64 | x64 only |
| Module offset | Instance + 0x230 | Instance + 0xC08 |
| Decoder stub (x86) | 7,637 B | N/A |
| Decoder stub (x64) | 23,925 B | 20,345 B |
The x64 decoder stub is 3x larger than x86 due to 64-bit register operations and wider instruction encoding.
Per-instance Chaskey keys (Oct05)
| Instance | Chaskey Key | Counter | AppDomain |
|---|---|---|---|
| oct05_mof | 63c77584066e65dc30f81dd14b782886 | 42342683636e4347129ce25e3ad4954b | T6X7NPPF |
| oct05_start_mof | b739245dfae51c8a399b7a189d8cf5c3 | b961ea70e68d7751f371f9c307976343 | FNCH7YMH |
| oct05_pyt | 8fce7db93aba587407f368cb5c138ea1 | bc71d7ebc0f2b0c512b5e8ff016b8840 | FYXCHCX9 |
| oct05_start_python | 2cddb22a1c0ea2c59943e7841bfcc709 | 7f6ff3e61f9b57585fe17af90202c515 | R9HRP33F |
Batch stager infrastructure
The batch stagers are the initial execution layer. 29 .bat files recovered across six evidence directories deduplicate to 13 unique templates in four categories.
Download stagers
Two template families handle the initial download:
2Embambed -- AV-branched ZIP selection from a single Cloudflare tunnel per wave. Checks for AvastUI.exe and AVGUI.exe via tasklist. If detected, downloads abb11.zip (OBKS-only, Avast-safe profile). If not, downloads quz11.zip (full WBKS + BKSNO deployment). Each stager also downloads Shoopify.bat, PWS.vbs, and pws1.vbs into the Startup folder.
2bibo -- dual-ZIP download with fallback servers and random filenames. Downloads two ZIPs (Mfraq.zip and Sgraq.zip) into separate staging directories. Generates random 5-character uppercase filenames for local ZIPs. Has primary Cloudflare tunnel URLs plus fallback HTTP servers for redundancy.
Nine Cloudflare tunnels
Each deployment wave uses 2--3 disposable tunnels. A new tunnel is generated for each wave, with one exception: the Feb 1 Main_HV_x86 stager reuses the Dec 17 tunnel.
| # | Tunnel | Stager | Date |
|---|---|---|---|
| 1 | mustang-allowing-them-legislative.trycloudflare.com | 2Embambed_Stark | Nov 19 |
| 2 | valuation-throws-sixth-disc.trycloudflare.com | 2Embambed_Main | Nov 28 |
| 3 | brochure-pot-tested-clubs.trycloudflare.com | 2bibo___Stark | Nov 28 |
| 4 | candidates-burlington-hugh-anymore.trycloudflare.com | Main_HVNC_x86___Stark | Nov 28 |
| 5 | render-seen-sensor-urban.trycloudflare.com | 2Embambed_Main | Dec 17 |
| 6 | avoiding-ended-holiday-encyclopedia.trycloudflare.com | 2bibo___ | Dec 17 |
| 7 | blowing-paris-indoor-links.trycloudflare.com | Main_HV_x86 | Dec 17 |
| 8 | brunette-assembled-america-homepage.trycloudflare.com | 2Embambed_Main | Feb 1 |
| 9 | stomach-cite-personality-money.trycloudflare.com | 2bibo___ | Feb 1 |
Fallback servers
The 2bibo template includes non-Cloudflare fallback servers for when tunnels go down:
| Server | Port | Used By |
|---|---|---|
fsankmas.it.com | 7333 | Nov 28 Stark stager |
fsankmas.xyz | 7333 | Nov 28 Stark stager |
tammhdka.shop | 5044 | Dec 17 + Feb 1 stagers |
RAT launcher evolution (Shoopify.bat)
Three versions of Shoopify.bat recovered, deployed to the Startup folder for persistence:
Version 1 (Nov 28): References Nov20 filenames for OBKS/WBKS, Sep07 filenames for BKSNO. No XWorm. AV detected: 5 loaders. No AV: 12 loaders.
Version 2 (Jan 7): References Dec16 filenames across all sets. No XWorm. Same loader counts.
Version 3 (Feb 1): Adds XWorm (Xwrm3) to the OBKS and WBKS sets. AV detected: 6 loaders. No AV: 13 loaders. XWorm was never added to the BKSNO background set -- it was patched in between Jan 7 and Feb 1 by adding two lines to the foreground sets only.
This is the largest deployment profile in the campaign. When no Avast/AVG is detected, the system boots with 13 simultaneous Python RAT loaders, plus the Oct05 pair via Kindle_x86.bat, plus the AutoIt chains, plus two anti-idle VBS scripts.
Anti-forensic evolution
Early stagers used simple taskkill /f /im python.exe. By Nov 28, they switched to WMI parent-process hunting:
Get-CimInstance Win32_Process -Filter "Name='explorer.exe'" |
ForEach-Object {
$p = Get-Process -Id $_.ParentProcessId -ErrorAction Stop
if ($p.ProcessName -eq 'python') {
Stop-Process -Id $p.Id -Force
}
}
The method is narrower: instead of killing all python.exe processes, it finds explorer.exe instances whose parent is python.exe and kills only that parent. Kindle_x86.bat extends this to also hunt notepad.exe parents, because Wave 1 MOF loaders inject into notepad.
Anti-idle scripts
Deployed by all download stagers to the Startup folder:
| Script | Key | Interval |
|---|---|---|
PWS.vbs | F15 (exists in USB HID spec, not on keyboards) | Every 55 seconds |
pws1.vbs | Shift | Every 40 seconds |
F15 resets the idle timer without producing visible keystrokes. Both scripts keep the session alive for PureHVNC remote access.
Encryption evolution summary
| Generation | Waves | Date | Encryption | Injection | File Sizes |
|---|---|---|---|---|---|
| 1 | 1 | Oct 5 | Double-XOR / RC4 | notepad.exe APC / in-process | ~820 KB |
| 2 | 2--3 | Nov 10 | AES-256-CBC + dual XOR | explorer.exe Early Bird APC | 119 KB -- 1,959 KB |
| 3 | 4--5 | Nov 19 | Kramer .pyc + RC4 | In-process CFUNCTYPE | 4,126 KB -- 20,684 KB |
| 4 | 6 | Dec 17 | Wave 1 redeployment | Same as Wave 1 | Same as Wave 1 |
| 5 | 7 | Dec 16 | Mixed (all prior generations) | Mixed | Mixed |
Progression summary: readable Python with two XOR keys -> AES-256 with PyCryptodome -> compiled bytecode with polymorphic Unicode PUA ciphertext and 30x file bloat. The RAT payload families stayed the same.
Indicators of compromise
Cloudflare tunnel domains
mustang-allowing-them-legislative.trycloudflare.com
valuation-throws-sixth-disc.trycloudflare.com
brochure-pot-tested-clubs.trycloudflare.com
candidates-burlington-hugh-anymore.trycloudflare.com
render-seen-sensor-urban.trycloudflare.com
avoiding-ended-holiday-encyclopedia.trycloudflare.com
blowing-paris-indoor-links.trycloudflare.com
brunette-assembled-america-homepage.trycloudflare.com
stomach-cite-personality-money.trycloudflare.com
Fallback servers
| Domain | Port |
|---|---|
fsankmas.it.com | 7333 |
fsankmas.xyz | 7333 |
tammhdka.shop | 5044 |
Python loader hashes (Wave 1)
| File | MD5 |
|---|---|
Oct05_MOF.py | f2eccf363c89098a445d3ed8a047c697 |
Oct05_pyt.py | ee6a79204c207b366e645f62ea493407 |
Start_Oct05_MOF.py | 9277a1bd906839af54584d3ac6523caf |
Start_Oct05_Python.py | 2cd05753d1355e9e95c6001d7d2a64b7 |
Batch stager hashes
| Template | MD5 |
|---|---|
| 2Embambed_Stark (Nov 19) | 55aa4cb824722e9dd8e64d0538bd932b |
| 2Embambed_Main (Nov 28) | 4bf46fa39a5d9a9b94af8552d58bc974 |
| 2Embambed_Main (Dec 17) | 891490ccab1f9eaa16a11be3330d60ee |
| 2Embambed_Main (Feb 1) | 89713e3f754164d104ea5ae4ee6e17fc |
| 2bibo___Stark (Nov 28) | c8d885e9eb7a90228ebd5bf34afa49c6 |
| 2bibo___ (Dec 17) | 8be09bb84bb3f2e8528862201420aa17 |
| 2bibo___ (Feb 1) | 29f3b9286beecd8ff405222e182c4226 |
| Shoopify (Nov 28) | aafd399291d36afb518d4cb22eceb616 |
| Shoopify (Jan 7) | a9c46ef1a21a5799f608742e1a26ff43 |
| Shoopify (Feb 1) | fd267fde4c228922c177fdda6e358b87 |
Behavioral indicators
| Indicator | Context |
|---|---|
python.exe spawning explorer.exe (suspended) | Early Bird APC injection (Waves 2--3) |
python.exe spawning notepad.exe (suspended) | Process injection (Wave 1 MOF) |
VirtualProtect with PAGE_EXECUTE_READWRITE from Python | In-process shellcode execution (Waves 1, 4--5) |
AmsiInitialize / AmsiScanBuffer patching at runtime | Donut AMSI bypass (all waves) |
WldpQueryDynamicCodeTrust patching | Donut WLDP bypass (all waves) |
WMI query killing python.exe parent of explorer.exe | Anti-forensic cleanup (Nov 28+) |
| F15 or Shift keypress every 40--55 seconds | Anti-idle keepalive (PWS.vbs / pws1.vbs) |
attrib +h on %USERPROFILE%\Contacts | Hidden staging directory |
Five posts, one kill chain
This post is the fifth and final entry in the SERPENTINE#CLOUD analysis series:
- PureLogs: Reverse Engineering a .NET RAT -- the ConfuserEx-protected infostealer with protobuf C2
- PureCrypter: Reverse Engineering a .NET Loader -- the 3DES event-driven pipeline
- Violet RAT v4.7: The Most Dangerous Payload in a 9-RAT Toolkit -- 120-command dispatcher with ransomware and HVNC
- Remcos Banking Fraud via Three AutoIt Persistence Chains -- BYOI persistence and Canadian banking fraud
- Python Loader Evolution (this post) -- the delivery pipeline
Most development effort went into the delivery pipeline: polymorphic build systems generating unique keys per file, AV-aware branching selecting deployment profiles at runtime, and disposable Cloudflare tunnels rotated by wave. The inner RATs -- DcRat, AsyncRAT, VenomRAT, Violet, and PureLogs -- ran on the same C2 infrastructure throughout.
The outer layers changed constantly. The inner layers barely changed at all.
Kirk
I like the internet. Want to get in touch? kirk@derp.ca