tags: [‘AST’,‘解混淆’]
categories: ‘逆向分析’

sticky: ‘8’


# 题目

猿人学 2023 届第三题 点击跳转

# 0x01 降维打击

这里 if 都是多层嵌套的,非常影响阅读体验,直接一步将它变为一维结构吧!

还原前先手动还原小部分代码,接着写反混淆脚本批量修改。

我们单看 if (o < 2) 这个 if 块,很明显 o < 1 其实就是 o==0 而 else 就是 o==1 , 以此类推。

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
(function (e) {
var o = e || 2;
for (;;)
if (o < 2) {
if (o < 1) {
o += 88;
} else {
v.v = !1;
o += 226;
}
} else {
if (o < 3) {
o += 243;
} else {
L = Y || l.length;
o += 4;
}
}
})();
// 手动还原一下 还原如下
(function (e) {
var o = e || 2;
switch (o) {
case 0:
o += 88;
case 1:
v.v = !1;
o += 226;
case 2:
o += 243;
case 3:
L = Y || l.length;
o += 4;
}
})();

这是我写的插件,将多层嵌套的 if 转换为 Switch 语法,可以方便我们后续调试。

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
const IfToCase = {
IfStatement: {
exit(path, state) {
const { test, consequent, alternate } = path.node;
let { name, cases } = state;
// 如果 test 不是 o < 数字,则不进行处理
if (!types.isBinaryExpression(test, { operator: "<" })) return;
if (!types.isIdentifier(test.left, { name: name })) return;
if (!types.isNumericLiteral(test.right)) return;

// alternate.body.push(types.BreakStatement());
// consequent.body.push(types.BreakStatement());

const right = test.right.value;

if (right % 2 === 0) {
// 处理 else 不是 if 节点的情况
if (types.isIfStatement(alternate.body[0])) return;
cases.push(types.SwitchCase(types.valueToNode(right), alternate.body));
return;
}

// 构建 case 节点
const case1 = types.SwitchCase(
types.valueToNode(right - 1),
consequent.body
);
const case2 = types.SwitchCase(types.valueToNode(right), alternate.body);
cases.push(case1, case2);
},
},
};

const IfToSwitch = {
ForStatement(path) {
const { init, test, update, body } = path.node;
const prev = path.getPrevSibling();

if (!types.isIfStatement(body)) return;

const discriminant = prev.node.declarations[0].id;

let cases = [];

path.traverse(IfToCase, { name: discriminant.name, cases: cases });

if (!cases.length) return;

const switchNode = types.SwitchStatement(discriminant, cases);

path.get("body").replaceInline(switchNode);
},
};

对比效果图如下,处理后清晰了不少。

# 0x02 打回原形

经过上一步还原后发现有很多 o += xx | o -= xx 的代码,这里的 o 其实就是 case 的条件,那么简化一下吧! o = o - xxx 其中 o - xxx 部分我们计算出来。

下面插件来喽!又香又脆,嘎嘎香!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const RestoreJump = {
AssignmentExpression(path) {
let { operator, left, right } = path.node;

if (!(operator[1] === "=" && types.isNumericLiteral(right))) return;

let caseNode = path.findParent((p) => p.isSwitchCase());

let { consequent, test } = caseNode.node;
// 构建 ast 节点 o -= xxx 改为 o - xxx ,

let _node = types.assignmentExpression(
"=",
left,
types.valueToNode(
operator[0] === "-"
? test.value - right.value
: test.value + right.value
)
);
path.replaceInline(_node);
},
};

对比效果图如下,处理后清晰了不少。

# 0x03 迷阵寻踪

经过上面还原后 发现很多 case 块只做 o 的修改,那我就知道它下一步要到那个 case 块,唉!我们是不是可以将他们合并起来,插件如下

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
function getJump(node, name) {
if (!types.isExpressionStatement(node)) return;
let { expression } = node;
if (!types.isAssignmentExpression(expression)) return;
if (expression.left.name !== name) return;
return expression.right.value;
}

function getCaseJump(map, name, node) {
let { test, consequent } = node;
let list = [];
let con = consequent[consequent.length - 1];
let num = getJump(con, name);
if (num !== undefined) {
list.push(num);
}
if (types.isIfStatement(con)) {
let { consequent, alternate } = con;
let num = getJump(consequent, name);
if (num !== undefined) {
list.push(num);
}
num = getJump(alternate, name);
if (num !== undefined) {
list.push(num);
}
}
map[test.value] = list;
}

function removeDuplicates(arr1, arr2) {
let result = []; // 存储删除的元素的数组

for (let i = 0; i < arr1.length; i++) {
let found = false;

for (let j = 0; j < arr2.length; j++) {
if (arr1[i] === arr2[j]) {
result.push(arr1[i]); // 将相同的元素存储到结果数组中
arr2.splice(j, 1); // 删除arr2中的相同元素
found = true;
break;
}
}
if (found) {
arr1.splice(i, 1); // 删除arr1中的相同元素
i--; // 由于删除了元素,需要调整索引
}
}
return result;
}

