环境搭建
该漏洞于commit 52a9e67a477bdb67ca893c25c145ef5191976220,因此可以切换至其上一版本568979f4d891bafec875fab20f608ff9392f4f29
进行漏洞复现。
可以直接利用如下脚本编译release
版本
1 |
|
本文涉及的环境及代码可以从此处下载。
漏洞原因
源码分析
漏洞存在于src/compiler/js-operator.cc:625
。在此处,代码定义了许多常见IR
操作的标识,存在问题的是对JSCreateObject
操作的判断。
1 |
|
关于IR
,是TurboFan
内部使用的一种基于图的中间表示,基于Sea-of-Nodes
思想。TurboFan
通过各节点的的控制依赖(Control dependence
)、数据依赖(Data dependence
)和操作依赖(Effect dependence
)构建IR
,并通过多次运行收集的类型信息进行推断优化(speculate
)。
而此处定义的IR
操作标识,标识在CreateObject
操作过程中不存在可见的副作用(side-effects
),即无需记录到影响链(effect chain
)中去。
关于标志的枚举定义在src/compiler/operator.h:28
1 | // Properties inform the operator-independent optimizer about legal |
而关于JSCreateObject
这个操作不存在副作用的推断是否正确,还需要进一步分析。
在Turbofan
的优化过程中,存在一个generic-lowering
阶段,其作用是将JS
前缀指令转换为更简单的调用和stub
调用。在src/compiler/js-generic-lowering.cc:404
,可以看到在generic-lowering
中,Turbofan
把JSCreateObject
节点用Builtins
函数kCreateObjectWithoutProperties
代替,而kCreateObjectWithoutProperties
就是一个stub
调用。
1 | void JSGenericLowering::LowerJSCreateObject(Node* node) { |
kCreateObjectWithoutProperties
函数定义在src/builtins/builtins-object-gen.cc:1101
。
Ps:这个函数在调试时没有办法直接设置运行断点,需要在函数开头自行添加DebugBreak()
1 | TF_BUILTIN(CreateObjectWithoutProperties, ObjectBuiltinsAssembler) { |
在kCreateObjectWithoutProperties
的最后调用了runtime
函数ObjectCreate
,定义在src/runtime/runtime-object.cc:316
,在对输入的Object
中的prototype
属性进行了简单判断后,调用了JSObject::ObjectCreate
。
1 | // ES6 section 19.1.2.2 Object.create ( O [ , Properties ] ) |
JSObject::ObjectCreate
函数定义在src/objects.cc:1360
,可以看到整个函数的流程是利用原有Object
中的Map
生成新的Map
,再根据Map
的类型,去生成新的Object
。其中Map
分为两个模式,dictionary mode
和fast mode
,dictionary mode
类似于hash
表存储,结构较复杂。fast mode
是简单的结构体模式。
1 | // Notice: This is NOT 19.1.2.2 Object.create ( O, Properties ) |
在Map::GetObjectCreateMap
函数中涉及了对输入的Object
的操作,定义于src/objects.cc:5450
。
首先对map
和prototype
的类型进行判断,当满足(prototype->IsJSObject()
且!js_prototype->map()->is_prototype_map()
调用JSObject::OptimizeAsPrototype(js_prototype);
输入的Object
进行优化。
1 | Handle<Map> Map::GetObjectCreateMap(Isolate* isolate, |
在JSObject::OptimizeAsPrototype
函数中 ,定义于src/objects.cc:12518
,当满足PrototypeBenefitsFromNormalization(object))
时,调用JSObject::NormalizeProperties
对原有Object
进行优化。
然后再根据原Object
的map
,申请并复制生成新map
。
1 | // static |
在JSObject::NormalizeProperties
函数中,src/objects.cc:6436
,可以发现该函数会调用Map::Normalize
根据原有的map
生成一个新的map
,并且利用新的map
重新构建输入的Object
,这明显是一个具有side-effect
的操作。
1 | void JSObject::NormalizeProperties(Handle<JSObject> object, |
继续跟进Map::Normalize
,src/objects.cc:9185
,新的map
是由Map::CopyNormalized
生成的。
1 | Handle<Map> Map::Normalize(Isolate* isolate, Handle<Map> fast_map, |
在Map::CopyNormalized
函数中, src/objects.cc:9247
,利用RawCopy
生成了新的map,随后进行了赋值,包括set_is_dictionary_map
,比较明显的是,新生成的map
是dictionary
模式的。
1 | Handle<Map> Map::CopyNormalized(Isolate* isolate, Handle<Map> map, |
在Map::RawCopy
中,src/objects.cc:9163
,首先新建了一个Handle<Map>
,并调用Map::SetPrototype
为其设置prototype
属性。
1 | Handle<Map> Map::RawCopy(Isolate* isolate, Handle<Map> map, int instance_size, |
在Map::SetPrototype
中,src/objects.cc:12792
,调用JSObject::OptimizeAsPrototype
为原有Object
的prototype
进行优化。
1 | // static |
经过JSObject::OptimizeAsPrototype
(src/objects.cc:12519
) 未满足条件,故不进行优化。
最终,原有Object
调用JSObject::MigrateToMap
,src/objects.cc:4514
,根据生成的dictionary mode map
进行了重构。
1 | void JSObject::MigrateToMap(Handle<JSObject> object, Handle<Map> new_map, |
从而我们找到了经过JSCreate
操作的数据,是可以被改变的,因此将JSCreate
认定为KNoWrite
的确是不正确的。
思路验证
关于JSCreate
操作可以利用Object.create
函数触发。
语法
Object.create(proto, [propertiesObject])
参数
新创建对象的原型对象。
propertiesObject
可选。如果没有指定为 undefined,则是要添加到新创建对象的可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)对象的属性描述符以及相应的属性名称。这些属性对应Object.defineProperties()的第二个参数。
返回值
一个新对象,带着指定的原型对象和属性。例外
如果propertiesObject参数是 null 或非原始包装对象,则抛出一个 TypeError 异常。
通过如下代码可以发现,在执行如下代码后,Object a
的map
的确从fast mode
变成了Dictionary
1 | let a = {x : 1}; |
1 | ┌─[p4nda@p4nda-virtual-machine] - [~/Desktop/browser/ctf/CVE-2018-17463/v8/out.gn/x64.debug] - [三 6月 12, 19:23] |
漏洞利用
漏洞触发
当前的漏洞已经可以影响一个Object
的结构,将其模式修改为Directory
,但究竟可以影响Object
哪些位置,还需要进一步探究,首先可以先从Object
自身结构入手,研究一下在Object.create
对传入对象的影响。
我们知道,在JavaScript
中对于一个对象属性的定义有两种,一种是在属性初始化时加入,另一种是在操作中加入,测试代码如下:
1 | let a = {x : 1,y:2,z:3}; |
在第一处readline
时,可以发现a
这个Object
的构造如下:
1 | pwndbg> v8print 0x31132b18e1e9 |
可以发现,a
拥有6个属性,其中b、c、d
标志为properties[x]
,继续查看这个Object
的map
,发现在map
中指明了整个Object
的大小是48字节,并存在3个inobject properties
也就是保存在结构体内部的属性,且是[FastProperties]
模式的。
1 | pwndbg> v8print 0x0bf82a48cb11 |
根据JS
中对象指针的形式,可以查看这个Object
的结构,显然我们在a
初始化中声明的属性值x,y,z
被保存在结构体内部,且符合map
中指出的三个结构体。
1 | pwndbg> x /6gx 0x31132b18e1e8 |
再看a
结构体中的第二个8字节,在v8print
中可以看出其指向properties
成员。
1 | pwndbg> v8print 0x31132b18e389 |
发现在后续操作中添加的a,b,c
被保存在这里,并且属性值的存储顺序是固定的。
1 | 0x31132b18e388: 0x0000006338503899 0x0000000300000000 |
而在执行Object.create
后,可以发现a
的map
成员发生了改变,符合我们之前对源码的分析,Object.create
对输入的map
进行了优化,改为了DictionaryProperties
模式:
1 | pwndbg> v8print 0x31132b18e1e9 |
而再次查看结构体,发现其中保存的x,y,z
属性值并未存在结构体中:
1 | pwndbg> x /6gx 0x31132b18e1e8 |
而观察properties
成员,发现长度发生明显变化,并且之前存在Object
结构体中的x,y,z
也进入了properties
中。
1 | pwndbg> v8print 0x31132b18e4f9 |
而且,其中保存的值也并非顺序保存的,并且结构比较复杂,前0x38个字节代表结构体的map,length
等成员,后面有0x35项数据,每个数据占16字节,前8字节代表属性名,后8字节代表属性值。
1 | pwndbg> x /25gx 0x31132b18e4f8 |
至此,我们大致可以将Object.create
对一个Object
的影响搞清了,该结构会把全部的属性值都放到properties
中存储,并将原先的线性结构改成hash
表的字典结构。
回到漏洞,如何将这个side-effect
推断错误的影响扩大化呢?一般的想法是利用优化来去掉一些检查的节点。
例如代码:
1 | function foo(o) { |
其生成的IR code
可能是如下的:
1 | CheckHeapObject o |
可以看到第二次当o
不变时,第二次CheckMap o, map1
是多余的,这次检查节点是可以消除的。
在src/compiler/checkpoint-elimination.cc:18
中,可以看到当两个检查节点中间的操作属性是kNoWrite
时,则第二个检查节点时多余的。
1 | // The given checkpoint is redundant if it is effect-wise dominated by another |
那么利用这一点,可以构造一个函数,首先访问一次其内部变量,然后调用Object.create
操作,再次访问另一个变量,那么可能造成第二个变量的类型检查消失,如果结合DictionaryProperties
和FastProperties
特性是可以构造一个非预期的情况。如首先构造一个数组x
,初始化时赋予属性a=0x1234
,增加属性b=0x5678
,构造函数bad_create
:首先访问x.a
,这里可以通过类型检查,而在后续返回x.b
,由于JSCreate
的属性是kNoWrite
的,则返回之前的x.b
二次检查消失,造成仍然返回一个与x.b
偏移相同的数据,但由于Properties
的内存分布发生变化,一定不会是0x5678
。
剩下的就是循环这个函数10000
次,触发优化发生。
1 | function check_vul(){ |
执行这个函数,会发现的确符合我们的预期,某次函数的返回值并不是0x5678,同时也观察到并不是函数每次执行都会发生这一现象。
1 | ┌─[p4nda@p4nda-virtual-machine] - [~/Desktop/browser/ctf/CVE-2018-17463/v8/out/x64.release] - [四 6月 13, 12:30] |
至此,取得了阶段性进展,可以稳定触发漏洞了。
类型混淆
当可以消除第二个检查节点后就可以获得DictionaryProperties
的稳定偏移数据了。但是DictionaryProperties
是一个hash
表,其每次触发时对应的保存数据位置并不相同,可能存在随机化的因素在,如下是之前测试代码两次执行的Properties
内存结构,可以发现各属性的偏移位置并不固定
第一次:
1 | pwndbg> v8print 0x1a8ce618e1d1 |
第二次:
1 | pwndbg> v8print 0x1bf5b6e0e1d1 |
但发现另一规律:在一次执行过程中,相同属性构造的Object
,在DictionaryProperties
中的偏移是相同的:
执行如下代码:
1 | let a1 = {x : 1,y:2,z:3}; |
发现a1
,a2
即使属性值不同,但在Properties
中属性名相同的仍存在同一位置。
1 | pwndbg> v8print 0x20913a10e231 |
那么我们可以得到一个结论,在一次利用中只要找到一对可以用于类型混淆的属性名就可以作为先验知识一直使用了。
我们可以通过构建一个对象,其中把属性名和属性值设置为有规律的键值对,如{‘bi’ => -(i+0x4869) },在恶意构造的函数中,返回全部可读的Properties
值,通过其值的规律性,可以找到一对在属性改变先后可以对应的属性名X1、X2
,达到恶意函数返回a.x1
,实质上是返回a.X2的目的,从而造成类型混淆。
搜索X1、X2
对的代码如下:
1 | // check collision between directory mode and fast mode |
结果可发现,在每次执行中键值对都不同:
1 | ┌─[p4nda@p4nda-virtual-machine] - [~/Desktop/browser/ctf/CVE-2018-17463/v8/out/x64.release] - [四 6月 13, 12:57] |
此后,可以通过得到的键值对可以造成类型混淆了。
addrof原语
通过得到的键值对设为X,Y
,那么构建一个新的Object
,
1 | o.X = {x1:1.1,x2:1.2}; |
并且构建恶意函数
1 | function bad_create(o){ |
那么在返回o.X.x1
的时候,实际上返回的是obj
结构体的地址,从而对浮点型进行转换就可以得到对应obj地址了。
任意地址读写原语
同样利用上文属性值对,与addrof
原语类似,当访问键值X
时,实际上是对键值Y
属性值相对偏移的操作。
对于任意地址读写,我们可以想到一个好用的数据结构ArrayBuffer
。一个ArrayBuffer
的结构体如下:
1 | pwndbg> v8print 0x1d4b8ef8e1a9 |
其长度由byte_length
指定,而实际读写的内存位于backing_store
,当可以修改一个ArrayBuffer
的backing_store
时就可以对任意地址进行读写。而此成员在结构体中的偏移是0x20:
1 | wndbg> x /10gx 0x1d4b8ef8e1a8 |
此时我们仅需构造一个对偏移+0x20写的操作就可以控制ArrayBuffer
的读写内存。此时根据对FastProperties
的了解,如果构建Object
为{x0:{x1:1.1,x2:1.2}}
,则对x0.x2
的写操作,恰好可以改变对应键值的backing_store
,造成内存任意写。
因此恶意函数构造如下:
1 | function bad_create(o,value){ |
Shellcode执行
综上,拥有了addrof
原语和任意地址读写的能力,可以利用wasm
机制来执行shellcode
。
例如一个wasm
实例构造如下:
1 | var buffer = new Uint8Array([0,97,115,109,1,0,0,0,1,138,128,128,128,0,2,96,0,1,127,96,1,127,1,127,2,140,128,128,128,0,1,3,101,110,118,4,112,117,116,115,0,1,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,146,128,128,128,0,2,6,109,101,109,111,114,121,2,0,5,112,52,110,100,97,0,1,10,145,128,128,128,0,1,139,128,128,128,0,1,1,127,65,16,16,0,26,32,0,11,11,150,128,128,128,0,1,0,65,16,11,16,72,97,99,107,101,100,32,98,121,32,80,52,110,100,97,0]); |
其中,f
是一个JSFunction
对象,只不过其实际执行代码存放于一个rwx
的内存中,通过写该内存的代码区域,最终调用f()
,触发来执行shellcode
。
具体思路如下:
首先,构造wasm
对象f
方便shellcode
执行,并利用addrof
原语泄露f
的地址。
然后,定义一个ArrayBuffer
对象,并利用gc
机制使其被放入Old Space
使地址更加稳定。
之后,不断的利用该ArrayBuffer
对象,泄露并修改其backing_store
成员指向待读写区域,具体修改顺序为从JSFucntion
到rwx
区域的寻址流程:
1 | JSFucntion -(0x18)->SharedFunctionInfo -(0x8)-> WasmExportedFunctionData -(0x10)-> WasmInstanceObject -(0xc8)-> imported_function_targets -(0)-> rwx_area |
最终,向rwx_area
写入shellcode
,调用f()
触发。
具体利用效果如下:
EXP
1 | function gc() |
漏洞补丁
漏洞补丁很简单,在commit 52a9e67a477bdb67ca893c25c145ef5191976220
中,将CreateObject
的flag
改为Operator::kNoProperties
。
1 | diff --git a/src/compiler/js-operator.cc b/src/compiler/js-operator.cc |
Reference
[1] http://phrack.org/papers/jit_exploitation.html
[2] https://www.jianshu.com/p/f23c5cad7160
[3] https://ponyfoo.com/articles/an-introduction-to-speculative-optimization-in-v8