分享

基于AST的babel库实现js反混淆还原基础案例荟萃

 小小明代码实体 2023-02-03 发布于广东

基本概念

AST简介

AST全称Abstract Syntax Tree,即抽象语法树,简称语法树(Syntax tree),树上的每个节点都表示源代码中的一种结构。

JavaScript 领域常用的 AST 解析库有 babel、esprima、espree 和 acorn 等,由于Babel在AST解析的基础上还能完成源码转换的功能,所以我们选择Babel应用于JS代码的反混淆。

Babel运行在nodejs上,还没有安装nodejs的,可以到https:///zh-cn/安装,建议安装左边的长期维护版。

Babel简介

Babel 是 JavaScript 源码到源码的编译器,通常也叫做“转换编译器(transpiler)。

Babel 使用一个基于 ESTree 并修改过的 AST,它的内核说明文档在https://github.com/babel/babel/blob/master/packages/babel-parser/ast/spec.md

语法树包含程序主体、声明类型、标识符、字面量等信息,对于一个变量申明语句包括以下三种节点:

  • VariableDeclarator:变量声明
  • Identifier:标识符
  • Literal:字面量

更多节点参考后续 AST 节点类型对照表。

Babel 主要包含以下几个功能包:

  1. @babel/core:Babel 编译器本身,提供了 babel 的编译 API;
  2. @babel/parser:将 JavaScript 代码解析成 AST 语法树;
  3. @babel/traverse:遍历、修改 AST 语法树的各个节点;
  4. @babel/generator:将 AST 还原成 JavaScript 代码;
  5. @babel/types:判断、验证节点的类型、构建新 AST 节点等。

AST Explorer 直观的认识 AST 节点。网址:https:///

该网站支持多种解析为AST库,我们选择**@babel/parser**,保持一致:

image-20230114222511244

例如对于:

var a=1;

可以看到解析结果为:

image-20230114221027950

AST 的每一层都拥有相同的结构:

{
  type: "VariableDeclaration",
  id: {...},
  init: {...},
  kind: "var"
}
{
  type: "Identifier",
  name: ...
}
{
  type: "NumericLiteral",
  value: ...
}

这样的每一层结构也被叫做 节点(Node)。 一个 AST 可以由单一的节点或是成百上千个节点构成。

每一个节点都有如下接口(Interface):

interface Node {
  type: string;
  loc: SourceLocation | null;
}

字符串形式的 type 字段表示节点的类型(如: "FunctionDeclaration""Identifier",或 "Binary")。 每一种类型的节点定义了一些附加属性用来进一步描述该节点类型。

Babel 还为每个节点额外生成了 startendloc 属性用于描述该节点在原始代码中的位置。

常见节点信息:

节点属性记录的信息
type当前节点的类型
start当前节点的起始位
end当前节点的末尾
loc当前节点所在的行列位置 起始于结束的行列信息
errorsFile节点所持有的特有属性
program包含整个源代码,不包含注释节点
comments源代码中所有的注释会显示在这里

Babel涉及的文档

Babel 各种节点类型所拥有的属性:https://www./docs/babel-types

中文官方文档:https://www./docs/

非官方 Babel API 中文文档:https:///Babel-traverse-api-doc/

插件开发手册:https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md

babel库官方插件:https://www./docs/plugins

AST可视化:

  • http:///
  • https://resources./demos/rappid/apps/Ast/index.html

Babel 的处理步骤

Babel 的三个主要处理步骤分别是: 解析(parse)转换(transform)生成(generate)

解析步骤接收代码并输出 AST。 这个步骤分为两个阶段:**词法分析(Lexical Analysis) **和 语法分析(Syntactic Analysis)

词法分析阶段把字符串形式的代码转换为 令牌(tokens) 流。

你可以把令牌看作是一个扁平的语法片段数组:

n * n;
[
  { type: { ... }, value: "n", start: 0, end: 1, loc: { ... } },
  { type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },
  { type: { ... }, value: "n", start: 4, end: 5, loc: { ... } },
  ...
]

每一个 type 有一组属性来描述该令牌,和 AST 节点一样它们也有 startendloc 属性。

语法分析阶段会把一个令牌流转换成 AST 的形式。 这个阶段会使用令牌中的信息把它们转换成一个 AST 的结构:

program: Program  {
    type: "Program"
    sourceType: "module"
    body:  [
        Statement  {
            type: "Statement"
            : Binary  {
                type: "Binary"
                left: Identifier  {
                    type: "Identifier"
                    name: "n"
                }
                operator: "*"
                right: Identifier  = $node {
                    type: "Identifier"
                    name: "n"
                }
    		}
    	}
    ]
    directives: [ ]
}

转换步骤:接收 AST 并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。 这也是我们需要编码的部分。

**代码生成:**深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串。

Babel插件的安装

Babel 的核心功能包含在 @babel/core 模块中。通过以下命令安装:

npm install --save-dev @babel/core

注意:@babel/core模块不能使用-g全局安装,否则会出现无法导入库的情况。

使用 --save-dev 安装的插件,被写入到 devDependencies对象里面去;而使用 --save 安装的插件,则是被写入到 dependencies对象里面去。

package.json 文件 devDependencies 和 dependencies 的区别:

  • devDependencies 里面的插件只用于开发环境,不用于生产环境。
  • dependencies 是需要发布到生产环境的。

我们只需要使用Babel 进行源码反混淆,所以仅使用开发环境即可,当然省略--save-dev使用默认的 --save也不影响。

jstools的安装和使用

下载地址:https://github.com/cilame/v_jstools

我们下面zip压缩包后解压得到v_jstools-main文件夹。谷歌游览器或360游览器可以访问以下地址管理插件:chrome://extensions/

启用开发者模式后,点击加载已解压的扩展程序选择 v_jstools-main 文件夹。

安装后,点击打开配置页面,即可看到如下界面:

image-20230131000259411

使用 修改返回值-》动态修改被调试页面的所有js代码 的功能可以动态替换js的代码。

使用 AST混淆解密-》打开本地ast页面 可以使用本地的ast解析功能。

使用示例,访问https://match./match/2

打开开发者工具,清空cookie后刷新页面可以看到代码为:

image-20230131005302047

下面我们基于默认代码基础上填写如下代码:

function fetch_hook(code, url) {
    var ast = parser.parse(code);
    const simplifyLiteral = {
        "NumericLiteral|StringLiteral"({node}) {
            node.extra = undefined;
        },
    }
    traverse(ast, simplifyLiteral);
    var {code} = generator(ast, {
        jsescOption: { minimal: true},
        compact: true,
    });
    return code
}

即:

image-20230131005508204

任何通过右键启动ast hook:

image-20230131005542249

然后清空cookie后,重新刷新页面(可能需要先重启一下开发者工具),可以看到代码已经被替换:

image-20230131005802879

Babel基本知识

path和node

使用AST Explorer 查看:

var a = 123;
var b;

默认情况下我们点击一下var整个变量节点被标黄。

image-20230117172529680

如果点击一下等号:

image-20230117172635422

点击"123"也能高亮对应的位置:

image-20230117180717397

而鼠标移动到上述任意节点区域内,代码对应位置也会高亮。

遍历的时候可以这样编写插件:

const visitor = {
   VariableDeclaration(path)
   {
     //to do something;
   },
}

VariableDeclaration 和 VariableDeclarator 有什么区别?

可以看到,VariableDeclaration 是 VariableDeclarator 的父节点。针对如下代码,再进行解析:

var a = 123,b = 456;

image-20230117191909622

说明,VariableDeclarator只对应一个 变量的定义,而VariableDeclaration 对应整行申明语句。