function controlFlowStructure(si, map, cases, stack = [], body = []) {
if (!map.loop) map.loop = [];
if (stack.includes(si)) {
if (map.loop.indexOf(si) === -1) map.loop.push(si);
return body;
}
let item = map[si];
body = body.concat(cases[si].consequent);
switch (item.length) {
case 0:
return body;
case 1:
return controlFlowStructure(item[0], map, cases, stack, body);
case 2:
stack.push(si);
body[body.length - 1].consequent = types.blockStatement(
controlFlowStructure(item[0], map, cases, stack, [])
);
if (map.loop.includes(si)) {
let { test, consequent } = body[body.length - 1];
body[body.length - 1] = types.whileStatement(test, consequent);
body = body.concat(
controlFlowStructure(item[1], map, cases, stack, [])
);
} else {
body[body.length - 1].alternate = types.blockStatement(
controlFlowStructure(item[1], map, cases, stack, [])
);
body = body.concat(
removeDuplicates(
body[body.length - 1].consequent.body,
body[body.length - 1].alternate.body
)
);
}
stack.pop();
return body;
}
}

const MergeCases = {
SwitchStatement(path) {
const { discriminant, cases } = path.node;
const name = discriminant.name;
let binding = path.scope.getBinding(name);
let start = binding.path.node.init.right.value;
let map = {};
for (let i = 0; i < cases.length; i++) {
getCaseJump(map, name, cases[i]);
}
path.replaceInline(controlFlowStructure(start, map, cases));
},
};

对比图如下,都有实质的代码了。

控制流程图

# 0x04 移除污秽

将指针修改的代码去除

1
2
3
4
5
6
7
8
9
10
11
12
const CleaningUpGarbage = {
ForStatement(path) {
let p = path.getPrevSibling();
let name = p.node.declarations[0].id.name;
path.scope.traverse(path.scope.block, {
AssignmentExpression(_path) {
if (!types.isIdentifier(_path.node.left, { name: name })) return;
_path.remove();
},
});
},
};

移除无关代码

# 0x05 结构优化

优化 ifelse 结构,使其更便于阅读。

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
function isEndNode(nodes) {
let typeList = ["ReturnStatement", "ThrowStatement"];
for (let i = 0; i < nodes.length; i++) {
if (typeList.includes(nodes[i].type)) {
return true;
}
}
return false;
}

const ifOptimization = {
IfStatement(path) {
let { test, consequent, alternate } = path.node;
if (!alternate) return;
if (isEndNode(consequent.body)) {
path.insertAfter(alternate.body);
path.node.alternate = null;
} else if (isEndNode(alternate.body)) {
path.insertAfter(consequent.body);
path.replaceInline(
types.ifStatement(types.unaryExpression("!", test), alternate, null)
);
} else if (
types.isIfStatement(consequent.body[consequent.body.length - 1]) &&
isEndNode(consequent.body[consequent.body.length - 1].consequent.body)
) {
if (
generator(alternate.body[0]).code ===
generator(consequent.body[consequent.body.length - 1].alternate.body[0])
.code
) {
path.insertAfter(
consequent.body[consequent.body.length - 1].alternate.body
);
consequent.body[consequent.body.length - 1].alternate = null;
path.node.alternate = null;
}
}
},
};

处理后逻辑尽现。

# 0x06 答辩还原

这里其实就是嵌套的三元表达式,先手动还原几条能发现他本质是 switch

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
const TernaryToSwitch = {
ConditionalExpression(path, { cases, _name }) {
let { test, consequent, alternate } = path.node;
if (!types.isBinaryExpression(test, { operator: "==" })) return;
let { right, left } = test;
if (!types.isIdentifier(right) || !types.isNumericLiteral(left)) return;
if (!cases[right.name]) {
cases[right.name] = [];
}
let body = [
types.expressionStatement(
types.assignmentExpression("=", types.identifier(_name), consequent)
),
];
body.push(types.breakStatement());
cases[right.name].push(types.switchCase(test.left, body));
},
};
const TernaryReturn = {
ReturnStatement(path) {
let { argument } = path.node;
if (!types.isConditionalExpression(argument)) return;
if (!types.isBinaryExpression(argument.test, { operator: "==" })) return;
let cases = {};
let name = argument.test.right.name;
if (!name) return;
let _name = `${name}${path.node.start}`;
path.traverse(TernaryToSwitch, { cases: cases, _name: _name });
path.insertBefore(
types.variableDeclaration("var", [
types.variableDeclarator(types.identifier(_name), null),
])
);
path.replaceInline(
types.switchStatement(types.identifier(name), cases[name])
);
path.insertAfter(types.returnStatement(types.identifier(_name)));
},
};

对比效果图如下,处理后犹如拨云见日,茅塞顿开。(还原它主要是为了反编译 jsvmp)

# 0x07 窥探本源

反编译后魔改点与环境检测清晰可见