Dynamically Retrieving System Call (syscall) Leveraging PTEs

Locate instantiated PTE by leaking the base address and dynamically using read primitive to retrieve the syscall id

It is well known that different system versions have different system call numbers that enter the kernel. Before manually rewriting functions, it was inevitable to hard-code the call numbers. The technique I'm going to show you is inspired by a blog written by the guys over at crowdstrike. The part entitled "Modern Mitigation #1: Page Table Randomization" kinda ruined what I had planned for the night is shown below.

Here is an excerpt from that very blog.

As some of you might know after the windows 1607 version, PTE carried out randomized base address processing.

However, a certain fairy security researcher disclosed in blackhat that the instantiated PTE can be obtained through the nt!MiGetPteAddress function (which is super cool). Through this idea, I think the same can be applied to syscall.

Now I'm not going to do anything crazy just something cool. As all of you know syscall id is directly hard-coded in ntdll.dll.

Here is the plan:

  1. Get the functions in ntdll through GetProcAddress.
  2. Read the function offset 0x04 to get the system call number
  3. Edit the function template and fill in the call number
  4. Write function pointers to call function templates

As a reminder, we can easily see the syscall IDs for NT functions via WinDBG or any other debugger. The syscall ID is 2 bytes in length and starts 4 bytes into the function, so for example, the syscall ID for NtCreateProcess is 0x00B5.

Also - in green are the bytes, that I refer to as syscall stub for NtCreateProcess and these are the bytes that we want to be able to retrieve at run-time for any given NT function, and hence this lab. As we'll see utilizing an arbitrary read primitive, it is possible to extract the base of the page table entries utilizing this technique. With the base of the PTEs in hand, the aforementioned trivial calculation primitive remains valid

Different from the page table, ntdll can also directly parse the PE format to obtain the call number.

Because I am kinda lazy, ill only be writing code that does a dynamic memory read. Personally for getting a valid POC this is always a good reference. And this is how its done.

#include <Windows.h>
#include <stdio.h>
#include <tchar.h>
#pragma comment(linker, "/section:.data,RWE")//.data section executable

CHAR FuncExample[] = {
	0x4c,0x8b,0xd1,		 //mov r10,rcx
	0xb8,0xb9,0x00,0x00,0x00, //mov eax,0B9h
	0x0f,0x05,		//syscall
	0xc3	       //ret
};

typedef NTSTATUS(NTAPI* pNtAllocateVirtualMemory)(//Function pointer
	HANDLE ProcessHandle,
	PVOID* BaseAddress,
	ULONG_PTR ZeroBits,
	PSIZE_T RegionSize,
	ULONG AllocationType,
	ULONG Protect);


DOUBLE GetAndSetSysCall(TCHAR* szFuncName) {
	DWORD SysCallid = 0;
	HMODULE hModule = GetModuleHandle(_T("ntdll.dll"));
	DWORD64 FuncAddr = (DWORD64)GetProcAddress(hModule, (LPCSTR)szFuncName);
	LPVOID CallAddr = (LPVOID)(FuncAddr + 4);
	ReadProcessMemory(GetCurrentProcess(), CallAddr, &SysCallid, 4, NULL);
	memcpy(FuncExample + 4, (CHAR*)&SysCallid, 2);
	return (DOUBLE)SysCallid;
}

int main() {
	LPVOID Address = NULL;
	SIZE_T uSize = 0x1000;
	DOUBLE call = GetAndSetSysCall((TCHAR*)"NtAllocateVirtualMemory");
	pNtAllocateVirtualMemory NtAllocateVirtualMemory = (pNtAllocateVirtualMemory)&FuncExample;
	NTSTATUS status = NtAllocateVirtualMemory(GetCurrentProcess(), &Address, 0, &uSize, MEM_COMMIT, PAGE_READWRITE);
	return 0;

}

I'll leave leveraging this in a cool way as an exercise to the reader.