发布时间:2020-01-17编辑:佚名阅读(2091)
最近在研究一些 XSS 蠕虫的时候遇到了类似如下代码混淆:
观察其代码风格,发现这个混淆器做了这几件事:
字符串字面量混淆:首先提取全部的字符串,在全局作用域创建一个字符串数组,同时转义字符增大阅读难度,然后将字符串出现的地方替换成为数组元素的引用。
变量名混淆:不同于压缩器的缩短命名,此处使用了下划线加数字的格式,变量之间区分度很低,相比单个字母更难以阅读。
成员运算符混淆:在 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.traverse
和 estraverse.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]);
关键字: Javascript 反混淆
上一篇:cheerio中文文档
下一篇:Esprima语法树结构详解
1人
0人
1人
0人