Reversing the First Stage
I don’t typically tend to reverse engineer Delphi binaries, as most of the malicious software written in Delphi is actually the wrapper/packer for the main payload written in something like C/C++. However, scrolling through Twitter one day, I noticed @abuse.ch replying to a tweet about a somewhat unknown loader currently spreading FormBook. After doing some further research, it was clear that while there are YARA rules for this particular loader, not much is known about the functionality. Based on the fact it was Delphi-based, I thought it would be a pretty neat learning experience to dive into, relying on IDA Pro and x32dbg to reverse the sample, rather than using IDR which I’m not much of a fan of. So, let’s get into it!
ModiLoader/DBatLoader/NatsoLoader is a 2 stage malware loader that was first spotted on the 9th of June (uploaded to MalwareBazaar). The initial loader reaches out to a cloud based service, in certain cases Google Drive, and downloads the second stage loader, which is responsible for dropping the final payload to the disk and executing it. This final payload is commonly FormBook, however it has also dropped Netwire RAT and Remcos in the past.
The preferred method of distribution for this particular loader is Malspam, often targeting specific regions, although based on the method of storage for the second stage loader, geolocking is not possible (as far as I am aware).
The packer used to pack the sample we will be focusing on is fairly simple, and is also present in 3 other samples of ModiLoader I looked at. Four functions are responsible for extracting, decoding, and executing the actual payload, and so it can be assumed the remaining functions are junk. There are 3 important hardcoded strings in the packer; the “key” to decode the executable, the encoded second stage URL, and a replacement string.
The key is an integer (stored as a string), that is used in a simple operation to decode each byte of the payload. An implementation of this operation can be seen in Python 2.7 below the image.
hardcoded_int = int(hardcoded_int) calculation = hardcoded_int & 0x800000FF calculation = (calculation | 0xFFFFFF00) decoded_payload = “” for byte in encoded_data: new_byte = abs(calculation + ord(byte)) & 0xFF decoded_payload += struct.pack("B", new_byte)
Once the payload has been decoded, the packer will then search for a placeholder in the decoded payload (the replacement string is the same in both the packer and the decoded payload), and then replace that with the encoded URL. Interestingly, this prevented unpac.me from unpacking one of the samples correctly, as it dumped the decoded payload before the encoded URL was copied over. This wasn’t the case for every sample, but writing a quick static unpacker using some Regex isn’t the most difficult task in the world for this packer, and may save you some issues with incorrectly unpacked files.
Once the payload is ready for execution, the packer will allocate a region of memory, map the executable into the region (after resolving imports), and then execute it.
Interested in how to statically unpack these payloads, and automate the rest of the analysis? We will be covering automated analysis for this sample, and many others, as part of our Zero2Automated Advanced Malware Analysis course! If you’re interested in checking out the course, you can find it here! We look forward to seeing you there!
First Stage Loader:
Opening the first stage in IDA, we are met with the DLLEntryPoint. In this function, we can see one unnamed call (sub_4206A0()), which is the important function, followed by a GetMessage() loop. Take note of the variable v3 and v4 – the NtTib access and savedregs variable seem fairly constant in most, if not all, functions, and have no major effect on the flow of the program, so it seems like this has simply been added during compilation by the compiler. Similarly, the __writefsdword() calls also do not affect the program flow.
The important call (sub_4206A0()) simply calls timeSetEvent(), which will start a specified timer event. The multimedia timer runs in its own thread. After the event is activated, it calls the specified callback function or sets or pulses the specified event object.
result = timeSetEvent(a1, 0, (LPTIMECALLBACK)fptc, 0, 1u);
In this case, the callback function has been named fptc, and will be executed by the call to timeSetEvent. Stepping into this function, we finally get a wrapper of the main loader code,
As we are analysing a Delphi based binary rather than C/C++, IDA can run into a few issues, such as not correctly setting the function end address, which can cause decompilation errors such as code blocks not appearing, or unused variables being inserted into decompiled blocks. In this function, you can see v8 is passed into the main_loader_func(), however it is not declared anywhere else. In cases like this, it can be much easier to analyse the sample using the disassembly graph view, as you are able to correctly trace back variables.
Put simply, this function will get the file name, get the file age of the file, test the internet connection by trying to connect to microsoft.com, before finally calling the main_loader_func().
The most important functions to look at inside this function are the sub_4202B0() and deal_with_url_and_payload() functions. deal_with_url_and_payload() accepts 3 arguments, with the first being some kind of hexlified string, and the second being the string YAKUZA2020. The third argument is an output buffer, which the function will use for storing data. sub_4202B0() takes 2 arguments, however in this screenshot, IDA has failed to decompile it correctly. The first argument is the same as the third argument for the previous function, which is v17 in this case. The second argument acts as another output buffer. Before continuing, let’s step into deal_with_url_and_payload().
At first glance, this function only seems to convert the hexlified string to raw bytes, but this isn’t the case. If we jump to the disassembly view, we can see an entire block of code between return_string_len() and convert_char_code(), which involves an XOR operation. In a nutshell, this function will loop through the hexlified string, taking 2 characters on each loop, and unhexlifying them. This is then XORed with a byte from the second argument, YAKUZA2020, which is the decryption key. Once decrypted, the byte is then concatenated to the third argument, which is the output buffer. An example of this algorithm in Python can be seen below the images.
def hex_decoder(data, key): outbuf = "" data = [int(data[i:i+2], 16) for i in range(0, len(data), 2)] for i in range(0, len(data)): current_byte = data[i] key_byte = ord(key[i % len(key)]) outbuf += chr(current_byte ^ key_byte) return outbuf
Decrypting the hex string results in a URL, which is passed into sub_4202B0(), AKA grab_payload(). All this function does is connect to the remote server, and read the response, storing it in the second argument. The function then returns.
The payload is stored in a similar hexlified fashion to the URL. After downloading, the sample will flip the data, and proceed to decrypt it using the same method as before, and the same key in this case.
decoded_binary = hex_decoder(content[::-1], sample_key)
Once downloaded and decrypted, the sample will allocate a region of memory, map the downloaded second stage into that region, and then execute it.
This second stage is responsible for grabbing the main payload, which in many cases is FormBook. We will be diving into this second stage in the next post!
IOCs (MD5): Packed Sample: B30459D88F2E3146E248763643FF86EF C2: hxxps://cdn[.]discordapp[.]com/attachments/732298690575990898/740083604071251978[/]Ruybbbb