各位老铁,大家好!今天咱们来聊聊 JavaScript 逆向里一个绕不开的话题:JavaScript De-obfuscation,也就是反混淆。这玩意儿,说白了,就是把那些被加密、压缩、改得乱七八糟的代码,给它还原成人能看懂的样子。
咱们这次重点讲两种比较厉害的技术:AST 还原和符号执行。我会尽量用大白话,配上代码,让大家都能听明白,就算你是新手,也能有点收获。
一、 啥是混淆?为什么要反混淆?
在深入技术细节之前,咱们先搞清楚一个问题:为啥要混淆?简单来说,就是为了保护代码,防止别人直接复制粘贴,或者分析你的算法。常见的混淆手段有很多,比如:
- 压缩: 去掉空格、注释,缩短变量名,让代码体积更小,可读性更差。
- 加密: 使用各种加密算法,把代码变成乱码。
- 变量名替换: 把有意义的变量名改成
a
、b
、c
这种鬼东西。 - 控制流扁平化: 把正常的代码逻辑打乱,用
switch
语句或者if-else
语句来实现复杂的跳转。 - 死代码插入: 往代码里塞一些没用的代码,干扰分析。
- 字符串加密: 将字符串加密,防止直接搜索到关键信息。
反混淆的目的很明确:就是要把这些乱七八糟的代码还原成可读、可理解的样子,方便我们分析、调试,甚至修改。
二、 AST 还原:代码的“骨骼重塑”
AST,全称 Abstract Syntax Tree,抽象语法树。 简单来说,就是把代码转换成一种树状结构,每个节点代表一个语法单元,比如变量、函数、表达式等等。AST 还原就是通过分析 AST,把混淆过的代码结构还原成原始的样子。
2.1 AST 的基本概念
先看个简单的例子:
const x = 1 + 2;
这段代码对应的 AST 大概是这样的:
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "x"
},
"init": {
"type": "BinaryExpression",
"operator": "+",
"left": {
"type": "Literal",
"value": 1,
"raw": "1"
},
"right": {
"type": "Literal",
"value": 2,
"raw": "2"
}
}
}
],
"kind": "const"
}
],
"sourceType": "script"
}
可以看到,AST 把代码分解成了各种节点,每个节点都有 type
属性,表示节点的类型,还有其他属性,表示节点的值、运算符等等。
2.2 AST 还原的流程
AST 还原的大致流程是这样的:
- 解析代码: 使用 JavaScript 解析器(比如 Esprima、Acorn、Babel Parser),把代码转换成 AST。
- 遍历 AST: 递归遍历 AST 的每个节点。
-
节点处理: 根据节点的类型,进行相应的处理,比如:
- 变量名还原: 把混淆过的变量名替换成有意义的名字。
- 常量计算: 把常量表达式计算出来。
- 控制流解混淆: 把扁平化的控制流还原成正常的结构。
- 死代码移除: 移除没用的代码。
- 生成代码: 使用代码生成器(比如 escodegen),把修改后的 AST 转换成代码。
2.3 AST 还原的例子
咱们来看一个简单的例子,假设有这样一段混淆过的代码:
const _0xabc1 = 'console';
const _0xabc2 = 'log';
const _0xabc3 = 'Hello, world!';
function _0xabc4() {
window[_0xabc1][_0xabc2](_0xabc3);
}
_0xabc4();
这段代码把 console
、log
、Hello, world!
都换成了奇怪的变量名。咱们可以用 AST 还原把它还原成这样:
const consoleName = 'console';
const logName = 'log';
const message = 'Hello, world!';
function printMessage() {
window[consoleName][logName](message);
}
printMessage();
下面是用 JavaScript 实现 AST 还原的代码:
const esprima = require('esprima');
const escodegen = require('escodegen');
function deobfuscate(code) {
// 1. 解析代码
const ast = esprima.parseScript(code);
// 2. 遍历 AST
function traverse(node) {
if (node.type === 'VariableDeclaration') {
// 变量名还原
for (const declaration of node.declarations) {
if (declaration.id.type === 'Identifier' && declaration.init && declaration.init.type === 'Literal') {
const originalName = declaration.id.name;
const value = declaration.init.value;
// 替换变量名
replaceVariableName(ast, originalName, value);
}
}
}
}
// 替换变量名
function replaceVariableName(ast, originalName, value) {
traverseAst(ast, (node) => {
if (node.type === 'Identifier' && node.name === originalName) {
node.name = value;
}
});
}
// 遍历AST
function traverseAst(ast, visitor) {
function walk(node) {
visitor(node);
for (let key in node) {
if (node.hasOwnProperty(key)) {
let child = node[key];
if (typeof child === 'object' && child !== null) {
if (Array.isArray(child)) {
child.forEach(walk);
} else {
walk(child);
}
}
}
}
}
walk(ast);
}
traverse(ast);
// 3. 生成代码
const deobfuscatedCode = escodegen.generate(ast);
return deobfuscatedCode;
}
// 混淆过的代码
const obfuscatedCode = `
const _0xabc1 = 'console';
const _0xabc2 = 'log';
const _0xabc3 = 'Hello, world!';
function _0xabc4() {
window[_0xabc1][_0xabc2](_0xabc3);
}
_0xabc4();
`;
// 反混淆
const deobfuscatedCode = deobfuscate(obfuscatedCode);
console.log(deobfuscatedCode);
这段代码使用了 esprima
来解析代码,escodegen
来生成代码。traverse
函数用来遍历 AST,replaceVariableName
函数用来替换变量名。
三、 符号执行:代码的“模拟运行”
符号执行是一种程序分析技术,它不直接执行代码,而是使用符号值来代替具体的数值,模拟程序的执行过程。通过符号执行,我们可以分析程序的各种执行路径,找到潜在的 bug,或者提取程序的关键信息。
3.1 符号执行的基本概念
举个例子,假设有这样一段代码:
function foo(x, y) {
if (x > 0) {
return x + y;
} else {
return x - y;
}
}
如果使用具体的数值来执行这段代码,比如 foo(1, 2)
,那么程序只会执行 x + y
这条路径。但是,如果使用符号值来执行这段代码,比如 x = X
,y = Y
,其中 X
和 Y
是符号值,那么程序会同时执行两条路径:
- 如果
X > 0
,那么返回X + Y
。 - 如果
X <= 0
,那么返回X - Y
。
3.2 符号执行的流程
符号执行的大致流程是这样的:
- 初始化: 把程序的输入参数替换成符号值。
- 模拟执行: 按照程序的逻辑,模拟程序的执行过程。
- 路径分支: 当遇到条件语句时,根据条件表达式的值,创建不同的执行路径。
- 状态维护: 维护每个执行路径的状态,包括变量的值、程序计数器等等。
- 约束求解: 当需要判断条件表达式的值时,使用约束求解器(比如 Z3、CVC4)来求解约束条件。
- 结果分析: 分析每个执行路径的结果,提取程序的关键信息。
3.3 符号执行的例子
咱们来看一个简单的例子,假设有这样一段混淆过的代码:
function _0xdef1(_0xdef2, _0xdef3) {
const _0xdef4 = _0xdef2 + _0xdef3;
return _0xdef4;
}
const result = _0xdef1(1, 2);
console.log(result);
这段代码把函数名和参数名都换成了奇怪的名字。咱们可以用符号执行来分析这段代码,提取函数的关键信息。
下面是用 JavaScript 实现符号执行的代码:
const symbolic = require('symbolic-execution');
function execute(code) {
const result = symbolic.execute(code);
return result;
}
// 混淆过的代码
const obfuscatedCode = `
function _0xdef1(_0xdef2, _0xdef3) {
const _0xdef4 = _0xdef2 + _0xdef3;
return _0xdef4;
}
const result = _0xdef1(1, 2);
console.log(result);
`;
// 符号执行
const result = execute(obfuscatedCode);
console.log(result);
这段代码使用了 symbolic-execution
库来实现符号执行。
四、 AST 还原 vs 符号执行:各有千秋
AST 还原和符号执行都是很强大的反混淆技术,但是它们各有优缺点:
技术 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
AST 还原 | 速度快,易于实现,可以还原代码结构。 | 对于复杂的混淆,效果可能不好。 | 变量名替换、常量计算、控制流解混淆、死代码移除。 |
符号执行 | 可以分析程序的各种执行路径,提取程序的关键信息,对于复杂的混淆,效果可能更好。 | 速度慢,实现复杂,可能会出现路径爆炸问题。 | 字符串解密、算法分析、漏洞挖掘。 |
总的来说,AST 还原适合处理一些简单的混淆,比如变量名替换、常量计算等等。符号执行适合处理一些复杂的混淆,比如字符串解密、算法分析等等。在实际应用中,我们可以把这两种技术结合起来使用,以达到更好的效果。
五、 实战案例:某盾滑块验证码破解
咱们来看一个实战案例:某盾滑块验证码破解。滑块验证码是一种常见的验证方式,它要求用户拖动滑块,拼合图像,以证明自己是人类。为了防止被机器破解,滑块验证码通常会采用各种混淆手段,比如图片加密、算法混淆等等。
要破解滑块验证码,首先要搞清楚它的验证逻辑。咱们可以用 AST 还原和符号执行来分析验证码的代码,提取关键信息。
- 图片还原: 滑块验证码的图片通常会被加密,我们需要用 AST 还原找到解密算法,把图片还原成原始的样子。
- 轨迹分析: 用户拖动滑块的轨迹是很重要的信息,我们需要用符号执行来分析轨迹的生成算法,找到破解的方法。
- 验证提交: 最后,我们需要模拟用户的行为,提交验证结果,完成验证。
六、 总结与展望
今天咱们聊了 JavaScript 反混淆的两种技术:AST 还原和符号执行。这两种技术都是很强大的工具,可以帮助我们分析、调试、破解各种混淆过的 JavaScript 代码。
当然,反混淆也不是万能的。随着混淆技术的不断发展,反混淆的难度也会越来越大。未来,我们需要不断学习新的技术,探索新的方法,才能更好地应对各种挑战。
希望今天的分享对大家有所帮助!如果有什么问题,欢迎随时提问。
就到这里,各位老铁,下课!