Analysis of CVE-2018-0840

Analysis of CVE-2018-0840

Intro: Simple analysis of CVE-2018-0840

Reference

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function opt(arr, index) {
arr[0] = 1.1;
typeof(arr[index]);
arr[0] = 2.3023e-320;
}

function main() {
let arr = [1.1, 2.2, 3.3];
for (let i = 0; i < 0x10000; i++) {
opt(arr, {});
}

opt(arr, {toString: () => {
arr[0] = {};
throw 1;
}});

print(arr[0]);
}

main();

Root Cause

1
2
3
4
5
6
7
8
9
template <class Fn>
inline Js::Var ExecuteImplicitCall(Js::RecyclableObject * function, Js::ImplicitCallFlags flags, Fn implicitCall)
{
...
Js::ImplicitCallFlags saveImplicitCallFlags = this->GetImplicitCallFlags();
Js::Var result = implicitCall();
this->SetImplicitCallFlags((Js::ImplicitCallFlags)(saveImplicitCallFlags | flags));
return result;
}

SetImplicitCallFlag 函数调用在 implicitCall 函数之后,这是极其危险的:倘若 implicitCall 函数中执行 throwSetImplicitCallFlag 函数将不会被运行。进一步来看,如果 throw 以某种方式被忽略掉的话,那么我们就可以 bypass ImplicitCallFlags checks。比较巧合的是,typeof 可以做到这一点,原因来自于

  • Opcode 的设计缺陷
  • Try Catch & ThrowC++ 处理

Analysis

首先在 Js::Var ExecuteImplicitCall 断下来,查看函数调用栈:

我们可以初步判断编译器的行为如下:

  • JIT 代码内调用了 TyeofElemRuntime 代码
  • TyeofElem 内调用了 GetIndexType
  • GetIndexType 最终调用了 ExecuteImplicitCall

结合漏洞描述,我们可以预知的接下来的行为可能是

  • ExecuteImplicitCall 调用了 lamda 表达式/ ImpliciteCall
  • lamda 表达式/ ImpliciteCall 内部调用解释器处理 js 代码
  • ( unkown )
  • TyeofElem 捕捉到 throw

( unkown ) 的地方是暂时不确定 Chakracore 如何处理 jsthrow 语句,以及如何将 throw 传递到外部的 C++ 代码中。当然我们会在后面进行具体的调试。

断在 Js::Var ExecuteImplicitCall 之后,继续单步执行

单步步过

程序来到了 TyeofElem 的异常处理代码。

疑问是,implicitCall 如何产生 throw,接下来使用 TTD 调试。

TTD 调试

针对 TTD 调试,有一个技巧是:

  • 找到一个比较上层的函数设置断点
  • 单步步过并在每一个函数调用的位置设置断点
  • 如果某次步过触发了大的跳转或是 Crash,则 Go Back 回到上一个状态,然后单步步入。
  • 继续第二步,知道准确定位问题函数。

例如本次分析我们首先在 JavascriptOperators::TypeofElem 函数下断点


单步步过,一路设置断点,跳转到 catch。我们想弄清楚如何 chakra 如何发出 throw 的。
因此 Goback,回到上一个断点,并步入。


同理接着往下执行,最终来到了解释器选择 opcode 的地方

这里值得注意的地方是栈帧窗口,描述了解释器调用一个 js 函数的基本过程:

继续按照上述方式执行。

若干步之后,程序执行到了 chakracore!_CxxThrowException(该函数需要在反汇编窗口调试) 。实际上这里我们就可以大致通过名称猜出,该函数产生了一个 C++ 可以捕捉的异常。

对应的函数调用栈如下

注意到 chakracore!_CxxThrowException 在末尾调用了 chakracore!_imp_RaiseException ,紧接着调用 KERNELBASE!_imp_RtlRaiseException 完成异常抛出。

回过头来看,除了 JavascriptOperators::TypeofElem 内部的 catch 模块,我们未在其他位置遇到异常处理。因此该 catch 会捕捉 chakracore!_CxxThrowException 产生的异常。

因此我们可以补全编译器的行为:

  • ExecuteImplicitCall 调用了 lamda 表达式/ ImpliciteCall
  • lamda 表达式/ ImpliciteCall 内部调用解释器处理 js 代码
  • 解释器处理 throw opcode 的时候产生了 C++ 可以捕捉的异常
  • TyeofElem 捕捉到 throw

Patch

Patch 分析

  • 针对 throw 之后跳过的 flag 更新的代码进行析构处理

Patch 策略

  • 由于 chakra 针对 try/catch/throw 的处理均转化为 C++ 的处理,因此原本在 Callback 调用 js function 之后有一些其他的工作,但是由于解释器在解释 throw 的时候直接转化为 C++throw ,这些其他的工作被忽略了。
  • 对应的 Patch 是试图采用析构函数来补救被忽略的代码
  • patch 中忽略的工作是 ImplicitCallFlag 的更新

对漏洞挖掘的启发

  • 本漏洞的 root cause 在于 chakratry catch throw 的设计模式的缺陷,patch 并没有从根本上解决这个问题,仅仅弥补了在 ImplicitCall 的时候由于 throw 产生的关键代码跳过的问题,并且该关键代码仅仅指 ImplicitCallFlag 更新。因此可以从以下角度出发审计源码:
    • 除了 ImplicitCall,还有什么逻辑是可以调用用户 js 来执行 throw 的,以及可以绕过什么关键的函数?
    • 除了 ImplicitCallFlagImplicitCall中调用 throw 还可以忽略什么问题?

Resource

【+】https://xlab.tencent.com/cn/2015/12/24/bypass-cfg-using-chakra-engine/

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×