精华 一例简易静态 Javascript 反混淆
发布于 9 年前 作者 ChiChou 17285 次浏览 最后一次编辑是 8 年前 来自 分享

最近在研究一些 XSS 蠕虫的时候遇到了类似如下代码混淆: obfuscated-js.png

观察其代码风格,发现这个混淆器做了这几件事:

  • 字符串字面量混淆:首先提取全部的字符串,在全局作用域创建一个字符串数组,同时转义字符增大阅读难度,然后将字符串出现的地方替换成为数组元素的引用。
  • 变量名混淆:不同于压缩器的缩短命名,此处使用了下划线加数字的格式,变量之间区分度很低,相比单个字母更难以阅读。
  • 成员运算符混淆:在 Javascript 中,window[‘top’] 和 window.top 是等价的。混淆器便利用这一特性,将成员访问复杂化,首先替换成字符串,然后对字符串进行混淆。

经过我的搜索,这样的代码很有可能是通过 javascriptobfuscator.com 的免费版生成的。其中免费版可以使用的三个选项(Encode Strings / Strings / Replace Names)也印证了前面观察到的现象。

这些变换中,变量名混淆是不可逆的。如果程序能智能到自动给变量命名,不仅 IDA 的 F5 工具会更好用,也能给有命名恐惧症的程序员节省不少时间呢。说回来变量名替换可以通过人工标注的方式,使用 IDE(如 WebStorm)的代码重构功能,结合代码行为分析和自己的理解进行手工重命名还原。

而字符串的还原是否可以使用脚本进行自动化呢?答案是肯定的。 要对一段代码进行静态分析或者更进一步执行,我们需要一个 parser 来获得代码的抽象语法树(Abstract Syntax Tree,AST),也就是源代码的抽象语法结构的树状表现形式。通过 AST 我们可以对代码进行分析或者修改(重构),比单纯的正则匹配更准确且和具有通用性。 在这里我使用了 esprima 作为词法解析工具。其接口很简单,调用一个静态方法即可:

var ast = esprima.parse('var a = /hello\s+world/;');

esprima 返回的语法树的具体格式可以参考其文档。另外 Esprima 提供了一个在线工具,可以把任意(合法的)Javascript 代码解析成为 AST 并输出: esprima.org/demo/parse.html

要实现具体的行为分析和代码替换,还得对语法树进行遍历。可以直接手写树的遍历(非递归、递归方式),不过使用与 esprima 同门的 estraverse 将更为简单。Estraverse 的接口给我的感觉有点像 PULL 方式解析 XML。Estraverse 提供两个静态方法,estraverse.traverseestraverse.replace。前者单纯遍历 AST 的节点,通过返回值控制是否继续遍历到叶子节点;而 replace 方法则可以在遍历的过程中直接修改 AST,实现代码重构功能。

回到之前的代码混淆上。其中的字符串将会被提取到一个全局的数组,在语法树中我们可以观察到这样的特征: 在全局作用域下,出现一个 VariableDeclarator,其 init 属性为 ArrayExpression,而且所有元素都是 Literal。这说明这个数组所有元素都是常量。我简单地将其还原为字符串数组,并用 hash 表与变量名(标识符)关联起来。

接下来进入第二个 pass,也就是将数组元素的引用替换为原本的字面量(内联)。取数组成员的表达式将被解析为 MemberExpression 节点,其 property 即是下标。在这里下标直接取了数字,我们直接读出先前暂存的数组内容,替换上去即可。如果混淆器再猥琐一点,是可以无限次迭代,将数字继续展开为更复杂的表达式的(如 2 转换为 (Math.log(1024) / Math.log(2)) / (Math.pow(2, 2) + 1))。

说个题外话,其实作用域管理是有现成的模块(escope)。对付这个混淆器可以简单用一个计数器来处理作用域的深度,判断变量否在全局作用域声明。事实上这里简化了处理。在 Javascript 中,作用域链上存在变量名的优先级,全局上的变量名是可以被局部变量重新定义的。如果混淆器再变态一点,在不同的作用域上使用相同的变量名,对付起来就复杂了。

