← All posts

Python Loader Evolution: Five Encryption Generations

Kirk19 min read
malwarepythondonutincident-responsepurecoderobfuscation

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.

ArtifactSHA-256VTTria.ge
BKSNOLazyNov10_Viooooooo.pyc9dc14435857fd00e3f81e219c362c4dc235774dea08fb49fca874dfa108cdd2Not indexedhttps://tria.ge/260225-syqahsb15d (score 3)
1vio-obf.py (EMP Kramer)97e71a0347439a169a3278ca85c11f28fe2098c42f7513e2621c0a3f3258b7f2Not indexedhttps://tria.ge/260225-syq7tab15f (score 3)
1uunov24.pyced8796ae647584824c9907d0513f2d2f4b63f9b5bb9cf713e5a59a7773358241/76https://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

WaveDateEncryptionExecutionObfuscationDonutLoaders
1Oct 4--5Double-XOR (16B keys) + RC4 (8-char)APC into notepad.exe / in-process CFUNCTYPEJunk variables, readablev0.9.2 (x86+x64)4
2Nov 10AES-256-CBC + dual XOR (16B keys)Early Bird APC into explorer.exeNone (plaintext .py)v0.9.2 (x64)7
3Nov 10AES-256-CBC + dual XOR (different keys)Early Bird APC into explorer.exeNone (plaintext .py)v0.9.2 (x64)7
4Nov 19RC4 (8-char key), Kramer .pycIn-process CFUNCTYPEKramer bytecode obfuscatorv0.9.3 (x64)5
5Nov 19--20RC4 (8-char key), Kramer .pycIn-process CFUNCTYPEKramer bytecode obfuscatorv0.9.3 (x64)10
6Dec 17Same as Wave 1 (redeployed)Same as Wave 1Same as Wave 1v0.9.22
7Dec 16Mixed AES+XOR and RC4Mixed APC and in-processMixed plaintext and KramerMixed13

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
FileXOR Key 1XOR Key 2
Oct05_MOF.py896a01e960d97a791133c51c7bf6629a2faa926e690a4fd4e7c5c1acbb910a28
Start_Oct05_MOF.pyd69d1f100b74ffa8bcc8eae2394e1bfd4079c58306a2e15568bb2c006fa65eb5

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:

FileRC4 Key
Oct05_pyt.pyxNuJ5Z75
Start_Oct05_Python.py0ADNeZBG

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:

LoaderRATShellcode Size
BKSNOLazyNov10_Anacrack.pyDcRat103,186 B
BKSNOLazyNov10_Annorii.pyDcRat103,186 B
BKSNOLazyNov10_Anoriihv.pyPureHVNC101,138 B
BKSNOLazyNov10_Asyncccc.pyAsyncRAT84,754 B
BKSNOLazyNov10_Ploggggggg.pyPureLogs629,522 B
BKSNOLazyNov10_Venommm.pyVenomRAT103,186 B
BKSNOLazyNov10_Viooooooo.pyViolet RAT1,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.

  1. Opens __file__ and checks if the strings print or input appear anywhere in the source. Since the files are compiled bytecode, these strings are absent by default. If an analyst adds print() statements for debugging, the check triggers exit().
  2. Layer 1: Hex unhexlify via Lambda 1 -- produces Unicode codepoints
  3. Layer 2: Character shift: chr(ord(c) - SHIFT_CONSTANT) for non-zeta characters; Greek zeta (U+03B6) maps to newline
  4. 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.

SetFileRATShift ConstantRC4 Key
EMP1an-obf.pyVenomRAT808,394YMYpepLY
EMP1as-obf.pyAsyncRAT112,985SgfdbVz0
EMP1hv-obf.pyPureHVNC196,742zLning3R
EMP1plog-obf.pyPureLogs774,771teSGI2Zb
EMP1vio-obf.pyViolet RAT966,348NOpzga4k
OBKSOBKSLazyNov20_ancrack.pyVenomRAT933,876f9AibaSR
OBKSOBKSLazyNov20_as.pyAsyncRAT749,8379V00O0eh
OBKSOBKSLazyNov20_hv.pyPureHVNC362,7095vCtSuqf
OBKSOBKSLazyNov20_plog.pyPureLogs671,200oxlFPpGn
OBKSOBKSLazyNov20_vio.pyViolet RAT239,463NQzn2pMo
WBKSWBKSLazyNov20_ancrack.pyVenomRAT529,419u04dVbwt
WBKSWBKSLazyNov20_as.pyAsyncRAT556,625LgQpDep7
WBKSWBKSLazyNov20_hv.pyPureHVNC441,2067HJ4oP5i
WBKSWBKSLazyNov20_plog.pyPureLogs597,727gkT854GZ
WBKSWBKSLazyNov20_vio.pyViolet RAT491,726Hs3TFDx1

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:

RATEMP/OBKS ShellcodeWBKS ShellcodeDelta
VenomRAT360,702 B387,330 B+26,628
AsyncRAT342,270 B368,898 B+26,628
PureHVNC884,478 B911,106 B+26,628
PureLogs887,038 B913,666 B+26,628
Violet RAT1,722,110 B1,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

