Intro: Simple analysis of CVE-2017-11802
Reference
POC
1 | function main() { |
Root Cause
由于 String.prototype.replace 可以 inline 到 JIT 里,所以在该方法中,所有可能破坏 JIT 边界条件的 Call 都必须更新 ImplicitCallFlags。但是 RegexHelper::StringReplace 调用 replace 时并没有更新该 flag。也就是说未能正确识别 replace调用中的 callback,对于该 call 没有封装为 ExcuteimplitCall。
Analysis
首先,RegexHelper::StringReplace 作为一个重载函数,接受两种类型的参数:1
21: Var RegexHelper::StringReplace(JavascriptString* match, JavascriptString* input, JavascriptString* replace)
2: Var RegexHelper::StringReplace(ScriptContext* scriptContext, JavascriptString* match, JavascriptString* input, JavascriptFunction* replacefn)
类型1中的参数分别为match,input 以及一个字符串类型的 replace;
类型2中的参数则为sc,match,input,JavascriptFunction* 类型的 replacefn;
根据 MDN:1
2
3replace() 方法返回一个由替换值(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
21Line 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)
注意:
这个部分是 JIT 对 Runtime 函数的调用,查看调用栈帧:
找到 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::StringReplace和RegexHelper::RegexEs5ReplaceImpl函数中分别Patch,然而两个函数均在JavascriptString::DoStringReplace被调用。- 处理
JavascriptOperators::NewScObject则直接Patch在JavascriptArray::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的函数,留意其同级函数是否被封装,是否有副作用。

