(距《Chakra漏洞调试笔记1——ImplicitCall》的发布有一个多月了,期间有一些同学私信我第二篇的更新时间。因为这些case的分析主要是业余时间完成的,也是看一个case学一部分Chakra的源码,所以比较难保证更新速度,不过我还是会坚持把这个坑填完的XD。
另外我最近写了一个IE浏览器的0day的exploit,可以在最新的Win10系统中稳定利用,等微软修复这个漏洞后会公布具体技术细节,感兴趣的同学可以在我的推特@elli0tn0phacker上观看demo。)
0x0 DynamicObject
Javascript是一种动态弱类型的脚本语言。Javascript中同一变量可以指向不同数据类型,而一些数据类型的结构也可以被动态修改。但并非所有的数据结构都可以被动态修改,Chakra中存在两种数据类型:static type和dynamic type。其中static type是基本数据类型,对应了Javascript中的原始数据类型(Primitive),比如String,Number,Boolean等。static type不能动态修改属性,以String为例:
而dynamic type可以动态修改属性,比如:
Javascript中Object是一个动态数据类型,Object在Chakra中通过DynamicObject实现,DynamicObject.h中给出了DynamicObject的Memory Layout:
DynamicObject中比较重要的两个成员变量是auxSlots和inline slots. 那么什么是slots呢?Chakra又是如何通过slots来实现对象属性的访问呢?
我们知道,Javascript的Object是由若干key (property name) -value (property value)组成的数据结构。一个Object的实现一般需要key, value等成员变量来保存相应的数据。这样的实现是比较简单的,但是带来的问题就是每个Object的实例化对象都需要相应的Memory Layout来保存这些key-value。然而同一个Object在属性不被动态修改的前提下Memory Layout是相同的,key也是一样的,这样的设计无疑增加了内存空间的占用,降低了属性的访问效率。
那么Chakra是如何高效的读写一个类不同对象的不同属性呢?
看这样一个例子:
上面的脚本创建了两个Point对象one和two,属性分别为x和y。Chakra需要在runtime中保存相关信息:
- Object one和two包含属性x和y
- Object one属性x的值是10,y的值是20
- Object two属性x的值是40,y的值是50
通过记录以上信息就可以实现Object属性的访问。可以看到对象one和two保存的(1)的信息是相同的,(2)(3)的信息则因对象的属性值不同而不同。因此Chakra通过建立一个property map和一个 slots array的对应关系来实现属性的读写功能:
- property map用来映射属性名(实际上是PropertyId)和slots array的索引,比如属性x存储在slots array的索引 = 0
- slots array用来存储具体的属性值,比如one->slots[0]=10, one->slots[1]=20, two->slots[0]=40,two->slots[1]=50
Chakra中slots array存放在DynamicObject中(auxSlots或者inline slots),property map存放在Type中(准确的说是保存在Type的TypeHandler对象中)。当访问one.x时,Chakra通过one->Type->TypeHandler(->TypePath)取得property map,找到属性x在slots array中的索引为0,然后通过one->slots[0]取得属性x的值10:
观察对象one,two在内存中的布局:
可以看到他们的Type是一样的,TypeHandler也是一样的,他们有同样的property map。所以通过Type,Chakra可以更加高效地访问对象属性,并降低对象本身的内存占用。
理解了Chakra是如何通过Type和slots实现属性访问后,我们再回到DynamicObject的3种Memory Layout就比较容易理解了,其中auxSlots存放的是slots array的指针,指向了slots array的首地址,inline slots则是直接将slots array存放在DynamicObject中。比如:
let o = {a:1, b:2};
o.c = 3;
o.c = 3语句向Object动态添加了新的属性c,Object的Type发生变化,inline slots被auxSlots替代。
0x1 JIT Object Check Optimize
在笔记1中笔者简单地介绍了Chakra JIT的过程。考虑如下一段代码片段函数opt被JIT后的结果:
假设第一次opt()后,函数opt被JIT,Lowerer阶段后的dump如下:
可以看到obj.a = 2语句前有两次Bailout机制的类型检查,而obj.b = 1语句则没有进行相关类型检查,直接赋值到inline slots(obj+0x18)。这里obj.b = 1语句中obj的类型检查被优化了,显然这种优化是合理的,因为刚对obj做过类型检查没有必要再检查一次(obj在当前block还是活跃的)。但是并不是任何情况都会把第二次对象的类型检查优化掉,比如:
如果some statements可能改变Object的Memory Layout,那么第二次对obj的类型检查就不应该被优化掉。
some statements改变Object Memory Layout的方式有多种,笔记1中介绍了通过脚本回调的方式改变Object Memory Layout,笔记2中会介绍另一种不需要脚本回调的方式:OpCode Side Effect。
0x2 Case Study: CVE-2018-8617
这里的some statements为b.push(0)。 根据case的描述可以知道,当向一个有inline slots的Object添加一个数字属性时,Object的Type会发生改变,从而memory layout也会发生改变,原来保存inline slots被替换为auxSlots指针。
补丁前Lowerer阶段的dump:
因为ForwardPass阶段在分析a.b = 2语句后,对象a被认为是活跃的,并且补丁前JIT不知道OpCode::InlineArrayPush存在Side Effect会改变对象a的Type。因此分析a.a = 0x1234语句时会认为对象a已经经过了类型检查,从而直接使用已经保存的对象a的Type来访问属性a,最终直接将常量0x1234写入对象a原来的inline slots处。
实际上OpCode::InlineArrayPush存在Side Effect:
b.push(0);会调用JavascriptArray::Push,JavascriptArray::Push中如果push的Object不是JavascriptArray则进入EntryPushNonJavascriptArray:
进一步会调用DynamicObject::SetItem,因为当前的DynamicObject没有objectArray属性,会调用DynamicObject::SetObjectArray为DynamicObject添加objectArray属性:
SetObjectArray内部会调用DeoptimizeObjectHeaderInlining() 删除原来DynamicObject的inline slots:
最终通过DynamicTypeHandler::AdjustSlots将原来保存的inline slots替换为auxSlots指针:
由于JIT不知道obj2.push(0)语句存在side effect,对象a的类型检查被优化,从而auxSlots指针被常量0x1234当作inline slots覆盖,最终在Interpreter中print(o.a);语句触发auxSlots指针解引用异常:
0x3 Patch Analysis
因为漏洞产生的原因是OpCode::InlineArrayPush存在Side Effect,因此通过:
KillObjectHeaderInlinedTypeSyms(this->currentBlock, false);删除当前block已经ObjectHeaderInlined的Object的Symbol。具体过程:
KillObjectHeaderInlinedTypeSyms内部调用MapObjectHeaderInlinedTypeSymsUntil:
MapObjectHeaderInlinedTypeSymsUntil内部从当前block的objectTypeSyms中逐一取出Object的Symbol ID,根据Symbol ID再从globOptData中取出该Object的Value,如果当前Object的Type是DynamicType并且IsObjectHeaderInlinedTypeHandler()为true,则调用从KillObjectHeaderInlinedTypeSyms传入的函数指针从活跃变量列表中删除该Object的Symbol:
当ForwardPass分析到IR:
s16(s5<s19>[LikelyObject]->a) [CanBeTaggedValue_Int].var! = StFld 0x1000000001234.var #001d
在OptDst时,因为这里是一个Object的属性操作,因此会从活跃变量列表中查找是否存在该Object,如果存在则标记该目标操作数已经经过类型检查,否则需要重新进行类型检查:
补丁后Lowerer阶段的dump:
可以看到这里对对象a生成了两次类型检查,分别是Type和InlineCache,如果发现类型检查失败则调用Op_PatchPutValueNoLocalFastPath重新获取对象属性并更新InlineCache。
0x4 Thinking
对于这种OpCode存在Side Effect的情况可以看到微软目前的修复方法是case by case。观察GlobOpt::ProcessFieldKills函数可以发现有很多类似的Kill情况。实际上GPZ的bug列表中还有一些漏洞原理类似的case,笔者将不再详细说明,感兴趣的同学可以自行分析:
发表评论
您还未登录,请先登录。
登录