基本概念
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):
interfaceNode{
type:string;
loc: SourceLocation |null;}
字符串形式的
type
字段表示节点的类型(如:
"FunctionDeclaration"
,
"Identifier"
,或
"BinaryExpression"
)。 每一种类型的节点定义了一些附加属性用来进一步描述该节点类型。
Babel 还为每个节点额外生成了
start
,
end
,
loc
属性用于描述该节点在原始代码中的位置。
常见节点信息:
节点属性记录的信息type当前节点的类型start当前节点的起始位end当前节点的末尾loc当前节点所在的行列位置 起始于结束的行列信息errorsFile节点所持有的特有属性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 模块中。通过以下命令安装:
npminstall --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后刷新页面可以看到代码为:
下面我们基于默认代码基础上填写如下代码:
functionfetch_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 constFunctionDeclaration函数声明声明一个函数,例如 functionExpressionStatement表达式语句通常是调用一个函数,例如 console.log()BlockStatement块语句包裹在 {} 块内的代码,例如 if (condition){var a = 1;}BreakStatement中断语句通常指 breakContinueStatement持续语句通常指 continueReturnStatement返回语句通常指 returnSwitchStatementSwitch 语句通常指 Switch Case 语句中的 SwitchSwitchCaseCase 语句通常指 Switch 语句中的 CaseIfStatementIf 控制流语句控制流语句,通常指 if(condition){}else{}Identifier标识符标识,例如声明变量时 var identi = 5 中的 identiCallExpression调用表达式通常指调用一个函数,例如 console.log()BinaryExpression二进制表达式通常指运算,例如 1+2MemberExpression成员表达式通常指调用对象的成员,例如 console 对象的 log 成员ArrayExpression数组表达式通常指一个数组,例如 [1, 3, 5]NewExpressionNew 表达式通常指使用 New 关键词AssignmentExpression赋值表达式通常指将函数的返回值赋值给变量UpdateExpression更新表达式通常指更新成员值,例如 i++Literal字面量字面量BooleanLiteral布尔型字面量布尔值,例如 true falseNumericLiteral数字型字面量数字,例如 100StringLiteral字符型字面量字符串,例如 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;// 遍历ASTconst 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,出现上述代码,下面我们给访问者内第一行代码打上断点:
恢复运行后,可以查看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返回的对象有三个部分:
functionevaluate(){const state ={confident:true,deoptPath:null,seen:newMap()};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));},}
打印结果:
!![]truetrue'Hello '+'world'+'!'true Hello world!2+3*50true152
Math.abs(-200)%19true10true?123:456true123
Math.ceil(2.8)true3
Math.ceil(a)true1'a'+'b'+'c'+ error +'e'+'f'falseundefined'a'+'b'+'c'+ error +'e'falseundefined'a'+'b'+'c'+ error
falseundefined'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");
假如自定义函数也需要替换值呢?
对于一个简单的自定义函数:
varXor=function(p,q){return p ^ q;}let a =Xor(111,222);functionxor2(p,q){return p ^ q;}let b =xor2(333,444);
假设js脚本中全部都是简单的函数,我们希望计算出所有调用简单函数的最终值进行替换得到:
let a =177;let b =241;
根据上述脚本只需要简单改造一下,同时也需要将这些简单的函数复制到脚本中:
varXor=function(p,q){return p ^ q;}functionxor2(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));},}
执行后,即可得到结果:
varXor=function(p, q){return p ^ q;};let a =177;functionxor2(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");varXor=function(p,q){return p ^ q;}let a =Xor(111,222);functionxor2(p,q){return p ^ q;}let b =xor2(333,444);
综合解析代码:
varXor=function(p,q){return p ^ q;}functionxor2(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;functionget_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 类型
对于一个简单的函数调用语句:
varXor=function(p,q){return p ^ q;}let a =Xor(111,222);
我们希望提取出函数体中的表达式,将简单的自定义函数调用还原:
varXor=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);// 判断函数体长度是否为1const 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=newSet();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);// 判断函数体长度是否为1const 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);elseif(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 =newFunction("",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 =newFunction("",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 herefunctionhi(){
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));functionhi(){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混淆的核心处理代码为:
functionpas_ob_encfunc(ast){// 找到关键的函数var obfuncstr =[]var obdecname;var obsortname;functionfindobsortfunc(path){if(!path.getFunctionParent()){functionget_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()}}functionfindobsortlist(path){if(path.node.id.name == obsortname){
obfuncstr.push(generator(path.node,{minified:true}).code)
path.stop()
path.remove()}}functionfindobfunc(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 =[]functionjudge(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
}functioncollect_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 =[]functioncollect_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(';'));functionparse_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})functioncollect_removefunc(path){if(judge(path)&& collect_removes.indexOf(path.node.id.name)!=-1)
path.remove()}functioncollect_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);
还原后:
functionhi(){
console["log"]("Hello World!你好");}hi();
版权归原作者 小小明-代码实体 所有, 如有侵权,请联系我们删除。