What is DotRunpex
DotRunpeX is a recent family of malwares used as a vector to distribute many other malware families. Its structure is generally made of three stages.
The first stage is a dropper. Generally distributed via email attachment, scam websites or even trough malicious Google Ads, the dropper is generally a simple piece of malware that just downloads its second stage
from a C2 server.
The second stage payload is the core of this blog post. It is a dotnet sample, made difficult to analyze through the usage of ConfuserEx
(an open-source protector for .NET applications) and KoiVM (a virtual machine made to work on ConfuserEx, that turns the .NET opcodes into new ones that only are understood by
KoiVM itself). This second stage sample carries an encrypted payload inside, which is the actual malware that is being distributed by the dotrunpex injector.
At the time of writing, the main resource about dotrunpex malware family comes from Checkpoint Research.
Actually there are few other technical blog posts about this topics, but they essentially follow the road traced by Checkpoint.
Checkpoint researchers essentially provide two ways to unpack the encrypted payload. The first one is based on educated guessing: they somehow
managed to guess the decryption key and the decryption algorithm (simple XOR
decryption), and armed with this knowledge they managed to decrypt the payload.
This approach is not very reproducible in my opinion and so I don't like it much.
The second approach instead is programmatic: it is based on the usage of libraries to debug the dotnet sample via scripting, inspect its
heap allocation and then dump all the bytes[]
data structures whose content starts with MZ
.
This second approach is general and reproducible and works on a corpus of samples of the same family. However, I found it complex, and in addition I
was not able to make the scripts work out of the box, so I proceeded with my own approach.
So, in this blog I will explain an alternative approach to unpack the sample that proved to be effective.
This approach needs manual interaction with the sample and should be easy to follow and reproduce.
The analysis of the third-stage payload will be carried out in a separate blog post instead.
Analysis of the sample
So, let's start by loading the sample into PeID. It seems that the PE header is not well-formed:
Opening it with CFF Explorer, we see that it is able to read the file section
Indeed we notice that it is a .NET
file and that is has been obfuscated with KoiVM
protector
In addition to standard .NET sections, a Koi
section is present.
Based on the version information, the name of this sample is RunpeX.Stub.Framework
. All the various flavours of this malware family have the same version name.
As already stated, this is a second-stage payload downloaded by a first-stage dropper.
In this specific case, our analysis will show that this second stage loader is used to deliver Agent Tesla malware.
Since it is a .NET sample, let’s open it with DNSSpy. The entry point is at address 0x6000041
:
To find the entry point we have to go to the sixth table of the #~stream
, method number 65 (41 in hex), which turns out to be the method _OA()
.
This method invokes the Run
method of the VmEntry
class, which belongs to the KoiVM runtime.
Dynamic Analysis
After this basic static analysis I decided to run the sample and observe its behaviour, to get a rough idea of how it works.
It goes without saying that running a malware may be dangerous, and at the very least you should run it on a virtual machine (better if it is detached from
the network) and for which you have already taken a clean snapshot (in case something goes wrong you can then easily revert to a clean state).
Running the sample, we can see that it spawns a cmd prompt with the following command line arguments: "C:\windows\system32\cmd.exe" /K "fodhelper.exe"
and after that it runs the CasPol.exe
executable:
Let's start with fodhelper. It has a documented history of being abused to bypass Microsoft UAC,
so it deserved a deeper investigation with ProcMon. However, after checking the related events, I did not find evidence that it was involved in the unpacking process
and so I moved on.
However, since I knew, thanks to the Checkpoint report, that this malware family generally implements the Process Hollowing technique, directly injecting the unpacked
payload into a target process, I thought that maybe I would have better luck with the analysis of CasPol.exe.
So, I opened again DnsSpy and I started to look for functions involved in the Process Hollowing technique, most notably CreateProcess.
I run a search for CreateProcess and I had immediate luck in finding all these functions involved in Process Hollowing:
CreateProcess, VirtualAllocEx, ZwUnmapViewOfSection, GetThreadContext, SetThreadContext and ResumeThread
These functions can be called from dotnet code trough the P/Invoke platform, that allows managed code (the sample dotnet code) to call native code (the APIs mentioned above).
Inside this obfuscated code, CreateProcess is called trough the method _2b._GB
. I put a breakpoint on it and once it triggered, I inspected the variables in the
Locals tab: DAHOTNPR
contains the process that is being created, which is exactly CasPol.exe!
The signature of CreateProcess is the following one. If you look at the sixth parameter, you will see that its name is dwCreationFlags and its value is 4U (U stays for unsigned integer),
which is used to create a process in Suspended mode:
BOOL CreateProcessA( [in, optional] LPCSTR lpApplicationName, [in, out, optional] LPSTR lpCommandLine, [in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes, [in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes, [in] BOOL bInheritHandles,[in] DWORD dwCreationFlags, [in, optional] LPVOID lpEnvironment, [in, optional] LPCSTR lpCurrentDirectory, [in] LPSTARTUPINFOA lpStartupInfo, [out] LPPROCESS_INFORMATION lpProcessInformation );
Indeed, after stepping over this instruction with the debugger, we can see that CasPol has been created in Suspended state as expected:
It is not done yet, because the payload is still encrypted on disk. However, there will be a moment in which it will be decrypted by the malware itself to
be injected in the target process memory space. That will be a good moment to get the payload for free.
So, I moved on following this idea, but at some point I got stuck around the following loop:
Indeed, moving on one instruction at a time would not let me exit from the loop (at least, not in reasonable time), while if I tried to let the sample run and stop after the control flow
exited from the loop, the CasPol.exe process was already in execution (likely carrying the injected payload inside).
Also trying with conditional breakpoints did not help much, because I could not exit the loop in a reasonable time as well, nor identify the point where the target process
was run.
I knew that a possible solution would have been to stop the debugger when the native APIs were called, but due to code obfuscation I was having a hard time in finding the right point.
Since I was stuck and I had to deal with native code, I decided to analyze the sample with x64dbg
.
So I launched again the sample from dnSpy, this time telling it to run the sample without debugging, and as soon as the sample started its execution I attached
to the runing process with x64dbg.
After setting a breakpoint on the CreateProcess API, I arrived to the same point that I reached in dnSpy:
I have highlighted the most important parts, namely:
- CreateProcess is invoked to create the CasPol.exe process, whose path is passed in the RCX
register
- the new process is created in suspended state as we saw before. To prove it, I inspected the stack at memory location RSP+30
, which corresponds to address BFA3FFE418
,
where we can see that the value is 4. This is the value assigned to the parameter dwCreationFlags
.
Now it is a good moment to set breakpoints also on the other native functions that we already discovered using dnSpy: after resuming the execution the next breakpoint hit is NtWriteVirtualMemory
.
Now, this function is undocumented, but googling around it is possibile to see that its signature is like this:
NtWriteVirtualMemory(
IN HANDLE ProcessHandle,
IN PVOID BaseAddress,
IN PVOID Buffer,
IN ULONG NumberOfBytesToWrite,
OUT PULONG NumberOfBytesWritten OPTIONAL );
in my case, such value are:
- ProcessHandle <- RCX = 000000000000033Ch
- BaseAddress <- RDX = 0000000000E10000h
- Buffer <- R8 = 000001D60DD75970h
- NumberOfBytesToWrite <- R9 = 00000000000011C0h
NtWriteVirtualMemory is called several times before I may find a call that looks promising, which seems to be writing a PE file at address 0x0000020753529D78
.
Since this call was writing 1600 bytes (0x200 in hex), I dumped exactly 1600 bytes starting from memory address 0x0000020753529D78 and saved them to file.
Opening this file with CFF Explorer, we immediately notice that we didn't dump the whole executable but only the header (indeed SizeOfHeaders
is equal to 0x200).
The field SizeOfImage
specifies that the PE image is 0x6C000 bytes long, so in order to get the full PE file we will have to perform a dump of the very same memory region, but instead
of dumping 0x200 bytes we will dump 0x6C000 bytes. This memory area contains the payload (the PE file) that is injected into to CasPol.exe suspended process.
At this point, my goal is to dump that memory region in the moment before the target process is resumed, to be confident that I have the final,working version of the executable.
To do this, I resume the execution and I see that function SetThreadContext
is called on the target process CasPol.exe. This is common in Process Hollowing techniques,
since the malware uses this API to adjust the context of the target process, for instance to set the new entry point.
Going on with the execution the debugger stops at ResumeThread
: at this point the process injection has been completed, and the sample is going to start the execution of
the target process, who is now carrying the injected payload.
So this is a good moment to dump the unpacked payload. It seems that the process was correct, indeed CFF explorer recognizes the sample as a valid PE file and
I can even see an image associated to the program from Windows explorer:
We can see that the payload is a dotnet sample as well, so it can be loaded into dnSpy.
DnSpy recognizes it as a valid executable and is able to correctly identify the entry point of this new unpacked sample:
I will analyze this sample in part 2 of the blog post. This new sample is actually the third-stage payload, the malware that is distributed trough the dotrunpex injector.
Conclusion
In this blog post we focused on how to unpack the payload contained in a dotrunpex sample, a dotnet sample obfuscated and virtualized with KoiVM to make more difficult the analysis.
We saw that in order to run its payload, the sample was implementing the Process Hollowing technique, relying on the P/Invoke framework to call the native code needed to create a new legitimate process (CasPol.exe),
replace its memory space with the malicious payload and finally run the process.
The MD5 hash of the unpacked PE file is: 0D97BA9ED7CDBC6183E446B7CB43391C
and at the time of writing, it does not match any known sample on VirusTotal or on other online sandboxes.
References
- Checkpoint Research
- KoiVM
- Microsoft UAC Bypass
- Process Hollowing Technique Explained (Mastering Malware Analysis, 2nd edition, pp.177-178)