从0到1学习v8漏洞之路(一)
字数 1324 2025-08-22 12:22:48
V8漏洞利用从入门到精通:环境搭建与调试基础
1. V8环境搭建
1.1 准备工作
- 科学上网:建议开启Clash for Windows的TUN模式以加速下载过程
- 系统要求:Linux环境(推荐Ubuntu 18.04+)
1.2 工具安装
# 安装depot_tools
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH="$PATH":`pwd`/depot_tools
# 安装ninja构建工具
git clone https://github.com/ninja-build/ninja.git
cd ninja && ./configure.py --bootstrap && cd ..
export PATH="$PATH":`pwd`/ninja
1.3 获取V8源码
fetch v8 && cd v8
gclient sync
2. 编译特定版本V8
2.1 标准编译流程
# 生成构建配置
tools/dev/v8gen.py x64.debug
# 开始编译
ninja -C out.gn/x64.debug
编译输出位于:
./out/x64.debug/d8./out/x64.debug/shell
2.2 编译指定漏洞版本
# 重置到特定commit
git reset --hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598
# 应用漏洞补丁
git apply < oob.diff
# 同步模块
gclient sync
# 编译debug版本
tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug d8
# 编译release版本
tools/dev/v8gen.py x64.release
ninja -C out.gn/x64.release d8
2.3 常见问题解决
- Python版本问题:
- 可能需要手动修改脚本或使用批量转换工具
- 修改后必须创建新分支提交:
git checkout -b "1"
git add .
git commit -m "提交信息"
- 同步问题:
- 使用
gclient sync -D时遇到问题需要先创建分支再修改
- 使用
3. 调试环境配置
3.1 GDB调试脚本
将以下脚本保存为.gdbinit文件:
# Copyright 2014 the V8 project authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# Print tagged object.
define job
call (void) _v8_internal_Print_Object((void*)($arg0))
end
document job
Print a v8 JavaScript object
Usage: job tagged_ptr
end
# Print content of v8::internal::Handle.
define jh
call (void) _v8_internal_Print_Object(*((v8::internal::Object**)($arg0).location_))
end
document jh
Print content of a v8::internal::Handle
Usage: jh internal_handle
end
# Print content of v8::Local handle.
define jlh
call (void) _v8_internal_Print_Object(*((v8::internal::Object**)($arg0).val_))
end
document jlh
Print content of a v8::Local handle
Usage: jlh local_handle
end
# Print Code objects containing given PC.
define jco
call (void) _v8_internal_Print_Code((void*)($arg0))
end
document jco
Print a v8 Code object from an internal code address
Usage: jco pc
end
# Print LayoutDescriptor.
define jld
call (void) _v8_internal_Print_LayoutDescriptor((void*)($arg0))
end
document jld
Print a v8 LayoutDescriptor object
Usage: jld tagged_ptr
end
# Print TransitionTree.
define jtt
call (void) _v8_internal_Print_TransitionTree((void*)($arg0))
end
document jtt
Print the complete transition tree of the given v8 Map.
Usage: jtt tagged_ptr
end
# Print JavaScript stack trace.
define jst
call (void) _v8_internal_Print_StackTrace()
end
document jst
Print the current JavaScript stack trace
Usage: jst
end
# Print TurboFan graph node.
define pn
call _v8_internal_Node_Print((void*)($arg0))
end
document pn
Print a v8 TurboFan graph node
Usage: pn node_address
end
# Skip the JavaScript stack.
define jss
set $js_entry_sp=v8::internal::Isolate::Current()->thread_local_top()->js_entry_sp
set $rbp=*(void**)$js_entry_sp
set $rsp=$js_entry_sp + 2*sizeof(void*)
set $pc=*(void**)($js_entry_sp+sizeof(void*))
end
document jss
Skip the jitted stack on x64 to where we entered JS last.
Usage: jss
end
# Print stack trace with assertion scopes.
define bta
python
import re
frame_re = re.compile("^#(\d+)\s*(?:0x[a-f\d]+ in at (.+)")
assert_re = re.compile("^\s*(\S+) = .+<v8::internal::Per\w+AssertScope<v8::internal::(\S*), (false|true)>")
btl = gdb.execute("backtrace full", to_string = True).splitlines()
for l in btl:
match = frame_re.match(l)
if match:
print("[%-2s] %-60s %-40s" % (match.group(1), match.group(2), match.group(3)))
match = assert_re.match(l)
if match:
if match.group(3) == "false":
prefix = "Disallow"
color = "\033[91m"
else:
prefix = "Allow"
color = "\033[92m"
print("%s -> %s %s (%s)\033[0m" % (color, prefix, match.group(2), match.group(1)))
end
end
document bta
Print stack trace with assertion scopes
Usage: bta
end
# Search for a pointer inside all valid pages.
define space_find
set $space = $arg0
set $current_page = $space->first_page()
while ($current_page != 0)
printf "# Searching in %p - %p\n", $current_page->area_start(), $current_page->area_end()-1
find $current_page->area_start(), $current_page->area_end()-1, $arg1
set $current_page = $current_page->next_page()
end
end
define heap_find
set $heap = v8::internal::Isolate::Current()->heap()
printf "# Searching for %p in old_space n", $arg0
space_find $heap->old_space() ($arg0)
printf "# Searching for %p in map_space n", $arg0
space_find $heap->map_space() $arg0
printf "# Searching for %p in code_space n", $arg0
space_find $heap->code_space() $arg0
end
document heap_find
Find the location of a given address in V8 pages.
Usage: heap_find address
end
set disassembly-flavor intel
set disable-randomization off
# Install a handler whenever the debugger stops due to a signal. It walks up the
# stack looking for V8_Dcheck and moves the frame to the one above it so it's
# immediately at the line of code that triggered the DCHECK.
python
def dcheck_stop_handler(event):
frame = gdb.selected_frame()
select_frame = None
message = None
count = 0 # limit stack scanning since they're usually shallow and otherwise stack
# overflows can be very slow.
while frame is not None and count < 5:
count += 1
if frame.name() == 'V8_Dcheck':
frame_message = gdb.lookup_symbol('message', frame.block())[0]
if frame_message:
message = frame_message.value(frame).string()
select_frame = frame.older()
break
if frame.name() is not None and frame.name().startswith('V8_Fatal'):
select_frame = frame.older()
frame = frame.older()
if select_frame is not None:
select_frame.select()
gdb.execute('frame')
if message:
print('DCHECK error: {}'.format(message))
gdb.events.stop.connect(dcheck_stop_handler)
end
# Code imported from chromium/src/tools/gdb/gdbinit
python
import os
import subprocess
import sys
compile_dirs = set()
def get_current_debug_file_directories():
dir = gdb.execute("show debug-file-directory", to_string=True)
dir = dir[
len('The directory where separate debug symbols are searched for is "'):-len('".\n"')
]
return set(dir.split(":"))
def add_debug_file_directory(dir):
# gdb has no function to add debug-file-directory, simulates that by using
# `show debug-file-directory` and `set debug-file-directory <directories>`.
current_dirs = get_current_debug_file_directories()
current_dirs.add(dir)
gdb.execute(
"set debug-file-directory %s" % ":".join(current_dirs),
to_string=True)
def newobj_handler(event):
global compile_dirs
compile_dir = os.path.dirname(event.new_objfile.filename)
if not compile_dir:
return
if compile_dir in compile_dirs:
return
compile_dirs.add(compile_dir)
# Add source path
gdb.execute("dir %s" % compile_dir)
# Need to tell the location of .dwo files.
# https://sourceware.org/gdb/onlinedocs/gdb/Separate-Debug-Files.html
# https://crbug.com/603286#c35
add_debug_file_directory(compile_dir)
# Event hook for newly loaded objfiles.
# https://sourceware.org/gdb/onlinedocs/gdb/Events-In-Python.html
gdb.events.new_objfile.connect(newobj_handler)
gdb.execute("set environment V8_GDBINIT_SOURCED=1")
end
3.2 调试辅助函数
-
JavaScript调试函数:
%DebugPrint(arg1):打印目标对象的内存地址和主要信息%SystemBreak():在脚本中设置断点
-
GDB命令:
- 启动调试:
gdb ./out/x64.debug/d8 - 设置参数:
set args --allow-natives-syntax test.js - 运行:
r
- 启动调试:
4. V8对象结构分析
4.1 对象内存布局示例
a = [1.1,5,6];
%DebugPrint(a);
%SystemBreak();
输出示例:
DebugPrint: 0x17df00288ec9: [JSArray]
- map: 0x17df0008d33d <Map[16](PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x17df0008cca9 <JSArray[0]>
- elements: 0x17df00288ea1 <FixedDoubleArray[4]> [PACKED_DOUBLE_ELEMENTS]
- length: 4
- properties: 0x17df00000775 <FixedArray[0]>
- All own properties (excluding elements): {
0x17df00000dc1: [String] in ReadOnlySpace: #length: 0x17df002517b1 <AccessorInfo name= 0x17df00000dc1 <String[6]: #length>, data= 0x17df00000069 <undefined>> (const accessor descriptor, attrs: [W__]), location: descriptor
}
- elements: 0x17df00288ea1 <FixedDoubleArray[4]> {
0: 1.1
1: 2.3
2: 3.4
3: 4.4
}
4.2 Map结构详解
Map是V8中最重要的数据结构之一,包含以下信息:
- 对象的动态类型(String, Uint8Array, HeapNumber等)
- 对象的大小(字节)
- 对象的属性及其存储位置
- 数组元素的类型(unboxed双精度数或带标记的指针)
- 对象的原型
- 构造函数信息
Map的关键作用:
- 属性名称通常存储在Map中,属性值存储在对象本身
- Map定义了如何访问对象:
- 对象数组:存储每个对象的地址
- 浮点数组:以浮点数形式存储数值
4.3 类型混淆原理
漏洞利用关键点:
- 浮点型数组的elements后面紧跟着数组结构
- 如果存在溢出,可以覆盖到map
- map控制着类型标识,可以导致类型混淆
利用场景:
- 将对象数组的map换成浮点数组map → 以浮点数形式解释对象地址
- 将浮点数组的map换成对象数组map → 以对象形式解释浮点数值
5. 不同类型变量结构对比
示例代码:
a = [1.1,5,6]; // 浮点数组
%DebugPrint(a);
%SystemBreak();
b = {"a": 45}; // 普通对象
%DebugPrint(b);
%SystemBreak();
c = [b]; // 对象数组
%DebugPrint(c);
%SystemBreak();
关键观察:
- 浮点数组和对象数组在内存布局上的差异
- 浮点数组的elements直接包含数值
- 对象数组的elements包含的是其他对象的指针
6. 漏洞利用基础
6.1 关键概念
- Map混淆:通过修改对象的map字段,改变V8对对象的解释方式
- 类型转换:利用错误的类型解释实现任意地址读写
- 内存布局:理解V8对象在内存中的排列方式
6.2 利用步骤概述
- 触发漏洞导致内存越界写入
- 覆盖相邻对象的map字段
- 构造类型混淆实现任意地址读写
- 利用读写原语执行shellcode或劫持控制流
7. 参考文献
- V8漏洞利用系列文章
- V8官方文档
- Chromium源码
这份文档涵盖了V8漏洞利用的基础知识,从环境搭建到调试技巧,再到关键的对象结构和漏洞原理。后续可以在此基础上深入探讨具体的漏洞利用技术和高级技巧。