Going Deep | A Guide to Reversing Smoke Loader Malware

Working in infosec and supporting clients and SOCs has always exposed me to a huge number of alerts and incidents. Some of these are more interesting than others. Recently we stumbled across a particular sample of Smoke Loader malware. Smoke Loader has been in-the-wild since circa 2013 and is often used to distribute additional malicious components or artifacts. While the sample is not new, it did prove to be a good opportunity to revisit this threat and walk through some of the internals. 

This alert was raised against a suspicious file, classified as a trojan, that was killed and quarantined. What raised my curiosity was the number of detections over only a few hours, always from the same workstation, and only from the same user.

image of raw data

Knowing that the threat was killed without doing any harm, I decided to dig into it a bit more. Just looking at the SentinelOne console, I was able to see :

  • The full path where the detection was made.
  • The associated risk level is High: this implies that it’s a positive detection.
  • File unique hash that can be tested for any public Indicators of Compromise (IoC).

image of smoke loader analysis

In order to do a walk-through of malware reverse engineering steps, I downloaded the threat file and started the analysis.

First Layer: A Packed VB Win32 Program

With the downloaded file in my pocket, I quickly fired up an isolated analysis machine equipped with the Flare tools and started to investigate. At first glance, the sample appears to be a Visual Basic program leveraging Win32 APIs.

image of smoke loader PEiD

Let’s see what else we can get from its headers. Looks like pretty standard information, confirming a Visual Basic program due to its import table.

image of smoke loader headers

We can spot some rude and folkloristic words inside the binary strings, some of which make me think of a regional dialect of southern Italy.

image of smoke loader in PE Studio

With a bit of experience, we can safely assume that the file is packed with an external layer of Visual Basic that tries to stop, or at least slow down, static analysis. But what about its runtime behavior?

Observing the sample during runtime, we can observe the process injection: this behaviour is common for VB packers and luckily for us, is often trivial to defeat.

Defeating Visual Basic Packer

We won’t spend too much time on this: there are plenty of resources on how to unpack such packers and I highly recommend the OALabs Youtube video tutorials. It’s necessary and enough to put a breakpoint at CreateProcessInternalW API inside the debugger to stop the execution at the right time.

image of setting breakpoint

At this point, somewhere in memory, there is a PE file ready to be run. We only need to find it. To do so, we can search the entire memory map for a clue: I decided to search for the “DOS” substring that can usually be found as part of the “This program cannot be run in DOS mode” string within the PE.

image of memory search

We got plenty of results for the string, whose hex is 44 4F 53.

image of load address

However, we are particularly interested in just a few locations. Usually the executable is loaded at 0x00400000 address, so the result we had at 0x0040006C looks like our executable itself.

Things become particularly interesting at 0x002F0094, which we can follow in the memory dump.

image of smoke loader memory dump

A memory region with a PE file inside, mapped as Executable, Read, Write. This is definitely our injected file. 

image of smoke loader PE file in memory

We can simply dump out this memory region to file, clean the junk before the MZ header and analyze its headers.

image of memory region

It seems like a legitimate executable, but something is going on: no imports at all. This is interesting!

Second layer: Static Analysis

When we load this new executable in IDA Pro, this was the only chunk of code that was disassembled. 

image of smoke loader in ida pro 1

Here we recognize that it’s a XOR loop that will decode, from address 0x00401567, a blob of code with the size of 0xCD bytes with a XOR key equal to 0xCB. At the end of the loop, the same starting address 0x00401567 is pushed onto the stack and with the RET instruction the program flow will be branched over there.

Decoding the Buffer

With a little bit of IDA scripting, we can XOR the encrypted buffer and move forward in the analysis.

image of decoding the buffer

After de-xoring the buffer we are met with a mixture of anti-disassembly and anti-debug techniques. It is now possible to map the purpose of the code blocks.

image of graph overview

