嘘~ 正在从服务器偷取页面 . . .

AST抽象语法树


AST 原理 与 反混淆

  1. 可以将加密的字符串扣在本地,查找调用的位置,还原回去。
  2. 可以将垃圾指令,比如必定不成功的 if 进行替换。
  3. 将+-*/等操作从函数变为标识符

  1. AST 抽象语树生成

    1. 在网页解析的时候:语言 JavaScript
    2. 解析方式选择 :**@babel/parser**
  2. 不论编译器还是解释器:由源代码到 AST 抽象语法树所需要的过程主要分为两步

  3. 将源代码进行词法分析,生成 token 符号流

  4. token 符号流通过语法分析,生成语法树。


AST 中各节点解释


  • start:开始位置

  • end:总字符长度。

  • type:表示节点类型

  • VariableDeclaration:说明变量

  • kind:定义变量的时候使用的关键字

  • declarations:指声明的具体的变量(数组)

  • 因为 let 可以连续定义 let a,b,c,d 数组内就是每一个的具体信息。

  • 但这里我们只定义了一个 let,所以内部只有一个

  • 数组内部:VariableDeclarator

  • 内部每一个变量的定义都以VariableDeclarator表示

  • 属性都是 id 和 init

  • id:是唯一的标识符

  • name:是定义的名字

  • init:对于 init 是定义对象的属性值,没有给定则是 null

  • 这里是定义了一个对象,所以 init 内部包含了一个对象(包含定义的每个值)

  • Identifier:标识符

  • ObjectProperty:对象表达式(定义对象)

  • 对象属性内部是由键值对组成

  • 所以内部的对象名字:key

  • 内部对象对应的值是:value

  • computed:是否以对象形式调用

  • StringLiteral:字符串文字

  • FunctionExpression:函数表达式

  • 这里如果是匿名函数,那么 id 就是 null

  • params:对应函数的参数

  • 因为函数参数可以有多个,所以也是一个数组

  • 如果函数没有参数,也是一个空数组,不是 null

  • BlockStatement:函数体一般都会用BlockStatement进行包裹

  • ReturnStatement:函数执行的return语句

  • argument:函数返回的内容。

  • BinaryExpression:二项式,JS 中所有二元运算都可以解析成二项式。

    • 二项式主要分为:left,operator,right
  • CallExpression:调用表达式,相当于函数直接调用。

  • 在 AST 调用 PATH 的时候,PATH.NODE 才是当前节点

  • 完成一些功能可以先写一个简单的例子,然后例子成功了,再放入真实代码执行


