环境配置
准备
测试
PoC如下:
1 | <html> |
使用EdgeDbg启动edge,并直接指定URL,效果如下(当然在这里我们直接指定的是test.html的本地存放路径)
1 | H:\dev\C\EdgeDbg>EdgeDbg_x86.exe http://example.com |
然后在windbg里attach上MicrosoftEdgeCP.exe的pid即可。
在windbg里输入,设置符号服务器。
1 | .sympath SRV*c:\localsymbols*http://msdl.microsoft.com/download/symbols |
然后下断,输入g继续运行,观察是否断下,若顺利断下,则代表环境测试通过。
1 | bu chakra!Js::Math::Cosh |
Root Cause
PoC
1 | <html> |
UAF
UAF(Use After Free):即释放后使用。
内存释放后未将pointer置为NULL,变成Dangling pointer;将Dangling pointer所指向的内存重新分配回来,且尽可能使该内存中的内容可控。
[0]
创建JSArrayBuffer Object,并通过VirtualAlloc分配内存空间buffer,JSArrayBuffer Object存有指向其申请的缓冲区的引用
uf chakra!Js::ArrayBuffer::NewInstance
1 | 0:010> uf chakra!Js::ArrayBuffer::NewInstance |
uf chakra!Js::JavascriptArrayBuffer::Create
1 | 0:010> uf chakra!Js::JavascriptArrayBuffer::Create |
uf chakra!Js::JavascriptArrayBuffer::JavascriptArrayBuffer
1 | 0:010> uf chakra!Js::JavascriptArrayBuffer::JavascriptArrayBuffer |
uf Js::JavascriptArrayBuffer::AllocWrapper是VirtualAlloc的封装
1 | 0:010> uf Js::JavascriptArrayBuffer::AllocWrapper |
1 | 0:010> dqs 000001d9`15dc7e80 l1 |
[1]
创建JSTypedArray Object,存有指向JSArrayBuffer的引用和JSArrayBuffer Object申请的缓冲区的引用
bu chakra!Js::TypedArrayBase::CreateNewInstance
1 | 0:010> ub $ip |
[2]
- worker.postMessage(buffer,[buffer]);
移交buffer的所有权给worker线程 - worker.terminate();
结束worker线程,触发buffer的释放操作
chakra里这种结束线程并释放buffer的操作是我以前没有了解过的,而这也是这个漏洞的关键之一。
Free but no set NULL
将长度设置为0,但是未将View对ArrayBuffer Object对象申请的缓冲区的引用置NULL。
bu chakra!Js::ArrayBuffer::DetachAndGetState
1 | 0:010> bu chakra!Js::ArrayBuffer::DetachAndGetState |
call chakra!Js::ArrayBuffer::ClearParentsLength
- TypeId
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128enum TypeId
{
TypeIds_Undefined = 0,
TypeIds_Null = 1,
TypeIds_UndefinedOrNull = TypeIds_Null,
TypeIds_Boolean = 2,
// backend typeof() == "number" is true for typeIds
// between TypeIds_FirstNumberType <= typeId <= TypeIds_LastNumberType
TypeIds_Integer = 3,
TypeIds_FirstNumberType = TypeIds_Integer,
TypeIds_Number = 4,
TypeIds_Int64Number = 5,
TypeIds_UInt64Number = 6,
TypeIds_LastNumberType = TypeIds_UInt64Number,
TypeIds_String = 7,
TypeIds_Symbol = 8,
TypeIds_LastToPrimitiveType = TypeIds_Symbol,
TypeIds_Enumerator = 9,
TypeIds_VariantDate = 10,
// SIMD types
//[Please maintain Float32x4 as the first SIMDType and Bool8x16 as the last]
TypeIds_SIMDFloat32x4 = 11,
TypeIds_SIMDFloat64x2 = 12,
TypeIds_SIMDInt32x4 = 13,
TypeIds_SIMDInt16x8 = 14,
TypeIds_SIMDInt8x16 = 15,
TypeIds_SIMDUint32x4 = 16,
TypeIds_SIMDUint16x8 = 17,
TypeIds_SIMDUint8x16 = 18,
TypeIds_SIMDBool32x4 = 19,
TypeIds_SIMDBool16x8 = 20,
TypeIds_SIMDBool8x16 = 21,
TypeIds_LastJavascriptPrimitiveType = TypeIds_SIMDBool8x16,
TypeIds_HostDispatch = 22,
TypeIds_WithScopeObject = 23,
TypeIds_UndeclBlockVar = 24,
TypeIds_LastStaticType = TypeIds_UndeclBlockVar,
TypeIds_Proxy = 25,
TypeIds_Function = 26,
//
// The backend expects only objects whose typeof() === "object" to have a
// TypeId >= TypeIds_Object. Only 'null' is a special case because it
// has a static type.
//
TypeIds_Object = 27,
TypeIds_Array = 28,
TypeIds_ArrayFirst = TypeIds_Array,
TypeIds_NativeIntArray = 29,
#if ENABLE_COPYONACCESS_ARRAY
TypeIds_CopyOnAccessNativeIntArray = 30,
#endif
TypeIds_NativeFloatArray = 31,
TypeIds_ArrayLast = TypeIds_NativeFloatArray,
TypeIds_Date = 32,
TypeIds_RegEx = 33,
TypeIds_Error = 34,
TypeIds_BooleanObject = 35,
TypeIds_NumberObject = 36,
TypeIds_StringObject = 37,
TypeIds_SIMDObject = 38,
TypeIds_Arguments = 39,
TypeIds_ES5Array = 40,
TypeIds_ArrayBuffer = 41,
TypeIds_Int8Array = 42,
TypeIds_TypedArrayMin = TypeIds_Int8Array,
TypeIds_TypedArraySCAMin = TypeIds_Int8Array, // Min SCA supported TypedArray TypeId
TypeIds_Uint8Array = 43,
TypeIds_Uint8ClampedArray = 44,
TypeIds_Int16Array = 45,
TypeIds_Uint16Array = 46,
TypeIds_Int32Array = 47,
TypeIds_Uint32Array = 48,//---->0x30
TypeIds_Float32Array = 49,
TypeIds_Float64Array = 50,
TypeIds_TypedArraySCAMax = TypeIds_Float64Array, // Max SCA supported TypedArray TypeId
TypeIds_Int64Array = 51,
TypeIds_Uint64Array = 52,
TypeIds_CharArray = 53,
TypeIds_BoolArray = 54,
TypeIds_TypedArrayMax = TypeIds_BoolArray,
TypeIds_EngineInterfaceObject = 55,
TypeIds_DataView = 56,
TypeIds_WinRTDate = 57,
TypeIds_Map = 58,
TypeIds_Set = 59,
TypeIds_WeakMap = 60,
TypeIds_WeakSet = 61,
TypeIds_SymbolObject = 62,
TypeIds_ArrayIterator = 63,
TypeIds_MapIterator = 64,
TypeIds_SetIterator = 65,
TypeIds_StringIterator = 66,
TypeIds_JavascriptEnumeratorIterator = 67, /* Unused */
TypeIds_Generator = 68,
TypeIds_Promise = 69,
TypeIds_SharedArrayBuffer = 70,
TypeIds_WebAssemblyModule = 71,
TypeIds_WebAssemblyInstance = 72,
TypeIds_WebAssemblyMemory = 73,
TypeIds_WebAssemblyTable = 74,
TypeIds_LastBuiltinDynamicObject = TypeIds_WebAssemblyTable,
TypeIds_GlobalObject = 75,
TypeIds_ModuleRoot = 76,
TypeIds_LastTrueJavascriptObjectType = TypeIds_ModuleRoot,
TypeIds_HostObject = 77,
TypeIds_ActivationObject = 78,
TypeIds_SpreadArgument = 79,
TypeIds_ModuleNamespace = 80,
TypeIds_ListIterator = 81,
TypeIds_Limit //add a new TypeId before TypeIds_Limit or before TypeIds_LastTrueJavascriptObjectType
};TypeIds_Uint32Array = 48,//---->0x30
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
510:010> uf chakra!Js::ArrayBuffer::ClearParentsLength
chakra!Js::ArrayBuffer::ClearParentsLength:
00007ffa`d30d88cc 4889542410 mov qword ptr [rsp+10h],rdx
00007ffa`d30d88d1 48894c2408 mov qword ptr [rsp+8],rcx
00007ffa`d30d88d6 4883ec28 sub rsp,28h
00007ffa`d30d88da 488b4c2438 mov rcx,qword ptr [rsp+38h]
00007ffa`d30d88df 4885c9 test rcx,rcx---->if TypedArrayBuffer==NULL,则直接return
00007ffa`d30d88e2 742d je chakra!Js::ArrayBuffer::ClearParentsLength+0x45 (00007ffa`d30d8911) Branch-->return
chakra!Js::ArrayBuffer::ClearParentsLength+0x18:
00007ffa`d30d88e4 488bc1 mov rax,rcx
00007ffa`d30d88e7 48c1e830 shr rax,30h
00007ffa`d30d88eb 4883f801 cmp rax,1----->如果是一个small int值,那么其右移48位得到的值应该是1,不是TypedArray Object,则直接return
00007ffa`d30d88ef 7420 je chakra!Js::ArrayBuffer::ClearParentsLength+0x45 (00007ffa`d30d8911) Branch --->return
chakra!Js::ArrayBuffer::ClearParentsLength+0x25:
00007ffa`d30d88f1 e8fa8cd4ff call chakra!Js::JavascriptNumber::Is_NoTaggedIntCheck (00007ffa`d2e215f0)
00007ffa`d30d88f6 84c0 test al,al----->如果是一个NoTaggedInt值,不是TypedArray Object,则直接return
00007ffa`d30d88f8 7517 jne chakra!Js::ArrayBuffer::ClearParentsLength+0x45 (00007ffa`d30d8911) Branch---->return
chakra!Js::ArrayBuffer::ClearParentsLength+0x2e:
00007ffa`d30d88fa 488b4108 mov rax,qword ptr [rcx+8]
00007ffa`d30d88fe 83382a cmp dword ptr [rax],2Ah //小于0x2A即42,即非TypedArray,则return
00007ffa`d30d8901 7c0e jl chakra!Js::ArrayBuffer::ClearParentsLength+0x45 (00007ffa`d30d8911) Branch //-->return
chakra!Js::ArrayBuffer::ClearParentsLength+0x37:
00007ffa`d30d8903 833836 cmp dword ptr [rax],36h //大于0x2A(42),小于等于0x36(54),则将长度置为0
TypeIds_Uint8Array = 43,
TypeIds_Uint8ClampedArray = 44,
TypeIds_Int16Array = 45,
TypeIds_Uint16Array = 46,
TypeIds_Int32Array = 47,
TypeIds_Uint32Array = 48,//---->0x30
TypeIds_Float32Array = 49,
TypeIds_Float64Array = 50,
TypeIds_TypedArraySCAMax = TypeIds_Float64Array, // Max SCA supported TypedArray TypeId
TypeIds_Int64Array = 51,
TypeIds_Uint64Array = 52,
TypeIds_CharArray = 53,
TypeIds_BoolArray = 54,
00007ffa`d30d8906 7e05 jle chakra!Js::ArrayBuffer::ClearParentsLength+0x41 (00007ffa`d30d890d) Branch---->跳转
...
...
chakra!Js::ArrayBuffer::ClearParentsLength+0x41:
00007ffa`d30d890d 83612000 and dword ptr [rcx+20h],0----->将长度设置为0,但是未将View对ArrayBuffer Object对象申请的缓冲区的引用置NULL。
chakra!Js::ArrayBuffer::ClearParentsLength+0x45:
00007ffa`d30d8911 4883c428 add rsp,28h
00007ffa`d30d8915 c3 ret
将长度设置为0
对应到ClearParentsLength源码中
1 | void ArrayBuffer::ClearParentsLength(ArrayBufferParent* pare nt) |
注意此时其实ArrayBuffer Object所分配的buffer已经被释放了,所以可以被我们重新分配出来,占位。
Patch
指针
https://github.com/Microsoft/ChakraCore/commit/1ae7e3ce95515758b4cd7215cb4e48539a0f4031
patch就是将未置为NULL的指针置为NULL了
1 | + void TypedArrayBase::ClearLengthAndBufferOnDetach() |
虚表
从patch里找一个case
1 | case TypeIds_Int32Array: |
注意到patch里有这样的代码,目的是什么呢?
SetVirtualTable将虚表由Int32VirtualArray修改为Int32Array。
而我们知道0234触发的条件是如下代码,注意IsLikelyOptimizedVirtualTypedArray
1 | if (baseValueType.IsLikelyOptimizedVirtualTypedArray() && !Js::IsSimd128LoadStore(instr->m_opcode) /*Always extract bounds for SIMD */) |
这个Patch的目的仍然和0234有关,将漏洞联系起来看。
这样去修改虚表我测试了一下,暂时没找到什么可能引入的安全问题。
开发者的Assumption
即使存在对free了的内存的引用,由于MemGC,并不会直接造成UAF,因为不可占位。而且将length置为0,就不存在可以继续操作这个缓冲区的的可能。
- 在开发者的假设里,MemGC对引用进行扫描,从而不释放仍有引用指向的缓冲区,可以很好的缓解UAF,但是通过控制ArrayBuffer的长度,我们可以让它使用VirtualAlloc分配,而不是GC。于是就没有上述的检查。
- 其实不把指针置为NULL这种写法理论上并不一定能造成影响,因为我们已经把length置为0了,理论上说我们已经无法控制分配的缓冲区了,无法读写。
但是由于利用0234,可以在JIT时消除上界下界,于是我们就有了一个越界读写,可以打破length等于0给我们造成的限制。
Pattern
这个漏洞的发现是由0234逐次引入的,要找到一个UAF,首先要定位到一个可控对象的释放操作,这也是我不熟知的一个点。
还有就是在其中找到直接将length置为0,就“不可操作”了,这种释放方式。
从该函数的其他case可以看到dataview也是这么操作的,删除Length但是不置NULL,但是由于无法触发JIT的消除边界优化,所以更难以利用了。(也被一起补了)
1 | case TypeIds_DataView: |
总结
0234(OOB)触发的条件(由有限到推广)
有限 | 推广 |
---|---|
ArrayBuffer(0x10000) | 需要一个大于0x10000大小的buffer,这样才会让TypedArray的虚表类型是VirtualArray,从而触发JIT优化 |
Uint32Array(buffer) | 单个element size大于1字节的VirtualTypedArray(即除了Uint8Array和Int8Array) |
循环次数足够大,触发JIT优化(循环体内是对数组的赋值,在优化后去掉边界检查) | 循环次数足够大,触发JIT优化(循环体内是对数组的赋值,在优化后去掉边界检查) |
0236(UAF)触发的条件(由有限到推广)
有限 | 推广 |
---|---|
ArrayBuffer(0x10000) | 需要一个大于0x10000大小的buffer,使得通过VirtualAlloc分配,从而绕过MemGC的引用计数,延迟释放,才能引发UAF |
Uint32Array(buffer) | ArrayBuffer的一个parent对象。detach之后在其中仍然保留一个指向缓冲区的引用,从而造成UAF,注意这里不仅TypedArray可以UAF,DateView也可以。但是DataView无法和0234结合使用,不满足IsLikelyOptimizedVirtualTypedArray |
worker.postMessage(buffer,[buffer]);worker.terminate(); | 进行Detach,触发UAF |
交集
关于虚表的部分补充在上面patch第二部分。
1 | ArrayBuffer(0x10000) |
虽然PoC里这部分相同,但是目的却各不相同。
ArrayBuffer(0x10000)在0236的主要目的是绕过MemGC的UAF缓解机制
而在0234,目的则是由于通过VA分配,update虚表为VirtualArray,从而触发JIT优化。
1 | if (baseValueType.IsLikelyOptimizedVirtualTypedArray() && !Js::IsSimd128LoadStore(instr->m_opcode) /*Always extract bounds for SIMD */) |
update虚表操作如下:
1 | chakra!Js::TypedArray<unsigned int,0,0>::TypedArray<unsigned int,0,0>+0xd1: |
源码如下:
1 | template <typename TypeName, bool clamped, bool virtualAllocated> |
而Uint32对于0234是为了OOB,对于0236是保留一个指向缓冲区的引用,UAF。
但是0236的PoC单独使用是没有意义的,因为ClearLength将TypedArray的length清零,无法控制内存。
但是通过0234的JIT优化,去掉了边界检查,从而可以修改缓冲区,发挥UAF的威力。
总结
我觉得必要条件和充分条件是很有趣和有用的想法。
这其中还有一种对抗与脆弱性的思维在里面,一个有价值的漏洞引入的很可能不是一个地方有问题,而是类似实现的地方都有问题,通过这个poc可以触发,patch之后能不能找到类似的实现或者因为没有补全找到更多触发,甚至由于patch的不好引发新的安全问题,都有可能。
除此之外,将漏洞联系起来看,而不是单纯的去看一个OOB或者单纯一个UAF,能够更深入的思考漏洞的本质,找到更多利用的想法。