Yuebin Sun(@yuebinsun2020 )
1 环境信息
Windows 10 X64 14393 (1607) - 没有安装过任何补丁
Microsoft Edge 38.14393.0.0
Microsoft EdgeHTML 14.14393
2 Crash Point 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 (1648.f78): Access violation - code c0000005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. chakra!Js::DynamicProfileInfo::RecordCallSiteInfo+0x75: 00007ff9`dc43d0c5 66418500 test word ptr [r8],ax ds:000001e5`b6a4f048=???? 0:010> kb # RetAddr : Args to Child : Call Site 00 00007ff9`dc23d3ea : 000001e5`b6950020 000001e5`d56f01a0 00000000`0000fefa 00007ff9`dc9583c8 : chakra!Js::DynamicProfileInfo::RecordCallSiteInfo+0x75 01 00007ff9`dc23def7 : 000001e5`b8ff97c0 0000004a`3a3fb100 000001e5`d0f407e0 00007ff9`dc37fefa : chakra!Js::ProfilingHelpers::ProfiledNewScObjArray+0x9e 02 00007ff9`dc38d74a : 0000004a`3a3fb410 000001e5`d6f3dc64 000001e5`b7bfa760 00007ff9`dc374943 : chakra!Js::InterpreterStackFrame::OP_NewScObjArray_Impl<Js::OpLayoutT_CallI<Js::LayoutSizePolicy<1> >,0>+0x8f 03 00007ff9`dc374cdd : 0000004a`3a3fb1e8 000001e5`d6f3dc63 000001e5`d6f3dc5f 00000000`00000000 : chakra!Js::JavascriptRegExpConstructor::GetPropertyBuiltIns+0xd22 04 00007ff9`dc374b07 : 0000004a`3a3fb410 00000000`00000000 00000000`00000001 00000000`00000000 : chakra!Js::InterpreterStackFrame::ProcessUnprofiled+0xbd 05 00007ff9`dc3736c9 : 0000004a`3a3fb410 0000004a`3a3fb410 0000004a`3a3fb410 00000000`00000001 : chakra!Js::InterpreterStackFrame::Process+0x1a7 06 00007ff9`dc375a04 : 0000004a`3a3fb410 000001e5`d6f3dc47 000001e5`d6f3dc47 00000000`00000000 : chakra!Js::InterpreterStackFrame::OP_TryCatch+0x61 07 00007ff9`dc374b07 : 0000004a`3a3fb410 00000000`00000000 00000000`00000000 00000000`00000000 : chakra!Js::InterpreterStackFrame::ProcessUnprofiled+0xde4 08 00007ff9`dc378b5e : 0000004a`3a3fb410 000001e5`d56f01a0 0000004a`3a3fbd80 00007ff9`e46a3f00 : chakra!Js::InterpreterStackFrame::Process+0x1a7 09 00007ff9`dc37a265 : 000001e5`d0f407e0 0000004a`3a3fbf50 000001e5`b65b0fba 0000004a`3a3fbf68 : chakra!Js::InterpreterStackFrame::InterpreterHelper+0x48e 0a 000001e5`b65b0fba : 0000004a`3a3fbfa0 00000000`00000001 0000004a`3a3fc378 00007ff9`dc4a0fe0 : chakra!Js::InterpreterStackFrame::InterpreterThunk+0x55 0b 00007ff9`dc4a1393 : 000001e5`d0f407e0 00000000`10000001 000001e5`ce93ff90 00000000`00000001 : 0x000001e5`b65b0fba 0c 00007ff9`dc36ef6d : 000001dd`a72844f0 00000000`00000008 000001e5`d0300110 0000004a`3a3fc001 : chakra!amd64_CallFunction+0x93 0d 00007ff9`dc372797 : 0000004a`3a3fc230 000001e5`d5874036 000001e5`d0f407e0 000001e5`00000001 : chakra!Js::InterpreterStackFrame::OP_CallCommon<Js::OpLayoutDynamicProfile<Js::OpLayoutT_CallIWithICIndex<Js::LayoutSizePolicy<0> > > >+0x15d 0e 00007ff9`dc376842 : 0000004a`3a3fc230 000001e5`d5874036 000001e5`00000119 00000000`00000000 : chakra!Js::InterpreterStackFrame::OP_ProfiledCallIWithICIndex<Js::OpLayoutT_CallIWithICIndex<Js::LayoutSizePolicy<0> > >+0xa7 0f 00007ff9`dc374aa2 : 0000004a`3a3fc230 00000000`00000000 00000000`00000000 00000000`00000000 : chakra!Js::InterpreterStackFrame::ProcessProfiled+0x132 10 00007ff9`dc378b5e : 0000004a`3a3fc230 000001e5`d56f0000 0000004a`3a3fc390 ffffffff`ffffff01 : chakra!Js::InterpreterStackFrame::Process+0x142 11 00007ff9`dc37a265 : 000001e5`d0f40900 0000004a`3a3fc560 000001e5`b65b0fc2 0000004a`3a3fc578 : chakra!Js::InterpreterStackFrame::InterpreterHelper+0x48e 12 000001e5`b65b0fc2 : 0000004a`3a3fc5b0 00000000`00000000 00000000`00000000 00007ff9`dc4a0fe0 : chakra!Js::InterpreterStackFrame::InterpreterThunk+0x55 13 00007ff9`dc4a1393 : 000001e5`d0f40900 00000000`00000000 00000000`00000000 00000000`00000000 : 0x000001e5`b65b0fc2 14 00007ff9`dc36d873 : 000001dd`a72844f0 00000000`00000000 000001e5`d0316f00 00007ff9`dc3d2f87 : chakra!amd64_CallFunction+0x93 15 00007ff9`dc3dc2ec : 000001e5`d0f40900 00007ff9`dc4a15a0 0000004a`3a3fc6c0 000001e5`d030e6d0 : chakra!Js::JavascriptFunction::CallFunction<1>+0x83 16 00007ff9`dc3db8b6 : 000001e5`d0f40900 0000004a`3a3fc7a0 000001e5`d030e6d0 0000004a`3a3fc700 : chakra!Js::JavascriptFunction::CallRootFunctionInternal+0x104 17 00007ff9`dc486259 : 000001e5`d0f40900 0000004a`3a3fc840 000001e5`d030e6d0 00000000`00000000 : chakra!Js::JavascriptFunction::CallRootFunction+0x4a 18 00007ff9`dc3e1d41 : 000001e5`d0f40900 0000004a`3a3fc8a0 00000000`00000000 0000004a`3a3fc880 : chakra!ScriptSite::CallRootFunction+0xb5 19 00007ff9`dc392a1d : 000001e5`d030cf00 000001e5`d0f40900 0000004a`3a3fc950 00000000`00000000 : chakra!ScriptSite::Execute+0x131 ...
3 找到存在该漏洞的 Chakracore 版本源码进行源码调试 既然当前 Windows 版本(1607)可以触发崩溃,那我们就下载该版本对应的 ChakraCore 源码。
Windows 10 1607 的发布时间是 2016/08/02,我们从 GitHub ChakraCore 的 Roadmap 上找一个大概 8 月份的源码版本:https://github.com/Microsoft/ChakraCore/releases/tag/v1.2.0.0
下载其中的 ChakraCore-binaries.zip 二进制文件和 Source code(zip) 源码。
下载后用 ChakraCore-binaries 中的 x64\ch.exe 直接运行 poc.js(去掉其中的 alert 等非 JS 引擎内置函数),测试效果,可以看到 ch.exe 也崩溃了,而且错误代码也是 0xc000005,这也说明:
漏洞在该 Chakracore 版本上确实存在。
漏洞的触发只与 ChakraCore JavaScript 引擎有关。
所以我们完全可以不调试浏览器而只调试 ch.exe 来研究该漏洞,只调试 ch.exe 相对容易而且可以下载并编译源码调试。
4 调试分析 从崩溃点的调用栈 OP_NewScObjArray_Impl 函数可以判断,崩溃发生在 OP_NewScObjArray OpCode 的处理过程中。所以很自然地就想到要为 OP_NewScObjArray 处理过程下断点跟踪调试。
4.1 应该在哪里设置断点 Interpreter 模式执行时, Js::InterpreterStackFrame::INTERPRETERLOOPNAME() 内部,OpCode 被 Interpreter 逐一 Read 然后解释执行。
所以如果想为某个特定的 OpCode 下断点,可以先在 Js::InterpreterStackFrame::INTERPRETERLOOPNAME() 内部下断点
4.2 为 NewScObjArray 的 OpCode 设置断点 1 2 3 4 5 6 0:000> bp ch!runscript 0:000> g Breakpoint 0 hit CH!RunScript: 00007ff7`890c5e90 4c894c2420 mov qword ptr [rsp+20h],r9 ss:000000f1`528ff658=000000f1528ff6c8 0:004> bp chakracore!Js::InterpreterStackFrame::ProcessUnprofiled
断点命中到 lib\runtime\language\interpreterloop.inl 的 Var Js::InterpreterStackFrame::INTERPRETERLOOPNAME()
单步执行,进入 Interpreter While 循环中的 OpCode Read 的过程
1 INTERPRETER_OPCODE op = ReadByteOp <INTERPRETER_OPCODE>(ip);
ReadByOp 的函数实现如下:
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 template <> OpCode InterpreterStackFrame::ReadByteOp <OpCode>(const byte *& ip #if DBG_DUMP , bool isExtended #endif ) { #if DBG || DBG_DUMP this ->DEBUG_currentByteOffset = (void *) m_reader.GetCurrentOffset (); #endif OpCode op = ByteCodeReader::ReadByteOp (ip); #if DBG_DUMP this ->scriptContext->byteCodeHistogram[(int )op]++; if (PHASE_TRACE (Js::InterpreterPhase, this ->m_functionBody)) { Output::Print (_u("%d.%d:Executing %s at offset 0x%X\n" ), this ->m_functionBody->GetSourceContextId (), this ->m_functionBody->GetLocalFunctionId (), Js::OpCodeUtil::GetOpCodeName ((Js::OpCode)(op+((int )isExtended<<8 ))), DEBUG_currentByteOffset); } #endif return op; }
单步执行到 this->scriptContext->byteCodeHistogram[(int)op]++; 当前 c 代码对应的汇编代码为
1 2 3 4 0:004> p // 这一条指令对应 "this->scriptContext->byteCodeHistogram[(int)op]++;",此时刚好可以看到 op 的值 chakracore!Js::InterpreterStackFrame::ReadByteOp<enum Js::OpCode>+0x3d: 00007ffa`77f7e92d 0fb7442430 movzx eax,word ptr [rsp+30h] ss:000000fe`71dfd0a0=006b
此时 op 已经被赋值为将要处理的 OpCode 值,为此行代码设置条件断点,拦截 NewScObjArray OpCode
注意:
条件断点中使用寄存器而不是本地变量的方式,原因是实测条件断点中使用本地变量 op 下断点不能触发。
由于本条指令执行时,ax 寄存器刚好存储的就是 OpCode 值,所以改为借助 ax 寄存器下断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 0:004> bp . ".if ( @ax == 0n197){} .else {gc}" // 为避免无关的 ProcessUnprofiled 断点频繁命中,临时禁用 ProcessUnprofiled 断点 0:004> bl 0 e Disable Clear 00007ff7`890c5e90 [K:\edge\op_newscobjarray_type_confusion\chakracore-1.2.0.0\bin\ch\ch.cpp @ 278] 0001 (0001) 0:**** CH!RunScript 1 e Disable Clear 00007ffb`b8367e30 [K:\edge\op_newscobjarray_type_confusion\chakracore-1.2.0.0\lib\runtime\language\interpreterloop.inl @ 37] 0001 (0001) 0:**** chakracore!Js::InterpreterStackFrame::ProcessUnprofiled 2 e Disable Clear 00007ffb`b82be92d [K:\edge\op_newscobjarray_type_confusion\chakracore-1.2.0.0\lib\runtime\language\interpreterstackframe.cpp @ 2285] 0001 (0001) 0:**** chakracore!Js::InterpreterStackFrame::ReadByteOp<enum Js::OpCode>+0x3d ".if ( @ax == 0n197){} .else {gc}" 0:004> bd 1 // 此时断下的正是 NewScObjArray OpCode(0xc5) 0:004> g (338c.360c): C++ EH exception - code e06d7363 (first chance) (338c.360c): C++ EH exception - code e06d7363 (first chance) (338c.360c): C++ EH exception - code e06d7363 (first chance) (338c.360c): C++ EH exception - code e06d7363 (first chance) chakracore!Js::InterpreterStackFrame::ReadByteOp<enum Js::OpCode>+0x3d: 00007ffb`b82be92d 0fb7442430 movzx eax,word ptr [rsp+30h] ss:000000f1`528fd1b0=00c5
4.3 NewScObjArray OpCode 的处理及漏洞触发过程 OpCode 各个分支代码都是通过宏生成的,需要一步步展开来看。
Js::InterpreterStackFrame::ReadByteOp 返回之后,回到 lib\runtime\language\interpreterloop.inl (如注释代码位置所示)
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 case INTERPRETER_OPCODE::MediumLayoutPrefix: { Var yieldValue = nullptr; ip = [this, &yieldValue](const byte * ip) -> const byte * { INTERPRETER_OPCODE op = ReadByteOp<INTERPRETER_OPCODE>(ip); switch (op) { case INTERPRETER_OPCODE::Yield: m_reader.Reg2_Medium(ip); yieldValue = GetReg(GetFunctionBody()->GetYieldRegister()); break ; #define DEF2_WMS(x, op, func) PROCESS_##x##_COMMON(op, func, _Medium) #define DEF3_WMS(x, op, func, y) PROCESS_##x##_COMMON(op, func, y, _Medium) #define DEF4_WMS(x, op, func, y, t) PROCESS_##x##_COMMON(op, func, y, _Medium, t) #include "InterpreterHandler.inl" default : AssertMsg(false , "dispatch to bad opcode" ); __assume(false ); } return ip; }(ip);
继续跟踪,新得到的 NewScObjArray OpCode 进入 switch case 分支,其具体的分支处理代码由宏实现
1 2 3 4 5 EXDEF3_WMS(CALL, NewScObjectSpread, OP_NewScObjectSpread, CallIExtended) DEF3_WMS(CALL, NewScObjArray, OP_NewScObjArray, CallI) DEF3_WMS(CALL, NewScObjArraySpread, OP_NewScObjArraySpread, CallIExtended)
上面 switch case 中, DEF3_WMS 的定义为
1 #define DEF3_WMS(x, op, func, y) PROCESS_##x##_COMMON(op, func, y, _Medium)
所以宏展开之后为
1 PROCESS_CALL_COMMON(NewScObjArray, OP_NewScObjArray, CallI, _Medium)
我们继续看 PROCESS_CALL_COMMON 的定义,也是个宏
1 2 3 4 5 6 7 #define PROCESS_CALL_COMMON(name, func, layout, suffix) \ case OpCode::name: \ { \ PROCESS_READ_LAYOUT(name, layout, suffix); \ func(playout); \ break; \ }
PROCESS_READ_LAYOUT 的定义如下
1 2 3 4 #define PROCESS_READ_LAYOUT(name, layout, suffix) \ CompileAssert(OpCodeInfo<OpCode::name> ::Layout == OpLayoutType::layout); \ const unaligned OpLayout##layout##suffix * playout = m_reader.layout##suffix(ip); \ Assert((playout != nullptr) == (Js::OpLayoutType::##layout != Js::OpLayoutType::Empty));
继续展开
注意这里的展开用到了 ## 连接符,另外一个就是宏在参数替换的时候是以 token 为单位的,在下面的例子中,就是根据 ## 和标点符号将表达式划分成 token 列表,与参数列表项匹配的 token 将会被替换。
在这里我纠结了很久 m_reader.layout##suffix 到底替换不替换,以及 OpLayout##layout##suffix 为什么不写成 OpLayoutlayout##suffix,后来查资料,比较少的资料提到了替换的基本单元是 token,
也就是 token 内部的匹配项是不替换的。
譬如下面的参数中有 layout,而 token 中有 playout,此时不会将 playout 替换为 pCALLI。
参考: https://www.jianshu.com/p/f8cb57957c03
参考: https://zhuanlan.zhihu.com/p/26978356
全部展开之后的 case 分支为:
1 2 3 4 5 6 7 8 9 case OpCode::NewScObjArray: \ { \ CompileAssert(OpCodeInfo<OpCode::NewScObjArray>::Layout == OpLayoutType::CallI); \ const unaligned OpLayoutCallI_Medium* playout = m_reader.CallI_Medium(ip); \ Assert((playout != nullptr) == (Js::OpLayoutType::CallI != Js::OpLayoutType::Empty)); OP_NewScObjArray(playout); \ break ; \ }
ByteCodeReader::CallI_Medium 是通过宏的方式生成的,一步步的展开之后是:
1 2 3 4 5 6 7 8 9 10 const unaligned OpLayoutCallI_Medium * ByteCodeReader::CallI_Medium () \ { \ return GetLayout<OpLayoutCallI_Medium>(); \ } \ const unaligned OpLayoutCallI_Medium * ByteCodeReader::CallI_Medium (const byte*& ip) \ { \ return GetLayout<OpLayoutCallI_Medium>(ip); \ }
跟进 CallI_Medium 调用,调用至 GetLayout
1 2 3 4 5 6 7 0:004> t chakracore!Js::ByteCodeReader::CallI_Medium: 00007ffb`c0814570 4889542410 mov qword ptr [rsp+10h],rdx ss:0000000a`a2bfcae8=0000000aa2bfcb48 0:004> t chakracore!Js::ByteCodeReader::GetLayout<Js::OpLayoutT_CallI<Js::LayoutSizePolicy<1> > >: 00007ffb`c079e010 4889542410 mov qword ptr [rsp+10h],rdx ss:0000000a`a2bfcab8=00000125d6bf3900
GetLayout 函数的定义如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 template<typename LayoutType> const unaligned LayoutType * ByteCodeReader::GetLayout (const byte*& ip) { size_t layoutSize = sizeof (LayoutType); AssertMsg((layoutSize > 0 ) && (layoutSize < 100 ), "Ensure valid layout size" ); const byte * layoutData = ip; ip += layoutSize; m_currentLocation = ip; Assert(m_currentLocation <= m_endLocation); return reinterpret_cast<const unaligned LayoutType *>(layoutData); }
此时的 LayoutType 类型定义为: Js::OpLayoutT_CallI<Js::LayoutSizePolicy<1> >
1 2 3 4 5 6 7 template <typename SizePolicy> struct OpLayoutT_CallI // Return = Function(ArgCount) { typename SizePolicy::ArgSlotType ArgCount; typename SizePolicy::RegSlotSType Return; typename SizePolicy::RegSlotType Function; };
LayoutType 的大小为 5 字节
1 2 3 0:004> dv layoutSize layoutSize = 5
GetLayout 函数主要是从当前 ip 的位置读取 sizeof(LayoutType) 字节的数据作为 LayoutType 类型的 layoutData 返回,然后 ip 向后偏移 sizeof(LayoutType) 字节
参考上面的 Case OpNewScObj 分支,从 GetLayout 返回之后,进入 OP_NewScObjArray(),OP_NewScObjArray() 继续调用其具体实现 OP_NewScObjArray_Impl()
1 2 3 4 template <class T > void OP_NewScObjArray (const unaligned T* playout) { OP_NewScObjArray_Impl<T, false >(playout); } template <class T , bool Profiled > void InterpreterStackFrame::OP_NewScObjArray_Impl (const unaligned T* playout, const Js::AuxArray<uint32> *spreadIndices) ;
OP_NewScObjArray_Impl 的 playout 即为通过 GetLayout 得到的 layoutData
在 OP_NewScObjArray_Impl() 内部,它会调用 ProfilingHelpers::ProfiledNewScObjArray() 以实现对 OP_NewScObjArray 的 Profile 支持。
特别需要注意的是它调用 ProfilingHelpers::ProfiledNewScObjArray() 时的参数,这里的 playout 将通过类型转换至 const unaligned OpLayoutDynamicProfile2 * 即 const unaligned OpLayoutDynamicProfile2< Js::OpLayoutT_CallI<Js::LayoutSizePolicy<1> > > *
1 2 3 4 5 6 7 8 9 SetReg( (RegSlot)playout->Return, ProfilingHelpers::ProfiledNewScObjArray( GetReg(playout->Function), args, function, static_cast<const unaligned OpLayoutDynamicProfile2<T> *>(playout)->profileId, static_cast<const unaligned OpLayoutDynamicProfile2<T> *>(playout)->profileId2));
下面看一下 OpLayoutDynamicProfile2 结构体和之前的 Js::OpLayoutT_CallI 有何不同
OpLayoutDynamicProfile2 结构体的定义如下
1 2 3 4 5 6 7 8 template <typename LayoutType> struct OpLayoutDynamicProfile2 : public LayoutType{ ProfileId profileId; ProfileId profileId2; }; typedef uint16 ProfileId;
看来两个结构体之间是继承关系。
因为这里的 OpLayoutDynamicProfile2 继承了 LayoutType(Js::OpLayoutT_CallI<Js::LayoutSizePolicy<1> >)结构体,相当于扩充了结构体的大小,在尾部增加了两个结构体成员(uint16)
上面调试中,LayoutType 的大小为 5 字节,此时 OpLayoutDynamicProfile2 为 9 个字节。
而在 InterpreterStackFrame::OP_NewScObjArray_Impl,调用 ProfilingHelpers::ProfiledNewScObjArray 函数传入的参数正是类型转换之后的 profileId 和 profileId2。
我们继续动态跟踪一下调用时的状态及实际的参数
首先来看一下调用 ProfilingHelpers::ProfiledNewScObjArray 之前 playout 的内存状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 0:004> dt playout Local var @ 0xbdf2bfce48 Type Js::OpLayoutT_CallI<Js::LayoutSizePolicy<1> >* 0x0000024b`c2111c64 +0x000 ArgCount : 0x2 '' +0x001 Return : 0n254 +0x003 Function : 0xfe // playout 是个指针,需要 poi 查看具体的内存对象 0:004> db poi(playout) 0000024b`c2111c64 02 fe 00 fe 00 fa fe e9-09 1c 00 e7 fb 06 07 05 ................ 0000024b`c2111c74 00 5c 00 0a 5c 01 f4 5c-02 fb 5c 03 f5 5c 04 09 .\..\..\..\..\.. 0000024b`c2111c84 f6 05 ff f6 06 00 e9 a8-00 24 00 00 00 00 00 00 .........$...... 0000024b`c2111c94 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 0000024b`c2111ca4 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 0000024b`c2111cb4 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 0000024b`c2111cc4 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 0000024b`c2111cd4 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
注意 playout 内存的前 5 个字节属于 LayoutType(Js::OpLayoutT_CallI<Js::LayoutSizePolicy<1> >),而后面的四个字节 0xfa 0xfe 0xe9 0x09 是 OpLayoutDynamicProfile2 自己派生的两个变量
这里就是问题所在了,我们在理清一下思路。
也就是说,本来我们通过 GetLayout 读取到的 playout 是一个 5 字节的结构体,但是此时类型转换之后, profileId(0xfefa) 和 profileId2(0x09e9) 被作为参数传入 ProfilingHelpers::ProfiledNewScObjArray
下面来跟踪证实一下我们的判断,看一下这两个参数的值
进入 ProfilingHelpers::ProfiledNewScObjArray 函数后,打印两个参数值,发现正是 0xfefa 和 0x09e9
1 2 3 4 5 6 7 8 9 10 0:004> chakracore!Js::ProfilingHelpers::ProfiledNewScObjArray+0x33a: 00007ffb`c10d058a 488b8c2400010000 mov rcx,qword ptr [rsp+100h] ss:000000bd`f2bfcd20=0000024bc04c3cc0 0:004> dt profileId Local var @ 0xbdf2bfcd28 Type unsigned short 0xfefa 0:004> dt arrayProfileId Local var @ 0xbdf2bfcd30 Type unsigned short 0x9e9
我们继续跟踪,ProfilingHelpers::ProfiledNewScObjArray 会将 profileId(0xfefa)参数作为 callSiteId 实参传递给 RecordCallSiteInfo()
1 2 3 4 5 6 7 8 profileInfo->RecordCallSiteInfo( callerFunctionBody, profileId, calleeFunctionInfo, caller, args.Info.Count, true );
1 2 3 4 5 6 7 0:004> chakracore!Js::DynamicProfileInfo::RecordCallSiteInfo+0x105: 00007ffb`c0f3b935 488b8424b8000000 mov rax,qword ptr [rsp+0B8h] ss:000000bd`f2bfcc38={chakracore!Js::JavascriptArray::EntryInfo::NewInstance (00007ffb`c1f7b418)} 0:004> dt callSiteId Local var @ 0xbdf2bfcc30 Type unsigned short 0xfefa
而 callSiteId(0xfefa)在 RecordCallSiteInfo 内部会作为数组索引访问 this->callSiteInfo。那么此时 this->callSiteInfo 数组的真实长度是多少呢?
这就要看一下 this->callSiteInfo 的初始化过程了,this->callSiteInfo 的初始化是在 DynamicProfileInfo::New()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 DynamicProfileInfo* DynamicProfileInfo::New (Recycler* recycler, FunctionBody* functionBody, bool persistsAcrossScriptContexts) { size_t totalAlloc = 0 ; Allocation batch[] = { { (uint)offsetof(DynamicProfileInfo, callSiteInfo), functionBody->GetProfiledCallSiteCount() * sizeof (CallSiteInfo) }, { (uint)offsetof(DynamicProfileInfo, ldElemInfo), functionBody->GetProfiledLdElemCount() * sizeof (LdElemInfo) }, { (uint)offsetof(DynamicProfileInfo, stElemInfo), functionBody->GetProfiledStElemCount() * sizeof (StElemInfo) }, { (uint)offsetof(DynamicProfileInfo, arrayCallSiteInfo), functionBody->GetProfiledArrayCallSiteCount() * sizeof (ArrayCallSiteInfo) }, { (uint)offsetof(DynamicProfileInfo, fldInfo), functionBody->GetProfiledFldCount() * sizeof (FldInfo) }, { (uint)offsetof(DynamicProfileInfo, divideTypeInfo), functionBody->GetProfiledDivOrRemCount() * sizeof (ValueType) }, { (uint)offsetof(DynamicProfileInfo, switchTypeInfo), functionBody->GetProfiledSwitchCount() * sizeof (ValueType)}, { (uint)offsetof(DynamicProfileInfo, slotInfo), functionBody->GetProfiledSlotCount() * sizeof (ValueType) }, { (uint)offsetof(DynamicProfileInfo, parameterInfo), functionBody->GetProfiledInParamsCount() * sizeof (ValueType) }, { (uint)offsetof(DynamicProfileInfo, returnTypeInfo), functionBody->GetProfiledReturnTypeCount() * sizeof (ValueType) }, { (uint)offsetof(DynamicProfileInfo, loopImplicitCallFlags), (EnableImplicitCallFlags(functionBody) ? (functionBody->GetLoopCount() * sizeof (ImplicitCallFlags)) : 0 ) }, { (uint)offsetof(DynamicProfileInfo, loopFlags), functionBody->GetLoopCount() ? BVFixed::GetAllocSize(functionBody->GetLoopCount() * LoopFlags::COUNT) : 0 } }; ... }
可以看到 callSiteInfo 数组的长度是通过 functionBody->GetProfiledCallSiteCount() 获得的,也就是 FunctionBody 对象的 profiledCallSiteCount 成员变量的值。
1 ProfileId GetProfiledCallSiteCount () const { return this->profiledCallSiteCount; }
那么此时在内存中 callSiteInfo 数组的真实长度是多少呢?也就是 functionBody 的 profiledCallSiteCount 成员的值。通过 functionBody 对象我们来看一下
1 2 3 4 5 0:004> dt functionBody profiledCallSiteCount Local var @ 0xbdf2bfcc28 Type Js::FunctionBody* 0x0000024b`c19b01d0 +0x170 profiledCallSiteCount : 2
callSiteInfo 数组的长度是 2
在 callSiteInfo 数组长度是 2 的情况下,竟然尝试以 0xfefa 索引访问它,所以当然是内存越界了。
我们继续通过调试器验证一下这个动态的过程
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 // this->callSiteInfo 的地址为 0x0000024b`c0a100b8 0:004> dt this callSiteInfo Local var @ 0xbdf2bfcc20 Type Js::DynamicProfileInfo* 0x0000024b`c0a10020 +0x008 callSiteInfo : 0x0000024b`c0a100b8 Js::DynamicProfileInfo::CallSiteInfo // 查看 js::DynamicProfileInfo::CallSiteInfo 具体的数据结构定义 0:004> dt js::DynamicProfileInfo::CallSiteInfo chakracore!Js::DynamicProfileInfo::CallSiteInfo +0x000 isArgConstant : Pos 0, 13 Bits +0x000 isConstructorCall : Pos 13, 1 Bit +0x000 dontInline : Pos 14, 1 Bit +0x000 isPolymorphic : Pos 15, 1 Bit +0x002 returnType : ValueType +0x004 ldFldInlineCacheId : Uint4B +0x008 u : Js::DynamicProfileInfo::CallSiteInfo::<unnamed-type-u> // 计算 this->callSiteInfo 数组项 js::DynamicProfileInfo::CallSiteInfo 结构体的大小,0x10 字节 0:004> ?? sizeof(js::DynamicProfileInfo::CallSiteInfo) unsigned int64 0x10 // this->callSiteInfo[callSiteId] 的地址 0000024b`c0b0f058 第 0xfefa 项的真实地址是多少? 是 0x0000024b`c0b0f058 0:004> ?? 0x0000024b`c0a100b8+0x10*0xfefa int64 0n2524378624088 0:004> ? 0x0000024b`c0a100b8+0x10*0xfefa Evaluate expression: 2524378624088 = 0000024b`c0b0f058
实际调试验证最终崩溃点 if (!callSiteInfo[callSiteId].isPolymorphic) 地址是不是我们上一步自己计算的地址
1 2 3 4 5 6 7 8 9 10 11 12 0:004> p chakracore!Js::DynamicProfileInfo::RecordCallSiteInfo+0x158: 00007ffb`c0f3b988 0fb78424b0000000 movzx eax,word ptr [rsp+0B0h] ss:000000bd`f2bfcc30=fefa // 内存访问异常的地址正是 0x0000024b`c0b0f058 0:004> (3628.1794): Access violation - code c0000005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. chakracore!Js::DynamicProfileInfo::RecordCallSiteInfo+0x170: 00007ffb`c0f3b9a0 0fb70401 movzx eax,word ptr [rcx+rax] ds:0000024b`c0b0f058=????
4.4 漏洞的根本原因 截至目前,我们可以总结一下漏洞的根本原因了
在处理 OP_NewScObjArray Opcode 时,通过 GetLayout 得到的 layoutData 本身是个 5 字节长度的 Js::OpLayoutT_CallI<Js::LayoutSizePolicy<1> > 类型,但是在 ProfilingHelpers::ProfiledNewScObjArray() 内部竟然被转换为 9 字节长度的 const unaligned OpLayoutDynamicProfile2 * 类型。
转换之后,越界访问得到的错误 profileId 被当作了 DynamicProfileInfo->callSiteInfo 的数组索引,造成内存越界访问异常。
总结起来,这是个类型混淆导致的越界访问漏洞。
5 补丁分析 5.1 如何知道该漏洞在哪个版本被修复的 本来希望可以通过 git log 的 -L 参数实现对函数的追踪,不过实际测试发现不起作用。
最后想到的方法是先通过 git log + grep 碰碰运气,然后手动测试
1 2 test@test MINGW64 /k/edge/ChakraCore (master) $ git log --reverse -p v1.2.0.0..HEAD lib/Runtime/Language/InterpreterStackFrame.cpp |grep "OP_NewScObjArray_Impl" -B 120
发现只有一条 commit 涉及 OP_NewScObjArray_Impl,我们来看一下这个 commit
下面来分别编译补丁前后两个版本测试 poc 能否触发
5.2 checkout 打上疑似补丁的版本测试 从 master 分支回退到上面发现的补丁版本(8bd6826aea01ff1af36f2a83fe00c44799ba80cb),即打上疑似补丁的版本
1 2 3 4 test@test MINGW64 /k/edge/ChakraCore (master) $ git checkout 8bd6826aea01ff1af36f2a83fe00c44799ba80cb Checking out files: 100% (1579/1579), done. Note: checking out '8bd6826aea01ff1af36f2a83fe00c44799ba80cb'.
VS2017 编译,然后测试
1 2 3 C:\Users\test>"K:\edge\ChakraCore\Build\VcBuild\bin\x64_debug\ch.exe" "K:\edge\OP_NewScObjArray_Type_Confusion\poc\pure_js_poc.js" C:\Users\test>
发现没有崩溃,证明这个版本该漏洞已经被修复。
5.3 checkout 打上疑似补丁之前版本测试 再来测试回退到补丁之前版本
1 2 3 4 5 6 test@test MINGW64 /k/edge/ChakraCore ((8bd6826ae...)) $ git reset --hard HEAD is now at 8bd6826ae [CVE-2018-8290] OOB profile read/write - Google, Inc test@test MINGW64 /k/edge/ChakraCore ((8bd6826ae...)) $ git checkout 7af07fdfb3cf3ac2b21dd71bf565ab1135e62d4d
VS2017 编译,然后测试
1 2 3 4 5 6 C:\Users\test> C:\Users\test>"K:\edge\ChakraCore\Build\VcBuild\bin\x64_debug\ch.exe" "K:\edge\OP_NewScObjArray_Type_Confusion\poc\pure_js_poc.js" FATAL ERROR: ch.exe failed due to exception code c0000005 C:\Users\test>
发现崩溃了,说明这个 commit 8bd6826aea01ff1af36f2a83fe00c44799ba80cb 正是该漏洞的补丁
5.2 补丁代码分析 补丁代码中,OP_NewScObjArray OpCode 的处理函数 OP_NewScObjArray_Impl 不再实现具体的逻辑,改为调用 OP_NewScObject_Impl。
1 2 3 4 5 6 7 8 9 - template <class T, bool Profiled> void OP_NewScObjArray_Impl(const unaligned T* playout, const Js::AuxArray<uint32> *spreadIndices = nullptr); + template <class T, bool Profiled, bool ICIndex> void OP_ProfiledNewScObject_Impl(const unaligned T* playout, InlineCacheIndex inlineCacheIndex = Js::Constants::NoInlineCacheIndex, const Js::AuxArray<uint32> *spreadIndices = nullptr) { OP_NewScObject_Impl<T, Profiled, ICIndex>(playout, inlineCacheIndex, spreadIndices); } + template <class T, bool Profiled> void OP_NewScObjArray_Impl(const unaligned T* playout, const Js::AuxArray<uint32> *spreadIndices = nullptr) { OP_NewScObject_Impl<T, Profiled, false>(playout, Js::Constants::NoInlineCacheIndex, spreadIndices); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 template <class T , bool Profiled, bool ICIndex> void InterpreterStackFrame::OP_NewScObject_Impl (const unaligned T* playout, InlineCacheIndex inlineCacheIndex, const Js::AuxArray<uint32> *spreadIndices) { if (ICIndex) { Assert (inlineCacheIndex != Js::Constants::NoInlineCacheIndex); } Var newVarInstance = #if ENABLE_PROFILE_INFO Profiled ? ProfiledNewScObject_Helper ( GetReg (playout->Function), playout->ArgCount, static_cast <const unaligned OpLayoutDynamicProfile<T> *>(playout)->profileId, inlineCacheIndex, spreadIndices) : #endif NewScObject_Helper (GetReg (playout->Function), playout->ArgCount, spreadIndices); SetReg ((RegSlot)playout->Return, newVarInstance); }
当进入到 OP_NewScObject_Impl 函数的时候,可以发现此时传入的 playout 内存的数据与补丁之前是一致的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 0:004> p chakracore!Js::InterpreterStackFrame::OP_NewScObject_Impl<Js::OpLayoutT_CallI<Js::LayoutSizePolicy<1> >,0,0>+0x18: 00007ffd`cfc16b88 33c0 xor eax,eax 0:004> dt playout Local var @ 0x4da20fc468 Type Js::OpLayoutT_CallI<Js::LayoutSizePolicy<1> >* 0x000001c3`1a7a4c9b +0x000 ArgCount : 0x2 '' +0x001 Return : 0n254 +0x003 Function : 0xfe 0:004> db poi(playout) 000001c3`1a7a4c9b 02 fe 00 fe 00 fa fe e9-09 24 00 e7 fb 06 07 05 .........$...... 000001c3`1a7a4cab 00 5b 00 0a 5c 01 f4 08-00 5c 02 fb 08 00 5c 03 .[..\....\....\. 000001c3`1a7a4cbb f5 08 00 5c 04 09 08 00-ee 05 ff f6 08 00 e9 a7 ...\............ 000001c3`1a7a4ccb 00 24 00 00 00 00 00 00-00 00 00 00 00 00 00 00 .$.............. 000001c3`1a7a4cdb 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 000001c3`1a7a4ceb 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 000001c3`1a7a4cfb 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 000001c3`1a7a4d0b 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
接下来,因为 OP_NewScObjArray_Impl 调用 OP_NewScObject_Impl 时传入的模板参数 Profiled 为 false。所以会调用 NewScObject_Helper 创建 newInstance 赋值给 playout->Return 寄存器。
注意在这个 OP_NewScObjArray 的处理过程中,已经不像补丁之前的代码存在类型转换,所以漏洞也就不再存在了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 template <class T, bool Profiled> - void InterpreterStackFrame::OP_NewScObjArray_Impl(const unaligned T* playout, const Js::AuxArray<uint32> *spreadIndices) + void InterpreterStackFrame::OP_ProfiledNewScObjArray_Impl(const unaligned T* playout, const Js::AuxArray<uint32> *spreadIndices) { // Always profile this operation when auto-profiling so that array type changes are tracked #if ENABLE_PROFILE_INFO @@ -6212,7 +6222,7 @@ namespace Js Assert(!Profiled); #endif { - OP_NewScObject_Impl<T, Profiled, false>(playout, Js::Constants::NoInlineCacheIndex, spreadIndices); + OP_NewScObjArray_Impl<T, Profiled>(playout, spreadIndices); return; }
总结一下补丁,补丁后的版本,OP_NewScObjArray_Impl 仅仅是 OP_NewScObject_Impl 的封装,不再有之前复杂的判断和逻辑,不再有类型转换,所以漏洞也就被修复了。
参考链接