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
的函数,留意其同级函数是否被封装,是否有副作用。