首页 > 其他分享 >基于AST的babel库实现js反混淆还原基础案例荟萃

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

时间:2023-02-09 22:36:03浏览次数:71  
标签:node return AST babel js let var path 节点


基本概念

AST简介

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

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

Babel运行在nodejs上,还没有安装nodejs的,可以到​​https://nodejs.org/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://astexplorer.net/​

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

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

例如对于:

var a=1;

可以看到解析结果为:

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

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"​​​,或 ​​"BinaryExpression"​​)。 每一种类型的节点定义了一些附加属性用来进一步描述该节点类型。

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

常见节点信息:

节点属性

记录的信息

type

当前节点的类型

start

当前节点的起始位

end

当前节点的末尾

loc

当前节点所在的行列位置 起始于结束的行列信息

errors

File节点所持有的特有属性

program

包含整个源代码,不包含注释节点

comments

源代码中所有的注释会显示在这里

Babel涉及的文档

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

中文官方文档:​​https://www.babeljs.cn/docs/​

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

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

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

AST可视化:

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 节点一样它们也有 ​​start​​​,​​end​​​,​​loc​​ 属性。

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

program: Program  {
type: "Program"
sourceType: "module"
body: [
ExpressionStatement {
type: "ExpressionStatement"
expression: BinaryExpression {
type: "BinaryExpression"
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 文件夹。

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

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

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

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

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

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

基于AST的babel库实现js反混淆还原基础案例荟萃_作用域_04

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

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
}

即:

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

任何通过右键启动ast hook:

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

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

基于AST的babel库实现js反混淆还原基础案例荟萃_开发语言_07

Babel基本知识

path和node

使用​​AST Explorer​​ 查看:

var a = 123;
var b;

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

基于AST的babel库实现js反混淆还原基础案例荟萃_开发语言_08

如果点击一下等号:

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

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

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

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

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

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

VariableDeclaration 和 VariableDeclarator 有什么区别?

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

var a = 123,b = 456;

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

说明,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);
},
}

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

