Introduction

Hello, welcome to my blog. Today I’ll be discussing how we can log, monitor and if wanted, spoof the return value of a syscall. Typically malicious developers will try to bypass hooks made on Nt functions by an anticheat developer by directly calling the function via a syscall. The way this method works is by setting up a callback which is invoked directly after the kernel gets back to user-mode. By setting this up we can actually log almost all syscalls. This is really beneficial paired with a handful of well-written sanity checks. The reason I say this is because, alone, the callback will merely log, almost, ALL syscalls whether they’re legit or illegitimate all rely on the developer’s sanity checks. I’ll go into more detail in the next chapters about all of this information as well as more.

Syscalls

In Windows, all WinAPI functions go through a call stack similar to this our_application.exe->kernel32.dll->kernalbase.dll->ntdll.dll->syscall_invoked->kernel mode. As you can see, by using syscalls are indirectly telling the kernel land to execute code. Here is a little homebrewed image. As you can see from this image, by directly calling a syscall - malicious developers can basically bypass any hook placed on any of the previous functions (which is a common practice). Now, let’s see how we can combat this by using the Nirvana Debugging method. This places a callback when the kernel tries to relay information back to the user application. Here’s an updated image that will show where the callback will essentially sit. From this image, we can now see that instead of directly reporting back to the user-mode application and continuing execution, we go through a custom callback which gets invoked every time a syscall is fired!

Nirvana Debugging Explained

The way the Nirvana Debugging method works is by creating a hook that will call our callback with some parameters that we want to take a look into to create sanity checks to maintain the legitimacy of syscalls. The idea is useful to monitor direct syscalls which would otherwise be not possible in user mode. We use NtSetInformationProcess with the PROCESS_INFO_CLASS_INSTRUMENTATION (0x28 | 40) as the Process information class. We utilise an undocumented, but leaked, KPROCESS struct which is within kernel memory. Here’s how it looks.

typedef struct _PROCESS_INSTRUMENTATION_CALLBACK_INFORMATION
{
  ULONG Version;
  ULONG Reserved;
  PVOID Callback;
} PROCESS_INSTRUMENTATION_CALLBACK_INFORMATION, *PPROCESS_INSTRUMENTATION_CALLBACK_INFORMATION;
  • The version field refers to the architecture of the process we’re installing the hook on. 1 = x86; 0 = x64. For our PoC we’ll be using x64.
  • The reserved field is always 0.
  • The callback field is a pointer to our assembly function which will call our hook.

Setup

To get started, we’ll need to make sure our Visual Studio project can compile our .asm file. For this, we’ll simply tick the masm field in the build customisations & create a .asm file. Once our assembly file exists we’ll need to think what we want to do to use this method to find malicious syscalls. Ideally, we want to get the syscall pointer & return address. We can use both of these for heuristics & actually see what function is even invoking syscall! This is important as we’ll see later for some sanity checks. To begin with, since we want to store a value we’re going to want to create an 8-byte value within our data section, and within our code section, we’re going to create a function that will save registers, align stack, pass the parameters we want, call our hook, restore them after we’re done and set rax to the value returned from our function.

Our assembly function

include ksamd64.inc
EXTERN instrumentation_callback:proc ; our C function which is our actual hook

.data
	return_val QWORD 0

.code

instrumentation_callback_thunk proc
	; save registers
	push rax 
	push rcx
	push rbx
	push rbp
	push rdi
	push rsi
	push rsp

	sub rsp, 20h ; align stack
        mov rdx, rax
        mov rcx, r10
        call instrumentation_callback ; call hook with rax & r10
	mov QWORD PTR [return_val], rax ; set return value to what our function returned
	add rsp, 20h ; align stack

	; restore registers
	pop rsp
	pop rsi
	pop rdi
	pop rbp
	pop rbx
	pop rcx
	pop rax
	
	; set rax to what our function returned
	mov rax, QWORD PTR [return_val]
        ret
instrumentation_callback_thunk endp

end

Understanding our assembly function

We must understand how this works, as said before we’re simply preserving the registers, and after our hook is finished, we store the value returned from it and after restoring the registers to their original state we set rax to the value returned from our function. It’s not important to spoof the return value from the syscall. I’ve just added it in as a little bonus to show just how fascinating this entire project really is.

Creating our hook

This is the important part now, as you can see in the code;

mov rdx, rax
mov rcx, r10
call instrumentation_callback ; call hook with rax & r10

We need to create a function with two parameters being; r10 & rax. We’ll use the value within r10 to get the address of where the syscall was invoked and we’ll use rax to access what the function is returning, we can modify this or use it for an extra sanity check to combat malicious users attempting to spoof what syscall was actually called. Let’s begin with creating an empty function and setting up some really cheap anti-recursion methods to attempt to keep our hook thread safe.

