JavaScript代码反混淆技巧
详细介绍JavaScript代码反混淆的常用方法和工具,包括AST分析、格式化、变量重命名等技术
JavaScript反混淆:技巧与实战指南
在JavaScript逆向分析中,我们经常会遇到经过混淆的代码。本文将介绍一系列实用的反混淆技巧,帮助你更好地理解和分析混淆代码。
常见的混淆方式
1. 基础混淆
// 原始代码
function add(a, b) {
return a + b;
}
// 混淆后
var _0x4e8b=['return\x20a\x20+\x20b'];
(function(_0x38b2a0,_0x4e8b6d){
var _0x4e8b6e=function(_0x38b2a1){
while(--_0x38b2a1){
_0x38b2a0['push'](_0x38b2a0['shift']());
}
};
_0x4e8b6e(++_0x4e8b6d);
}(_0x4e8b,0x1a4));
2. 高级混淆
// 控制流扁平化
function _0x123456() {
var _0x789abc = 0;
while (true) {
switch (_0x789abc) {
case 0:
console.log('step 1');
_0x789abc = 1;
break;
case 1:
return 'result';
}
}
}
反混淆基本步骤
1. 代码格式化
首先使用格式化工具美化代码:
// 使用prettier进行格式化
const prettier = require('prettier');
const formatted = prettier.format(code, {
parser: 'babel',
semi: true,
singleQuote: true
});
2. 变量名还原
使用AST分析还原有意义的变量名:
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
function deobfuscate(code) {
// 解析AST
const ast = parser.parse(code);
// 变量映射表
const varMap = new Map();
// 遍历AST
traverse(ast, {
VariableDeclarator(path) {
const name = path.node.id.name;
if (name.match(/^_0x/)) {
const newName = `var_${varMap.size}`;
varMap.set(name, newName);
path.scope.rename(name, newName);
}
}
});
// 生成新代码
return generate(ast).code;
}
3. 字符串解密
处理编码过的字符串:
// 常见的字符串解密函数
function decryptStrings(code) {
// 提取字符串数组
const stringArray = [];
eval(`
var _0x4e8b = ${code.match(/var _0x4e8b = (.*?);/)[1]};
stringArray.push(..._0x4e8b);
`);
// 替换加密字符串
return code.replace(/_0x4e8b\[(\d+)\]/g, (_, index) => {
return JSON.stringify(stringArray[index]);
});
}
进阶反混淆技巧
1. 控制流还原
处理控制流扁平化:
function restoreControlFlow(ast) {
traverse(ast, {
WhileStatement(path) {
const body = path.node.body;
if (t.isBlockStatement(body) &&
body.body.length === 1 &&
t.isSwitchStatement(body.body[0])) {
// 提取switch语句
const switchStmt = body.body[0];
// 分析case语句
const cases = switchStmt.cases.map(caseNode => {
return {
test: caseNode.test,
consequent: caseNode.consequent
};
});
// 重建控制流
const newBody = [];
let currentCase = cases[0];
while (currentCase) {
newBody.push(...currentCase.consequent);
// 分析下一个case
const nextCaseIndex = cases.findIndex(c =>
c.test.value === currentCase.nextCase
);
currentCase = cases[nextCaseIndex];
}
path.replaceWithMultiple(newBody);
}
}
});
}
2. 死代码消除
删除永远不会执行的代码:
function removeDeadCode(ast) {
traverse(ast, {
// 删除不可达代码
IfStatement(path) {
const { consequent, alternate } = path.node;
if (t.isBooleanLiteral(path.node.test)) {
if (path.node.test.value) {
path.replaceWithMultiple(consequent);
} else if (alternate) {
path.replaceWithMultiple(alternate);
} else {
path.remove();
}
}
},
// 删除空函数
FunctionDeclaration(path) {
if (path.node.body.body.length === 0) {
path.remove();
}
}
});
}
3. 表达式简化
简化复杂的表达式:
function simplifyExpressions(ast) {
traverse(ast, {
BinaryExpression(path) {
const { left, right, operator } = path.node;
// 常量折叠
if (t.isNumericLiteral(left) && t.isNumericLiteral(right)) {
let result;
switch (operator) {
case '+':
result = left.value + right.value;
break;
case '-':
result = left.value - right.value;
break;
// ... 其他操作符
}
path.replaceWith(t.numericLiteral(result));
}
}
});
}
实战案例
案例一:处理eval混淆
// 原始混淆代码
eval(function(p,a,c,k,e,r){e=function(c){return c.toString(36)};
if('0'.replace(0,e)==0){while(c--)r[e(c)]=k[c];k=[function(e)
{return r[e]||e}];e=function(){return'[0-9a-z]+'};c=1};while(c--)
if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);
return p}('4 5(){3 6=7;8(2.1==\'9\'){3 a=2.b(\'c\');d(a)}',14,14,
'|readyState|document|var|function|init|x|0|if|complete|el|
getElementById|myElement|process'.split('|'),0,{}));
// 反混淆处理
function handleEvalObfuscation(code) {
// 提取eval参数
const evalContent = code.match(/eval\((.*)\)/)[1];
// 替换eval为console.log
const deobfuscated = eval(evalContent);
return deobfuscated;
}
案例二:WebPack混淆
// WebPack混淆代码处理
function deobfuscateWebpack(code) {
// 提取模块定义
const modules = {};
const moduleRegex = /\(\d+\)\s*{\s*"use strict";\s*(.*?)\s*}/g;
let match;
while ((match = moduleRegex.exec(code)) !== null) {
const moduleContent = match[1];
// 分析模块依赖
const requires = moduleContent.match(/require\((\d+)\)/g) || [];
modules[match.index] = {
content: moduleContent,
requires: requires.map(r => parseInt(r.match(/\d+/)[0]))
};
}
// 重建模块关系
return reconstructModules(modules);
}
工具推荐
- AST Explorer: 在线AST分析工具
- JavaScript Beautifier: 代码格式化
- de4js: 在线反混淆工具
- Babel: AST转换工具
注意事项
-
代码备份: 在开始反混淆前,务必备份原始代码。
-
环境依赖: 某些混淆代码可能依赖特定运行环境。
-
性能考虑: 反混淆过程可能比较耗时,需要优化处理。
-
安全检查: 注意检查代码中的恶意行为。
总结
JavaScript代码反混淆是一个需要耐心和技巧的过程。通过本文介绍的方法,你可以更好地处理混淆代码。记住,反混淆的目的是理解代码逻辑,应该在合法合规的前提下进行。
参考资源
如果你在反混淆过程中遇到问题,欢迎在评论区讨论交流。
评论