最后一步是将 AST 重新转回字符串的形式。同样地,你也可以手动遍历树来还原代码,但这个轮子已经有了,同属 estools 出品的 escodegen 可以轻松实现。

以下是 PoC 代码,需要使用 node.js 执行。稍作修改也可以在浏览器里跑。

/** * Author: ChiChou * * Deobfuscate code generated by free version of * JavascriptObfuscator (https://javascriptobfuscator.com/Javascript-Obfuscator.aspx) * * Usage: node deobfuscator.js file.js>output.js * */

var esprima = require('esprima');
var estraverse = require('estraverse');
var escodegen = require('escodegen');
 
function shouldSwitchScope(node) {
  return node.type.match(/^Function(Express|Declarat)ion$/);
}
 
function main(fileName) {
  var code = require('fs').readFileSync(fileName).toString();
  var ast = esprima.parse(code);
  var strings = {};
  var scopeDepth = 0; // initial: global
 
  // pass 1: extract all strings
  estraverse.traverse(ast, {
    enter: function(node) {
      if (shouldSwitchScope(node)) {
    scopeDepth++;
      }
       
      if (scopeDepth == 0 &&
    node.type === esprima.Syntax.VariableDeclarator &&
        node.init &&
        node.init.type === esprima.Syntax.ArrayExpression &&
    node.init.elements.every(function(e) {return e.type === esprima.Syntax.Literal})) {
        strings[node.id.name] = node.init.elements.map(function(e) {
          return e.value;
        });
        this.skip();
      }
    },
    leave: function(node) {
      if (shouldSwitchScope(node)) {
        scopeDepth--;
      }
    }
  });
 
  // pass 2: restore code
  ast = estraverse.replace(ast, {
    enter: function(node) {},
    leave: function(node) {
      // restore strings
      if (node.type === esprima.Syntax.MemberExpression &&
        node.computed &&
        strings.hasOwnProperty(node.object.name) &&
        node.property.type === esprima.Syntax.Literal
      ) {
        var val = strings[node.object.name][node.property.value];
        return {
          type: esprima.Syntax.Literal,
          value: val,
          raw: val
        }
      }
       
      if (node.type === esprima.Syntax.MemberExpression &&
        node.property.type === esprima.Syntax.Literal &&
        typeof node.property.value === 'string'
      ) {
        return {
          type: esprima.Syntax.MemberExpression,
          computed: false,
          object: node.object,
          property: {
            type: esprima.Syntax.Identifier,
            name: node.property.value
          }
        }
      }
    }
  });
 
  console.log(escodegen.generate(ast));
}
 
main(process.argv[2]);
7 回复

信息量很大,灰常好 esprima 作为 javascript parser 工具,会很有用,谢谢 @ChiChou

测试效果。从 YOU MIGHT NOT NEED JQUERY 随便摘录的一段脚本

(function() {
    var request = new XMLHttpRequest();
    request.open('GET', '/my/url', true);

    request.onload = function() {
        if (request.status >= 200 && request.status < 400) {
            // Success!
            var data = JSON.parse(request.responseText);
        } else {
            // We reached our target server, but it returned an error

        }
    };

    request.onerror = function() {
        // There was a connection error of some sort
    };

    request.send();

    var el = document.getElementById('the-element');

    function fadeIn(el) {
        el.style.opacity = 0;

        var last = +new Date();
        var tick = function() {
            el.style.opacity = +el.style.opacity + (new Date() - last) / 400;
            last = +new Date();

            if (+el.style.opacity < 1) {
                (window.requestAnimationFrame && requestAnimationFrame(tick)) || setTimeout(tick, 16)
            }
        };

        tick();
    }

    fadeIn(el);

    if (el.classList) {
        el.classList.toggle(className);
    } else {
        var classes = el.className.split(' ');
        var existingIndex = classes.indexOf(className);

        if (existingIndex >= 0)
            classes.splice(existingIndex, 1);
        else
            classes.push(className);

        el.className = classes.join(' ');
    }
})()

online-obfuscate.png

尝试还原,用 node.js 执行:node deobfuscator.js obfuscated.js>deobfuscated.js

deobfuscated.png

除了变量名还是一如既往的恶心外,代码已经可以阅读了。

惊现 chichou :)

回到顶部