这题是starctf2019 oob 的v8漏洞利用题。
从网上的博客里拿到了v8 的diff 文件,按照对应的命令进行patch 和 编译,编译的命令和diff 代码如下:
1 | git reset --hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598 |
1 | diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc |
从上面的diff 文件可以看到, 新加了一个array的方法叫 oob
, 代码中的len 代表了这个oob 方法的入参个数, 如果没有参数就是读操作,其他就是写操作。 代码的问题在于读写的length
出了问题,正常访问数组的最后一个元素,它的长度应该是length - 1, 相当于这里直接变成了越界读写数组后一位。
在TypedArray 中,array的结构体如下(不确定不同版本会不会有变化):
1 | elements ----> +------------------------+ |
当我们创建一个TypedArray的时候, 实际上是返回了一个ArrayObject 的指针, 这个Object中, map 指向的是这个Object的类型, prototype 指向的是非索引元素, elements 指向索引元素(也就是说可以用数字下标访问),length 代表了数组的长度,但这个length 是SMI 结构的。 properties暂时不太了解。
elements 也有一个类型map和数组长度,后面跟的就是数据了。
根据代码来看,越界读写的数据是ArrayObject 的 map 字段。
我们可以先试着写poc 看一下运行时数据:
1 | // ./d8 --allow-natives-syntax poc.js |
debug 版本会用DCHECK 确认array 的长度是否越界,所以没法直接触发。
release 的版本,DebugPrint 并没有提供太多的有用信息,只能gdb 调试了。
读别人的博客时发现,编辑out.gn/x64.release/args.gn,打开开关就可以打印信息了。
1 | v8_enable_backtrace = true |
启动gdb 调试后, 由于我们知道ArrayObject的地址,我们可以根据这个结构体,直接找到elements 所在的位置,然后 telescope 看一下具体的数据
1 | pwndbg> telescope 0x38b208ecddf8 |
1 | pwndbg> telescope 0x38b208ecddf8 |
结果可以看到,ArrayObject 的map 字段被修改了,也就是说这题考察如果map 相关的利用知识,这就触及到我知识的盲区了。
map 代表了对象的类型,如果能将不同类型的对象进行修改,就能够达到类型混淆的作用,这个类型混淆利用可以参考以下这就话:
如果我们定义一个FloatArray浮点数数组A,然后定义一个对象数组B。正常情况下,访问A[0]返回的是一个浮点数,访问B[0]返回的是一个对象元素。如果将B的类型修改为A的类型,那么再次访问B[0]时,返回的就不是对象元素B[0],而是B[0]对象元素转换为浮点数即B[0]对象的内存地址了;如果将A的类型修改为B的类型,那么再次访问A[0]时,返回的就不是浮点数A[0],而是以A[0]为内存地址的一个JavaScript对象了. 造成上面的原因在于,v8完全依赖Map类型对js对象进行解析
具体的操作如下:
计算一个对象的地址addressOf:将需要计算内存地址的对象存放到一个对象数组中的A[0],然后利用上述类型混淆漏洞,将对象数组的Map类型修改为浮点数数组的类型,访问A[0]即可得到浮点数表示的目标对象的内存地址。
将一个内存地址伪造为一个对象fakeObject:将需要伪造的内存地址存放到一个浮点数数组中的B[0],然后利用上述类型混淆漏洞,将浮点数数组的Map类型修改为对象数组的类型,那么B[0]此时就代表了以这个内存地址为起始地址的一个JS对象了。
具体利用代码如下:
1 |
|
有了这两个方法之后,我们希望能够将其转化为任意地址读写。根据上一篇博客,我们知道有2个地方可以让我们操作, 一个是TypedArray的elements 地址, 一个是ArrayBuffer 的backing_store.
在这题里面,我们只要伪造一个TypedArray 对象就行,然后对这个TypedArray 进行读写操作。
1 | var fake_array = [ |
这里有个比较有意思的现象,fake_array 里如果不加1.1,2.2 就会执行失败,因为是结构体没有伪造号导致的。
之后对这个fake_obj 进行操作就可以了,因为里面的数据是完全可控的。
另外fake_array 也可以构造成ArrayBuffer 的格式。
1 | function read64(addr) |
常见的方式就是加载wasm, 找到rwx的地址,然后通过修改ArrayBuffer的backing_store地址为rwx的地址,将shellcode 写入,然后调用wasm function 就行了。
wasm 找rwx 段(不同版本偏移kennel不一样):
func_addr->shared_info->data->instance + 0x88
由于我们有addrof了, 实际上addr_of(WebAssembly.Instance) 就能够拿到地址了,省去了自己找地址的麻烦。
1 | var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,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,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]); |
v8在生成一个数组对象过程中,会对应着生成一个code对象,这个code对象中存储了和该数组对象相关的构造函数指令,而这些构造函数指令又会去调用d8二进制中的指令地址来完成对数组对象的构造。因此可以用来泄漏d8地址,进而修改got表执行/bin/sh.
1 | var a = [1.1, 2.2, 3.3]; |
另外还有一种方法是通过map 找堆地址,然后进一步找程序基址的
1 | var test_addr = addrof(test); |
不稳定的方法和前面的方法差不多,关键在于内存搜索一定的特征,根据特征找d8的地址,进而找到libc等地址,然后进行利用。这部分就不展开了。
exp 来自于其他博客,仅供参考
1 | var buf = new ArrayBuffer(16); |
我又变强了一点点。
引用的文章太多了,但凡带了starctf oob 的博客我都翻了一遍,感谢这些博客:).