反爬虫技术需要,最近调研了关于 Javascript 静态分析相关知识,主要是协助进行 Javascript 的反混淆工作。

自己本身对于 Javascript 的技术掌握接近于 0,在网上也找到不少的反混淆的工具,这里放出来几个比较好用的:

  • prepack 代码简化在线工具
  • jstillery javascripts 代码反混淆在线工具

通过 https://obfuscator.io/ 生成的代码放到上面的两个工具中,基本上都可以反混淆出东西来,但是自己遇到的场景与上述还是有差异的,需要自己定制化一些东西,也就需要对 AST 这个工具比较熟悉,并且能够灵活使用起来。

AST 是什么

无论是 javascripts 还是 Python 的 AST 他们的作用都是相似的,就是把原代码转换成 token,这个过程是程序编译过程中一个重要的步骤。AST 全称是抽象语法树,简单点理解就是有了这个树你可以更加方便的遍历和修改之前的逻辑了,使用程序修改源代码远比使用编辑器更加的快速准确,自动化程度更高,使用抽象语法树还可以自动生成源代码。

初识 Javascript AST

Javascript AST 有很多的工具,而且很多都很成熟,可以把源代码快速成 JSON 对象,一个样例如下:

源代码:

console.log("hello, world")

解析过后的 AST ,使用 https://astexplorer.net/ 生成:

{
  "type": "Program",
  "start": 0,
  "end": 27,
  "body": [
    {
      "type": "ExpressionStatement",
      "start": 0,
      "end": 27,
      "expression": {
        "type": "CallExpression",
        "start": 0,
        "end": 27,
        "callee": {
          "type": "MemberExpression",
          "start": 0,
          "end": 11,
          "object": {
            "type": "Identifier",
            "start": 0,
            "end": 7,
            "name": "console"
          },
          "property": {
            "type": "Identifier",
            "start": 8,
            "end": 11,
            "name": "log"
          },
          "computed": false
        },
        "arguments": [
          {
            "type": "Literal",
            "start": 12,
            "end": 26,
            "value": "hello, world",
            "raw": "\"hello, world\""
          }
        ]
      }
    }
  ],
  "sourceType": "module"
}

其实看起来也不是很难,先是一个表达式,里面再是函数调用,然后是函数的来源,参数等。如果有兴趣可以自己去在线的站点输入自己 熟悉的语句,看看各个语句有什么不同的地方。关于抽象语法树的标准文档看这里

遍历 AST

解析 javascript 抽象语法书并不难,我觉得困难的地方是怎么遍历的问题,这就是很关键的一个部分了,有些库给的文档很全,有些库虽然 很多 star,但是文档太少了,对于初学者来说也不是好库。一般遍历一个树有深度优先和广度优先两个方式,这两个方式取到的东西的顺序也就不同。

这里我使用的一个库是 https://github.com/estools/estraverse,从源码考证是一个深度优先遍历工具,具体的我们可以写个 demo 测试一下:

function level1() {
	function level2() {
    	;
    }
}
function level1_1() {
    ;
}

如果是深度优先的话,首先读取到的应该是 leve1 -> level2 -> level1_1。

测试代码如下:

var esprima = require("esprima");
var estraverse = require("estraverse");

var ast = esprima.parse(`
function level1() {
	function level2() {
    	;
    }
}
function level1_1() {
    ;
}
`);

estraverse.traverse(ast, {
  enter: enter,
  leave: leave
});

function enter(node) {
  if (node.type === "FunctionDeclaration") {
    console.log("enter function: ", node.id.name);
  }
}

function leave(node) {
  if (node.type === "FunctionDeclaration") {
    console.log("leave function: ", node.id.name);
  }
}

安装代码需要的依赖:

npm install esprima
npm install estraverse

代码执行结果如下:

enter function:  level1
enter function:  level2
leave function:  level2
leave function:  level1
enter function:  level1_1
leave function:  level1_1

了解遍历顺序对于后面的实战更加重要,反混淆过程中通常会涉及到作用域的问题,如果自身对于遍历顺序不清楚的话,那么很难操作函数和变量的作用域。

实战获取函数的所有参数

接下来思考如何利用 AST 获取函数的所有入参,比如下面的示例代码:

var esprima = require("esprima");
var estraverse = require("estraverse");

var ast = esprima.parse(`
function level1(a, b) {
	function level2(c, d) {
    	;
    }
}
function level1_1(e, f) {
    ;
}
`);

var scopeList = [];

estraverse.traverse(ast, {
  enter: enter,
  leave: leave
});

function enter(node) {
  createNewScope(node);

  if (node.type === "FunctionDeclaration") {
    let scope = scopeList[scopeList.length - 1];
    for (const item of node.params) {
        scope.push(item.name);
    }
  }
}

function leave(node) {
  if (node.type === "FunctionDeclaration") {
    const a = scopeList.pop();
    console.log("params defined in ", node.id.name, ", params list: ", a);
  }
}

function createNewScope(node) {
    if (node.type === "FunctionDeclaration") {
        scopeList.push([])
    }
}

如何获取函数体内定义的所有变量

如何利用 AST 获取某个作用域里面的所有变量?如何检查某个变量在某个作用域下面是否已经定义?下节再续~