本文缘由
随着反爬的升级,前端的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映射进行动态调试即可,实在是保头发的不二选择。
文章来源: blog.csdn.net,作者:悦来客栈的老板,版权归原作者所有,如需转载,请联系作者。
原文链接:blog.csdn.net/qq523176585/article/details/111570618