可以看到与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.memberExpression(t.identifier('console'), t.identifier('log')),
args = [t.NumericLiteral(777)],
call_exp = t.callExpression(callee, args),
exp_statement = t.ExpressionStatement(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;

基于AST的babel库实现js反混淆还原基础案例荟萃_开发语言_13

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

(1 + 2) + 3;

再尝试解析如下代码:

1 + 2 * 3;

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

同样它等价于:

1 + (2 * 3);

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

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

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

基于AST的babel库实现js反混淆还原基础案例荟萃_作用域_15

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

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

如果指定括号优先级:

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

则解析为:

基于AST的babel库实现js反混淆还原基础案例荟萃_开发语言_16

AST 节点类型对照表

常用部分:

类型原名称

中文名称

描述

Program

程序主体

整段代码的主体

VariableDeclaration

变量声明

声明一个变量,例如 var let const

FunctionDeclaration

函数声明

声明一个函数,例如 function

ExpressionStatement

表达式语句

通常是调用一个函数,例如 console.log()

BlockStatement

块语句

包裹在 {} 块内的代码,例如 if (condition){var a = 1;}

BreakStatement

中断语句

通常指 break

ContinueStatement

持续语句

通常指 continue

ReturnStatement

返回语句

通常指 return

SwitchStatement

Switch 语句

通常指 Switch Case 语句中的 Switch

SwitchCase

Case 语句

通常指 Switch 语句中的 Case

IfStatement

If 控制流语句

控制流语句,通常指 if(condition){}else{}

Identifier

标识符

标识,例如声明变量时 var identi = 5 中的 identi

CallExpression

调用表达式

通常指调用一个函数,例如 console.log()

BinaryExpression

二进制表达式

通常指运算,例如 1+2

MemberExpression

成员表达式

通常指调用对象的成员,例如 console 对象的 log 成员

ArrayExpression

数组表达式

通常指一个数组,例如 [1, 3, 5]

NewExpression

New 表达式

通常指使用 New 关键词

AssignmentExpression

赋值表达式

通常指将函数的返回值赋值给变量

UpdateExpression

更新表达式

通常指更新成员值,例如 i++

Literal

字面量

字面量

BooleanLiteral

布尔型字面量

布尔值,例如 true false

NumericLiteral

数字型字面量

数字,例如 100

StringLiteral

字符型字面量

字符串,例如 vansenb

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

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

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

代码生成选项

常用选项:

参数

描述

auxiliaryCommentBefore

在输出文件内容的头部添加注释块文字

auxiliaryCommentAfter

在输出文件内容的末尾添加注释块文字

comments

输出内容是否包含注释

compact

输出内容是否不添加空格,避免格式化

concise

输出内容是否减少空格使其更紧凑一些

minified

是否压缩输出代码

retainLines

尝试在输出代码中使用与源代码中相同的行号

更多选项可查看:​​https://www.babeljs.cn/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安装教程

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

node-inspect demo.js

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

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

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

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

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

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子节点即可:

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

最终访问者的内容为:

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的解析结果为:

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

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

访问者代码为:

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

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

定义在一行的变量分离

还原前:

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

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

观察ast节点:

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

我们需要将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:

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

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

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 = {
MemberExpression(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;

解析结果:

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

最终我们需要转换为:

b["length"];

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

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

最终访问者代码为:

const visitor={
MemberExpression({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,
};

基于AST的babel库实现js反混淆还原基础案例荟萃_作用域_26

从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解析结果可以看到都是MemberExpression节点:

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

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

let arrName = "a";
var a = [1,2,3,[1213,234],{"code":"777"},"window"];
const visitor = {
MemberExpression(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.isArrayExpression(init)) return;

const binding = scope.getBinding(id.name);
if (!binding || !binding.constant) return;
for(referPath of binding.referencePaths){
let {node,parent} = referPath;
// 检查是否存在对数组的非下标取数据操作,存在则取消对该数组的任何替换
if (!t.isMemberExpression(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解析上述代码:

基于AST的babel库实现js反混淆还原基础案例荟萃_开发语言_28

可以看到有值的根节点有UnaryExpression、BinaryExpression、ConditionalExpression和CallExpression三种类型。

最终代码:

const visitor={
"UnaryExpression|BinaryExpression|ConditionalExpression|CallExpression"(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={
BinaryExpression(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 类型的表达式,不再是 BinaryExpression 类型。

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

const visitor={
"BinaryExpression": {
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解析结果为:

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

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

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

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

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

例如代码为:

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

访问者最终代码为:

const visitor={
UnaryExpression(path) {
let {operator,argument} = path.node;
if(operator!="!" || !t.isExpressionStatement(path.parentPath.node)) return;
path.replaceWith(argument);
},
CallExpression(path) {
let {callee,arguments} = path.node;
if (arguments.length !=0 || !t.isFunctionExpression(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解析的情况洗啊,根据前一节可知,函数调用都是CallExpression节点,由此编写访问者:

const visitor={
CallExpression(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={
CallExpression(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={
CallExpression(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={
CallExpression(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;

还原简单的CallExpression 类型

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

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.isFunctionExpression(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 expression = return_body.argument;
if(!t.isReturnStatement(return_body) || !t.isBinaryExpression(expression)) return;
let {left,right,operator} = expression;
if(!t.isIdentifier(left,{"name":p1}) || !t.isIdentifier(right,{"name":p2})) return;
console.log(name,p1,p2,operator);
},
}

运行结果:

Xor p q ^

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

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

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

path.scope.block;

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

traverse(scope.block,{"CallExpression":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,{"CallExpression":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.isFunctionExpression(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 expression = return_body.argument;
if(!t.isReturnStatement(return_body) || !t.isBinaryExpression(expression)) return;
let {left,right,operator} = expression;
if(!t.isIdentifier(left,{"name":p1}) || !t.isIdentifier(right,{"name":p2})) return;
traverse(path.scope.block,{"CallExpression":function(_path) {
let {callee,arguments} = _path.node;
if(arguments.length !== 2 || !t.isIdentifier(callee,{"name":name})) return;
_path.replaceWith(t.BinaryExpression(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​​ 节点:

基于AST的babel库实现js反混淆还原基础案例荟萃_作用域_30

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

访问者代码为:

const visitor = {
"IfStatement|ConditionalExpression"(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解析结构:

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

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

const visitor={
ForStatement(path) {
let body = path.get("body.body");
if (!body || body.length !== 2) return;
if (!t.isVariableDeclaration(body[0]) || !t.isExpressionStatement(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.expression.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.isExpressionStatement(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.expression.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 = {
CallExpression(path) {
let callee = path.get('callee');
let arguments=path.get("arguments")
// 确定是一个有参数的自执行函数,(被调用的是一个匿名函数没有id属性,代码块定义)
if (!callee.isFunctionExpression() || 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.isMemberExpression(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://obfuscator.io/​

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

// 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({CallExpression: 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 == 'CallExpression' && g.callee.name == obsortname){
obdecname = path.node.id.name
obfuncstr.push(generator(path.node, {minified:true}).code)
path.stop()
path.remove()
}
}
}
traverse(ast, {ExpressionStatement: 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 == 'CallExpression'
&& 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, {CallExpression: 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();


标签:node,return,AST,babel,js,let,var,path,节点
From: https://blog.51cto.com/u_11866025/6047324

相关文章

  • threejs_单例模式_项目结构_tansform控制器_css2dlabel_事件派发EventDispacher_事件
    /Users/song/Code/threejs_learn_vanilla_class_singleton/threejs_learn_vanilla_ts_class_singleton/src/main.tsimport"./style.css";importBasefrom"./threejs/......
  • js数据类型转换代码
    <!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metahttp-equiv="X-UA-Compatible"content="IE=edge"><metaname="viewport"content="width......
  • SequelizeJS 中的慢关联
    我正在尝试诊断使用SequlizeJS作为ORM的Express应用程序出现一些速度下降的原因。我有一个与其他2个模型有2xhasMany和hasOne关系的模型:更新:我已经使用cla......
  • 响应 json 的全局时间格式
    没有配置json的全局时间格式server:port:9090spring:profiles:#环境设置active:devdatasource:driver-class-name:com.mysql.cj.jdbc.Driv......
  • three.js教程7-PBR材质与环境贴图
    1、PBR材质PBR是基于物理的渲染(physically-basedrendering)。模拟物体表面的反射算法。Three.js提供了两个PBR材质相关的类MeshStandardMaterial和MeshPhysicalMateria......
  • three.js教程8-渲染器和前端UI界面
    1、html的UI交互界面与Canvas画布叠加需求:把threejsCavnas画布和HTML元素叠加布局,在canvas上添加按钮,通过按钮点击修改canvas场景。//canvas画布绝对定位renderer.do......
  • three.js教程6-加载外部三维模型gltf
    1、建模软件   3D美术常用的三维建模软件,比如Blender、3damx、C4D、maya等等Blender(轻量、免费、开源)3damxC4Dmaya   机械相关:SW、UG等   建筑......
  • JS混淆解密案例③
    今天收到一个特别大的js解密...部分代码如下functionQ0ooO(Q0oqq,Q0oqO){varoooQ0o=O0Q000o;try{if(Q0oqO&&(oooQ0o(0x293,'Qoqq')==typeofQ......
  • three.js教程5-几何体顶点UV坐标、纹理贴图
    1、纹理贴图纹理贴图,是给MeshLambertMaterial等材质一些纹理图片,以达到更好的视觉效果。使用方法:通过纹理贴图加载器TextureLoader的load()方法加载一张图片可以返回一个......
  • js函数作用域和作用域链
    定义:作用域就是限制某个变量只能在某个区域内有效。全局变量拥有全局作用域,而局部变量拥有局部作用域。在js中,作用域一共分为三类:全局作用域、局部(函数)作用域、块级作用域......