Bypass ACG With UnmapViewOfFile

https://bugs.chromium.org/p/project-zero/issues/detail?id=1435&can=1&q=&start=1200 p0 研究者 ifratric 提出的一种绕过 ACG 保护的方案,即通过调用函数 UnmapViewOfFile 来预先修改内存块内容,从而在可执行页上加入攻击者自定义代码。

Remote JIT

开启了 ACG 保护之后进程将不能够申请未签名代码页。但是现代浏览器为了提高响应效率,都提供了 JIT 功能用于把 JS 代码编译成本地代码。该功能的实现依赖于在进程空间中动态的生成未签名的代码页。为了同时支持 ACG 和 JIT,Chakra 将其 JIT 功能移动到一个单独的进程中,该进程在其独立的沙箱中运行。JIT进程负责将 JavaScript 编译为本地代码并将其映射到请求进程。渲染进程本身不允许直接映射或修改其自己的JIT代码页。

JIT 地址管理

(由于笔者对于 ACG 的分析基于的 Edge 版本较老因此,可能有些地方不匹配)
远程分配的地址空间来源于本进程中的 PreReservedSectionAllocWrapper 对象,该对象在 JIT 进程启动时进行初始化,专门负责管理那些远程保留空间。对象的结构简单表示如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class PreReservedSectionAllocWrapper
{
BVStatic<PreReservedAllocationSegmentCount> freeSegments; //0x00 // 用于索引整块内存空间,检查内存状态
LPVOID preReservedStartAddress; //0x200 // 当前应该分配出去的地址
CriticalSection cs; //0x208
|- PRTL_CRITICAL_SECTION_DEBUG DebugInfo; //0x208
|- LONG LockCount; //0x210
|- LONG RecursionCount; //0x214
|- HANDLE OwningThread; //0x218 from the thread's ClientId->UniqueThread
|- HANDLE LockSemaphore; //0x220
|- ULONG_PTR SpinCount //0x228
HANDLE process; //0x230 // 远程进程句柄
HANDLE section; //0x238 // 远程地址 section 句柄
EnsurePreReservedRegionInternal(); // 用于检查是否还存在可管理的 RESERVE 属性内存,若不存在,则在分配之
};

其中 preReservedStartAddress 用于管理远程空间的可操作地址,这块空间调用函数 PreReservedSectionAllocWrapper::EnsurePreReservedRegionInternal 在远程进程中进行映射,映射之后远程进程中\这块地址的内存属性为 Mapped,可在远程进程中查看到。PreReservedSectionAllocWrapper 函数及调用栈如下。

1
2
3
4
5
00000090`871fea90 00007ff8`71b40580 chakra!Memory::PreReservedSectionAllocWrapper::EnsurePreReservedRegionInternal+0x36
00000090`871fead0 00007ff8`71d944f9 chakra!Memory::PreReservedSectionAllocWrapper::EnsurePreReservedRegion+0x50
00000090`871feb10 00007ff8`71d942f6 chakra!<lambda_59f4555218dfa2b0373b0fdca25e82c3>::operator()+0xb9
00000090`871feb40 00007ff8`7206252d chakra!ServerCallWrapper<<lambda_59f4555218dfa2b0373b0fdca25e82c3> >+0x56
00000090`871feb80 00007ff8`94c29083 chakra!ServerInitializeThreadContext+0x17d

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
__int64 __fastcall Memory::PreReservedSectionAllocWrapper::EnsurePreReservedRegionInternal(Memory::PreReservedSectionAllocWrapper *this)
{
Memory::PreReservedSectionAllocWrapper *v1; // rbx@1
HANDLE renderProcessHandle; // rdx@4
__int64 v4; // rax@5
ULONG v5; // [rsp+48h] [rbp-1h]@4
HANDLE hSection; // [rsp+50h] [rbp+7h]@3
PVOID v7; // [rsp+58h] [rbp+Fh]@1
LARGE_INTEGER sectionSize; // [rsp+60h] [rbp+17h]@3
SIZE_T v9; // [rsp+68h] [rbp+1Fh]@4
OBJECT_ATTRIBUTES objAttrs; // [rsp+70h] [rbp+27h]@3
if (! this->preReservedStartAddress )
{
objAttrs.RootDirectory = 0i64;
objAttrs.ObjectName = 0i64;
hSection = 0i64;
sectionSize.QuadPart = (unsigned int)(dword_1807C9C54 << 24);
objAttrs.Length = 48;
objAttrs.Attributes = 512; // OBJ_KERNEL_HANDLE
_mm_storeu_si128((__m128i *)&objAttrs.SecurityDescriptor, 0i64);
if ( !(unsigned int)NtCreateSection(&hSection, 15i64, &objAttrs, &sectionSize, 64, 0x4000000, 0i64) )
{
renderProcessHandle = (HANDLE)*(this->process);
v9 = 0i64;
v5 = 1073741840;
if ( (unsigned int)NtMapViewOfSection(
hSection,
renderProcessHandle,
&v7,
0i64,
0i64,
0i64,
&v9,
2, // ViewUnmap
0,
*(_QWORD *)&v5)
|| (v4 = (__int64)v7) == 0 )
{
NtClose(hSection);
}
else
{
*(this->section) = hSection;
*(this->preReservedStartAddress) = v7;
}
}
}
return _guard_ss_common_verify_stub();
}

