文章目录
- AST反混淆脚本
-
- [1. 基础概念](#1. 基础概念)
- [2. 前置准备](#2. 前置准备)
- [3. 反混淆脚本结构](#3. 反混淆脚本结构)
- [4. 反混淆脚本](#4. 反混淆脚本)
-
- [4.1 字面量还原](#4.1 字面量还原)
- [4.2 字符串拼接](#4.2 字符串拼接)
- [4.3 解密函数还原](#4.3 解密函数还原)
AST反混淆脚本
1. 基础概念
- AST反混淆原理: https://blog.csdn.net/2401_84054263/article/details/138774370
- 什么是js混淆: https://blog.csdn.net/qq_15326513/article/details/131741451
2. 前置准备
-
安装必要的npm包:
bashnpm install @babel/parser @babel/traverse @babel/generator @babel/types -
准备混淆的JS文件: 例如
14.js -
AST在线转换为抽象语法树的网站: AST Explorer
3. 反混淆脚本结构
-
脚本内容:
javascript/** * 反混淆脚本结构.js */ // @babel/parser 用于将JavaScript代码转换为ast树 const parser = require('@babel/parser'); // @babel/traverse 用于遍历各个节点的函数 const traverse = require('@babel/traverse').default; // @babel/generator 将处理完毕的AST转换成JavaScript源代码 const generator = require('@babel/generator').default; // @babel/types 节点的类型判断及构造等操作 const types = require('@babel/types'); // fs模块 用于操作文件的读写 const fs = require('fs'); // 混淆的js代码文件 const input_path = 'xx.js'; // 反混淆的js代码文件 const output_path = input_path.replace(/\.js$/, '_deobfuscated.js') // 读取混淆的 JS 文件 const code = fs.readFileSync(input_path, 'utf-8'); // 解析代码生成 AST const ast = parser.parse(code); // ------------------------- 中间插入反混淆的代码 ------------------------------ // 解析AST生成 反混淆后的代码 const output = generate(ast, { compact: false, // 不压缩代码,保持格式化输出(换行和缩进) comments: true, // 保留源代码中的注释 jsescOption: { // JavaScript 转义选项配置 minimal: false, // 最小化转义,只对必要的字符进行转义(Unicode 字面量) es6: true, // 允许使用 ES6 语法 quotes: 'single' // 使用单引号作为字符串的引号类型(single/double) } }); // 输出到文件或控制台 fs.writeFileSync(output_path, output.code); console.log('\n✅ ---> 反混淆完成!输出文件:' + output_path); -
脚本功能: 读取混淆的JS文件, 解析为AST, 遍历AST, 对特定节点进行反混淆, 生成反混淆后的JS代码
-
运行脚本:
bashnode 反混淆脚本结构.js -
输出结果: 生成
xx_deobfuscated.js文件, 内容为反混淆后的JS代码
4. 反混淆脚本
- 混淆文件:
14.js(猿人学14题动态js文件: https://match.yuanrenxue.cn/api2/14)
4.1 字面量还原
JavaScript
traverse(ast, {
"StringLiteral|NumericLiteral"(path) {
// 关键点:直接删除 extra 属性显示为字面量
path.node.extra && delete path.node.extra;
}
});
console.log('✅ ---> 字面量还原完成');
4.2 字符串拼接
JavaScript
traverse(ast, {
BinaryExpression: {
exit(path) {
const node = path.node;
if (node.operator !== '+') {
return;
}
const left = node.left;
const right = node.right;
// 如果左右都是字符串字面量,则合并
if (types.isStringLiteral(left) && types.isStringLiteral(right)) {
const mergedValue = left.value + right.value;
const mergedNode = types.stringLiteral(mergedValue);
path.replaceWith(mergedNode);
}
}
}
});
console.log('✅ ---> 字符串拼接完成');
4.3 解密函数还原
JavaScript
// 加载解密函数
let bigArray_name, bigArray_index, index = 0;
let arrayMove_index, arrayMove_arg1, arrayMove_arg2;
let decrypt_func, decrypt_index, flag = false;
ast.program.body.some(v => {
// 数组移位操作
if (v.type === 'VariableDeclaration' && v.declarations[0].init && v.declarations[0].init.elements && v.declarations[0].init.elements.length > 200) {
bigArray_name = v.declarations[0].id.name;
bigArray_index = index;
}
if (index + 1 == ast.program.body.length && bigArray_name == undefined) throw '---------大数组获取失败---------';
// 根据数组名字,匹配出数组位移的函数
if (v.type == 'ExpressionStatement' && bigArray_name != undefined && v.expression && v.expression.arguments && v.expression.arguments[0].name === bigArray_name) {
arrayMove_index = index;
arrayMove_arg1 = v.expression.arguments[0].name;
arrayMove_arg2 = v.expression.arguments[1].value;
}
if (index + 1 == ast.program.body.length && arrayMove_index == undefined) throw '---------数组位移函数获取失败---------';
// 解密函数
if (bigArray_index != undefined && arrayMove_index != undefined && arrayMove_index + 1 == index && !flag) {
try {
decrypt_func = v.declarations[0].id.name;
decrypt_index = index;
flag = true; // 解密函数标志
return true;
} catch { console.log('---------解密函数名获取失败---------'); }
}
index += 1;
})
console.log('✅ ---> 找到大数组', bigArray_name)
console.log('✅ ---> 找到移位函数,参数:', arrayMove_arg1, arrayMove_arg2)
console.log('✅ ---> 找到解密函数', decrypt_func)
let newAst = parser.parse('');
newAst.program.body.push(ast.program.body[bigArray_index]) // 大数组
newAst.program.body.push(ast.program.body[arrayMove_index]) // 数组位移
newAst.program.body.push(ast.program.body[arrayMove_index + 1]) // 解密函数(采用+1的方式可能会有问题)
let eval_js = generator(newAst, { compact: true }).code; // 将3部分转换成js代码
eval(eval_js) // 执行加载到node内存
console.log('✅ ---> 解密函数加载到内存中')
// 解密函数还原
traverse(ast, {
VariableDeclaration(path) {
let { scope, node } = path
if (!(node.declarations[0].id.name == decrypt_func)) return;
// 当变量名与解密函数名相同的时候,就执行相应的操作
let binding = scope.getBinding(decrypt_func)
// 判断初始值是否被更改
if (!binding || !binding.constant) return
for (let referencePath of binding.referencePaths) {
if (referencePath.parentPath.node.type == 'CallExpression') {
// 替换操作
value = eval(referencePath.parentPath.toString())
referencePath.parentPath.replaceInline(types.valueToNode(value))
}
}
}
})
// 删除解密函数3部分
ast.program.body.splice(bigArray_index, 1);
ast.program.body.splice(arrayMove_index - 1, 1);
ast.program.body.splice(decrypt_index - 2, 1);
console.log('✅ ---> 解密函数解密完成');