Inside this code, we can observe plenty of tricks that try to fool the disassembly flow. A few examples: 

  • Abusing CALL and RET instruction to mess up function boundaries. The CALL instruction will push the return address onto the stack. The RET instruction will then pop off this address into the EIP register, which effectively makes these two instructions useless. However, these few opcodes make IDA think that the function ends there and that the next instruction is the end of another function.
  • Abusing branch instructions that do nothing: CALL <address> and at <address>: POP <reg>. It’s the easiest way to get an address inside the EIP register and so to control the program’s flow.
  • Abusing JMP instructions: simply putting a lot of JMP instructions that will jump back and forth only to make the life of the analyst miserable.

Obfuscated with these techniques, the malware checks if it’s being debugged. The code that implements this check is nothing complicated: it queries certain flags of the PEB in order to spot the debugger, IsDebuggerPresent.

mov eax, fs:[30h] ; Process Environment Block

cmp b [eax+2], 0 ; check BeingDebugged
jne being_debugged

As said, this code is heavily obfuscated with junk jumps and a lot of instructions with the only purpose of increasing complexity of analysis. As an example, this little chunk of code is the final part of a dozen lines of code used to put value 0x30 inside the EAX register with the purpose of locating the PEB.

image of locating smoke loader PEB

At the end of this function, we spot another XOR stub decoding routine that will decode another blob of code and, after that, redirect the execution flow. Decoding will start at address 0x004014E8, with a buffer size of 0x7F and the same XOR key 0xCB.

image of XOR stub in smoke loader

As before, we can proceed in the static analysis, manually decoding this buffer with the same script.

But wait! Here we go again, another anti-debugging trick, NtGlobalFlag check:

mov eax, fs:[30h] ; Process Environment Block
mov al, [eax+68h] ; NtGlobalFlag
and al, 70h
cmp al, 70h
je  being_debugged

 

This chunk of code checks if the process is attached to a debugger and, if it goes well, another XOR decoding stub starts from address 0x00401000, with buffer size 0x4E8 and XOR key 0xCB.

image of checking the debugger

After decoding the new buffer, we need to face another anti-disassembly trick; namely, JMP instructions with a constant value. This is the most common trick used by malware to fool static analysis. Basically, it creates jumps into a new location plus one or a few bytes. It results in an erroneous interpretation of the opcode by the disassembler. It’s trivial to defeat but time intensive. 

IAT Resolution at Runtime

At address 0x00401000 there’s a simple call to another address 0x00401049, where it starts to become interesting as the malware appears to dynamically resolve its imports. As we noted before, the binary header analysis showed no imports at all. With this code, from the PEB location found earlier, the malware finds the base address of ntdll.dll.

image of PEB location

But how is this happening? In all recent Windows versions, the GS register points to a data structure called the Thread Environment Block (TEB). At offset 0x30 of the TEB, there’s another data structure, namely the Process Environment Block (PEB) we saw earlier. 

image of Thread Environment Block

We can inspect these data structures with the help of Microsoft public symbols and WinDBG.

image of smoke loader in WinDBG 1

With the same tools we can inspect the PEB too:

image of smoke loader in WinDBG 2

With the third instruction, we are following the offset 0x0C, the _PEB_LDR_DATA structure. This structure is fairly important because it contains a pointer, InInitializationOrderModuleList, to the head of a double-linked list that contains the NTDLL loader data structures for the loaded modules. 

image of PEB LDR Data

Each item in the list is a pointer to an LDR_DATA_TABLE_ENTRY structure. If we inspect this structure, we get the DLLBase.

image of DLLBase

Looking at this inside the debugger helps to shed some light:

image of smoke loader in IDA PRO 2

We got the base address of module ntdll.dll into EDX register, because this is the first module loaded into every process in a Windows environment. We have added comments and renamed select functions to clear up some of the observables.

image of resolve imports

