JS `JavaScript De-obfuscation` (反混淆) 技术:AST 还原与符号执行

各位老铁,大家好!今天咱们来聊聊 JavaScript 逆向里一个绕不开的话题:JavaScript De-obfuscation,也就是反混淆。这玩意儿,说白了,就是把那些被加密、压缩、改得乱七八糟的代码,给它还原成人能看懂的样子。

咱们这次重点讲两种比较厉害的技术:AST 还原和符号执行。我会尽量用大白话,配上代码,让大家都能听明白,就算你是新手,也能有点收获。

一、 啥是混淆?为什么要反混淆?

在深入技术细节之前,咱们先搞清楚一个问题:为啥要混淆?简单来说,就是为了保护代码,防止别人直接复制粘贴,或者分析你的算法。常见的混淆手段有很多,比如:

  • 压缩: 去掉空格、注释,缩短变量名,让代码体积更小,可读性更差。
  • 加密: 使用各种加密算法,把代码变成乱码。
  • 变量名替换: 把有意义的变量名改成 abc 这种鬼东西。
  • 控制流扁平化: 把正常的代码逻辑打乱,用 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 还原的大致流程是这样的:

  1. 解析代码: 使用 JavaScript 解析器(比如 Esprima、Acorn、Babel Parser),把代码转换成 AST。
  2. 遍历 AST: 递归遍历 AST 的每个节点。
  3. 节点处理: 根据节点的类型,进行相应的处理,比如:

    • 变量名还原: 把混淆过的变量名替换成有意义的名字。
    • 常量计算: 把常量表达式计算出来。
    • 控制流解混淆: 把扁平化的控制流还原成正常的结构。
    • 死代码移除: 移除没用的代码。
  4. 生成代码: 使用代码生成器(比如 escodegen),把修改后的 AST 转换成代码。

2.3 AST 还原的例子

咱们来看一个简单的例子,假设有这样一段混淆过的代码:

const _0xabc1 = 'console';
const _0xabc2 = 'log';
const _0xabc3 = 'Hello, world!';

function _0xabc4() {
  window[_0xabc1][_0xabc2](_0xabc3);
}

_0xabc4();

这段代码把 consolelogHello, 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 = Xy = Y,其中 XY 是符号值,那么程序会同时执行两条路径:

  • 如果 X > 0,那么返回 X + Y
  • 如果 X <= 0,那么返回 X - Y

3.2 符号执行的流程

符号执行的大致流程是这样的:

  1. 初始化: 把程序的输入参数替换成符号值。
  2. 模拟执行: 按照程序的逻辑,模拟程序的执行过程。
  3. 路径分支: 当遇到条件语句时,根据条件表达式的值,创建不同的执行路径。
  4. 状态维护: 维护每个执行路径的状态,包括变量的值、程序计数器等等。
  5. 约束求解: 当需要判断条件表达式的值时,使用约束求解器(比如 Z3、CVC4)来求解约束条件。
  6. 结果分析: 分析每个执行路径的结果,提取程序的关键信息。

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 还原和符号执行来分析验证码的代码,提取关键信息。

  1. 图片还原: 滑块验证码的图片通常会被加密,我们需要用 AST 还原找到解密算法,把图片还原成原始的样子。
  2. 轨迹分析: 用户拖动滑块的轨迹是很重要的信息,我们需要用符号执行来分析轨迹的生成算法,找到破解的方法。
  3. 验证提交: 最后,我们需要模拟用户的行为,提交验证结果,完成验证。

六、 总结与展望

今天咱们聊了 JavaScript 反混淆的两种技术:AST 还原和符号执行。这两种技术都是很强大的工具,可以帮助我们分析、调试、破解各种混淆过的 JavaScript 代码。

当然,反混淆也不是万能的。随着混淆技术的不断发展,反混淆的难度也会越来越大。未来,我们需要不断学习新的技术,探索新的方法,才能更好地应对各种挑战。

希望今天的分享对大家有所帮助!如果有什么问题,欢迎随时提问。

就到这里,各位老铁,下课!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注