0


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

基本概念

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**,保持一致:

image-20230114222511244

例如对于:

var a=1;

可以看到解析结果为:

image-20230114221027950

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 文件夹。

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

image-20230131000259411

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

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

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

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

image-20230131005302047

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

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
}

即:

image-20230131005508204

任何通过右键启动ast hook:

image-20230131005542249

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

image-20230131005802879

Babel基本知识

path和node

使用AST Explorer 查看:

var a =123;var b;

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

image-20230117172529680

如果点击一下等号:

image-20230117172635422

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

image-20230117180717397

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

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

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

VariableDeclaration 和 VariableDeclarator 有什么区别?

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

var a =123,b =456;

image-20230117191909622

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

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

path常见的方法

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

let {node,scope} = path;

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

path.toString()

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

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

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

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

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

获取path的上一级路径

let parent = path.parentPath;

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

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

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

path.container

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

path.get('id');

删除path

path.remove();

计算表达式的值:

path.evaluate();

返回一个对象,其中的

confident

属性表示置信度,

value

表示计算结果。

替换path

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

或引入

@babel/types

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

替换方法有一下几种:

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

插入节点

NodePath.insertAfter()

方法用于在当前

path

前面插入节点

NodePath.insertBefore()

方法用于在当前

path

后面插入节点

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

关于node的一些操作

node其实是path的一个属性:

const node = path.node;

比如打印VariableDeclarator节点的内容:

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

image-20230117195116061

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

获取节点对应的源码:

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

删除init节点:

delete path.node.init;

path.node.init =undefined;

创建节点并生成代码

示例:

const t =require("@babel/types");const generator =require("@babel/generator").default;var callee = t.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;

image-20230117200141983

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

(1+2)+3;

再尝试解析如下代码:

1+2*3;

image-20230117210526811

同样它等价于:

1+(2*3);

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

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

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

image-20230117211612240

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

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

如果指定括号优先级:

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

则解析为:

image-20230117225307866

AST 节点类型对照表

常用部分:
类型原名称中文名称描述Program程序主体整段代码的主体VariableDeclaration变量声明声明一个变量,例如 var let 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搜索:

image-20230129184828079

代码生成选项

常用选项:
参数描述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,出现上述代码,下面我们给访问者内第一行代码打上断点:

image-20230114230956378

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

image-20230114231305043

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

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

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

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

还原unicode常量值

下面我们的目标是将:

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

还原成它本来的面目:

var a ="hello,AST";

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

//babel库相关,解析,模板引擎,转换,构建,生产const parser =require("@babel/parser");const template =require("@babel/template").default;const traverse =require("@babel/traverse").default;const t =require("@babel/types");const generator =require("@babel/generator").default;// 操作文件const fs =require("fs");let encode_file ="read.js",decode_file ="decode_result.js";if(process.argv.length >2)
  encode_file = process.argv[2];if(process.argv.length >3)
  decode_file = process.argv[3];var jscode = fs.readFileSync(encode_file,{encoding:"utf-8"});let ast = parser.parse(jscode);var visitor={}traverse(ast, visitor);let{code}=generator(ast,opts ={jsescOption:{"minimal":true}});
fs.writeFile(decode_file, code,(err)=>{});

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

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

image-20230117233113212

最终访问者的内容为:

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

可以将目标代码保存到

read.js

,然后运行上述模板代码,最终得到满足要求的结果文件

decode_result.js

顺利得到目标结果。

删除空行和空语句

代码示例:

var a =123;;var b =456;;;;var c=1789;

ast的解析结果为:

image-20230118212222639

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

访问者代码为:

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

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

定义在一行的变量分离

还原前:

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

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

观察ast节点:

image-20230130193138935

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

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

判断方法为:

t.isBlockStatement(path.parent)

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

path.parentPath.isBlock()

访问者代码为:

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

分离结果:

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

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

数组字面量元素替换

目标代码:

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

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

image-20230117231127999

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

var _ac =["\x67\x65\x74\x41\x74\x74\x72\x69\x62\x75\x74\x65","\x41\x63\x74\x69\x76\x65\x58\x4f\x62\x6a\x65\x63\x74","\x64\x6f\x61\x63\x74","\x4f\x70\x65\x6e\x20\x53\x61\x6e\x73","\x50\x49","\x73\x65\x6e\x64"];const visitor ={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;

解析结果:

image-20230117234033025

最终我们需要转换为:

b["length"];

image-20230117234343942

可以看到区别在于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,};

image-20230117235930831

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

访问者代码为:

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

结果:

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

Array类型元素还原

示例:

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

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

image-20230129191206137

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

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解析上述代码:

image-20230118101853349

可以看到有值的根节点有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解析结果为:

image-20230118172323300

根据解析结果我们需要从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");

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

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

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

节点:

image-20230130173544020

思路:遍历所有的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解析结构:

image-20230118221056693

是否符合上述结构的判断标准为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();

本文转载自: https://blog.csdn.net/as604049322/article/details/128872740
版权归原作者 小小明-代码实体 所有, 如有侵权,请联系我们删除。

“基于AST的babel库实现js反混淆还原基础案例荟萃”的评论:

还没有评论