Dancing with Windows.

Implementing and analysing Windows Messages Control-Flow Obfuscation

Introduction

Since my last post, many things have happened in my life and career. I will continue to bring more walkthroughs for other malware campaigns here or in other blog, but some months ago, we received a really awesome post from the Unit 42 Palo Alto team regarding new campaigns and variants of the ROMCOM RAT. That obviously caught my attention, and I decided to dive into those samples. Unsurprisingly, malware evolves a lot from one campaign to another. The evolution from the campaign I analyzed to this one is noticeable. More advanced techniques and approaches were introduced with the recent samples.

Today, we’re going to cover a really interesting technique used in the first stage of the new ROMCOM, something that Palo Alto’s team called Window Message Control-Flow Obfuscation. But what’s the best way to learn something new if not by trying to implement something similar? So first, we’re going to cover what that is and how it uses Windows internal features to overcomplicate our lives as malware analysts.

Fantastic messages and how to use them

Windows applications (I’m talking about the type, not the OS) are entirely event-driven. This means they are always waiting for the OS to pass input to them to process. The way the system passes input to a window is called messages, and these messages can be generated by the system or other applications, and have a queue that supports messages in a FIFO base. An event can be something like a mouse movement, a click, the system resizing a window, or other interactions. Applications can also send messages to their own windows or to other windows.

The window procedure is the function that gets executed when a window receives input to process. This procedure processes the input and determines the appropriate actions to take.

Programs usually utilize a loop to handle the messages that the application supports. Let’s take a look at the functions and structures needed to create and manage a window’s message queue.

Message structure

The most important parts of a message for us are the first four fields. hwnd is a handle to the window that should receive the message. message is an ID for the message, and these IDs should follow specific conventions: IDs below 0x03FF indicate System-Defined Messages, while IDs above that represent Application-Defined Messages, which may or may not be registered with the system. wParam and lParam are fields whose values depend on the type of message.

time, pt, and lPrivate are also fields present in the MSG structure but are not typically involved in the message manipulation workflow unless the developer uses specific functions to retrieve this information.

Window Class Structure

One of the main things we need to do to create our windows is to register the class. Once the class is registered, we can create windows based on it. To achieve this, we need to use the WNDCLASSEX structure.

For us, the most important field is lpfnWndProc, as it defines the Window Procedure.

Important functions

There are a bunch of functions that we’ll use later in our small POC. As previously mentioned, the first step is to register our class, which is done using RegisterClass. Later, we will need to create our main window, which is done using CreateWindow. Buttons and other elements within our window can also be created with this function.

After that, to handle our messages, we have PeekMessage, which looks for the first item in the message queue without removing it, and GetMessage, which does the same thing but removes the item and stores its values in a pointer.

TranslateMessage and DispatchMessage are often used in the loop that processes the received messages. TranslateMessage converts virtual-key messages into character ones, making keyboard interaction easier for the window procedure developer. DispatchMessage sends the message information to the window procedure to process it.

In the beginning, we’ve got a silly “malware”

Keep in mind that our goal here is not to generate a super malware, but to understand the concept. If you grasp it, I’m sure you’ll be able to create an awesome sample for your red team approach or anything else. What I’m trying to say is, take my C code with a grain of salt.

At the beginning, we’ll have a simple malware. This one, basically, just uses two really simple approaches to evade some reverse engineering techniques, then downloads a file and executes it.

This really simple code will also generate a staright foward graph. If you take the executable and put it on a disassembly tool, the main function will generate something like that.

Let’s put some spice

Now that we already know how Windows messages work, we will basically implement a new window for us to work with, featuring the main window and several buttons. The special part is that we’re going to slice your main workflow into many parts and place the logic for each part in the Windows procedure. Each part of the original code will be a step, and it will be executed at the press of a button. To start, in your main code, we will define the first button that will be pressed with PostMessage, and the window procedure will handle the next buttons pressed to continue the workflow.

Setting up our windows

Starting our new code, we can declare the main function and a global variable. This variable will help us better manage the stages and store information, regardless of the stage your code is currently in.

Before jumping to the most important part, which is the Windows procedure, let’s start by creating our class, window, and buttons. This part can be handled by a function like this:

Another important part is the function that will handle the message queue:

Here we have PeekMessage, which allows us to look at the current message without removing it. If no message is in our queue, our code will then post one to start our workflow.

