Chakra漏洞调试笔记2——OpCode Side Effect

k1   ·   发表于 2019-08-01 10:12:38   ·   漏洞文章

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中保存相关信息:

  1. Object one和two包含属性x和y
  2. Object one属性x的值是10,y的值是20
  3. Object two属性x的值是40,y的值是50

通过记录以上信息就可以实现Object属性的访问。可以看到对象one和two保存的(1)的信息是相同的,(2)(3)的信息则因对象的属性值不同而不同。因此Chakra通过建立一个property map和一个 slots array的对应关系来实现属性的读写功能:

  1. property map用来映射属性名(实际上是PropertyId)和slots array的索引,比如属性x存储在slots array的索引 = 0
  2. 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,笔者将不再详细说明,感兴趣的同学可以自行分析:

 

0x5 References

  1. http://abchatra.github.io/Type/
  2. https://i.blackhat.com/asia-19/Fri-March-29/bh-asia-Li-Using-the-JIT-Vulnerability-to-Pwning-Microsoft-Edge.pdf
  3. https://blogs.projectmoon.pw/2018/08/17/Edge-InlineArrayPush-Remote-Code-Execution/
  4. https://www.anquanke.com/post/id/180551


转载自安全课

打赏我,让我更有动力~

0 条回复   |  直到 2019-8-1 | 1663 次浏览
登录后才可发表内容
返回顶部 投诉反馈

© 2016 - 2024 掌控者 All Rights Reserved.