GuLoader is a prominent shellcode-based downloader that has been used in a large number of attacks to deliver a wide range of the “most wanted” malware.
GuLoader has been active for more than three years and is still undergoing further development. The latest version integrates new anti-analysis techniques, which results in it being significantly challenging to analyze. New GuLoader samples receive zero detections on VirusTotal, ensuring its malicious payloads also remain undetected.
GuLoader’s payload is fully encrypted, including PE headers. This allows threat actors to store payloads using well-known public cloud services, bypass antivirus protections, and keep payloads available for download for a long period of time.
Earlier versions of GuLoader were implemented as VB6 applications containing encrypted shellcode. Currently, the most common versions are based on the VBScript and the NSIS installer. The VBScript variant stores the shellcode on a remote server.
Antivirus products are constantly evolving to become more sophisticated and better equipped to handle complex threats. As a result, malware developers strive to create new threats that can bypass the defenses of antivirus products. “Packing” and “crypting” services are specifically designed to resist analysis. GuLoader is one of the most prominent services cybercriminals use to evade antivirus detection.
Figure 1 – The number of attacks using GuLoader in the past 6 months.
In addition to code encryption, GuLoader utilizes many other techniques including anti-debugging and sandbox evasion techniques. A distinguishing feature of GuLoader is that the encrypted payload is uploaded to a remote server. The would-be attacker gets a highly protected shellcode-based loader that downloads the payload from a remote server, and decrypts and runs it in memory without dropping the decrypted data to the hard drive.
Despite Google’s efforts to block GuLoader’s encrypted malicious payloads, GuLoader still downloads payloads from Google Drive in most cases. The following chart shows the statistics of the different hosting services used by GuLoader over the past month.
Figure 2 – Different hosting services used by GuLoader between March – April 2023.
We see evidence that GuLoader is currently being used to distribute the following malware:
Early GuLoader samples managed to avoid detection by antivirus products, but later different security solutions became capable of detecting this malware. However, in parallel with the ongoing development of antivirus software by cybersecurity vendors, the GuLoader developers also continued improving their product. In the absence of previous research findings and understanding of the anti-analysis mechanisms employed in GuLoader, analyzing the code of the new version would be exceedingly challenging. You will discover the reasons for this below in our report.
The earlier versions of GuLoader were implemented as VB6 applications containing encrypted shellcode. The shellcode performed the main functions of loading the encrypted payload, decrypting it and launching it from memory.
Currently, the most common versions are based on the VBScript and the NSIS installer (Nullsoft Scriptable Install System).
A distinctive feature of the new version is that the encrypted shellcode is hosted on a cloud service (usually Google Drive). VBScript itself contains only a small obfuscated PowerShell script and a lot of junk code. This allows GuLoader samples to maintain a very low detection rate.
Here is an example of an infection chain that uses the VBS variant of GuLoader:
Figure 3 – Infection chain that uses VBS variant of GuLoader.
Let’s consider a sample with SHA256 5fcfdf0e241a0347f9ff9caa897649e7fe8f25757b39c61afddbe288202696d5. At the time of uploading to VirusTotal (VT) on March 3, 2023, it had 0 detections:
Figure 4 – Zero detections of GuLoader sample in VT.
Two days after it was uploaded, only 17 out of 59 vendors flagged this sample as malicious.
We should note that at the time of writing this article, it has been 3 weeks since the specified sample was uploaded to VT, and the URLs for both downloading the GuLoader shellcode and for downloading the malicious payload (Remcos) were still active:
Let’s take a look inside the GuLoader VBScript. It contains a lot of pseudo-random comments and some useless commands. After cleaning it up a bit, the code we got looks like this:
Figure 5 – Cleaned GuLoader VBScirpt.
The purpose of this code is to call the PowerShell interpreter and pass it the code of the script collected in the “pa0” variable as a parameter.
If we look at the contents of the “pa0” variable after adding the omissions and hyphens, we get the following script:
Figure 6 – GuLoader obfuscated PowerShell script.
We see that this new script contains the function “Gothites9“, which implements cutting the passed string starting from the second character with a step of 3. Therefore, the result for the command “$Tjene0 = Gothites9 ‘ OIUlEDiXSa ‘;” is “IEX ”.
The string $Parrotb is converted in the same way. Starting from position 2, taking every third character from this string gives us a string that is another PowerShell script:
Figure 7 – GuLoader PowerShell script after removing the first layer of obfuscation.
This script is called either by using the IEX command (if the OS is 32-bit) or passed as a parameter to the PowerShell interpreter called from the SysWOW64 folder (if the OS is 64-bit). This is because the GuLoader shellcode must run in a 32-bit process.
We can already see that the script code contains the URL pointing to Google Drive.
However, the resulting script is still heavily obfuscated. The script starts with a function that is used to decode strings:
Figure 8 – Encoded strings in the GuLoader PowerShell script.
It is interesting that all lines in the nested script are stored in encoded form, except for the line with the URL.
After deobfuscating the script, we got the following code:
Now we can see that the script allocates 2 memory areas, downloads the data from the link to Google Drive, and saves it to a temporary file “%APPDATA%\Umig.For”. Next, the contents of the downloaded file are decoded using BASE64. The first 654 bytes of the decoded data are placed in the first memory area (“$Gamme2483” in the example), and the rest in the second (“$Nulstille” in the example). The first 654 bytes contain an obfuscated shellcode which is intended to decrypt the second copied area containing the main part of the shellcode in encrypted form.
Control is transferred to the decryptor by using the CallWindowsProc callback function, which also receives the address of the encrypted shellcode and the address of the NtProtectVirtualMemory function as arguments.
NSIS-installer based variant
Unlike the VBS variant, samples based on the NSIS contain the GuLoader shellcode, albeit in encrypted form. This allows you to run the sample in a sandbox and see the behavior of GuLoader even if the sandbox is not connected to the Internet. Static analysis of NSIS script and encrypted shellcode is also possible.
Such samples now receive a consistent number of detections by antivirus products at the time of the first upload to VirusTotal.
Figure 10 – Detection rate of NSIS-installer-based GuLoader variant.
Knowing the techniques used by the GuLoader shellcode, it is quite easy to bypass them by using a debugger in the process of dynamic analysis. However, in the new version, we encountered a technique that makes both debugging and static analysis extremely difficult.
A new anti-analysis technique
Starting from the end of 2022, the GuLoader shellcode uses a new anti-analysis technique, which consists of breaking the normal flow of code execution by deliberately throwing a large number of exceptions and handling them in a vector exception handler that transfers control to a dynamically calculated address.
To throw exceptions, the code uses the int3 instruction. It was possible to implement a script to automatically replace int3 instructions with jump instructions to the correct address:
Figure 11 – Replacement of the int3 instruction with the jmp instruction.
Accessing an invalid memory address to cause access violation.
This pattern is quite straightforward. First, as a result of mathematical operations, one of the registers is set to zero value. The shellcode then attempts to write data to the memory addressed by this register:
Figure 12 – Accessing invalid memory address to raise the access violation exception.
This causes the access violation exception (0xC0000005). The exception is handled in GuLoader by the registered VEH which calculates the new address to continue the shellcode execution. The numbers used and the mathematical operations that lead to the calculation of the zero value are always different.
Setting the Trap Flag to raise the single-step exception.
GuLoader uses the following combination of instructions to set TF in the EFALGS register:
Figure 13 – Setting a Trap Flag to raise the single-step exception.
At first glance, it is unclear what happens in this piece of code. However, if we calculate the valued in the register EDI, we get the value 0x100. The combination of the next few instructions is intended to push the EFLAGS and set the TF (Trap Flag) bit to “1”. The modified value from the stack is then set back to the EFLAGS register.
When the Trap flag is set in the EFLAGS register but no debugger is attached, the processor generates a single-step exception (0x80000004) after the execution of the next instruction. In GuLoader, the registered VEH is called in this case. However, if the debugger is attached, the GuLoader’s VEH is not called and execution follows the wrong path.
The code chunks in the GuLoader shellcode are always different; various combinations of registers can be used. As in the case of invalid memory address, the numbers used and the mathematical operations that lead to the calculation of the value 0x100 to set TF in the EFLAGS register are always different.
Using int3 to raise the breakpoint exception.
Using int3 as instruction as an anti-analysis technique was already implemented in the previous version of GuLoader. However, it is still used in various parts of the GuLoader shellcode. When the CPU encounters the int3 instruction in the absence of a debugger, it generates a breakpoint exception (0x80000003) and the registered VEH is called. However, if a debugger is attached, the control is transferred to the debugger’s interrupt handler which typically pauses the program’s execution.
The int3 instruction is usually followed by random bytes that break the normal execution of the shellcode:
Figure 14 – Using int3 to raise the breakpoint exception.
As a result, we cannot determine the correct execution path without analyzing the code of the GuLoader VEH.
To calculate a new jump address in the case of one of the 3 specified exceptions, and direct the program to a new execution path, GuLoader registers a vector exception handler (VEH) using the RtlAddVectoredExceptionHandler function.
To see how the jump address is calculated, let’s look at the VEH code.
Like other parts of the code, VEH code is obfuscated. It contains junk instructions, and important values are calculated dynamically using XOR operations:
Figure 15 – Obfuscated VEH code.
However, after the decompilation in IDA this code looks very simple:
Figure 16 – Decompiled VEH code.
As you can see, VEH actions are slightly different depending on the exception code. In the case of exceptions 0x80000004 (EXCEPTION_SIGNLE_STEP) and 0xC0000005 (EXCEPTION_ACCESS_VIOLATION), it gets the value of the byte at offset 2 from the instruction where the exception occurred and XORs that byte with some constant value (0x8B in the example). In the case of exception 0x80000003 (EXCEPTION_BREAKPOINT), the byte at offset 1 is taken and also XORs with the constant. It should be noted that the specified constant is different in all samples. The resulting value is then added to the EIP value in the exception context. Therefore, when exiting the exception handler, control is transferred to the new address.
In all cases, the exception handler also checks the status of the debug registers:
Figure 17 – Checking debug registers in VEH.
If any hardware breakpoints are set, the exception handler refers to the zero address instead of the ContextRecord address. This eventually causes the application to crash.
In the case of EXCEPTION_BREAKPOINT, the exception handler also looks for software breakpoints in the address space between the old EIP and the calculated new EIP values.
Despite the huge variety of code combinations that can be used to trigger the execution of the exception handler, they all follow 3 patterns, and we can implement a regular expression to find most of them. However, we expect that the GuLoader developers may change the patterns in new versions.
To patch one instruction on which an exception is raised and replace it with a jump to a correct address in x32dbg, you can use the following script (you must replace 0x8B with a constant value from the sample you analyze):
All the strings, including the URL for downloading the final payload, are encrypted and stored in a specific form in the shellcode:
; eax is set to the address of the allocated memory
8B 44 24 04 mov eax, [esp+target_enc_str_buffer]
; first 4 bytes of the buffer contain the length of the encrypted string
; the bytes are calculated using xor, add, and sub operations:
C7 00 75 9B D5 11 mov dword ptr [eax], 11D59B75h
81 30 0E 84 7B 49 xor dword ptr [eax], 497B840Eh
81 28 1C 8B 75 41 sub dword ptr [eax], 41758B1Ch
81 00 B3 6B C7 E8 add dword ptr [eax], 0E8C76BB3h ; 12 00 00 00
; increment eax by 4
05 97 47 CD 01 add eax, 1CD4797h
2D 93 47 CD 01 sub eax, 1CD4793h ; eax = eax + 4
; calculate next 4 bytes of the encrypted string
C7 00 B9 FD D8 E0 mov dword ptr [eax], 0E0D8FDB9h
81 30 06 79 13 36 xor dword ptr [eax], 36137906h
81 30 AD 51 65 B7 xor dword ptr [eax], 0B76551ADh
81 30 81 FA 9D 8C xor dword ptr [eax], 8C9DFA81h ; 12 00 00 00 93 2F 33 ED
For the example above, we deobfuscated the code, clearing it of junk instructions and jumps. In reality, the code contains a large number of garbage and invalid instructions. To help understand the obfuscation complexity, this is part of the original code corresponding to the previous example:
Figure 18 – Composing encrypted strings in the heavily obfuscated GuLoader shellcode.
Unlike strings, the decryption key is stored as a regular sequence of bytes following the decryption function:
Figure 19 – Strings decryption XOR key.
This key is usually not very long, with a maximum of 64 bytes.
The strings are decrypted using an XOR operation with the decryption key. After decrypting the strings, we can find a string that looks like a URL, but without a schema:
It’s obvious that the GuLoader authors realized the way the research community managed to decrypt URLs in the previous versions of the shellcode using the strings “http://” or “https://” in the known-plaintext attack to detect the first bytes of the decryption key. Therefore, in the new version, they replaced the URL scheme with random bytes.
If the 5th byte of the decrypted URL-string is equal to “s”, GuLoader replaces the first 8 bytes with “https://”. Otherwise, it replaces the first 7 bytes with “http://”.
Here are examples of more URL-strings extracted from different samples:
The payload decryption key is also stored in the same way as the encrypted strings but the key is not encrypted. The key length is usually in the range of 800-900 bytes.
For example, in a sample with MD5 40b9ca22013d02303d49d8f922ac2739, the length of the key is 844 bytes. However, another length is used for the decryption routine, and is stored in the obfuscated form:
Figure 20 – Key length used for decrypting the payload differs from the length stored with the key.
GuLoader used a different size, rather than the size stored with the key, to deceive automated analysis. If we don’t take this into account, we can only decrypt the first 843 bytes of the downloaded payload, and the rest of the data will be broken.
The payload decryption algorithm itself has not changed in comparison to the previous GuLoader versions. The first 64 bytes of the downloaded data are skipped. Then, to get the final key, GuLoader assumes that the first 2 bytes of the decrypted payload should be “MZ” and calculates the 2-bytes XOR key (rand_key). The payload decryption key is then XOR-ed with the calculated 2-bytes value:
Figure 21 – Calculating the final key used for decrypting the payload.
The resulting key is finally used to decrypt the payload.
Several years after its introduction, the threat posed by GuLoader continues to grow. This is primarily due to the fact that the GuLoader developers are continually working to improve their product. The advanced defense evasion of GuLoader made it a favored tool among threat actors for delivering malware.
GuLoader counteracts antivirus products using a variety of sandbox evasion techniques, code obfuscation, and multiple layers of encryption. The GuLoader developers continually improve the anti-analysis and anti-debugging techniques. This year we also saw the use of a new trick: moving the encrypted shellcode to the cloud, and using a VBScript to download the shellcode. As a result, victims receive a VBScript file, which is less suspicious than an .exe file and is less likely to trigger alerts.
The use of encryption and storing payloads in a raw binary format without any headers and separate from the loader makes them totally invisible to antiviruses. This allows threat actors to use Google Drive to store malicious payloads and bypass its antivirus protection. In some cases download links to GuLoader malicious payloads stored in Google Drive remain active for very long periods of time.