那么我们只需要在遍历时,向VariableDeclaration 插入VariableDeclarator节点就可以在一行语句内增加变量定义。

path常见的方法

path.node:获取当前path下的node节点。

let {node,scope} = path;

当前路径所对应的源代码:使用toString方法

path.toString()

path.scope:表示当前path下的作用域

path.type:获取当前path的节点类型字符串

path.key:获取当前节点在父节点对应的key值

判断path的类型:使用path.isXXX方法

if(path.isStringLiteral()) {
 //do something;
}

获取path的上一级路径

let parent = path.parentPath;

path.parent:用于获取当前path下的父node。其中:

path.parent == path.parentPath.node;//这两者是等价的

path.container:用于获取当前path下的所有兄弟节点(包括自身)

path.container

**获取path的子路径:**使用get方法

path.get('id');

删除path

path.remove();

计算表达式的值:

path.evaluate();

返回一个对象,其中的 confident 属性表示置信度,value 表示计算结果。

替换path

path.replaceWith({type:"NumericLiteral",value:3});

或引入@babel/types

const t = require("@babel/types");
path.replaceWith(t.NumericLiteral(3));

替换方法有一下几种:

  • replaceWith:用一个节点替换另一个节点;
  • replaceWithMultiple:用多个节点替换另一个节点;
  • replaceWithSourceString:将传入的源码字符串解析成对应 Node 后再替换,性能较差,不建议使用;
  • replaceInline:用一个或多个节点替换另一个节点,相当于同时有了前两个函数的功能。

插入节点

NodePath.insertAfter()方法用于在当前path前面插入节点
NodePath.insertBefore()方法用于在当前path后面插入节点

var node = t.NumericLiteral(1)  // 使用 types 来生成一个数字节点
path.insertAfter(node)  // 在当前path前面插入节点
node = t.NumericLiteral(3)
path.insertBefore(node)  // 在当前path后面插入

关于node的一些操作

node其实是path的一个属性:

const node = path.node;

比如打印VariableDeclarator节点的内容:

const visitor = {
	VariableDeclarator(path) {
		console.log(path.node);
	},
}

image-20230117195116061

可以看到与ast节点的内容。

获取节点对应的源码:

const generator = require("@babel/generator").default;
let {code} = generator(node);

删除init节点:

delete path.node.init;

path.node.init = undefined;

创建节点并生成代码

示例:

const t = require("@babel/types");
const generator = require("@babel/generator").default;

var callee = t.member(t.identifier('console'), t.identifier('log')),
    args = [t.NumericLiteral(777)],
    call_exp = t.call(callee, args),
    exp_statement = t.Statement(call_exp)

console.log(generator(exp_statement).code)

结果:

console.log(777);

Scope和Binding