After the malware gets the ntdll.dll base address, it loops twice calling a function named DecryptionFunction. This function receives as input a dword, which here is a hash.  As we’re going to see, it will walk the Export Address Table of the module searching for a particular function with the name matching to the passed hash. With this first loop, the malware finds two functions: strstr and LdrGetDllHandle.

As an example, in this particular case, the DecryptionFunction is walking, as we explained before for ntdll.dll, the module kernel32.dll, retrieving the address of VirtualAlloc put inside the EAX register as return value.

image of Virtual Alloc

DecryptionFunction

After fully disassembling the function(s) we have the following: 

image of Decryption Function

The hashes of the resolved and imported functions appear as follows: 

image of imported functions

After using the debugger to step into the loops of the DecryptionFunction, we were able to find what functions the malware uses next.

This part of the executable almost works the same way through libraries and functions. I highly suggest looking at the disassembly line by line to understand the inner working of the Windows Internal Subsystem and API calls.

Another interesting trick to be even more stealthy is the use of stack strings to build calls to LoadLibraryA. The secret here is that, by definition, the CALL instruction pushes the next address onto the stack as the return address. But this address is an ASCII null terminated string that will be an argument for the next LoadLibraryA call. Here you can see how it loads two libraries: advapi32 and user32.

image of Decryption Loop

Immediately after resolving the imports, the malware sleeps for 10 seconds and then retrieves a filename via GetModuleFileNameA.

image of Get Module File Name A

Interestingly, the image above also shows how the code checks if its own name contains the string “sample” and if so consequently terminates itself. You can see how the call to the strstr function is built and how the previous push is given to check for the “sample” string.

It’s a simple anti-analysis technique that might easily catch you out. Protip: do not call your sample “sample”. 🙂

image of smoke loader anti-analysis feature

Next, the malware performs another check via GetVolumeInformationA, which is thoroughly documented in MSDN. Let’s look inside this call to understand its purpose.

image of Get Volume Information A

From the above disassembly, we can see that it retrieves the volume serial number and checks if it’s equal to some two serials. It then opens a registry key with RegOpenKeyExA, pushing one of the arguments with the same CALL technique. It then obtains the value of the registry key, closes the handle, and converts the value to lowercase before proceeding.

image of convert to lower case

It looks clear when you see it in the debugger.

image of smoke loader in IDA PRO 3

With this string saved somewhere in memory, the code goes on to perform some other checks trying to find any sign of running inside a virtual environment. 

image of Check for virtual environment

As part of the anti-virtual machine checks, it initializes a 4 cycle loop; during this loop it performs a call to the strstr function to search inside the retrieved registry value for any sign of the strings: “qemu”, “virtual”, “vmware”, “xen”. If you notice in the previous debugger screenshot, I’m running the sample inside a VMWare machine, so to continue I will need to patch the return value of strstr function calls to return zero.

Other checks are waiting:

image of Get Module Handle A

As you can see, the malware tries to understand if it’s being debugged or executed inside a sandbox by trying to get a handle to modules sbiedll and dbghelp. If it’s able to detect one of these two libraries, it terminates the process and exits. 

Finally, The Payload!

Having passed all sorts of anti-analysis and anti-debugging checks, we finally reach the payload! Now, the malware begins to reveal its secrets in memory.

image of payload in memory

We can clearly see it’s a PE file, but it’s scrambled somehow. This code will be decoded and managed in memory with a complex routine.

image of obfuscated payload

Digging into this code will require more time and effort than the analyst will normally want to expend. Instead, we can detonate the malware in our isolated environment and observe its execution. As we will see in the next post, this will reveal that a new instance of svchost.exe is loaded into memory, which suggests some sort of process injection. If you enjoyed this deep dive and would like to know when the next Going Deep post is available, just subscribe to the SentinelOne blog newsletter!

IOCs

Sample Hash 07e81dfc0a01356fd96f5b75efe3d1b1bc86ade4

MITRE ATT&CK

Smoke Loader {S0226}
Virtualization/Sandbox Evasion {T1497}