Summary
Call stack spoofing isn’t a new technique, but it has become more popular in the last few years. Call stacks are a telemetry source for EDR software that can be used to determine if a process made suspicious actions (requesting a handle to the lsass process, writing suspicious code to a newly allocated area, and so on). The purpose of the technique is to construct a fake call stack that mimics a legitimate call stack in order to hide suspicious activity that might be detected by EDR or other security software.
Technical analysis
SHA256: 33FD050760E251AB932E5CA4311B494EF72CEE157B20537CE773420845302E49
When you want to create a file using Notepad, the call stack looks like in the following figure:
Please note the following order of operations:
1. CreateFileW (KernelBase.dll)
2. ZwCreateFile (ntdll.dll)
3. NtCreateFile (ntoskrnl.exe) -> Kernel space
Another important observation is that the process name “notepad.exe” is shown in the call stack.
The following call stack looks quite similar, however, the process name disappeared:
We’ll explain the implementation of the call stack spoofing technique that is used to call the NtCreateFile function presented above. The basic idea is to construct a “fake” stack that obscures the true origin of the function call. A malware called DodgeBox used by APT41 implemented the technique to trick the Antivirus and EDR software that rely on stack call analysis for detection.
Relevant strings are decrypted at runtime using the AES algorithm, with the key being hard-coded in the DLL:
The malicious process retrieves a handle to kernelbase.dll via a function call to LdrGetDllHandle (see Figure 5).
It traverses the “.text” section of the binary looking for the following bytes: 0xFF, 0x65, and 0x48. They correspond to the “jmp qword ptr [rbp+48]” JOP gadget, as detailed below.
The “.pdata” section has entries for all functions that allocate stack space. In the following paragraphs, we explain the steps of computing the unwind size for a function that contains the gadget.
The process extracts a DWORD from the “.pdata” section that is used to locate the RUNTIME_FUNCTION struct:
It also obtains the address of the Unwind codes array that contains multiple UNWIND_CODE structures having the following fields:
- Offset in prologue (1 byte)
- Unwind operation code (4 bits)
- Operation info (4 bits)
Figure 10 reveals an example of a value of the last two fields.
As we can see in the figure below, which presents multiple comparisons, the unwind operation code takes various values between 0 (UWOP_PUSH_NONVOL) and 10 (UWOP_PUSH_MACHFRAME):
Let’s take a simple example to show how it works. If the byte corresponding to the last two fields in the UNWIND_CODE structure is 0x72, then the operation code is 0x2 (UWOP_ALLOC_SMALL) and the operation info is 0x7. The code corresponds to an operation that allocates a small-sized area on the stack, with the size being equal to operation info * 8 + 8. Figure 12 displays the same example in ASM:
The unwind size is calculated by exhausting the UNWIND_CODE structures. The value must be higher than 0x88, as highlighted in the comparison below:
A total of 56 JOP gadgets are obtained using the method:
The GetTickCount64 API is utilized to retrieve the number of milliseconds that have elapsed since the system started. Based on this value, the process does a division and chooses which JOP gadget to use in the spoofing operation:
The binary extracts the address of the RtlUserThreadStart function by calling the LdrGetProcedureAddressEx method, as highlighted below:
In order to mimic the value from a legitimate stack call, the process adds 0x21 to the address and stores the resulting address on the stack (Figure 17).
The same process is repeated for BaseThreadInitThunk as well, however, the malware adds 0x14 to the address returned from the call:
Figure 19 reveals the instruction used to jump to the NtCreateFile function. Parameters are stored in RCX, RDX, R8, R9, and then in values stored on the stack.
NtCreateFile executes the 0x55 syscall and prepares to jump to the JOP gadget:
The return address was previously stored at RBP + 0x48 and is used to redirect the execution flow back to the malware’s code:
References
https://www.zscaler.com/blogs/security-research/dodgebox-deep-dive-updated-arsenal-apt41-part-1
https://labs.withsecure.com/publications/spoofing-call-stacks-to-confuse-edrs
I learned a lot from this post, great insights.