从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 常见问题解决

  1. Python版本问题
    • 可能需要手动修改脚本或使用批量转换工具
    • 修改后必须创建新分支提交:
git checkout -b "1"
git add .
git commit -m "提交信息"
  1. 同步问题
    • 使用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 调试辅助函数

  1. JavaScript调试函数

    • %DebugPrint(arg1):打印目标对象的内存地址和主要信息
    • %SystemBreak():在脚本中设置断点
  2. 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 类型混淆原理

漏洞利用关键点

  1. 浮点型数组的elements后面紧跟着数组结构
  2. 如果存在溢出,可以覆盖到map
  3. 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 关键概念

  1. Map混淆:通过修改对象的map字段,改变V8对对象的解释方式
  2. 类型转换:利用错误的类型解释实现任意地址读写
  3. 内存布局:理解V8对象在内存中的排列方式

6.2 利用步骤概述

  1. 触发漏洞导致内存越界写入
  2. 覆盖相邻对象的map字段
  3. 构造类型混淆实现任意地址读写
  4. 利用读写原语执行shellcode或劫持控制流

7. 参考文献

  1. V8漏洞利用系列文章
  2. V8官方文档
  3. Chromium源码

这份文档涵盖了V8漏洞利用的基础知识,从环境搭建到调试技巧,再到关键的对象结构和漏洞原理。后续可以在此基础上深入探讨具体的漏洞利用技术和高级技巧。

V8漏洞利用从入门到精通:环境搭建与调试基础 1. V8环境搭建 1.1 准备工作 科学上网 :建议开启Clash for Windows的TUN模式以加速下载过程 系统要求 :Linux环境(推荐Ubuntu 18.04+) 1.2 工具安装 1.3 获取V8源码 2. 编译特定版本V8 2.1 标准编译流程 编译输出位于: ./out/x64.debug/d8 ./out/x64.debug/shell 2.2 编译指定漏洞版本 2.3 常见问题解决 Python版本问题 : 可能需要手动修改脚本或使用批量转换工具 修改后必须创建新分支提交: 同步问题 : 使用 gclient sync -D 时遇到问题需要先创建分支再修改 3. 调试环境配置 3.1 GDB调试脚本 将以下脚本保存为 .gdbinit 文件: 3.2 调试辅助函数 JavaScript调试函数 : %DebugPrint(arg1) :打印目标对象的内存地址和主要信息 %SystemBreak() :在脚本中设置断点 GDB命令 : 启动调试: gdb ./out/x64.debug/d8 设置参数: set args --allow-natives-syntax test.js 运行: r 4. V8对象结构分析 4.1 对象内存布局示例 输出示例: 4.2 Map结构详解 Map是V8中最重要的数据结构之一,包含以下信息: 对象的动态类型(String, Uint8Array, HeapNumber等) 对象的大小(字节) 对象的属性及其存储位置 数组元素的类型(unboxed双精度数或带标记的指针) 对象的原型 构造函数信息 Map的关键作用: 属性名称通常存储在Map中,属性值存储在对象本身 Map定义了如何访问对象: 对象数组:存储每个对象的地址 浮点数组:以浮点数形式存储数值 4.3 类型混淆原理 漏洞利用关键点 : 浮点型数组的elements后面紧跟着数组结构 如果存在溢出,可以覆盖到map map控制着类型标识,可以导致类型混淆 利用场景 : 将对象数组的map换成浮点数组map → 以浮点数形式解释对象地址 将浮点数组的map换成对象数组map → 以对象形式解释浮点数值 5. 不同类型变量结构对比 示例代码: 关键观察: 浮点数组和对象数组在内存布局上的差异 浮点数组的elements直接包含数值 对象数组的elements包含的是其他对象的指针 6. 漏洞利用基础 6.1 关键概念 Map混淆 :通过修改对象的map字段,改变V8对对象的解释方式 类型转换 :利用错误的类型解释实现任意地址读写 内存布局 :理解V8对象在内存中的排列方式 6.2 利用步骤概述 触发漏洞导致内存越界写入 覆盖相邻对象的map字段 构造类型混淆实现任意地址读写 利用读写原语执行shellcode或劫持控制流 7. 参考文献 V8漏洞利用系列文章 V8官方文档 Chromium源码 这份文档涵盖了V8漏洞利用的基础知识,从环境搭建到调试技巧,再到关键的对象结构和漏洞原理。后续可以在此基础上深入探讨具体的漏洞利用技术和高级技巧。