Analysis of a self-debugging Sirefef cryptor
Recently I wrote a blog post about a legitimate website spreading Sirefef malware. Then I continued with a deeper analysis and noticed that it uses an interesting cryptor.
Malware authors spread many new variants of malware every day. These variants often look completely different at the first glance. That's why regular updates of your antivirus is important. However, when we look deeper into most malware spreading these days, we see that the core functions do not change very often. Most of the variability of today's malware is caused by encapsulating it by so-called "cryptors."
In most cases, these cryptors are pretty boring pieces of software. They usually take seemingly random data from the malicious file, reshuffle them in a correct way, so that these bytes then become an executable code, and then they execute them. However, authors of Sirefef malware often come up with more interesting methods of loading their programs, and we will look at their method in this blog post.
Now, let's get to Sirefef. Soon after it is executed, we can see the following scheme.
The applications tries to determine whether it is being run under a debugger by calling IsDebuggerPresent. If yes, it chooses the left branch (load_untfs_library).
In computer security, a "debugger" is the name for a computer program that is used to test and debug other programs. We call the debugged program a "target" program. It is usually a third party program, in which you can load any other program and observe its instruction flow. In this branch, it loads untfs.dll library (LoadLibraryW) from windows/system32 directory, then it gets an address of a function with the ordinal number 0x2302 (by calling function LdrGetProcedureAddress) and calls it.
Untfs.dll is a legitimate part of the Windows operating system. It's name is derived from "NTFS Utility Library" and it contains functions to work with NTFS file system. If we look at untfs.dll library, we can notice that there are functions with ordinals from 0x00 to 0x96 and no function with ordinal 0x2302.
And really, when run in a regular debugger, LdrGetProcedureAddress returns in register EAX error message 0xC0000138, which is STATUS_ORDINAL_NOT_FOUND. The left branch then finishes, and the whole application ends.
If an application is not run in a debugger (which is typical for most of the cases), it chooses the right branch (procedure execute_debugger, see figure below). At the beginning, we notice a call of DbgUiConnectToDbg and later in procedure create_debuggee CreateProcessW with dwCreationFlags = 0x2000001, where 0x0000001 is DEBUG_PROCESS and 0x02000000 is CREATE_PRESERVE_CODE_AUTHZ_LEVEL.
According to the documentation, a newly created child process will be executed without process restrictions that would normally be applied and it will also be debugged by the parent process, which executed it. The parent process therefore becomes a debugger of the target process.
lpCommandLine and lpApplicationName parameters are passed to execute_debugger and then to create_debuggee and they contain a path to the originally executed file, so as long as Sirefef gets executed, it becomes debugger, which then executes itself again as debuggee( target ).
Sirefef malware runs in two instances: Once as a debugger, once as a target application. In the figure above, you can see the screenshot from Process Explorer run on an infected machine. It displays two processes, a parent (debugger) with process ID 1548 and a child process (target) with process ID 2740.
Then the debugger starts cycling in a loop, which waits for and processes debug events. See the decision logic in the figure below.
There are several types of events which debugger listens to and processes. First, CREATE_PROCESS_DEBUG_EVENT is received just after the start of the target process, then LOAD_DLL_DEBUG_EVENT is called after loading each imported dll library into memory space of the target process. EXCEPTION_DEBUG_EVENT is received when exception, interrupt or previously set breakpoint is hit. The figure below shows excerpt of code which is run while processing LOAD_DLL_DEBUG_EVENT.
In function "process_load_dll_debug_event" ZwQueryInformationFile is called to get name of the library currently being loaded into the target process. Notice addresses 0x401723 where the address of an unicode string "untfs.dll" is pushed onto the stack, 0x401734 where dll library name is compared with untfs.dll. If equal (if currently loaded library is untfs.dll), then at 0x401769 a procedure setting trap flag in the target process is called. The trap flag instructs the target process to run in a single-step mode, i.e. it will execute just one instruction and then it will interrupt again. After untfs.dll library is loaded, the target process executes just one more instruction (because the trap flag is set) and then EXCEPTION_DEBUG_EVENT is received by debugger. Because trap flag is set, SINGLE_STEP exception with ExceptionCode 0x80000004 is received. DecryptPE and ZwUnmapViewOfSection are called.
In function "decryptPE", notice "AP32" marker, followed by "M8Z8" signature, which is the beginning of an executable image packed with aPlib compression library. Function "decryptPE" takes a block of memory starting with AP32 marker, reads a few following constants (header size, ...) and unpacks it.
After the call to "decryptPE" is finished, ZwUnmapViewOfSection unloads untfs.dll library from target process address space and later a call to ZwMapViewOfSection maps our unpacked (by aPlib) code into the target process at the same address untfs.dll was mapped to. Therefore, the debugger process waits for the target process to load untfs.dll library, and when loaded, it replaces it with its own code.
Before the debugger finishes processing of EXCEPTION_DEBUG_EVENT, it must set EAX register to value 0x40000003, which according to documentation is STATUS_IMAGE_NOT_AT_BASE.
If an executable image or dll library is loaded into a process address space and STATUS_IMAGE_NOT_AT_BASE is returned in EAX register, it tells the Windows loader that the library was assigned an address which is different from the preferred library address and relocation must be done. Relocation is a process of adjusting the code and data in the program to reflect the assigned addresses.
The debugger then keeps running. It listens and processes debug events passed from the target process. Meanwhile, what is going on in the target process? As mentioned in the beginning of this post, it checks the presence of the debugger. Unlike the run of the first instance of Sirefef malware, the second instance is run with intention to be debugged by the first instance of malware, so the left branch is chosen. Then LoadLibraryW loads untfs.dll. When untfs.dll library is loaded, the debugger sets the trap flag, then one more instruction is executed, debugger then unloads untfs.dll, replaces it with its own code and returns in EAX notification that a new image needs to be relocated. Windows loader then relocates the new image to the assigned address. LdrGetProcedureAddress now tries to get the address of procedure with ordinal 0x2302. Without the debugger, the error STATUS_ORDINAL_NOT_FOUND would be returned. However, the debugger replaces untfs.dll with another code, which returns an address of a function with ordinal 0x2302.
Therefore by the above mentioned process, another (3rd) stage of Sirefef malware was stealthily loaded and executed in memory. This stage is finally a clean version of the malware dropper, which installs the following modules into the compromised system.
To make an analysis more difficult, malware authors sometimes choose nonstandard methods of loading and executing their programs. At the first glance, it seemed that our analyzed sample would end with an error because of the desired function was not found. Our detailed analysis showed that there was a stealthy way of replacing the legitimate library with a malicious one. All this was achieved by authors implementing a simple debugging loop which debugged the second instance of itself. This scenario is called self-debugging. During debugging the second instance, it made clever modifications of process context which caused the loading of a malicious payload.
1st stage 3CFF3A5394FEFBD3BF032AA70AE2D725783F931C4888CBC41AD56CB5C094A415 (1st cryptor)
2nd stage 450A9CEA55578C7A87BD778CEEA13A97EA6EFC7937AD1ABFF0CB5CFFAB709007 (cryptor with self-debugging)
3rd stage BE63DED08912ED0CB40605B3E33C2F1EFCE52BF1B7905E1DD1691CA6F2D8E9A1 (dropper)