基本概念
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 主要包含以下几个功能包:
-
@babel/core
:Babel 编译器本身,提供了 babel 的编译 API; -
@babel/parser
:将 JavaScript 代码解析成 AST 语法树; -
@babel/traverse
:遍历、修改 AST 语法树的各个节点; -
@babel/generator
:将 AST 还原成 JavaScript 代码; -
@babel/types
:判断、验证节点的类型、构建新 AST 节点等。
AST Explorer 直观的认识 AST 节点。网址:https://astexplorer.net/
该网站支持多种解析为AST库,我们选择**@babel/parser**,保持一致:
例如对于:
var a=1;
可以看到解析结果为:
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 文件夹。
安装后,点击打开配置页面,即可看到如下界面:
使用 修改返回值-》动态修改被调试页面的所有js代码 的功能可以动态替换js的代码。
使用 AST混淆解密-》打开本地ast页面 可以使用本地的ast解析功能。
使用示例,访问https://match.yuanrenxue.com/match/2
打开开发者工具,清空cookie后刷新页面可以看到代码为:
下面我们基于默认代码基础上填写如下代码:
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 hook:
然后清空cookie后,重新刷新页面(可能需要先重启一下开发者工具),可以看到代码已经被替换:
Babel基本知识
path和node
使用AST Explorer 查看:
var a = 123;
var b;
默认情况下我们点击一下var整个变量节点被标黄。
如果点击一下等号:
点击"123"也能高亮对应的位置:
而鼠标移动到上述任意节点区域内,代码对应位置也会高亮。
遍历的时候可以这样编写插件:
const visitor = {
VariableDeclaration(path)
{
//to do something;
},
}
VariableDeclaration 和 VariableDeclarator 有什么区别?
可以看到,VariableDeclaration 是 VariableDeclarator 的父节点。针对如下代码,再进行解析:
var a = 123,b = 456;
说明,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节点的内容。
获取节点对应的源码:
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
常用的属性和方法:
- scope.block
表示当前作用域下的所有node - scope.dump()
输出当前每个变量的作用域信息。调用后直接打印,不需要加打印函数 - scope.crawl()
重构scope,在某种情况下会报错,不过还是建议在每一个插件的最后一行加上。 - scope.rename(oldName, newName, block)
修改当前作用域下的的指定的变量名,oldname、newname表示替换前后的变量名,为字符串。注意,oldName需要有binding,否则无法重命名。 - scope.traverse(node, opts, state)
遍历当前作用域下的某个节点和全局的traverse用法一样。 - 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
中查看到它的定义。
关键属性有:
-
identifier
:标识符的 Node 对象; -
scope
:所在作用域 -
path
:用于定位初始拥有binding的path; -
kind
:变量类型,param参数、 hoisted提升、var变量、local内部 -
constantViolations
:如果标识符被修改,则会存放所有修改该标识符节点的 Path 对象; -
constant
:标识符是否为常量; -
referenced
:标识符是否被引用; -
referencePaths
:如果标识符被引用,则会存放所有引用该标识符节点的 Path 对象。 -
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;
可以看到整体被解析成一个BinaryExpression,前1+2又被解析成一个BinaryExpression,所以这个表达式等价于:
(1 + 2) + 3;
再尝试解析如下代码:
1 + 2 * 3;
同样它等价于:
1 + (2 * 3);
下面我们看看下面这个例子:
zc(s === Sc || s === Ac ? 192 : 204);
“||” 与 “?” 的优先级到底哪个高呢?我们看看ast的解析结果:
很明显的可以看到 “||” 的优先级高于 “?” ,等价于:
zc((s === Sc || s === Ac) ? 192 : 204);
如果指定括号优先级:
zc(s === Sc || (s === Ac ? 192 : 204));
则解析为:
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搜索:
代码生成选项
常用选项:
参数 | 描述 |
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,出现上述代码,下面我们给访问者内第一行代码打上断点:
恢复运行后,可以查看path在内存中的内容:
也可以在控制台输入目标变量查看对应内容:
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子节点即可:
最终访问者的内容为:
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的解析结果为:
generator生成代码默认是去掉空行的,我们只需要直接删除EmptyStatement节点即可。
访问者代码为:
const visitor={
EmptyStatement(path) {
path.remove();
}
}
运行后顺利删除了空行和空语句。
定义在一行的变量分离
还原前:
var a = 123,b = 456;
for(let c = 789,d = 120;false;);
现在需要将其还原为每行仅定义一个变量。
观察ast节点:
我们需要将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:
使用上一节的模板,将上述数组定义复制到模板中,并编写访问者:
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;
解析结果:
最终我们需要转换为:
b["length"];
可以看到区别在于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解析结果可以看到,将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节点:
将数组定义复制粘贴到访问者代码之上,最终代码为:
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解析上述代码:
可以看到有值的根节点有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解析结果为:
根据解析结果我们需要从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));
},
}
注意点:
- eval内的代码执行后返回值为1,替换会导致生成的代码无法执行原始逻辑,所以取消执行
- 从global中取出的全局变量是function类型时,表示是全局函数。
- 计算出的结果为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
节点:
思路:遍历所有的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解析结构:
是否符合上述结构的判断标准为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();