Speaking of workflow, I’ve drawn a really simple one that we’ll work on implementing.

Button 3 will always be our first button at each new run. Some buttons, like 2 or 6, are not only used to check information and exit (for example, if isDebugged is true) but also mark the end of a “run.” After those buttons are pressed, the queue will be empty, and our main code will post a message for Button 3 again.

We will often control which button will be next by referencing information stored in global variables.

The perfect workflow for us will be something like 3, 6, 3, 1, 5, 2, 3, 1, 4.

Starting the WndProc

That’s the main part of the procedure. After this, I will show each button individually.

If you take a closer look at each button, you will notice that the main logic is spread across all the buttons, following the order I mentioned.

If you load that binary into a disassembly tool and examine the Windows procedure (which corresponds to the main function from the previous binary), you will see something like this.

That’s a much more challenging function to approach. Keep in mind that this is just a simple example, in a real-world scenario, it would be far more complex. Speaking of real-world scenarios…

A Wild Malware Appears

Now, let’s take a look at one of the many workflows used by the new ROMCOM variant.

For register the main class, it have a pretty close approach to what we previously developed (0x3be0 and 0x3d30).

Looking at the message loop, we find the function at 0x3e30.

The main monster

Remember when I said that the implementation we made is really simple and that, in real life, things can be much harder? I was referring to this big boy here. That’s 0x1250, also known as the Windows Procedure of that malware sample.

Don’t panic yet! As I mentioned, we’re now going to cover a small part of the workflow it uses.

First Run

First of all, each time the workflow results in returning to the main code and posting the first button, I will call it a “run.” Each button that is pressed will be referred to as a “stage.” So, let’s start analyzing the first run.

Buttons will be identified from 0x4000 to 0x4011. Since the queue starts empty, the button 0x400A will always be called first, making it the first stage in every run.

The main goal of this run is to write some data to the heap and prevent the subsequent runs from doing the same. So, the next time button _0x400e is pressed, it will be redirected to the next button.

It’s important to note that even though this stage only wrote 0x20 bytes, it allocated 0x50, which can be used in future runs and stages.

Second Run

The approach in the second run is quite similar to the first one, but this time it allocated 0x30 in the heap. To keep you on track, this process will be repeated multiple times before any real action begins.

Third Run

In the third run, we encounter something different: the use of SendMessage, which will trigger a custom command 0x8403. In this run, this stage will allocate 0x2C in the heap and, like the previous runs, store some information about the heap and numbers there.

Fourth Run

For our fourth run, we will stop at 0x4009. This stage will allocate 0x48 bytes in memory, and this time it will fill them with important information. Starting at offset + 0x20, we will have pointers to TEB, PEB, LDR_DATA, InLoadOrderModuleList and ImageBaseAddress. It will also write a pointer in a part of the memory that will affect the next run at stage 0x4003.

Fifth Run

You are not seeng it wrong, the difference in th graph betrween fourfh an fifth run is thet the fifth will end after 0x4003.

This time, this step approach will be way different, since one of the values that at the first time was different.

After that, we now have two functions being used: 0x3a90 and 0x1080. The first one will basically initialize some buffers. The second one is much more interesting. It receives a string (usually a module name, since the code will use the previous run’s information to iterate through loaded modules) and generates a new value, essentially creating a type of hash. The focus of this function is to locate bcrypt.dll on the system. Afterward, it will allocate memory again and store a pointer to the bcrypt.dll list entry.

Sixth Run

Just like the previous run, this run will also stop one step earlier. The changes are straightforward: this time, it will run the function at 0x1080 again, passing the name of the binary that’s running and comparing it to 0x1a233bfa. Any value different from that will be moved to the end of the application. To continue with the rest of the code, we need to change that.

But in case you got courious, the name that should result in the correct hash is Attachment_Medical reports.exe.

Conclusion

We’ve covered six runs to address a relatively simple check. The main goal here was to demonstrate the technique used and implement it in a simplified way, making it easier to understand in a more complex environment. I hope I’ve managed to achieve that.

While we’ve covered the key aspects, there’s much more to explore if you’re interested. For those who want to dive deeper into the analysis, feel free to continue exploring. Alternatively, I may present the full version in a future post, where we can analyze each step in more detail and explore additional obfuscation and evasion techniques used in real-world malware.

References