实际进行 JIT 代码编译时,JIT 进程通过 PreReservedSectionAllocWrapper 获取之前映射的地址,使用 VirtualAllocEx 将 Render 进程中之前 reserverd 的地址空间 commit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
void *__fastcall Memory::PreReservedSectionAllocWrapper::Alloc(Memory::PreReservedSectionAllocWrapper *this, void *a2, SIZE_T a3, int a4)
{
Memory::PreReservedSectionAllocWrapper *v4; // rdi@1
struct _RTL_CRITICAL_SECTION *v5; // rbp@1
__int64 v6; // rdx@1
bool v7; // r8@1
signed __int64 v8; // rsi@2
unsigned __int64 v9; // r15@2
void *v10; // rbx@3
unsigned int v12; // eax@8
char v13; // [rsp+30h] [rbp-48h]@5
__int64 v14; // [rsp+38h] [rbp-40h]@1
Memory::PreReservedSectionAllocWrapper *v15; // [rsp+40h] [rbp-38h]@1
void *lpAddress; // [rsp+88h] [rbp+10h]@1
SIZE_T dwSize; // [rsp+90h] [rbp+18h]@1
int v18; // [rsp+98h] [rbp+20h]@1
v18 = a4;
dwSize = a3;
lpAddress = a2;
v14 = -2i64;
v4 = this;
v5 = (struct _RTL_CRITICAL_SECTION *)((char *)this + 520);
v15 = (Memory::PreReservedSectionAllocWrapper *)((char *)this + 520);
EnterCriticalSection((LPCRITICAL_SECTION)this + 13);
if ( !Memory::PreReservedSectionAllocWrapper::EnsurePreReservedRegionInternal(v4, v6, v7) )
{
LABEL_14:
v10 = 0i64;
goto LABEL_7;
}
LODWORD(v8) = -1;
v9 = dwSize / (unsigned int)(dword_180744D14 << 12);
if ( !lpAddress )
{
while ( 1 )
{
v12 = BVStatic<4096>::GetNextBit(v4, (unsigned int)(v8 + 1));
LODWORD(v8) = v12;
if ( 4096 - v12 < v9 || v12 == -1 )
goto LABEL_14;
if ( (unsigned __int8)BVStatic<4096>::TestRange(v4, v12, (unsigned int)v9) )
{
v10 = (void *)(*((_QWORD *)v4 + 64) + (unsigned int)((_DWORD)v8 * dword_180744D14 << 12));
goto LABEL_4;
}
}
}
v10 = lpAddress;
v8 = (_QWORD)((_QWORD)lpAddress - *((_QWORD *)v4 + 64)) / (unsigned int)(dword_180744D14 << 12);
LABEL_4:
if ( v18 & 0x1000 )
{
Memory::AutoEnableDynamicCodeGen::AutoEnableDynamicCodeGen((Memory::AutoEnableDynamicCodeGen *)&v13, 1);
v10 = VirtualAllocEx(*((HANDLE *)v4 + 70), v10, dwSize, 0x1000u, 0x40000010u);
Memory::AutoEnableDynamicCodeGen::~AutoEnableDynamicCodeGen((Memory::AutoEnableDynamicCodeGen *)&v13);
}
if ( !lpAddress && v10 )
BVStatic<4096>::ClearRange(v4, (unsigned int)v8, (unsigned int)v9);
LABEL_7:
LeaveCriticalSection(v5);
return v10;
}