bool disable_recurs = false;
unsigned __int64 instrumentation_callback(unsigned __int64 r10, unsigned __int64 rax) {
	if (!disable_recurs) {
		disable_recurs = true;
		
        // Checks written here

		disable_recurs = false;
		return rax;
	}
	return rax;
}

We’ll need to use extern “C” on both of our functions, the one we wrote in assembly and the one we wrote just above.

extern "C" void instrumentation_callback_thunk();
extern "C" unsigned __int64 instrumentation_callback(unsigned __int64 r10, unsigned __int64 rax);

Initialising our hook

Now that we’ve created our hook, we’ll need to use NtSetInformationProcess to assign our hook. But just before that, let’s create the structure and fill in the fields accordingly.

PROCESS_INSTRUMENTATION_CALLBACK_INFORMATION create_debugger() {
	PROCESS_INSTRUMENTATION_CALLBACK_INFORMATION nirvana_debugger;
	nirvana_debugger.Reserved = 0; 
	nirvana_debugger.Version = 0; // x64
	nirvana_debugger.Callback = instrumentation_callback_thunk; // our assembly function

	return nirvana_debugger;
}

VOID enable_debugger(PROCESS_INSTRUMENTATION_CALLBACK_INFORMATION nirvana_debugger) {
	NtSetInformationProcess(GetCurrentProcess(), PROCESS_INFO_CLASS_INSTRUMENTATION, &nirvana_debugger, sizeof(nirvana_debugger));
}

Just like that, if we were to have an output within our hook and we were to call any function really, you’d get a hit. This is because our hook is fully installed, after every syscall the kernel will interact with our callback before the original call.

Parsing information from the paramters

Now that we are given r10 & rax, let’s actually put these parameters to use and get our information from them. As we remember, r10 will have information regarding the actual function that invoked the syscall. We’ll use SymFromAddr(…) to get symbol information from the address. While we’re at it, let’s write a simple function which’ll traverse the function bytes and extract the syscall. We can then use rax to output what the kernel returned and to top it off, we’ll use r10 to get the return address for some extra debugging.

extract_syscall_from_fn

uint16_t extract_syscall_from_fn(unsigned __int64 start_address) {
	unsigned __int64 curr_byte_addr = start_address;

	while (*reinterpret_cast<byte*>(curr_byte_addr) != 0xC3) {
		
		if (*reinterpret_cast<DWORD*>(curr_byte_addr) == 0xb8d18b4c) { // mov r10, rcx
			return *reinterpret_cast<byte*>(curr_byte_addr + 4); // mov eax, id 
		}
		curr_byte_addr += 0x4;
	}
	return 0x0;
}

Our finished hook

bool disable_recurs = false;
unsigned __int64 instrumentation_callback(unsigned __int64 r10, unsigned __int64 rax) {
	if (!disable_recurs) {
		disable_recurs = true;

		CHAR buffer[sizeof(SYMBOL_INFO) + MAX_SYM_NAME] = { 0 };
		PSYMBOL_INFO pSymbol = (PSYMBOL_INFO)buffer;
		pSymbol->SizeOfStruct = sizeof(SYMBOL_INFO);
		pSymbol->MaxNameLen = MAX_SYM_NAME;
		DWORD64 Displacement;

		BOOLEAN result = SymFromAddr(GetCurrentProcess(), r10, &Displacement, pSymbol);
		if (result) {
			printf("[+] Func Called: %s\n[+] Syscall id : %x\n[+] Func returned: 0x%I64X\n[+] Func return address: 0x%I64X\n[-] Setting return\n", pSymbol->Name, syscalls::extract_syscall_from_fn(pSymbol->Address), rax, r10);
			rax = 0xDEEC59;		// Hehe, your return value is now dx9!	
		}
		disable_recurs = false;
		return rax;
	}

	return rax;
	
}

Do note, spoofing the return value isn’t necessary. This PoC was merely to show that it’s possible.

Our hook in action

Now that our hook is finished just for the bare debugging we’ll do as of now, let’s directly invoke a syscall and see how our hook reacts. Congratulations, the manual syscall was logged and even the output was spoofed. Now that we can catch all syscalls we can create some sanity checks. These all depend on the developer but since we have the return address we can check if the invoked syscall was from an actual nt function or if it was directly invoked through inline assembly outside of the respective nt function, if it was through inline assembly that isn’t from an nt function we can assume it was a malicious user trying to avoid a possible check an anti-cheat developer has written for any of the nt functions. A simple check could literally be bool was_nt_func = (pSymbol->Address == address_from_list_of_nt_functions);

Conclusion

In conclusion, we can tell just how powerful registering an instrumentation callback really is. It has allowed us to monitor almost all syscalls, manual or not - they’ll get logged. I hope you learned something new from this blog, there are insane amounts of sanity checks you could create in paired with this to really monitor a large number of function calls within your application.

References/Resources