scope:作用域,是名字(name)与实体(entity)的绑定(binding

binding:名字绑定把实体(数据 或 代码)关联到标识符,
标识符绑定到实体称为引用该对象。

简单的理解为:

  • 一个函数就是一个作用域
  • 一个变量就是一个绑定,依附在作用域

scope常用方法及属性

参考scope相关的源代码:

node_modules\@babel\traverse\lib\scope\index.js

常用的属性和方法:

  1. scope.block

    表示当前作用域下的所有node

  2. scope.dump()

    输出当前每个变量的作用域信息。调用后直接打印,不需要加打印函数

  3. scope.crawl()

    重构scope,在某种情况下会报错,不过还是建议在每一个插件的最后一行加上。

  4. scope.rename(oldName, newName, block)

    修改当前作用域下的的指定的变量名,oldname、newname表示替换前后的变量名,为字符串。注意,oldName需要有binding,否则无法重命名。

  5. scope.traverse(node, opts, state)

    遍历当前作用域下的某个节点和全局的traverse用法一样。

  6. scope.getBinding(name)

    获取某个变量的binding,可以理解为其生命周期。包含引用,修改之类的信息

查看基本的作用域与绑定信息:

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

const jscode = `
function squire(i){
    return i * i * i;
}
function i(){
    var i = 123;
    i += 2;
    return 123;
}`;
let ast = parser.parse(jscode);
const visitor = {
    "FunctionDeclaration"(path){
        console.log(`函数${path.node.id.name}`)
        path.scope.dump();
    }
}
traverse(ast, visitor);
函数squire
------------------------------------------------------------
# FunctionDeclaration
 - i { constant: true, references: 3, violations: 0, kind: 'param' }
# Program
 - squire { constant: true, references: 0, violations: 0, kind: 'hoisted' }
 - i { constant: true, references: 0, violations: 0, kind: 'hoisted' }
------------------------------------------------------------
函数i
------------------------------------------------------------
# FunctionDeclaration
 - i { constant: false, references: 0, violations: 1, kind: 'var' }
# Program
 - squire { constant: true, references: 0, violations: 0, kind: 'hoisted' }
 - i { constant: true, references: 0, violations: 0, kind: 'hoisted' }
------------------------------------------------------------

输出形式

  • 作用域以#标识输出,绑定都以-标识输出
  • 先输出当前作用域,再输出父级作用域,再输出父级的父级作用域……

对于单个绑定Binding,会输出4种信息:

  • constant 是否为常量
  • references 被引用次数
  • violations 被重新定义的次数
  • kind 声明类型:param 参数, hoisted 提升,var 变量, local 内部

这两个函数都有共同的父级作用域Program的信息。

binding常用方法及属性

Binding 对象用于存储 绑定 的信息,这个对象会作为Scope对象的一个属性存在,同一个作用域可以包含多个 Binding

@babel/traverse/lib/scope/binding.js 中查看到它的定义。

关键属性有:

  1. identifier:标识符的 Node 对象;
  2. scope:所在作用域
  3. path:用于定位初始拥有binding的path;
  4. kind :变量类型,param参数、 hoisted提升、var变量、local内部
  5. constantViolations:如果标识符被修改,则会存放所有修改该标识符节点的 Path 对象;
  6. constant:标识符是否为常量;
  7. referenced:标识符是否被引用;
  8. referencePaths:如果标识符被引用,则会存放所有引用该标识符节点的 Path 对象。
  9. references:标识符被引用的次数;

查看binding信息:

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

const jscode = `
function a(){
    var a = 1;
    a = a + 1;
	var b = 1;
    var c = 2;
    b = b - c;
    return a,b;
}`;
let ast = parser.parse(jscode);
const visitor = {
    BlockStatement(path){
        console.log("源码:\n", path.toString())
        var bindings = path.scope.bindings
        console.log('作用域内被绑定的变量:', Object.keys(bindings))
		console.log('----------------------------------------')
        for(var b in bindings){
			b = bindings[b];
			console.log('标识符:', b.identifier.name)
			console.log('变量类型:', b.kind)
			console.log('是常量:', b.constant)
			console.log('被引用:', b.referenced)
			console.log('被引用次数', b.references)
			console.log('被修改次数', b.constantViolations.length)
			// console.log('被引用信息NodePath记录', b.referencePaths)
			// console.log('被修改的Path对象', b.constantViolations)
			console.log("----------");
        }
    }
}
traverse(ast, visitor);

运行结果:

源码:
 {
  var a = 1;
  a = a + 1;
  var b = 1;
  var c = 2;
  b = b - c;
  return a, b;
}
作用域内被绑定的变量: [ 'a', 'b', 'c' ]
----------------------------------------
标识符: a
变量类型: var
是常量: false
被引用: true
被引用次数 2
被修改次数 1
----------
标识符: b
变量类型: var
是常量: false
被引用: true
被引用次数 2
被修改次数 1
----------
标识符: c
变量类型: var
是常量: true
被引用: true
被引用次数 1
被修改次数 0
----------

也可以根据名称获取指定binding:

let binding = scope.getBinding(name);

运算符的优先级

首先解析如下代码:

1 + 2 + 3;

image-20230117200141983

可以看到整体被解析成一个Binary,前1+2又被解析成一个Binary,所以这个表达式等价于:

(1 + 2) + 3;

再尝试解析如下代码:

1 + 2 * 3;

image-20230117210526811

同样它等价于:

1 + (2 * 3);

下面我们看看下面这个例子:

zc(s === Sc || s === Ac ? 192 : 204);

“||” 与 “?” 的优先级到底哪个高呢?我们看看ast的解析结果:

image-20230117211612240

很明显的可以看到 “||” 的优先级高于 “?” ,等价于:

zc((s === Sc || s === Ac) ? 192 : 204);

如果指定括号优先级:

zc(s === Sc || (s === Ac ? 192 : 204));

则解析为:

image-20230117225307866

AST 节点类型对照表

常用部分:

类型原名称中文名称描述
Program程序主体整段代码的主体
VariableDeclaration变量声明声明一个变量,例如 var let const
FunctionDeclaration函数声明声明一个函数,例如 function
Statement表达式语句通常是调用一个函数,例如 console.log()
BlockStatement块语句包裹在 {} 块内的代码,例如 if (condition){var a = 1;}
BreakStatement中断语句通常指 break
ContinueStatement持续语句通常指 continue
ReturnStatement返回语句通常指 return
SwitchStatementSwitch 语句通常指 Switch Case 语句中的 Switch
SwitchCaseCase 语句通常指 Switch 语句中的 Case
IfStatementIf 控制流语句控制流语句,通常指 if(condition){}else{}
Identifier标识符标识,例如声明变量时 var identi = 5 中的 identi
Call调用表达式通常指调用一个函数,例如 console.log()
Binary二进制表达式通常指运算,例如 1+2
Member成员表达式通常指调用对象的成员,例如 console 对象的 log 成员
Array数组表达式通常指一个数组,例如 [1, 3, 5]
NewNew 表达式通常指使用 New 关键词
Assignment赋值表达式通常指将函数的返回值赋值给变量
Update更新表达式通常指更新成员值,例如 i++
Literal字面量字面量
BooleanLiteral布尔型字面量布尔值,例如 true false
NumericLiteral数字型字面量数字,例如 100
StringLiteral字符型字面量字符串,例如 vansenb

更多类型查看:https://www./docs/babel-types

比如对于variableDeclarator,我们可以直接使用Ctrl+F搜索:

image-20230129184828079

代码生成选项

常用选项:

参数描述
auxiliaryCommentBefore在输出文件内容的头部添加注释块文字
auxiliaryCommentAfter在输出文件内容的末尾添加注释块文字
comments输出内容是否包含注释
compact输出内容是否不添加空格,避免格式化
concise输出内容是否减少空格使其更紧凑一些
minified是否压缩输出代码
retainLines尝试在输出代码中使用与源代码中相同的行号

更多选项可查看:https://www./docs/babel-generator

Unicode转中文或者其他非ASCII码字符:

let {code} = generator(ast,opts={jsescOption:{"minimal":true}});

代码压缩:

let {code} = generator(ast,opts={"compact":true});

删除所有注释:

let {code} = generator(ast,opts={"comments":false});

保留空行:

let {code} = generator(ast,opts={"retainLines":true});

Babel 反混淆入门示例

hello world

使用Babel 修改代码var a=1;的变量名和值:

// 将JS源码转换成语法树
const parser = require("@babel/parser");
// 模板引擎
const template = require("@babel/template").default;
// 遍历AST
const traverse = require("@babel/traverse").default;
// 操作节点,比如判断节点类型,生成新的节点等
const t = require("@babel/types");
// 将语法树转换为源代码
const generator = require("@babel/generator").default;

jscode="var a=1;";
let ast = parser.parse(jscode);
// console.log(JSON.stringify(ast,null,'\t'));
var visitor={
	VariableDeclarator(path) {
		path.node.id=t.identifier("xxm");
		path.node.init=t.numericLiteral(25);
	}
}
traverse(ast, visitor);
let {code} = generator(ast);
console.log(code);

运行代码:

>node demo.js
var xxm = 25;

要修改变量名,必须修改变量申明节点的子节点,通过AST Explorer 可以清晰看到ast节点情况。当然也可以自己将ast节点打印出来(被注释的代码):

JSON.stringify(ast,null,'\t');

通过插件开发手册可知访问者的基本申明方式。

我们可以借助前面全局安装的node-inspect进行调试,得知path的内容。

node-inspect安装教程:https://blog.csdn.net/as604049322/article/details/128584447

使用node-inspect执行上述代码:

node-inspect demo.js

稍等片刻,游览器DevTools,出现上述代码,下面我们给访问者内第一行代码打上断点:

image-20230114230956378

恢复运行后,可以查看path在内存中的内容:

image-20230114231305043

也可以在控制台输入目标变量查看对应内容:

JSON.stringify(path.node.id)
"{"type":"Identifier","start":4,"end":5,"loc":{"start":{"line":1,"column":4,"index":4},"end":{"line":1,"column":5,"index":5},"identifierName":"a"},"name":"a"}"

t.xxx则是用于生成xxx类型节点,传入的参数决定节点的属性,例如:

t.identifier("xxm")
{type: "Identifier", name: "xxm"}

还原unicode常量值

下面我们的目标是将:

var a = "\u0068\u0065\u006c\u006c\u006f\u002c\u0041\u0053\u0054";

还原成它本来的面目:

var a = "hello,AST";

从现在开始我们将要反混淆的代码都放入单独的文件中,然后将脚本设计为可以传参执行,最终模板代码为:

//babel库相关,解析,模板引擎,转换,构建,生产
const parser = require("@babel/parser");
const template = require("@babel/template").default;
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;
// 操作文件
const fs = require("fs");

let encode_file = "read.js",decode_file = "decode_result.js";
if (process.argv.length > 2)
  encode_file = process.argv[2];
if (process.argv.length > 3)
  decode_file = process.argv[3];
var jscode = fs.readFileSync(encode_file, {
    encoding: "utf-8"
});

let ast = parser.parse(jscode);
var visitor={
}
traverse(ast, visitor);

let {code} = generator(ast,opts = {jsescOption:{"minimal":true}});
fs.writeFile(decode_file, code, (err)=>{});

官网手册查询得知,NumericLiteral、StringLiteral类型的extra节点并非必需,这样在将其删除时,不会影响原节点。

通过ast解析结果也可以看到只需要删除extra子节点即可:

image-20230117233113212

最终访问者的内容为:

const visitor={
	NumericLiteral({node}) {
		delete node.extra
	},
	StringLiteral({node}) {
		delete node.extra
	},
}

可以将目标代码保存到read.js,然后运行上述模板代码,最终得到满足要求的结果文件decode_result.js顺利得到目标结果。

删除空行和空语句

代码示例:

var a = 123;
;
var b = 456;

;;;
var c=1789;

ast的解析结果为:

image-20230118212222639

generator生成代码默认是去掉空行的,我们只需要直接删除EmptyStatement节点即可。

访问者代码为:

const visitor={
	EmptyStatement(path) {
		path.remove();
	}
}

运行后顺利删除了空行和空语句。

定义在一行的变量分离

还原前:

var a = 123,b = 456;
for(let c = 789,d = 120;false;);

现在需要将其还原为每行仅定义一个变量。

观察ast节点:

image-20230130193138935

我们需要将VariableDeclaration 中的每个VariableDeclarator提取出来生成一个VariableDeclaration节点,最后进行多节点替换。

需要注意,定义在for循环里面的多个变量不能进行分离,为了判断一个VariableDeclaration节点能否分离,可以查看其父节点是否为BlockStatement。

判断方法为:

t.isBlockStatement(path.parent)

还可以使用path内部的方法判断:

path.parentPath.isBlock()

访问者代码为:

const visitor = {
	VariableDeclaration(path) {
		let {parentPath, node} = path;
		// 跳过不在块节点下面定义的变量
		if(!parentPath.isBlock()) return;
		let {declarations, kind} = node;
		// 只定义一个变量,无需分离
		if(declarations.length==1) return;
		declarations=declarations.map(v=>t.VariableDeclaration(kind, [v]));
		path.replaceWithMultiple(declarations);
    },
}

分离结果:

var a = 123;
var b = 456;
for (let c = 789, d = 120; false;);

可以看到顺利还原了需要分离的变量。

数组字面量元素替换

目标代码:

var _ac = ["\x67\x65\x74\x41\x74\x74\x72\x69\x62\x75\x74\x65", "\x41\x63\x74\x69\x76\x65\x58\x4f\x62\x6a\x65\x63\x74", "\x64\x6f\x61\x63\x74","\x4f\x70\x65\x6e\x20\x53\x61\x6e\x73", "\x50\x49", "\x73\x65\x6e\x64"];
bmark[_ac[4]]=_ac[1];
bmark[_ac[3]]=_ac[2];
bmark[_ac[0]]=_ac[5];

希望能够计算出_ac[xxx]的结果并计算,查看ast:

image-20230117231127999

使用上一节的模板,将上述数组定义复制到模板中,并编写访问者:

var _ac = ["\x67\x65\x74\x41\x74\x74\x72\x69\x62\x75\x74\x65", "\x41\x63\x74\x69\x76\x65\x58\x4f\x62\x6a\x65\x63\x74", "\x64\x6f\x61\x63\x74","\x4f\x70\x65\x6e\x20\x53\x61\x6e\x73", "\x50\x49", "\x73\x65\x6e\x64"];
const visitor = {
	Member(path) {
		let {object,property} = path.node;
		if (!t.isIdentifier(object,{name:"_ac"}) || !t.isNumericLiteral(property)) return;
		let value = _ac[property.value];
		path.replaceWith(t.valueToNode(value));
	}
}

运行后得到:

var _ac = ...;
bmark["PI"] = "ActiveXObject";
bmark["Open Sans"] = "doact";
bmark["getAttribute"] = "send";

key值Literal化

有时有些表达式为:

String.fromCharCode

有些表达式却为:

String["fromCharCode"]

由于这两种形式的节点类型不同,所以我们形式能够形式统一化,全部转变为StringLiteral节点即后者的形式。

假设代码为:

b.length;

解析结果:

image-20230117234033025

最终我们需要转换为:

b["length"];

image-20230117234343942

可以看到区别在于computed属于变为true,property由Identifier转变为StringLiteral。

最终访问者代码为:

const visitor={
	Member({node}){
		const prop = node.property;
		if (!node.computed && t.isIdentifier(prop)) {
			node.property = t.StringLiteral(prop.name);
			node.computed = true;
		}
	},
}

另外如下代码也需要规范化:

var foo = {
  const: function() {},
  var: function() {},
  "default": 1,
  [a]: 2,
  foo: 1,
};

image-20230117235930831

从ast解析结果可以看到,将key属性的Identifier转变为StringLiteral即可,computed都是false无需处理。

访问者代码为:

const visitor={
	ObjectProperty({node}){
		const key = node.key;
		if (!node.computed && t.isIdentifier(key)) {
			node.key = t.StringLiteral(key.name);
		}
	},
}

结果:

var foo = {
  "const": function () {},
  "var": function () {},
  "default": 1,
  [a]: 2,
  "foo": 1
};

Array类型元素还原

示例:

var a = [1,2,3,[1213,234],{"code":"777"},"window"];
b = a[1] + a[2] + a[3];
c = a[4];
d = a[5];

我们需要将所有的数组调用替换为对应的元素,查看ast解析结果可以看到都是Member节点:

image-20230129191206137

将数组定义复制粘贴到访问者代码之上,最终代码为:

let arrName = "a";
var a = [1,2,3,[1213,234],{"code":"777"},"window"];
const visitor = {
	Member(path){
		let {object,property} = path.node;
		if(!t.isIdentifier(object,{name:arrName}) || !t.isNumericLiteral(property))
			return;
		let value = eval(path.toString());
		path.replaceWith(t.valueToNode(value));
	}
}

运行后,结果:

var a = [1, 2, 3, [1213, 234], {
  "code": "777"
}, "window"];
b = 2 + 3 + [1213, 234];
c = {
  code: "777"
};
d = "window";

下面我们考虑使用更通用的方法解决该问题。

思路:遍历VariableDeclarator类型的节点,使用scope获取作用域对应的binding对象。通过 binding.referencePaths 来定位引用的位置,获取父节点进行替换。替换完毕后,删除此节点,减少垃圾代码。

const visitor = {
	VariableDeclarator(path){
		let {node,scope} = path;
		let {id,init} = node;
		if (!t.isArray(init)) return;
		
		const binding = scope.getBinding(id.name);
		if (!binding || !binding.constant) return;
		for(referPath of binding.referencePaths){
			let {node,parent} = referPath;
			// 检查是否存在对数组的非下标取数据操作,存在则取消对该数组的任何替换
			if (!t.isMember(parent,{"object":node}) || !t.isNumericLiteral(parent.property))
				return;
		}
		for(referPath of binding.referencePaths){
			let {parent,parentPath} = referPath;
			parentPath.replaceWith(init.elements[parent.property.value]);
		}
		path.remove();
		scope.crawl();
	},
}

最终顺利还原结果:

b = 2 + 3 + [1213, 234];
c = {
  "code": "777"
};
d = "window";

注意:此方法较为通用,要求对应的数组未发生修改。

表达式还原

一段明显可以直接计算出具体值的代码:

var a = !![],b = 'Hello ' + 'world' + '!',c = 2 + 3 * 50,d = Math.abs(-200) % 19,e = true ? 123:456,f = Math.ceil(2.8),g = Math.ceil(a);
var t1 = 'a' + 'b' + 'c' + error + 'e' + 'f';
var t2 = 'a' + 'b' + 'c' + 'd' + 'e' + 'f';

还原这些式子的思路有很多,下面我们尝试使用path的evaluate方法进行还原。

查看@babel\traverse\lib\path\evaluation.js的源码可以看到evaluate返回的对象有三个部分:

function evaluate() {
  const state = {
    confident: true,
    deoptPath: null,
    seen: new Map()
  };
  let value = evaluateCached(this, state);
  if (!state.confident) value = undefined;
  return {
    confident: state.confident,
    deopt: state.deoptPath,
    value: value
  };
}

使用ast解析上述代码:

image-20230118101853349

可以看到有值的根节点有Unary、Binary、Conditional和Call三种类型。

最终代码:

const visitor={
	"Unary|Binary|Conditional|Call"(path) {
		console.log(path.toString())
		const {confident,value} = path.evaluate();
		console.log(confident,value)
		confident && path.replaceWith(t.valueToNode(value));
	},
}

打印结果:

!![]
true true
'Hello ' + 'world' + '!'
true Hello world!
2 + 3 * 50
true 152
Math.abs(-200) % 19
true 10
true ? 123 : 456
true 123
Math.ceil(2.8)
true 3
Math.ceil(a)
true 1
'a' + 'b' + 'c' + error + 'e' + 'f'
false undefined
'a' + 'b' + 'c' + error + 'e'
false undefined
'a' + 'b' + 'c' + error
false undefined
'a' + 'b' + 'c'
true abc
'a' + 'b' + 'c' + 'd' + 'e' + 'f'
true abcdef

最终输出:

var a = true,
  b = "Hello world!",
  c = 152,
  d = 10,
  e = 123,
  f = 3,
  g = 1;
var t1 = "abc" + error + 'e' + 'f';
var t2 = "abcdef";

从结果可以看到通过confident进行判断值的有效性可以顺利跳过无法合并的节点,例如error压根不存在,直接替换会导致结果为undefined。

enter和exit的区别

示例:

var t1 = 'a' + 'b' + 'c' + d + 'e' + 'f';

访问者代码:

const visitor={
	Binary(path) {
		console.log(path.toString())
		const {confident,value} = path.evaluate();
		confident && path.replaceWith(t.valueToNode(value));
	},
}

打印结果:

'a' + 'b' + 'c' + d + 'e' + 'f'
'a' + 'b' + 'c' + d + 'e'
'a' + 'b' + 'c' + d
'a' + 'b' + 'c'

默认情况下,没有明确指定时,以enter方式进行遍历,遍历顺序是先父后子

最终合并结果为:

var t1 = "abc" + d + 'e' + 'f';

如果我们将示例调整为:

var t2 = 'a' + 'b' + 'c' + 'd' + 'e' + 'f';

再次运行,打印结果:

'a' + 'b' + 'c' + 'd' + 'e' + 'f'

结果显示只遍历了一次,这是因为第一次遍历后,已经变成StringLiteral 类型的表达式,不再是 Binary 类型。

假如我们指定以exit方式进行遍历时

const visitor={
	"Binary": { 
		exit: function(path) {
			console.log(path.toString())
			const {confident,value} = path.evaluate();
			confident && path.replaceWith(t.valueToNode(value));
		}
	},
}

结果:

'a' + 'b'
"ab" + 'c'
"abc" + 'd'
"abcd" + 'e'
"abcde" + 'f'

可以很清楚的看到,exit方式是以先子后父的顺序遍历。

优化无实参的自执行函数

比如有如下无实参的自执行函数:

!(function(){
  var a = 123;
})();

自执行函数没有参数就可以进行简化,最终我们希望简化到:

var a = 123;

ast解析结果为:

image-20230118172323300

根据解析结果我们需要从Unary节点开始遍历子节点,然后判断是否具备无实参的自执行函数,最终将内层定义替换整个Unary节点。访问者代码如下:

const visitor={
	Unary(path) {
		let {operator,argument} = path.node;
		if (operator != "!" || !t.isCall(argument)) return;
		let {callee,arguments} = argument;
		// 参数为空,被调用的是一个函数
		if (arguments.length !=0 || !t.isFunction(callee)) return;
		let {id,params,body} = callee;
		// 匿名函数没有id属性,函数的参数为空,代码块定义
		if (id != null || params.length !=0 || !t.isBlockStatement(body)) return;
		path.replaceWithMultiple(body.body);
	},
}

执行上述代码最终顺利取出自执行函数的内部代码。

如果需要兼容没有!符号的自执行函数,可以直接对Call节点的处理,然后对Unary判断是否为开头。

例如代码为:

!!(function(){
  var a = !!123;
})();
(function(){
  var b = !456;
})();

访问者最终代码为:

const visitor={
	Unary(path) {
		let {operator,argument} = path.node;
		if(operator!="!" || !t.isStatement(path.parentPath.node)) return;
		path.replaceWith(argument);
	},
	Call(path) {
		let {callee,arguments} = path.node;
		if (arguments.length !=0 || !t.isFunction(callee)) return;
		let {id,params,body} = callee;
		if (id != null || params.length !=0 || !t.isBlockStatement(body)) return;
		path.replaceWithMultiple(body.body);
	},
}

简化后代码为:

var a = !!123;
var b = !456;

以上代码假设了任何代码体,开头是!的都是无意义的,可以直接删除。

全局函数计算值替换

获取实参,计算出全局函数调用的结果,并用结果替换该全局函数的调用表达式。

示例:

var a = parseInt("12345",16),b = Number("123"),c = String(true),d = unescape("hello%2CAST%21");
eval("a = 1");

在不使用ast解析的情况洗啊,根据前一节可知,函数调用都是Call节点,由此编写访问者:

const visitor={
	Call(path) {
		let {callee,arguments} = path.node;
		//函数名是id节点,所有的参数都是 Literal 字面量
		if (!t.isIdentifier(callee) || callee.name == "eval") return; 
		if (!arguments.every(arg=>t.isLiteral(arg))) return;
		// 根据函数名获取对应函数
		let func = global[callee.name];
		if (typeof func !== "function") return;
		// 遍历取出参数值
		let args = arguments.map(e =>e.value);
		let value = func.apply(null,args);
		if (typeof value == "function") return;
		path.replaceWith(t.valueToNode(value));
	},
}

注意点:

  1. eval内的代码执行后返回值为1,替换会导致生成的代码无法执行原始逻辑,所以取消执行
  2. 从global中取出的全局变量是function类型时,表示是全局函数。
  3. 计算出的结果为function类型时,可能是闭包函数,不能进行替换。

执行代码后结果:

var a = 74565,
  b = 123,
  c = "true",
  d = "hello,AST!";
eval("a = 1");

假如自定义函数也需要替换值呢?

对于一个简单的自定义函数:

var Xor = function (p,q) {
  return p ^ q;
}
let a = Xor(111,222);

function xor2(p,q) {
  return p ^ q;
}
let b = xor2(333,444);

假设js脚本中全部都是简单的函数,我们希望计算出所有调用简单函数的最终值进行替换得到:

let a = 177;
let b = 241;

根据上述脚本只需要简单改造一下,同时也需要将这些简单的函数复制到脚本中:

var Xor = function (p,q) {
  return p ^ q;
}
function xor2(p,q) {
  return p ^ q;
}
var callees ={
	"Xor":Xor,
	"xor2":xor2
}
const visitor={
	Call(path) {
		let {callee,arguments} = path.node;
		//函数名是id节点,所有的参数都是 Literal 字面量
		if (!t.isIdentifier(callee) || !arguments.every(arg=>t.isLiteral(arg))) return;
		// 根据函数名获取对应函数
		let func = callees[callee.name];
		if (typeof func !== "function") return;
		// 遍历取出参数值
		let args = arguments.map(e =>e.value);
		let value = func.apply(null,args);
		path.replaceWith(t.valueToNode(value));
	},
}

执行后,即可得到结果:

var Xor = function (p, q) {
  return p ^ q;
};
let a = 177;
function xor2(p, q) {
  return p ^ q;
}
let b = 241;

然后第二遍处理将对应的VariableDeclaration和FunctionDeclaration节点删除:

const visitor2={
	"FunctionDeclaration|VariableDeclarator"(path) {
		if(path.node.id.name in callees) path.remove();
	},
}
traverse(ast, visitor2);

假如全局函数和简单的自定义函数都需要删除:

var a = parseInt("12345",16),b = Number("123"),c = String(true),d = unescape("hello%2CAST%21");
eval("a = 1");
var Xor = function (p,q) {
  return p ^ q;
}
let a = Xor(111,222);
function xor2(p,q) {
  return p ^ q;
}
let b = xor2(333,444);

综合解析代码:

var Xor = function (p,q) {
  return p ^ q;
}
function xor2(p,q) {
  return p ^ q;
}
var callees ={
	"Xor":Xor,
	"xor2":xor2
}
const visitor={
	Call(path) {
		let {callee,arguments} = path.node;
		//函数名是id节点,所有的参数都是 Literal 字面量
		if (!t.isIdentifier(callee) || !arguments.every(arg=>t.isLiteral(arg))) return;
		if (callee.name == "eval") return; 
		// 根据函数名获取对应函数
		let func = global[callee.name]||callees[callee.name];
		if (typeof func !== "function") return;
		// 遍历取出参数值
		let args = arguments.map(e =>e.value);
		let value = func.apply(null,args);
		if (typeof value == "function") return;
		path.replaceWith(t.valueToNode(value));
	},
}
traverse(ast, visitor);
const visitor2={
	"FunctionDeclaration|VariableDeclarator"(path) {
		if(path.node.id.name in callees) path.remove();
	},
}
traverse(ast, visitor2);

调用结果:

var a = 74565,
  b = 123,
  c = "true",
  d = "hello,AST!";
eval("a = 1");
let i = 177;
let j = 241;

eval函数内部代码还原

示例:

eval("a = 1");
eval("b = 2;c=3;var d=4,e=5");

下面我们需要将其还原为基本的语句,可以使用template模板引擎将eval中的代码解析为节点,然后直接替换即可。

访问者代码为:

const visitor={
	Call(path) {
		let {callee, arguments} = path.node;
		// 确保是eval函数并且参数唯一
		if(!t.isIdentifier(callee, {name: "eval"})) return;
		if (arguments.length != 1 || !t.isLiteral(arguments[0]))
			return;
		const evalNode = template.statements.ast(arguments[0].value);
		path.replaceWithMultiple(evalNode);
	},
}

解析后的结果:

a = 1;
b = 2;
c = 3;
var d = 4,
  e = 5;

删除代码中没有被用到的变量或函数

比如有如下代码:

var a = 12345,b;
const c = 5;
a += 5 ;
function get_copyright() {
	return
}

我们需要尽可能的将没有被使用到的变量和函数删除。

我们需要通过作用域获取binding对象:

path.scope.getBinding(path.node.id.name)

访问者代码为:

const visitor = {
    "VariableDeclarator|FunctionDeclaration"(path) {
		const binding = path.scope.getBinding(path.node.id.name);
		// 如果标识符被修改过,则不能进行删除动作。
		if (!binding || !binding.constant) return;
		// 删除没有被引用过的变量
		if(binding && !binding.referenced) path.remove();
    },
}

还原结果:

var a = 12345;
a += 5;

还原简单的Call 类型

对于一个简单的函数调用语句:

var Xor = function (p,q) {
  return p ^ q;
}
let a = Xor(111,222);

我们希望提取出函数体中的表达式,将简单的自定义函数调用还原:

var Xor = function (p,q) {
  return p ^ q;
}

let a = 111 ^ 222;

我们需要遍历VariableDeclarator 节点进行判断,然后遍历函数所在的作用域进行判断,函数名相同的进行替换。

首先我们需要获取所需的数据:

const visitor={
	VariableDeclarator(path) {
		let {id,init} = path.node;
		if (!t.isFunction(init)) return;
		if (init.params.length !== 2) return;
		let name = id.name;
		let [p1,p2] = init.params.map(e=>e.name);
		// 判断函数体长度是否为1
		const body = init.body;
		if (!body.body || body.body.length !== 1) return;
		let return_body = body.body[0];
		let  = return_body.argument;
		if(!t.isReturnStatement(return_body) || !t.isBinary()) return;
		let {left,right,operator} = ;
		if(!t.isIdentifier(left,{"name":p1}) || !t.isIdentifier(right,{"name":p2})) return;
		console.log(name,p1,p2,operator);
	},
}

运行结果:

Xor p q ^

然后我们需要遍历所有的Call并判断,符合条件的使用上面获取到的数据进行替换。

为了实现这一目标,我们可以通过作用率重新获取节点并进行遍历。

获取函数申明所在的作用域以及作用域对应的块节点:

path.scope.block;

可以使用块节点在内部继续遍历:

traverse(scope.block,{"Call":function(_path) {
    let {callee,arguments} = _path.node;
    if (arguments.length !== 2) return;
    args=arguments.map(e=>e.value);
    console.log(callee.name,args);
    console.log(name,p1,p2,operator);
}});

也可以这样写:

let scope=path.scope;
scope.traverse(scope.block,{"Call":function(_path) {
    let {callee,arguments} = _path.node;
    if (arguments.length !== 2) return;
    args=arguments.map(e=>e.value);
    console.log(callee.name,args);
    console.log(name,p1,p2,operator);
},});

结果:

Xor [ 111, 222 ]
Xor p q ^

可以看到所有需要的数据都能成功获取,最终完整的访问者代码为:

var names=new Set();
const visitor={
	VariableDeclarator(path) {
		let {id,init} = path.node;
		if (!t.isFunction(init)) return;
		if (init.params.length !== 2) return;
		let name = id.name;
		let [p1,p2] = init.params.map(e=>e.name);
		// 判断函数体长度是否为1
		const body = init.body;
		if (!body.body || body.body.length !== 1) return;
		let return_body = body.body[0];
		let  = return_body.argument;
		if(!t.isReturnStatement(return_body) || !t.isBinary()) return;
		let {left,right,operator} = ;
		if(!t.isIdentifier(left,{"name":p1}) || !t.isIdentifier(right,{"name":p2})) return;
		traverse(path.scope.block,{"Call":function(_path) {
			let {callee,arguments} = _path.node;
			if(arguments.length !== 2 || !t.isIdentifier(callee,{"name":name})) return;
			_path.replaceWith(t.Binary(operator, arguments[0], arguments[1]));
		},});
		names.add(name);
	},
}
traverse(ast, visitor);
const visitor2={
	VariableDeclarator(path) {
		if(names.has(path.node.id.name)) path.remove();
	},
}
traverse(ast, visitor2);

最终顺利还原得到:

let a = 111 ^ 222;

删除冗余逻辑代码

有些混淆框架生成的代码会嵌套很多if-else 语句,存在大量必定判断为假的冗余逻辑代码。我们可以删除这些冗余代码,只留下判断为真的代码。

示例:

let a;
if ("jZPVk" == "boYNa") {
	a = 1;
} else {
	if ("esUCW" !== "YVaOc") {
		a = 2;
	} else {
		a = 3;
	}
}

察 AST,判断条件对应的是 test 节点,if 对应的是 consequent 节点,else 对应的是 alternate 节点:

image-20230130173544020

思路:遍历所有的if表达式,取出test子节点判断是否能直接计算出值。为true则使用consequent子节点替换当前节点,否则使用alternate节点替换当前if表达式。有些if语句没有alternate节点,如果确定条件为假则可以将整个节点删除。

访问者代码为:

const visitor = {
	"IfStatement|Conditional"(path) {
		var {consequent,alternate} = path.node;
		if(t.isBlockStatement(consequent))
			consequent=consequent.body
		if(t.isBlockStatement(alternate))
			alternate=alternate.body
		let {confident,value}=path.get('test').evaluate();
		// 跳过无法确定能计算出结果的表达式
		if(!confident) return;
		if(value)
			path.replaceWithMultiple(consequent);
		else if(alternate!=null)
			path.replaceWithMultiple(alternate);
		else
			path.remove();
    },
}

运行后最终结果:

let a;
a = 2;

ob混淆会遗留类似下面的代码:

if ("jZPVk" !== "boYNa") {
    _0x46f96b=!![];
    var _0x115fe4 = _0x46f96b ? function() {
        var _0x42130b = {"mLuUC": "2|1|5|0|4|3"};
        if ("esUCW" !== "YVaOc") {
            if (_0x64f451) {
                if ("VPudA" !== "PlTuN") {
                    var _0x2a304c = _0x64f451["apply"](_0x40f1bc, arguments);
                    _0x64f451 = null;
                    return _0x2a304c;
                } else {
                    function _0x2d452d() {
                        var _0x3f7283 = "2|1|5|0|4|3"["split"]('|')
                          , _0x4c2460 = 0;
                        var _0x17e744 = _0x5d33d5["constructor"]["prototype"]["bind"](_0x476920);
                        var _0x219476 = _0x115ed3[_0x53f8ef];
                        var _0x268b00 = _0x1dbdcc[_0x219476] || _0x17e744;
                        _0x17e744["__proto__"] = _0x559202["bind"](_0x4e83d7);
                        _0x17e744["toString"] = _0x268b00["toString"]["bind"](_0x268b00);
                        _0x15fb48[_0x219476] = _0x17e744;
                    }
                }
            }
        } else {
            function _0x2b55e5() {
                sloYzO["PgbPP"](_0xb1234d, 0);
            }
        }
    }
    : function() {}
} else {
    function _0x4e016() {
        sloYzO["RgqlK"](_0x6358d1);
    }
}

还原后:

_0x46f96b = !![];
var _0x115fe4 = _0x46f96b ? function () {
  var _0x42130b = {
    "mLuUC": "2|1|5|0|4|3"
  };
  if (_0x64f451) {
    var _0x2a304c = _0x64f451["apply"](_0x40f1bc, arguments);
    _0x64f451 = null;
    return _0x2a304c;
  }
} : function () {};

for循环混淆字符串申明还原

有些混淆框架会将一段简单的字符串申明:

var a = "hello,AST!";

混淆成如下形式:

for (var e = "\u0270\u026D\u0274\u0274\u0277\u0234\u0249\u025B\u025C\u0229", a = "", s = 0; s < e.length; s++) {
	var r = e.charCodeAt(s) - 520;
	a += String.fromCharCode(r);
}

代码格式固定,for循环 + String.fromCharCode 完成代码的还原。

AST解析结构:

image-20230118221056693

是否符合上述结构的判断标准为ForStatement节点下面的body必定是BlockStatement,BlockStatement下面的body有两个子节点,这两个子节点的源码分别包含charCodeAtfromCharCode。基于此编写访问者:

const visitor={
	ForStatement(path) {
		let body = path.get("body.body");
		if (!body || body.length !== 2) return;
		if (!t.isVariableDeclaration(body[0]) || !t.isStatement(body[1])) return;

		let body0_code = body[0].toString();
		let body1_code = body[1].toString();
		if (body0_code.indexOf("charCodeAt") == -1 || body1_code.indexOf("String.fromCharCode") == -1) return;
		
	},
}

经过以上代码可以找出具备目标特征的for循环语句。

下面我们需要想办法执行for循环得到结果,然后构造VariableDeclaration 节点进行替换。

当然,我们需要先获取目标变量名:

let name = body[1].node..left.name;

为了避免eval导致变量污染,使用Function构造函数并执行代码:

let code=path.toString() + "\nreturn " + name;
let value = new Function("",code)();

最后构造节点并替换:

let new_node = t.VariableDeclaration("var", [t.VariableDeclarator(t.Identifier(name), t.valueToNode(value))]);
path.replaceWith(new_node);

完整的访问者代码:

const visitor={
	ForStatement(path) {
		let body = path.get("body.body");
		if (!body || body.length !== 2) return;
		if (!t.isVariableDeclaration(body[0]) || !t.isStatement(body[1])) return;

		let body0_code = body[0].toString();
		let body1_code = body[1].toString();
		if (body0_code.indexOf("charCodeAt") == -1 || body1_code.indexOf("String.fromCharCode") == -1) return;
		let name = body[1].node..left.name;
		let code=path.toString() + "\nreturn " + name;
		let value = new Function("",code)();
		let new_node = t.VariableDeclaration("var", [t.VariableDeclarator(t.Identifier(name), t.valueToNode(value))]);
        path.replaceWith(new_node);
	},
}

执行后,代码已经顺利还原。

自执行函数实参还原与替换

还原前:

(function(a,b,t,c,d) {
	console.log("abcdefg"[c]);
	console.log(a[0]+a[1]);
	console.log(b[0]-b[1]);
	console.log(c);
	console.log(d);
	t = 123;
})([1,2],[5,3],5,6,-5);

我们需要将在函数体内没有被修改的参数进行替换。

最终访问者代码为:

const visitor = {
	Call(path) {
		let callee = path.get('callee');
		let arguments=path.get("arguments")
		// 确定是一个有参数的自执行函数,(被调用的是一个匿名函数没有id属性,代码块定义)
		if (!callee.isFunction() || arguments.length==0) return;
		let {id,body} = callee.node;
		if (id != null || !t.isBlockStatement(body)) return;
		let params = callee.get('params');
		// 获取匿名函数代码块的作用域
		body_scope=path.get("callee.body").scope
		for(let i=0;i<params.length;i++){
			let paramPath=params[i];
			let argumentPath=arguments[i];
			let binding = body_scope.getBinding(paramPath.node.name);
			// 跳过有修改的节点
			if(!binding || !binding.constant) continue;
			for(let referPath of binding.referencePaths){
				let {node,parent,parentPath} = referPath;
				if(t.isMember(parent,{"object":node})){
					parentPath.replaceWith(argumentPath.node.elements[parent.property.value]);
				}else{
					referPath.replaceWith(argumentPath.node);
					const {confident,value} = parentPath.evaluate();
					confident && parentPath.replaceWith(t.valueToNode(value));
				}
			}
			paramPath.remove();
			argumentPath.remove();
		}
	}
}

还原结果为:

(function (t) {
  console.log("g");
  console.log(1 + 2);
  console.log(5 - 3);
  console.log(6);
  console.log(-5);
  t = 123;
})(5);

去控制流平坦化入门:while-switch

常见的 switch-case 基本都在10个分支以内,示例代码:

var _0x42b38e = "5|4|3|1|2|0"["split"]('|'), _0x435210 = 0;
while (!![]) {
  switch (_0x42b38e[_0x435210++]) {
  case '0':
    _0x352bac[_0x4447b2] = _0x38b230;
    continue;
  case '1':
    _0x38b230["__proto__"] = _0x529196["bind"](_0x529196);
    continue;
  case '2':
    _0x38b230["toString"] = _0x1bd819["toString"]["bind"](_0x1bd819);
    continue;
  case '3':
    var _0x1bd819 = _0x352bac[_0x4447b2] || _0x38b230;
    continue;
  case '4':
    var _0x4447b2 = _0x124cae[_0x31cdb9];
    continue;
  case '5':
    var _0x38b230 = _0x529196["constructor"]['prototype']["bind"](_0x529196);
    continue;
 }
  break;
}

这类代码结构固定,case语句中无更改索引值的代码,因此,取出来的代码顺序是固定的。

AST 还原思路:获取控制流原始数组遍历,取出每个值对应的case节点,存储其中的consequent 数组,最终将所有取到的数组整体替换整个while节点。要获取控制流数组,可以取switch传入的变量名,然后获取其绑定对象,从而得到数组的源码并计算出结果。

最终访问者代码:

const visitor = {
	WhileStatement(path) {
		// 获取下面的switch节点
		let switchNode = path.node.body.body[0];
		// 获取Switch判断条件上的 控制的数组名 和 自增变量名
		let arrayName = switchNode.discriminant.object.name;
		let increName = switchNode.discriminant.property.argument.name;
		// 获取控制流数组和自增变量的绑定对象
		let bindingArray = path.scope.getBinding(arrayName);
        let bindingAutoIncrement = path.scope.getBinding(increName);
		// 计算出对应的顺序数组
		let array=eval(bindingArray.path.get("init").toString());
		let replace = array.flatMap(i=>{
			let consequent = switchNode.cases[i].consequent;
			// 删除末尾的continue节点
			if(t.isContinueStatement(consequent[consequent.length-1])) consequent.pop();
			return consequent
		});
		path.replaceWithMultiple(replace);
		// 删除控制数组和对应的自增变量
		bindingArray.path.remove();
		bindingAutoIncrement.path.remove();
	}
}

还原结果为:

var _0x38b230 = _0x529196["constructor"]['prototype']["bind"](_0x529196);
var _0x4447b2 = _0x124cae[_0x31cdb9];
var _0x1bd819 = _0x352bac[_0x4447b2] || _0x38b230;
_0x38b230["__proto__"] = _0x529196["bind"](_0x529196);
_0x38b230["toString"] = _0x1bd819["toString"]["bind"](_0x1bd819);
_0x352bac[_0x4447b2] = _0x38b230;

OB混淆代码还原

JavaScript在线混淆网站:https:///

在网站给出的默认代码:基础上加点中文

// Paste your JavaScript code here
function hi() {
  console.log("Hello World!你好");
}
hi();

生成ob混淆代码:

(function(_0x1b3572,_0x6ac8bd){var _0x57046c=_0x56d1,_0x35b1de=_0x1b3572();while(!![]){try{var _0x16cc81=-parseInt(_0x57046c(0x143))/0x1*(parseInt(_0x57046c(0x146))/0x2)+parseInt(_0x57046c(0x13e))/0x3+-parseInt(_0x57046c(0x145))/0x4+parseInt(_0x57046c(0x13f))/0x5+parseInt(_0x57046c(0x13d))/0x6+parseInt(_0x57046c(0x140))/0x7+-parseInt(_0x57046c(0x142))/0x8*(parseInt(_0x57046c(0x13c))/0x9);if(_0x16cc81===_0x6ac8bd)break;else _0x35b1de['push'](_0x35b1de['shift']());}catch(_0x5bbb0d){_0x35b1de['push'](_0x35b1de['shift']());}}}(_0x54f4,0xbf1b5));function hi(){var _0x8926e4=_0x56d1;console[_0x8926e4(0x144)](_0x8926e4(0x141));}function _0x56d1(_0x5d99fb,_0x15a588){var _0x54f461=_0x54f4();return _0x56d1=function(_0x56d18e,_0x2f9121){_0x56d18e=_0x56d18e-0x13c;var _0x205aad=_0x54f461[_0x56d18e];return _0x205aad;},_0x56d1(_0x5d99fb,_0x15a588);}function _0x54f4(){var _0x4f2cf1=['\x37\x33\x34\x70\x4e\x6b\x65\x66\x55','\x6c\x6f\x67','\x36\x32\x31\x31\x34\x38\x34\x49\x69\x43\x6a\x74\x49','\x33\x30\x39\x34\x48\x56\x4c\x69\x62\x74','\x31\x33\x34\x31\x39\x32\x37\x6c\x71\x66\x53\x75\x75','\x38\x34\x36\x35\x34\x36\x30\x41\x66\x51\x6d\x76\x4b','\x35\x34\x38\x38\x36\x32\x4c\x6a\x45\x47\x73\x69','\x36\x39\x35\x33\x30\x37\x30\x52\x7a\x41\x51\x66\x77','\x31\x30\x37\x31\x32\x36\x39\x35\x73\x78\x6a\x4c\x62\x57','\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64\x21\u4f60\u597d','\x35\x36\x47\x67\x58\x63\x4b\x45'];_0x54f4=function(){return _0x4f2cf1;};return _0x54f4();}hi();

ob混淆的核心处理代码为:

function pas_ob_encfunc(ast){
    // 找到关键的函数
    var obfuncstr = []
    var obdecname;
    var obsortname;
    function findobsortfunc(path){
        if (!path.getFunctionParent()){
            function get_obsort(path){
                obsortname = path.node.arguments[0].name
                obfuncstr.push('!'+generator(path.node, {minified:true}).code)
                path.stop()
                path.remove()
            }
            path.traverse({Call: get_obsort})
            path.stop()
        }
    }
    function findobsortlist(path){
        if (path.node.id.name == obsortname){
            obfuncstr.push(generator(path.node, {minified:true}).code)
            path.stop()
            path.remove()
        }
    }
    function findobfunc(path){
        var t = path.node.body.body[0]
        if (t && t.type === 'VariableDeclaration'){
            var g = t.declarations[0].init
            if (g && g.type == 'Call' && g.callee.name == obsortname){
                obdecname = path.node.id.name
                obfuncstr.push(generator(path.node, {minified:true}).code)
                path.stop()
                path.remove()
            }
        }
    }
    traverse(ast, {Statement: findobsortfunc})
    traverse(ast, {FunctionDeclaration: findobsortlist})
    traverse(ast, {FunctionDeclaration: findobfunc})
    eval(obfuncstr.join(';'));

    // 收集必要的函数进行批量还原
    var collects = []
    var collect_names = [obdecname]
    var collect_removes = []
    function judge(path){
        return path.node.body.body.length == 1
            && path.node.body.body[0].type == 'ReturnStatement'
            && path.node.body.body[0].argument.type == 'Call'
            && path.node.body.body[0].argument.callee.type == 'Identifier'
            // && path.node.params.length == 5
            && path.node.id
    }
    function collect_alldecfunc(path){
        if (judge(path)){
            var t = generator(path.node, {minified:true}).code
            if (collects.indexOf(t) == -1){
                collects.push(t)
                collect_names.push(path.node.id.name)
            }
        }
    }
    var collect_removes_var = []
    function collect_alldecvars(path){
        var left = path.node.id
        var right = path.node.init
        if (right && right.type == 'Identifier' && collect_names.indexOf(right.name) != -1){
            var t = 'var ' + generator(path.node, {minified:true}).code
            if (collects.indexOf(t) == -1){
                collects.push(t)
                collect_names.push(left.name)
            }
        }
    }
    traverse(ast, {FunctionDeclaration: collect_alldecfunc})
    traverse(ast, {VariableDeclarator: collect_alldecvars})
    eval(collects.join(';'));
    function parse_values(path){
        var name = path.node.callee.name
        if (path.node.callee && collect_names.indexOf(path.node.callee.name) != -1){
            try{
                path.replaceWith(t.StringLiteral(eval(path+'')))
                collect_removes.push(name)
            }catch(e){}
        }
    }
    traverse(ast, {Call: parse_values})
    function collect_removefunc(path){
        if (judge(path) && collect_removes.indexOf(path.node.id.name) != -1)
            path.remove()
    }
    function collect_removevars(path){
        var left = path.node.id
        var right = path.node.init
        if (right && right.type == 'Identifier' && collect_names.indexOf(right.name) != -1)
            path.remove()
    }
    traverse(ast, {FunctionDeclaration: collect_removefunc})
    traverse(ast, {VariableDeclarator: collect_removevars})
}
pas_ob_encfunc(ast);

还原后:

function hi() {
  console["log"]("Hello World!你好");
}
hi();

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约