Commit 远程地址空间时的调用栈如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
0:003> k
Child-SP RetAddr Call Site
00000012`02afd5a0 00007ff8`7bffc22d chakra!Memory::PreReservedSectionAllocWrapper::Alloc+0x48
00000012`02afd620 00007ff8`7bffbacc chakra!Memory::PageSegmentBase<Memory::PreReservedSectionAllocWrapper>::AllocDecommitPages<BVStatic<272>,1>+0x111
00000012`02afd6c0 00007ff8`7bffbbfa chakra!Memory::PageAllocatorBase<Memory::PreReservedSectionAllocWrapper,Memory::SegmentBase<Memory::PreReservedSectionAllocWrapper>,Memory::PageSegmentBase<Memory::PreReservedSectionAllocWrapper> >::TryAllocDecommittedPages<1>+0xa4
00000012`02afd780 00007ff8`7bffac57 chakra!Memory::PageAllocatorBase<Memory::PreReservedSectionAllocWrapper,Memory::SegmentBase<Memory::PreReservedSectionAllocWrapper>,Memory::PageSegmentBase<Memory::PreReservedSectionAllocWrapper> >::SnailAllocPages<1>+0x52
00000012`02afd830 00007ff8`7bffa6d1 chakra!Memory::CustomHeap::Heap<Memory::SectionAllocWrapper,Memory::PreReservedSectionAllocWrapper>::AllocNewPage+0xa3
00000012`02afd8d0 00007ff8`7bffa115 chakra!Memory::CustomHeap::Heap<Memory::SectionAllocWrapper,Memory::PreReservedSectionAllocWrapper>::Alloc+0xf9
00000012`02afd990 00007ff8`7bffa21b chakra!EmitBufferManager<Memory::SectionAllocWrapper,Memory::PreReservedSectionAllocWrapper,CriticalSection>::NewAllocation+0x59
00000012`02afda20 00007ff8`7bff9f9c chakra!EmitBufferManager<Memory::SectionAllocWrapper,Memory::PreReservedSectionAllocWrapper,CriticalSection>::AllocateBuffer+0x67
00000012`02afda80 00007ff8`7c11d872 chakra!JITOutput::RecordOOPNativeCodeSize+0x78
00000012`02afdaf0 00007ff8`7c1aeaef chakra!Encoder::Encode+0x4d2
00000012`02afdc90 00007ff8`7c1b11c4 chakra!Func::TryCodegen+0x2ef
00000012`02afe580 00007ff8`7bff9b28 chakra!Func::Codegen+0xf8
00000012`02afe9b0 00007ff8`7bff92d2 chakra!ServerContextManager::CheckLivenessAndAddref+0x1e8
00000012`02afead0 00007ff8`7bff9259 chakra!ServerRemoteCodeGen+0x162
00000012`02afeb10 00007ff8`7bff91ee chakra!ServerRemoteCodeGen+0xe9
00000012`02afeb80 00007ff8`886d0053 chakra!ServerRemoteCodeGen+0x7e
00000012`02afebf0 00007ff8`887389b8 RPCRT4!Invoke+0x73
00000012`02afec50 00007ff8`88731041 RPCRT4!Ndr64StubWorker+0xbe8
00000012`02aff320 00007ff8`886b193f RPCRT4!NdrServerCallNdr64+0x41
00000012`02aff370 00007ff8`886b09a6 RPCRT4!DispatchToStubInCNoAvrf+0x2f
00000012`02aff3c0 00007ff8`886b151c RPCRT4!RPC_INTERFACE::DispatchToStubWorker+0x1c6
00000012`02aff490 00007ff8`8869c3a6 RPCRT4!RPC_INTERFACE::DispatchToStubWithObject+0x15c
00000012`02aff530 00007ff8`8869d00a RPCRT4!LRPC_SCALL::DispatchRequest+0x186
00000012`02aff610 00007ff8`886a090b RPCRT4!LRPC_SCALL::HandleRequest+0x8ba
00000012`02aff720 00007ff8`886a2220 RPCRT4!LRPC_ADDRESS::HandleRequest+0x2fb
00000012`02aff7d0 00007ff8`886a3740 RPCRT4!LRPC_ADDRESS::ProcessIO+0x8d0
00000012`02aff910 00007ff8`8a319aec RPCRT4!LrpcIoComplete+0xe0
00000012`02aff9b0 00007ff8`8a317a37 ntdll!TppAlpcpExecuteCallback+0x26c
00000012`02affa20 00007ff8`882e66cd ntdll!TppWorkerThread+0x407
00000012`02affdd0 00007ff8`8a32666c KERNEL32!BaseThreadInitThunk+0x1d
00000012`02affe00 00000000`00000000 ntdll!RtlUserThreadStart+0x2c

