A walkthrough of the first RomCom RAT malware campaign
Introduction
DISCLAIMER: I’m currently a RE wannabe, but since information like YARA rules, hashes, and domain lists is already widely available on the internet, this text will focus only on a technical view of the chosen sample. You will find some simple kickstarters for certain topics, but my main goal is to provide a technical perspective. Throughout the text, all strings and memory addresses will be formatted like this and shown without the ImageBase. All other important information will be in italic.
Something that has always intrigued me is APTs. The idea of a government-supported hacker group, actually used mainly for geopolitical purposes, constantly catches my attention. It’s normal to see people hacking for fame and profit, but we have to agree that a nation being willing to support someone to be focused 24/7 on sabotage, spying, and threatening an enemy is basically the most Hollywood-like way to approach hacking.
And not so long ago, I had the opportunity to face an exciting challenge: reversing an APT malware. For me, as a beginner, this seemed like an impossible mission, but with amazing help, I was able to accomplish it. After that, I became interested in the path that led the malware to take its current form, since this malware family has been involved in many campaigns. This leads us to the malware we will discuss in this post.
Campaign Context
The beginning of the Russian-Ukrainian war in 2022 had some very interesting points from a threat analysis perspective. We have seen cases of known APTs being involved, but also hacktivist mercenaries and criminals who were initially focused on money started to shift their goals to geopolitical ones. Additionally, some new APTs were not made public until the war.
The first RomCom RAT campaign was marked around July 23, 2022 (which is just one day before Russia started bombing the first Ukrainian cities). However, we can also find information about spear-phishing targeting a European Parliament member earlier this year, as well as Google Ads advertisements. The objective was to infect Ukrainian political affairs, military, and even Ukrainian state enterprises with lure sites. The focus was on mimicking generic sites or applications that openly listed Ukrainian military entities as clients. The first major application to be cloned was “Advanced IP Scanner,” spread through hxxps://advanced-ip-scaner[.]com (mimicking the legitimate site hxxps://advanced-ip-scanner[.]com).
At the right we have the lure site, at the left the legit application site
Stage 0
I got my sample from VXUnderground, and I highly recommend that you visit their website. They have an amazing collection of papers and malware samples, all properly cataloged. I will leave the link for the sample in the References list at the end of this post.
The first thing we need to look at is the stage 0 of our malware. For the first “Advanced IP Scanner” campaign, they had two options: one was an .exe file, and the other was an .msi file. The first one I encountered was the .msi file, so I decided to stick with it for the rest of the analysis.
What’s a MSI
“Microsoft Windows Installer is an installation and configuration service provided with Windows.”
Skipping the “beeps and bops” of what constitutes a whole MSI file, it’s essentially a relational database. What matters to us are the binaries present and two tables in that database.
In the InstallExecuteSequence table, we can see a list of actions that are executed when the top-level INSTALL action is triggered. The columns are pretty straightforward, but the Action we need to examine more closely is “ExecApps.” It has the condition of not being installed yet, and in the sequence column, we can find the order in which the actions will be executed. More information about the “ExecApps” action can be found in the CustomAction table.
If we take a closer look, we will notice that this action indicates snt_combined.exe as the source. This is the file that will be executed during the installation step and is also the next stage of our analysis. We can easily extract the files using tools like 7-zip.
Stage 1
What’s a COM Object
Before starting to get our hands dirty with all those bytes, let’s first meet one of the core concepts of Windows: COM. COM is a Microsoft interface standard that basically makes inter-process communication possible in a client/server architecture. The clients don’t have to know how the server generates the object; they just need to create and receive it.
It’s clear that this approach solves problems like the server and client not needing to be written in the same programming language, but the benefits go way beyond that. Another thing I love about COM is that it also solves problems with libraries and versioning, since:
- Statically linking .lib = Wasted space from duplicate libraries.
- Dynamic linking .dll = No version control.
- COM = Separation of implementation from the interface.
So basically, if you use Windows, you’re interacting with COM daily without even noticing. It helps with things like using an Excel sheet inside a Word document without needing to load the Excel application, or every time you right-click and Windows shows you options to interact with a file—the results depend on the COM objects present on your computer.
That’s the basic flow of using COM. Each COM server will have a unique identifier, and Windows will assist the client by initializing and providing it with a loaded server. It’s important to note that it will consult the HKEY_CLASSES_ROOT\CLSID key, which, despite its name, is actually a merge of HKEY_CURRENT_USER\Software\Classes and HKEY_LOCAL_MACHINE\Software\Classes. The key point here is that the current user actually has higher priority than the local machine. So, in this case, user data takes precedence, which is advantageous because it’s easier to write to the current user registry than to the local machine registry.
Getting hands dirty
Okay, so let’s jump to our first real binary. Now I will cover the main parts and some interesting ones.
Encoded Strings
Noticed that I did not mention any strings of the file yet? That’s because the malware’s strings are all encoded, everytime it loads a string it will decode that after. For example, the main function can be found at 0x2e870, and at 0x2e94e it will load a encoded string and start decipting it until 0x2e9b6 and the decripted string will be indicated at RAX, this procedure will be done at many times in the malware chain.
Important functions
After that, it will load a very important function located at 0x2e230. This function will receive a pointer to the recently decrypted string \winver.dll. Inside the function, it will allocate memory, search for the user’s temporary directory, concatenate the directory with the given string, and use that value to create a file there with the content of the resource ID 8888 or 0x22b8, as observed during the debugging process.
Returning to the main process, it will transform the ASCII string \winver.dll into its Unicode version using the function at 0x2eaae. After that, it will call another important function at 0x2eb0d. This function will first decrypt the string SOFTWARE\CLASSES\CLSID{BCDE0395-E52F-467C-8E3D-C4579291692E}\InprocServer32 and then call the previous function to convert ASCII to Unicode. With the Unicode string in hand, it will call RegCreateKeyExW, passing 0xffffffff80000001 as the first parameter, which indicates that a key in HKEY_CURRENT_USER should be created. After that, it will perform the same process to generate the file path for \winver.dll and store it in the registry using RegSetValueExW.
Going back to the main process and moving on to the next important function: called at instruction 0x2eb4f and located at 0x2ddf0, this function has a straightforward goal: locate the PID of explorer.exe and terminate it. This action will cause Explorer to reload and call the main function of the malicious DLL, which is our next step.
Stage 2
Anti-analysis and tricks
Before debugging that sample, due to ASLR reasons, we need to change the DYNAMIC BASE flag in DLLCharacteristics at the IMAGE_OPTIONAL_HEADER, this can be easly done with tools like DiE.
Now we are almost ready to go. Don’t let the AddressOfEntryPoint fool you, the real magic of the main function starts at 0x73bb8. In the first instructions, we already need to make some changes to continue debugging. The first Sleep function at 0x73bf4 has a value that is way too high for debugging purposes. To move forward, you’ll need to change that (you can simply insert some NOPs and patch the binary). The next behavior of the malware is very interesting. It will allocate memory, retrieve the application that loaded the DLL using GetModuleFileNameW, and then determine the string size. After that, it will search from the end of the string to the beginning for a “\” character, which should provide an offset to the application name.
After that, it will use the size of the application name to allocate memory, store the application name there, and finally use a function to convert the uppercase characters to lowercase.
We are getting to a very interesting part. All the work that the malware is doing is leading to a point where it will compare each letter of the application name that loads the DLL to explorer.exe. This ensures that no other applications will properly trigger the malware, only Explorer. To proceed, we need to patch the application again and interfere with the natural flow, or start debugging the Explorer app (which I strongly do not recommend, it will give you a massive headache).
Getting into the main part of that function, it will allocate memory multiple times and then call a function to get the path to rundll32.exe. This path is then used to properly construct two main strings (Note: you must have the COM registry configured, or it will not generate the correct string. However, the main idea behind it is easy to grasp):
- C:\Windows\System32\rundll32.exe C:\Users\XXXX\AppData\Local\Temp\winver.dll,startInet winver.dll0
- C:\Windows\System32\rundll32.exe C:\Users\XXXX\AppData\Local\Temp\winver.dll,startFile winver.dll0
This will be very important, as each of these strings will be used in a CreateProcessA call (but note the 0x11170 milliseconds sleep before that).
The process will then continue by opening a loopback connection on port 5656, expecting to receive commands like add bot, update bot, and delete bot. I won’t delve further into that part; instead, let’s move on to the exported functions.
Stage 2.1 startInet
Now we know that we need to look into the functions startFile and startInet. Interestingly, both of these functions have nearly identical graphs, and they ultimately call the same function, passing _filewinver.dll0 for startFile and _inetwinver.dll0 for startInet. For the next steps, we will examine that function more closely. It is located at 0x692d0 and is called at 0x6e775 in startFile and 0x692a5 in startInet.
The beginning of this common function is pretty straightforward. It will decrypt the value _init and compare it to the first 5 bytes of the given argument (_initwinver.dll0 or _filewinver.dll0). This interaction can be observed at 0x6937c, where we see sete sil. This means that the least significant byte of RSI will be set according to the zero flag of the previous operation test eax, eax. In other words, sil will only be zero if eax is not zero. The crucial point of this comparison is at 0x693cd, where it will test sil, sil and then jump to the main logic of startInet or startFile. Essentially, any byte different from _init will lead to the startFile approach. For the rest of this post we will focus on that function.
For the startInet path of the function, it will continue allocating memory until it reaches 0x694b1. This function is responsible for creating the first part of the heartbeat message, and after that, the function called at 0x6953b will generate the final part of the heartbeat message. The next important step occurs at 0x69593, where the called function at 0x5b8d0 is responsible for sending the data and handling the response from the C2 server located at combinedresidency[.]org (another encrypted string within the sample). The protocol used is HTTPS, and the C2 server does not use the standard 443 port; instead, it uses port 4444.
An interesting point is that this function returns 0 in case of success and 1 in case of failure. This leads to a significant difference in behavior at the function called at 0x69d03. In the case of failure, instead of spawning a new thread to handle the function 0x687d8 (more details on this in the following sections), it will execute it in the main thread.
the previous mentioned function 0x687d8 is responsible for:
- Create a socket and listen at 127.0.1.2 in the next available port starting at 5555.
- Receive the results of the command sended by the main thread to the startFile process.
- Use the same 0x5b8d0 function to send the information to C2
With that approach, heatbeat messages are independent of command results, the process will not waste time waiting for the startFile, and the C2 will still be receiving the response of the command. In a test enviroment this can also lead to a infinite creation of new threads, so keep that in mind.
The process loading startInet can essentially handle some ‘commands’ and payloads received from the C2 server, but the main command logic is managed by startFile, so I will leave that for the next section.
At 0x69cfe, there’s an interesting function that is responsible for attempting the first connection to the other process, the one running startFile. As we will see later, this function will open a port to handle inter-process communication. An important point is that it will try ports from 5555 to 5580. If it still doesn’t detect the presence of the startFile process, it will take the same approach as the main function and call CreateProcess to continue the infection.
Stage 2.2 startFile
The last part of your analysis is the main function, the one that actually executes the commands. After the 0x693d0 instruction, we are led to the isolated logic for startFile, and we quickly encounter an interesting section. Not long after, the IPC socket is started using the well-known sequence: WSAStartup, socket, gethostbyname, bind, listen, and WSAAccept (it will listen on 127.0.1.2:5555, or a higher port if it fails to start on that one).
Another point I didn’t mention before: remember the string that the main function uses to call the other ones? It includes an argument, winver.dll0. The 0 indicates that the first port it will try to bind to is 5555. If you pass winver.dll1, the first attempt will be on 5556, and so on.
After that, it will wait for the other program running startInet to start a communication and pass a command for it to execute. Commands will have the following structure:
If the first byte is zero, the function won’t do anything; it will simply return to WSAAccept and start waiting for future connections again. But if the first byte is anything different, the real magic begins. The byte that really matters to us is the fifth one. This byte determines which command will be executed. I won’t go into detail listing the commands since you can find an excellent article in the references section that provides a comprehensive list of commands.
In the end, the communication graph will look something like the one below. For example, if after the heartbeat startInet receives a 0x0100000006… command, startFile will receive it, take the content after the command byte, continue its operations, and at 0x5fa84, it will call DeleteFileA, passing the content as an argument.
This RAT can gather information from the machine, download files, upload files, execute commands, call exported functions in certain DLLs, and more.
Conclusion
So that’s my analysis. It took quite a bit of time, and I think there’s still more to uncover. This project was a really interesting challenge, and I’m thankful for all the help I got along the way. A big thanks to everyone who supported me and took the time to read through my work. I really appreciate it!
References
- Sample: hxxps://samples[.]vx-underground[.]org/Samples/Families/ROMCOMRat/3e3a7116eeadf99963077dc87680952cca87ff4fe60a552041a2def6b45cbeea.7z
- MSI Base documentation: https://learn.microsoft.com/en-us/windows/win32/msi/windows-installer-portal
- MSI Install table: https://learn.microsoft.com/en-us/windows/win32/msi/installexecutesequence-table
- MSI Custom Action table: https://learn.microsoft.com/en-us/windows/win32/msi/customaction-table
- Awesome COM talk: https://www.youtube.com/watch?v=pH14BvUiTLY&t=553s
- Awesome COM video: https://www.youtube.com/watch?v=svFundrBIiQ
- TrendMicro’s Romcom RAT newer campaign analysis: https://www.trendmicro.com/en_us/research/23/e/void-rabisu-s-use-of-romcom-backdoor-shows-a-growing-shift-in-th.html