path 和 node 的区别


  • path.node:能取出来节点的 Node 对象,与网页解析出来的一致

  • 节点是生成 JS 代码的原材料,是 Path 中的一部分。

  • Path 是一个对象,用来描述两个节点之间的连接。

  • Path 除了具有上述显示的这些属性以外,还包含添加,更新,移动,删除节点等许多操作

  • 获取节点属性:path.node.n**:获取节点属性值

  • 通过这种方式获取到的是具体属性值(就不能再用 path 方法了

  • 要获取该属性的 Path 需要使用 get 方法

  • path.get(‘n***‘):取该属性的 Path

  • 多节点之间用.链接:path.get(‘name.str’)

  • path.get(‘left’).isIdentifier():判断获取到的 left 节点是否是Identifier类型

  • path.get(‘left’).assertIdentifier():断言 left 是Identifier类型,如果不是,就会报错。

  • path.node.left = t.Identifier(‘f’):将 left 节点属性替换其他属性。

  • path.parentPath:返回父级 path

  • **path.stop()**:用来停止遍历节点

  • **path.evaluate()**:用来计算节点的值

  • 返回一个对象,对象中的 confident 的 bool 值表示是否可以计算出值

  • 如果可以,那么对象中的 value 就存放着该值。


节点操作


替换节点

  • 上面是替换属性,这里是替换整个节点。

  • 替换节点的时候要注意,替换的节点也是可以被遍历到的,要防止出现递归调用。

  • replaceWith:用一个节点替换另一个节点

// 当节点是 StringLiteral 并且 value 为 6666 的时候
if (t.isStringLiteral(path.node, { value: "6666" })) {
  // 将节点文本替换为 fuck
  path.replaceWith(t.valueToNode("fuck"));
}
  • **path.replaceWithMultiple([])**:多个节点替换一个节点,在列表内部实现。
  • path.replaceInline():如果参数是是列表,则等于replaceWithMultiple,否则是replaceWith
  • **path.replaceWithSourceString(‘’)**:用指定的字符串替换节点。
  • 替换节点用节点的 body 配合多节点替换,可以删除外部的{}号

删除节点:path.remove()

插入节点:

  • **path.insertBefore(t.expressionStatement(t.stringLiteral(‘Before’)))**:指定节点前插入
  • **path.insertAfter(t.expressionStatement(t.stringLiteral(‘After’)))**:指定节点后插入

作用域:

  • path.scope.block:获取当前定位到节点的作用域。

代码部分


AST 和代码的转换


// fs 读取本地文件
const fs = require("fs");
// @babel/parser 用来将JS代码转换成AST
const parser = require("@babel/parser");
// @babel/traverse 用来遍历AST中节点
const traverse = require("@babel/traverse").default;
// @babel/types 用来判断AST的节点类型 生成新的节点
const type = require("@babel/types");
// @babel/generator 用来将AST转换成JS代码
const generator = require("@babel/generator").default;

// 读取JS代码
const jsCode = fs.readFileSync("./demo.js", {
  encoding: "utf-8",
});
// 将代码转换成ast
let ast = parser.parse(jsCode, {
  // 默认参数就是script
  // 如果js代码中有import,export,则需要将参数改为module
  sourceType: "script",
});
// 输出ast请使用以下格式
// console.log(JSON.stringify(ast))
// 下面进行自己的一系列操作

// -------------------------------------------------------------------------------

// -------------------------------------------------------------------------------

// 将ast转换成JS代码
// 转换成功之后是一个对象,对象内部的code才是需要的代码
let code = generator(ast, {
  // 是否与源代码使用相同行号,false:表示返回格式化后的代码
  retainLines: false,
  // comments:是否保留注释
  comments: true,
  //compact:是否压缩代码
  compact: false,
}).code;
// 写入本地
fs.writeFile("./demoNew.js", code, (err) => {});

// 更多babel参数可以访问 https://babeljs.io/

AST 遍历时进行的操作


// 先定义一个对象
const ass = {
  // 第一种写法
  // 为对象定义一个 FunctionExpression 这个方法就是需要遍历的节点类型
  // traverse 会遍历所有节点类型,当遇到 FunctionExpression 节点时,就会调用方法
  // 需要处理节点,只需要一直增加即可
  // 传入的Path参数是当前节点的Path对象,不是节点。
  FunctionExpression(path) {
    console.log("1");
  },
  // 第二种写法
  // 可以在进入和退出节点时进行调用
  Identifier: {
    enter(path) {
      console.log("2");
    },
    exit(path) {
      console.log("2");
    },
  },
  // 还可以用 | 进行连接,将一个函数运用在多个节点,需要用字符串的形式
  "Identifier|BlockStatement": {
    enter(path) {
      console.log("2");
    },
    exit(path) {
      console.log("2");
    },
  },
  // 还可以将函数以列表的形式传入,会依次执行函数,传入参数。
  FunctionExpression: {
    enter: [func1, func2],
  },
};

// 然后传递给traverse 就会自动从头开始遍历
traverse(ast, ass);

Type 组件


  • 该组件主要用来判断节点类型 以及 创建新的节点
  1. 判断节点类型
// 当 Identifier 节点的name是a,则修改为ccc
Identifier(path) {
    if (path.node.name === 'a'){
        path.node.name = 'ccc';
    }
}
// 可以等价的修改为Type组件的方式。

// 这里改为进入每个节点
// 要判断其他节点只要修改is后面的内容即可 is****
enter(path) {
        if (t.isIdentifier(path.node, {name: 'a'})) {
            path.node.name = 'ccc2';
        }
    }

  1. 可以创建新节点
  • 主要在网页上创建,之后在本地模拟网页的格式就可以
// 下面代码将使用ast语句创建以下代码
// 并且上面的所有代码都是以这个对象为准
let obj = {
  name: "6666",
  add: function (a, b) {
    return a + b + 1000;
  },

  mul: function (a, b) {
    return a + b + 1000;
  },
};

// -----------------------------------------------------------

// 函数接收两个参数,所以定义a和b
let a = t.identifier("a");
let b = t.identifier("b");

// 下面定义函数执行体
// 函数主体二项式用binaryExpression定义
let binExpr2 = t.binaryExpression("+", a, b);
let binExpr3 = t.binaryExpression("*", a, b);
// 定义函数的return 用 returnStatement 因为原始字符串是 a + b + 1000 所以下面将代码再分为左右的二项式
let retSta2 = t.returnStatement(t.binaryExpression("+", binExpr2, t.numericLiteral(1000)));
let retSta3 = t.returnStatement(t.binaryExpression("+", binExpr3, t.numericLiteral(1000)));

// 函数的主体内容需要用 blockStatement 包裹起来 (要求就是这样)
// 如果希望创建一个空函数,则需要传入空数组,而不是null
let bloSta2 = t.blockStatement([retSta2]);
let bloSta3 = t.blockStatement([retSta3]);
// 然后生成两个函数,但这个函数还没有主体内容
// 第一个参数id:因为是匿名函数,所以为null
// 第二个参数是接收的参数列表
// 第三个是函数执行体
let funcExpr2 = t.functionExpression(null, [a, b], bloSta2);
let funcExpr3 = t.functionExpression(null, [a, b], bloSta3);

// 定义主体内容的属性和方法。
let objProp1 = t.objectProperty(t.identifier("name"), t.stringLiteral("6666"));
// 这里的函数,还需要单独定义
let objProp2 = t.objectProperty(t.identifier("add"), funcExpr2);
let objProp3 = t.objectProperty(t.identifier("mul"), funcExpr3);

// 主体内容定义
let objExpr = t.objectExpression([objProp1, objProp2, objProp3]);
// 定义了一个在let右侧的obj名称,然后主体内容还没定义
let varDec = t.variableDeclarator(t.identifier("obj"), objExpr);

// 定义一个变量, 第一个参数 var | let | const
// 第二个参数是变列表,参数可以连续定义,所以这是一个列表 let a,b,c;
let localAst = t.variableDeclaration("let", [varDec]);
code = generator(localAst).code;
console.log(code);
  • 更多 babel 参数可以访问: https://babeljs.io/
  • 还提供了一个 valueToNode:来快速转换数据类型
    • let obj = t.valueToNode([1, "2", false, null, {name: 'pink', age: 6,}, undefined])
    • 可以直接输出结果,注意要放在列表内部

AST 自动化防护


混淆前代码的处理

  1. 改变成员访问方式:在网页中看到.访问和对象访问,只有属性 path.node.computed 的真假发生变化。
// 进行以下替换可以将属性访问改为对象访问
// 将data.prototype 改为 data['prototype']
MemberExpression(path){
    if(t.isIdentifier(path.node.property)){
        path.node.property = t.stringLiteral(path.node.property.name);
    }
    path.node.computed = true;
}
  1. 修改存在于 window 下的属性访问
// 将 Data['prototype'] 改为 window['Data']['prototype']
Identifier(path) {
    let name = path.node.name;
    // 当找到Date时,创建一个对象,替换掉原先的Date
    if ('Date|eval'.indexOf(name) !== -1) {
        path.replaceWith(t.memberExpression(t.identifier('window'), t.stringLiteral(name), true))
    }
}
// 原本是Date字符串,现在创建了一个memberExpression对象,把原本的替换掉了
  1. 常量与标识符混乱

    • 比如一个数字是 1000,是属于 NumericLiteral
    • 那么可以创建一个二项式(BinaryExpression),然后计算 500+500。
    • 避免 1000 直接被搜到,简单的一个思路。
  2. 字符串常量混乱

    • 比如用 base64 进行加密
    • 然后网页运行的时候在用 base64 解密
    StringLiteral(path) {
        // 获取value
        let value = path.node.value
        // 构建函数调用 atob('cmVwbGFjZQ') 进行解码
        let call = t.callExpression(t.identifier('atob'), [t.stringLiteral(new Buffer.from(value, "binary").toString("base64"))])
        // 将原文的文本,替换为函数调用
        path.replaceWith(call)
        // 因为在新的节点中也有 StringLiteral 类型,所以要终止,要不然会无限调用。
        path.skip()
    }
  3. 数组混淆

    • 现在字符串都加密了,但他们还是在原始的位置上。
    • 可以将数组提取出来,然后原始字符串的位置,改为数组下标的形式进行访问。
    // 先定义一个对象
    let arr = [];
    const ass = {
      MemberExpression(path) {
        if (t.isIdentifier(path.node.property)) {
          path.node.property = t.stringLiteral(path.node.property.name);
        }
        path.node.computed = true;
      },
      Identifier(path) {
        let name = path.node.name;
        if ("Date|eval".indexOf(name) !== -1) {
          path.replaceWith(t.memberExpression(t.identifier("window"), t.stringLiteral(name), true));
        }
      },
      StringLiteral(path) {
        // 首先将字符串提取出来进行加密
        let cipherText = new Buffer.from(path.node.value, "binary").toString("base64");
        // 加密之后,判断是否已经存在于列表中了
        let bigArrIndex = arr.indexOf(cipherText);
        let index = bigArrIndex;
    
        if (bigArrIndex === -1) {
          // 等于-1表示不存在
          let length = arr.push(cipherText);
          // 将新加入的文本索引给index
          index = length - 1;
        }
        let call = t.callExpression(t.identifier("atob"), [t.memberExpression(t.identifier("arr"), t.numericLiteral(index), true)]);
        path.replaceWith(call);
        // 因为修改之后,就没有 StringLiteral 就不会无限循环
        // path.skip();
      },
    };
    // 然后传递给traverse 就会自动从头开始遍历
    traverse(ast, ass);
    
    // 因为下面的数组是转换成ast了,所以要在 traverse 遍历完毕之后再进行添加
    
    // 这里目前只是JS中的字符串,需要将内部的每个值都转为AST中的stringLiteral
    arr = arr.map(function (v) {
      return t.stringLiteral(v);
    });
    // 声明一个变量 arr ,内容是上面的 arr 列表
    arr = t.variableDeclarator(t.identifier("arr"), t.arrayExpression(arr));
    // 上面是声明变量arr  这里是定义上面声明的变量
    arr = t.variableDeclaration("var", [arr]);
    // ast代表生成好的代码,program 是整个JS文件,body是一个列表,将前面的ast列表代码加在body最前面,就达到了增加数组的效果
    ast.program.body.unshift(arr);
  4. 数组乱序

    • 现在数组已经提取出来,但数组的顺序还是有迹可循的,下面来打乱数组的顺序。

    • 下面先在代码中,把数组混淆(这里是简单的混淆一下)

    // 在添加之前把列表顺序打乱(这里简单的打乱,就是把数组倒过来)
    (function (myArr) {
      var newArr = [];
      for (const myArrKey in myArr) {
        newArr.unshift(myArr[myArrKey]);
      }
      arr = newArr;
    })(arr);
    • 然后在代码中,数组的后面插入还原函数
    • 还原函数也是上面的代码,这里没有进行混淆,可以用标识符混淆一下。
    // 这里读入数组混淆的还原代码
    const jscodeFront = fs.readFileSync("./astFront.js", {
      encoding: "utf-8",
    });
    
    // 将代码转成ast代码,插入到代码最前面
    let astFront = parser.parse(jscodeFront, {
      // 默认参数就是script
      // 如果js代码中有import,export,则需要将参数改为module
      sourceType: "script",
    });
    // 将代码插入到最前面 astFront.program.body 是代码段开始  [0] 个就是真实代码
    ast.program.body.unshift(astFront.program.body[0]); // 加入到代码最前面
  5. 标识符混乱

    • 在前面的标识符混乱中,混淆的标识符都是不一样的。
    • 其实可以将标识符改为不同作用域中相同的名字,并且改为没有全局引用的标识符名称。
    • 这样更加具有迷惑性。


文章作者: 林木木
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 林木木 !
评论
 上一篇
NDK开发 NDK开发
NDK 是 Native Developmentit的缩写,是Google在Android开发中提供的一套用于快速创建native工程的一个工具。
2022-07-15
下一篇 
Django Django
Django 是用 Python 开发的一个免费开源的 Web 框架,几乎囊括了 Web 应用的方方面面,可以用于快速搭建高性能、优雅的网站,Django 提供了许多网站后台开发经常用到的模块,使开发者能够专注于业务部分。
2022-06-27
  目录