FeatureNov10 (plaintext)Nov19 (Kramer)
File formatPython source (.py)Python bytecode (.pyc as .py)
ObfuscationNoneHex/Unicode/Caesar + anti-debug
File size range119 KB -- 1,959 KB4,126 KB -- 20,684 KB
Payload encryptionAES-256-CBC + 2x XORRC4 with 8-char key
Execution methodEarly Bird APC into explorer.exeIn-process CFUNCTYPE
Anti-debugNoneSource string checks (print/input)
Build variants1 set (7 payloads)3 sets (15 files)
Key managementFixed per payloadUnique per file (polymorphic)
Donut versionv0.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:

FunctionDLLPurpose
AmsiInitializeamsi.dllDisable AMSI startup
AmsiScanBufferamsi.dllBypass buffer scanning
AmsiScanStringamsi.dllBypass string scanning
WldpQueryDynamicCodeTrustwldp.dllBypass Windows Lockdown Policy
WldpIsClassInApprovedListwldp.dllBypass 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

Featurev0.9.2 (Waves 1--3)v0.9.3 (Waves 4--5)
Module type.NET assembly directlyNative x64 PE wrapper
Architecturesx86 + x64x64 only
Module offsetInstance + 0x230Instance + 0xC08
Decoder stub (x86)7,637 BN/A
Decoder stub (x64)23,925 B20,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)

InstanceChaskey KeyCounterAppDomain
oct05_mof63c77584066e65dc30f81dd14b78288642342683636e4347129ce25e3ad4954bT6X7NPPF
oct05_start_mofb739245dfae51c8a399b7a189d8cf5c3b961ea70e68d7751f371f9c307976343FNCH7YMH
oct05_pyt8fce7db93aba587407f368cb5c138ea1bc71d7ebc0f2b0c512b5e8ff016b8840FYXCHCX9
oct05_start_python2cddb22a1c0ea2c59943e7841bfcc7097f6ff3e61f9b57585fe17af90202c515R9HRP33F

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.

#TunnelStagerDate
1mustang-allowing-them-legislative.trycloudflare.com2Embambed_StarkNov 19
2valuation-throws-sixth-disc.trycloudflare.com2Embambed_MainNov 28
3brochure-pot-tested-clubs.trycloudflare.com2bibo___StarkNov 28
4candidates-burlington-hugh-anymore.trycloudflare.comMain_HVNC_x86___StarkNov 28
5render-seen-sensor-urban.trycloudflare.com2Embambed_MainDec 17
6avoiding-ended-holiday-encyclopedia.trycloudflare.com2bibo___Dec 17
7blowing-paris-indoor-links.trycloudflare.comMain_HV_x86Dec 17
8brunette-assembled-america-homepage.trycloudflare.com2Embambed_MainFeb 1
9stomach-cite-personality-money.trycloudflare.com2bibo___Feb 1

Fallback servers

The 2bibo template includes non-Cloudflare fallback servers for when tunnels go down:

ServerPortUsed By
fsankmas.it.com7333Nov 28 Stark stager
fsankmas.xyz7333Nov 28 Stark stager
tammhdka.shop5044Dec 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:

ScriptKeyInterval
PWS.vbsF15 (exists in USB HID spec, not on keyboards)Every 55 seconds
pws1.vbsShiftEvery 40 seconds

F15 resets the idle timer without producing visible keystrokes. Both scripts keep the session alive for PureHVNC remote access.


Encryption evolution summary

GenerationWavesDateEncryptionInjectionFile Sizes
11Oct 5Double-XOR / RC4notepad.exe APC / in-process~820 KB
22--3Nov 10AES-256-CBC + dual XORexplorer.exe Early Bird APC119 KB -- 1,959 KB
34--5Nov 19Kramer .pyc + RC4In-process CFUNCTYPE4,126 KB -- 20,684 KB
46Dec 17Wave 1 redeploymentSame as Wave 1Same as Wave 1
57Dec 16Mixed (all prior generations)MixedMixed

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

DomainPort
fsankmas.it.com7333
fsankmas.xyz7333
tammhdka.shop5044

Python loader hashes (Wave 1)

FileMD5
Oct05_MOF.pyf2eccf363c89098a445d3ed8a047c697
Oct05_pyt.pyee6a79204c207b366e645f62ea493407
Start_Oct05_MOF.py9277a1bd906839af54584d3ac6523caf
Start_Oct05_Python.py2cd05753d1355e9e95c6001d7d2a64b7

Batch stager hashes

TemplateMD5
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

IndicatorContext
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 PythonIn-process shellcode execution (Waves 1, 4--5)
AmsiInitialize / AmsiScanBuffer patching at runtimeDonut AMSI bypass (all waves)
WldpQueryDynamicCodeTrust patchingDonut WLDP bypass (all waves)
WMI query killing python.exe parent of explorer.exeAnti-forensic cleanup (Nov 28+)
F15 or Shift keypress every 40--55 secondsAnti-idle keepalive (PWS.vbs / pws1.vbs)
attrib +h on %USERPROFILE%\ContactsHidden staging directory

Five posts, one kill chain

This post is the fifth and final entry in the SERPENTINE#CLOUD analysis series:

  1. PureLogs: Reverse Engineering a .NET RAT -- the ConfuserEx-protected infostealer with protobuf C2
  2. PureCrypter: Reverse Engineering a .NET Loader -- the 3DES event-driven pipeline
  3. Violet RAT v4.7: The Most Dangerous Payload in a 9-RAT Toolkit -- 120-command dispatcher with ransomware and HVNC
  4. Remcos Banking Fraud via Three AutoIt Persistence Chains -- BYOI persistence and Canadian banking fraud
  5. 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.

K

Kirk

I like the internet. Want to get in touch? kirk@derp.ca