该过程完成之后,远程地址空间被 commit ,空间属性被设置为 PAGE_EXECUTE、MEM_PRIVATE

实际写入数据时,调用函数 Memory::PreReservedSectionAllocWrapper::AllocLocalPreReservedSectionAllocWrapper 对象中的 section 在当前 JIT 进程中再映射一份。将生成的 JIT 代码通过内存映射的方式写入 Render 进程中。

1
2
3
Encoder::Encode
|- CodePageAllocators<Memory::SectionAllocWrapper,Memory::PreReservedSectionAllocWrapper>::AllocLocal //将远程地址空间映射进本地空间
|- EmitBufferManager<CriticalSection>::CommitBuffer //将编译好的代码写入映射的本地空间中,这样代码便会同时写入远程地址空间
AllocLocal
1
AllocLocal -> Memory::AllocLocalView -> NtMapViewOfSection // 将远程地址空间映射到本地
CommitBuffer
1
CommitBuffer -> memcpy // 将编译好的数据拷贝到映射的本地空间中

Bypass ACG

通过前面的分析可以看到 Render 进程中的动态代码段的地址实际上在进程初始化阶段就已经确定了。 这段地址已经处于 Mapped 状态。

考虑如下情况

  1. 在 Render 进程中使用函数 UnmapViewOfFile 将这段 Mapped 状态的地址变为 unmapped
  2. 主动申请刚刚 unmap 的那块内存区域,将其访问属性设置为 Write,在这段内存中写入想要执行的 shellcode
  3. 那么在 JIT 进程端调用 VirtualAllocEx commit 这段区域时,尽管这段区域已经被分配了,但 VirtualAllocEx 依然会成功执行,并成功将这段内存空间的访问属性设置成 Exec,并且在第二步中写入的数据也将继续存留在内存中! 更有甚者,由于这段内存空间已经被 unmapped 了,因此在 JIT 进程端对内存映射的修改将不会影响 Render 进程中的数据!

下面笔者通过调试器实际进行测试。

Memory::PreReservedSectionAllocWrapper::Alloc 处下断点,运行一段会导致 JIT 的 js。 触发断点, JIT 进程中断在函数处,跟踪到调用 VirtualAllocEx ,此时的寄存器状态

1
2
3
4
5
6
7
8
9
10
11
0:001> r
rax=00000090867fd730 rbx=000001b88011e000 rcx=00000000000005fc
rdx=000001b88011e000 rsi=0000000000000001 rdi=000001b7cc423a38
rip=00007ff871b4049b rsp=00000090867fd700 rbp=000001b7cc423c40
r8=0000000000002000 r9=0000000000001000 r10=000001b7cc423fd0
r11=4000000000000000 r12=0000000000000000 r13=000001b7cc423eb0
r14=000001b880100000 r15=0000000000000000
iopl=0 nv up ei pl nz na pe nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
chakra!Memory::PreReservedSectionAllocWrapper::Alloc+0xcf:
00007ff8`71b4049b ff15df4f5c00 call qword ptr [chakra!_imp_VirtualAllocEx (00007ff8`72105480)] ds:00007ff8`72105480={EShims!NS_ACGLockdownTelemetry::APIHook_VirtualAllocEx (00007ff8`85843170)}

其中 rdx 为马上要被 Commit 的地址,此时我们转到 Render 进程中查看地址状态

1
2
3
4
5
6
7
8
9
10
11
12
0:001> !address 00000232c07be000
Usage: MappedFile
Base Address: 00000232`c07a0000
End Address: 00000232`d07a0000
Region Size: 00000000`10000000
State: 00002000 MEM_RESERVE
Protect: <info not present at the target>
Type: 00040000 MEM_MAPPED
Allocation Base: 00000232`c07a0000
Allocation Protect: 00000010 PAGE_EXECUTE
Mapped file name: PageFile

地址处于 Mapped 状态。主动 unmap 这段内存,再次查看内存状态

