本文缘由
随着反爬的升级,前端的JavaScript代码越来越难以阅读,一个简单的字符串声明竟然可以拆分成多行代码,虽然并不会给动态调试带来困难,但是在静态分析时着实让人难受。明明一行代码可以搞定的事情,偏偏写成了十行甚至百行,代码冗余非常严重。
我们以下面的代码为例,来讲讲如何还原。
for (var e = "\u0270\u026D\u0274\u0274\u0277\u0234\u0249\u025B\u025C\u0229", a = "", s = 0; s < e.length; s++) {
var r = e.charCodeAt(s) - 520;
a += String.fromCharCode(r);
}
代码很常见,一个for循环 + 一个String.fromCharCode 完成了代码的还原。在浏览器上运行可以得到结果:
其实运行下来,也就是将a的值设置为 "hello,AST!",如果结合实际的代码,其实可以发现它和下面的代码是等价的:
var a = "hello,AST!";
因为其他的变量根本就没有用到过!
也许你有疑问,为什么不转换成赋值语句,而变成声明语句,如果你处理过作用域就知道了。
从源代码:
for (var e = "\u0270\u026D\u0274\u0274\u0277\u0234\u0249\u025B\u025C\u0229", a = "", s = 0; s < e.length; s++) {
var r = e.charCodeAt(s) - 520;
a += String.fromCharCode(r);
}
变成目标代码:
var a = "hello,AST!";
现在只差一个AST的插件了。
分析
这是一个for循环,因此我们需要遍历 ForStatement
第二行源代码是一个声明语句,并且包含charCodeAt函数
第三行代码是一个表达式语句,并包含String.fromCharCode
根据这些特征,我们就可以写一个专用的插件:
const types = require("@babel/types");
const forToString = {
//遍历ForStatement
ForStatement(path) {
let body = path.get("body.body");
if (!body || body.length !== 2)
//根据在线解析网站可以看出body的长度为 2
return;
if (!body[0].isVariableDeclaration() || !body[1].isExpressionStatement()) {
//循环体的第一个语句为声明语句,第二个语句为表达式语句
return;
}
let body0_code = body[0].toString();
let body1_code = body[1].toString();
if (body0_code.indexOf("charCodeAt") != -1 && body1_code.indexOf("String.fromCharCode") != -1)
{//根据上面的分析而来
//dosomething
}
},
}
核心代码的处理
上面下来判断的代码,下面来写核心代码。
运行for循环,得到 "hello,AST!" 这个值。
构造一个 VariableDeclaration 节点,并进行替换
如何得到这个for循环的结果呢?我这里借助 new Function ,代码如下:
//获取赋值语句左边的 a
let expression = body[1].node.expression;
let name = expression.left.name;
//根据Function函数进行构造
let code = path.toString() + "\nreturn " + name;
//构造并运行,即可得到for循环的结果
let func = new Function("",code);
let value = func();
根据value构造VariableDeclaration节点,代码如下:
let new_node = types.VariableDeclaration("var",[types.VariableDeclarator(types.Identifier(name),types.valueToNode(value))]);
然后再进行替换:
path.replaceWith(new_node);
代码合并起来是这样的:
const for_to_string = {
ForStatement(path) {
let body = path.get("body.body");
if (!body || body.length !== 2)
return;
if (!body[0].isVariableDeclaration() || !body[1].isExpressionStatement()) {
return;
}
let body0_code = body[0].toString();
let body1_code = body[1].toString();
if (body0_code.indexOf("charCodeAt") != -1 && body1_code.indexOf("String.fromCharCode") != -1) {
try {
let expression = body[1].node.expression;
let name = expression.left.name;
let code = path.toString() + "\nreturn " + name;
let func = new Function("",code);
let value = func();
let new_node = types.VariableDeclaration("var", [types.VariableDeclarator(types.Identifier(name), types.valueToNode(value))]);
path.replaceWith(new_node);
} catch (e) {};
}
}
}
这里我用了try...catch语句是因为在真正遍历的时候,运行func可能会报错,有可能需要某些依赖,因此用 try...catch语句来捕捉异常。
结语
随着多次的还原练习,只要是结构固定的代码,皆可以编写一键还原的工具,再使用reres映射进行动态调试即可,实在是保头发的不二选择。