Microsoft Edge Chakra OP_NewScObjArray 类型混淆漏洞分析笔记

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 /*= false*/
#endif
)
{
#if DBG || DBG_DUMP
//
// For debugging byte-code, store the current offset before the instruction is read:
// - We convert this to "void *" to encourage the debugger to always display in hex,
// which matches the displayed offsets used by ByteCodeDumper.
//
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) // NewScObjArray 被 ReadByteOp 读取之后回到这里
{
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:
// Help the C++ optimizer by declaring that the cases we
// have above are sufficient
AssertMsg(false, "dispatch to bad opcode");
__assume(false);
}
return ip;
}(ip);

继续跟踪,新得到的 NewScObjArray OpCode 进入 switch case 分支,其具体的分支处理代码由宏实现

1
2
3
4
5
// lib\runtime\language\interpreterhandler.inl
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)); // Make sure playout is used

继续展开

注意这里的展开用到了 ## 连接符,另外一个就是宏在参数替换的时候是以 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)); // Make sure playout is used

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
diff --git a/lib/Runtime/Language/InterpreterStackFrame.h b/lib/Runtime/Language/InterpreterStackFrame.h
index 8ebc7c13c..a3ad7504e 100644
--- a/lib/Runtime/Language/InterpreterStackFrame.h
+++ b/lib/Runtime/Language/InterpreterStackFrame.h

- 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 的封装,不再有之前复杂的判断和逻辑,不再有类型转换,所以漏洞也就被修复了。

参考链接