1
2
3
4
5
6
7
8
9
0:001> !address 00000232c07be000
Usage: Free
Base Address: 00000232`c07a0000
End Address: 00000232`d07a0000
Region Size: 00000000`10000000
State: 00010000 MEM_FREE
Protect: 00000001 PAGE_NOACCESS
Type: <info not present at the target>

主动申请这段内存为 Write

1
2
3
4
5
6
7
8
9
10
11
0:001> !address 00000232c07be000
Usage: <unknown>
Base Address: 00000232`c07b0000
End Address: 00000232`c07c1000
Region Size: 00000000`00011000
State: 00001000 MEM_COMMIT
Protect: 00000004 PAGE_READWRITE
Type: 00020000 MEM_PRIVATE
Allocation Base: 00000232`c07b0000
Allocation Protect: 00000004 PAGE_READWRITE

修改内存内容

1
0:001> ea 00000232c07be000 "Hacked"

回到 JIT 进程,执行 VirtualAllocEx

1
2
3
4
5
6
7
8
9
10
11
0:001> !address 00000232c07be000
Usage: <unknown>
Base Address: 00000232`c07be000
End Address: 00000232`c07c0000
Region Size: 00000000`00002000
State: 00001000 MEM_COMMIT
Protect: 00000010 PAGE_EXECUTE
Type: 00020000 MEM_PRIVATE
Allocation Base: 00000232`c07b0000
Allocation Protect: 00000004 PAGE_READWRITE

查看内存内容,此时我们写入的 shellcode 已经被设置成了 Execute

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0:001> db 00000232c07be000
00000232`c07be000 48 61 63 6b 65 64 00 00-00 00 00 00 00 00 00 00 Hacked..........
00000232`c07be010 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000232`c07be020 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000232`c07be030 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000232`c07be040 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000232`c07be050 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000232`c07be060 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000232`c07be070 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
0:001> !address 00000232c07be000
Usage: <unknown>
Base Address: 00000232`c07be000
End Address: 00000232`c07c0000
Region Size: 00000000`00002000
State: 00001000 MEM_COMMIT
Protect: 00000010 PAGE_EXECUTE
Type: 00020000 MEM_PRIVATE
Allocation Base: 00000232`c07b0000
Allocation Protect: 00000004 PAGE_READWRITE

这里需要注意的一点是,在 Render 进程中申请的内存边界一定不能正好处于 JIT 进程申请的内存块中,即不能出现下面所示的情况,否则 VirtialAllocEx 会抛出 ERROR_INVALID_ADDRESS 异常

1
2
3
4
5
6
------VirtialAllocEx-------
| |
--RenderAlloc--
| |

补丁

微软在上个月对这个漏洞进行了修补,针对 VirtualAllocEx 可能会将已经 commit 的对象再次 commit 的情况进行了修改,删除了 VirtualAllocExVirtualProtectEx 的调用。

补丁之后 JIT 进程将不会再调用 VirtualAllocEx ,从而切断了在 Render 进程中运行时动态申请可执行页的路径。

而由于将 VirtualProtectEx 的使用也删除了,因此为了提高内存使用率,在释放这段共享内存空间时调用函数 UnlockVirtualMemory 来完成 VirtualProtectEx(NO_ACCESS) 相同的功能。

去除了 VirtualAllocEx 的调用 Render 进程端可执行的内存便不会在 Alloc 函数中分配。由于使用了共享内存的关系,因此当同一个 Section 在 JIT 进程中 Commit 共享内存时(函数AllocLocalView),Render 进程也会同时 Commit 相应的内存空间。换而言之,之前的设计中主动在 Render 进程中 Commit 内存的操作是不必要的!

(这部分内容笔者不甚了解,因此只能从现象分析出表面的情况,关于共享内存更加深入的情况笔者还在学习中,如有错误还望指正)

总结

ACG 的相关分析笔者在 RS2 预览版阶段就已经进行了,在预览版本中 JIT 进程对于 Render 进程内存的控制还是全程使用 VirtualAllcEX,即先把内存 Commit 成 Execute,在写入数据时将内存访问修改为 Write,写入完成之后再修改成 Execute。在发布版中,内存操作改为了使用内存映射来实现。当时主观上就没有进行进一步的思考,没有想到内存映射过后的地址在 Render 进程中也是可以自由操控的,在 Render 进程获得了内存权限之后,可以对自己范围内的地址进行任意操作。主要的原因和之前通过 Duplicate 绕过 ACG 一样,还是对系统内部的概念理解不深刻和全面。

这个漏洞的主要原因在笔者看来是进程间协作时对内存映射没有一个同步管理机制造成的,一个进程中对内存映射进行修改之后另一个进程并不会得到通知仍使用之前的数据进行操作从而产生问题。像这种两个进程间协同作用的情况,可以针对协同作用的对象,协作时的通信方法等关键点进行重点思考。

References

[1] https://bugs.chromium.org/p/project-zero/issues/detail?id=1435&can=1&q=&start=1200
[2] https://github.com/Microsoft/ChakraCore/commit/7c5326f6e250a7c4d0133b0ddb032a212a198b8d?diff=unified#diff-75094ae78e18d086b1542e005af60052L961