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.IsAttachedand nativeIsDebuggerPresent()viakernel32. - Virtualization detection: uses WMI (
ManagementObjectSearcher) to query system manufacturer/model and looks for keywords such asVMware,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%(viaEnvironment.SpecialFolder.ApplicationData), - writes a Base64-encoded blob (then XOR-decoded with key
0xF7) into a.tmpfile, - renames the
.tmpto.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) with0x15
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
.exeand atos.txtis 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.txtclaims “© 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.













