小试牛刀-利用AST平坦化一段瑞数代码
文章目录
- 前言
- 第一式:鬼影迷踪
- 第二式:森罗万象
- 处理逻辑
- Step1-处理if...else if...
- Step2-补全if...else...
- Step3-平坦化
- 小结
- 参考文章
前言
最近一直在学习AST相关的知识,本篇文章就来小小的尝试下,利用AST平坦化控制流。
正常的执行逻辑: a -> b -> c
混淆后的执行逻辑则可能是这样:a -> d -> b -> d -> c -> d
下方是从瑞数中提出的一段代码。
源代码
function func (_$eE) {var _$0n, _$2L, _$sC, _$o9, _$NU, _$qo, _$hK;var _$tg, _$DM, _$ZZ = 0, _$Ce = [7, 3, 4, 0, 9, 2, 8, 6, 1, 5];while (1) {_$DM = _$Ce[_$ZZ++];if (_$DM < 4) {if (_$DM < 1) {_$tg = !_$NU;} else if (_$DM < 2) {_$Uy(0);} else if (_$DM < 3) {_$NU = _$sC['$_ts'] = {};} else {_$sC = window, _$hK = String, _$o9 = Array;}} else if (_$DM < 8) {if (_$DM < 5) {_$NU = _$sC['$_ts'];} else if (_$DM < 6) {_$ZZ += -3;} else if (_$DM < 7) {return;} else {_$0n = [4, 16, 64, 256, 1024, 4096, 16384, 65536];}} else {if (_$DM < 9) {_$ZZ += 1;} else {if (!_$tg) _$ZZ += 1;}}}
}
先简单介绍上方代码的怎么执行的。
有一个整数数组和下标起始值(在本例中,起始值是
_$ZZ=0
)进入while循环后,通过下标值去数组中取出一个整数,这个整数将经历多次判断,最终执行一条语句。
第一式:鬼影迷踪
纵使直接在while循环中调试比较复杂,但是执行过的语句总是会留下痕迹。
不知道小伙伴是否看过《三体》,其中的曲率引擎飞船达到光速后会在身后留下“死线”,这条“死线”也就是飞船的航行轨迹。
我们可以在while循环中,加入一些语句,记录下它的执行轨迹。
var _$tg, _$DM, _$ZZ = _$eE, _$Ce = _$2z[0];window.trace = [];while (1) {_$DM = _$Ce[_$ZZ++];window.trace.push(_$DM)
当执行完毕之后,查看window.trace
就可以看到它的执行轨迹了。
最终通过执行轨迹就可以知道它具体执行了哪些语句。
Tips:
如果函数存在递归,直接加入window.trace
就不可行了,此时需要加入判定条件,比如判断递归的层数。
这个方法是我自己的胡乱瞎搞的,只是针对非常简单的情况,所以这里就不写详细的过程了。其实,我也更推荐第二种方法,第二种方法是我看**@Nanda** 文章总结出来的,也可以关注下这位大佬的公众号《爬虫术与道 》
第二式:森罗万象
相比“鬼影迷踪”,我更推荐“森罗万象”。这将包含所有情况,并且一旦完成可以说是一劳永逸。
因为_$DM
的值决定了执行流程,所以我们可以动态维护_$DM
,将整个流程走完就可以拿到所有执行的语句。
需要使用到以下模块:
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types")
const generator = require("@babel/generator").default;
const path = require("path");
const fs = require("fs");
let os = require("os");
处理逻辑
先把大致逻辑写出来:
将
if...else if...else
转为if...else
语句;补全
if...else...
的大括号;平坦化;
fs.readFile(path.resolve(__dirname, './source.js'), {'encoding': 'utf-8'}, function (err, data) {const ast = parser.parse(data);// 将if...else if...else语句转为if...else语句step1(ast);// 给没有括号的if...else语句加上括号step2(ast);// 平坦化step3(ast);let code = generator(ast).code;fs.writeFile('./result.js', code, {'encoding': 'utf-8'}, (err) => {console.log(err);})
})
Step1-处理if…else if…
先来看一下需要实现什么效果。
if (1) {console.log(1);
} else if (2) {console.log(2);
} else {console.log(3)
}
// 转换为
if (1) {console.log(1);
} else {if (2) {console.log(2)} else {console.log(3);}
}
这一步是为了方便后续的遍历操作,对于一个IfStatement
的语句
Step2-补全if…else…
为了确保IfStatement
的后续节点都是BlockStatement
所以就需要解决下方这样的情况:
if (!_$tg) _$ZZ += 1;
解析上方代码,可以看见consequent
对应的不是BlockStatement
而是ExpressionStatement
。
要解决这种情况也是非常的简单,我们只需要用BlockStatement
将其原本的语句给“包”起来即可。
在原来的基础上加上{}
再查看抽象树。
function step2(ast) {traverse(ast, {ExpressionStatement: funcToStr})function funcToStr(path) {let node = path.node;let parentNode = path.parent;// 当前节点的父节点也是IfStatement的情况if (isIfStatement(parentNode)) {path.replaceWith(blockStatement([node]));}}
}
这一步处理完之后的效果如下。
function func(_$eE) {var _$0n, _$2L, _$sC, _$o9, _$NU, _$qo, _$hK;var _$tg,_$DM,_$ZZ = 0,_$Ce = [7, 3, 4, 0, 9, 2, 8, 6, 1, 5];while (1) {_$DM = _$Ce[_$ZZ++];if (_$DM < 4) {if (_$DM < 1) {_$tg = !_$NU;} else {if (_$DM < 2) {_$Uy(0);} else {if (_$DM < 3) {_$NU = _$sC['$_ts'] = {};} else {_$sC = window, _$hK = String, _$o9 = Array;}}}} else {if (_$DM < 8) {if (_$DM < 5) {_$NU = _$sC['$_ts'];} else {if (_$DM < 6) {_$ZZ += -3;} else {if (_$DM < 7) {return;} else {_$0n = [4, 16, 64, 256, 1024, 4096, 16384, 65536];}}}} else {if (_$DM < 9) {_$ZZ += 1;} else {if (!_$tg) {_$ZZ += 1;}}}}}
}
Step3-平坦化
执行流程既然是依靠整数数组 和下标值 ,那么我们就来先来把这两个部分提出来。
在本例中,整数数组与下标变量的赋值均是在while
循环上方完成,然后我们又是需要平坦化并替换掉while
节点,所以我们递归的目标就是WhileStatemtent
function step2(ast) {traverse(ast, {WhileStatement: funcToStr})
}
查看抽象树,可知while
循环上方的赋值语句属于它的兄弟节点,如下图所示。
获取上一个兄弟节点路径和兄弟节点
var prevSiblingNodePath = path.getPrevSibling();
var prevSiblingNode = prevSiblingNodePath.node;
找到后节点后,获取数组和下标变量名。兄弟节点的类型是VariableDeclaration
,当对一个节点的结构不熟悉时,就去可视化抽象树上看看。
可以看到declaraions
中有四个变量声明符,与代码一一对应,我们所需要的就是第三个和第四个。
var node = path.node;
var prevSiblingNodePath = path.getPrevSibling();
var prevSiblingNode = prevSiblingNodePath.node;
var subscriptNameNode = prevSiblingNode.declarations[2];
var subscriptName = subscriptNameNode.id.name;
var arrNode = prevSiblingNode.declarations[3];
var arr = arrNode.init.elements;
接下来就是“模拟”while
循环,平坦化执行流。
之前处理成了层层嵌套的if...else...
语句,就是为了方便此时的遍历。
此时的遍历,可以看成是在遍历一颗二叉树 ,然后先写出一个大致的框架
function test(node, inx) {if (arr[inx] < curNode.test.right.value) {// 满足} else {// 不满足}
}
node
是BlockStatement
节点,inx
是数组的下标值,curNode.test.right.value
就是if
表达式右边的值。
通过在第一步的操作,if
后的子节点也都是BlockStatement
function test(node, inx) {if (t.isExpressionStatement(curNode[0])) {// 如果是表达式,表示已经走到底了,将表达式作为结果返回return node.body;}if (arr[inx].value < curNode.test.right.value) {// 满足return test(node.consequent, inx)} else {// 不满足return test(node.alternate, inx)}
}
经过上方代码,已经可以拿到第一个执行语句了,为了拿到所有的执行语句,则需要在每拿到一个语句后就对下标值+1, 然后再进行下一次遍历,直到遇到ReturnStatement
。
Tips:下标值+1是每次循环的默认操作
function test(node, inx) {var resultBody = [];while (1) {if (t.isExpressionStatement(node[0])) {// 如果是表达式,表示已经走到底了,将表达式作为结果返回return node.body;}if (t.isReturnStatement(resultBody[resultBody.length-1])) {break;}// curNode 为IfStatementvar curNode = node.body[1] || node.body[0];if (arr[inx] < curNode.test.right.value) {// 满足resultBody = resultBody.concat(test(curNode.consequent, inx));} else {// 不满足resultBody = resultBody.concat(test(curNode.alternate, inx));}inx += 1;}return resultBody;
}
上方代码仍有缺陷,因为while
循环是写在test
里面的,所以每次递归进入函数都需要进入一次while
循环,但是并不是每条语句后面都有return
,这就会导致死循环。
while
循环的本来意义是针对最顶层的遍历,所以可以再传入递归深度,如果递归深度超过1则直接跳出while循环。
function test(node, inx, depth) {depth += 1;var resultBody = [];while (1) {if (t.isExpressionStatement(node[0])) {// 如果是表达式,表示已经走到底了,将表达式作为结果返回return node.body;}// curNode 为IfStatementvar curNode = node.body[1] || node.body[0];if (arr[inx] < curNode.test.right.value) {// 满足resultBody = resultBody.concat(test(curNode.consequent, inx, depth));} else {// 不满足resultBody = resultBody.concat(test(curNode.alternate, inx, depth));}if (t.isReturnStatement(resultBody[resultBody.length-1]) || depth > 1) {// 如果深度超过1,则跳出while循环break}inx += 1;}return resultBody;
}
在源代码的while
循环中,可能因为判断结果而影响下标值 ,例如:
if (_$DM < 9) {_$ZZ += 1;} else {if (!_$tg) _$ZZ += 1;}
下标变量_$ZZ
是否+1
取决于_$tg
的值,而对于我们在外面的程序因为没有真正执行js代码所以完全不知道_$tg
的值,也就无法得知这里具体是“走左还是走右”。
所以,最稳妥的方法就是考虑两种情况,分别遍历满足与不满足的两种情况,最终汇总为一个IfStatement
。
1、 找到if(!_$tg)
首先它是一个IfStatement
并且test
为UnaryExpression
2、 计算下标值
if (t.isUnaryExpression(curNode.test)) {var real_val;if (t.isUnaryExpression(curNode.right)) {// 注意: 右边的值可能是负数, 例如 a += -1var val = curNode.consequent.body[0].expression.right.argument.value;var op = curNode.consequent.body[0].expression.right.operator;real_val = eval(`${op}${val}`);} else {real_val = curNode.consequent.body[0].expression.right.value;}// 获取操作符, += , -=var operator = curNode.consequent.body["0"].expression.operator;inx = eval(`${inx}${operator.replace('=', '')}${real_val}`);
}
3、 分别遍历两种情况
对于是否满足情况,都是继续调用递归函数,只是传入的实参不同罢了。
例如,条件为真的情况,在实际循环中,下一次遍历必将返回while
的开头,所以这里传入初始节点 ,下标值传入计算出来的新值,递归深度为0。
test(initNode, inx+1, 0)
这样就可以拿到满足这个条件时的后续执行流程。
对于不满足条件的情况,按正常流程执行即可。
随后调整“亿”点点代码即可完成递归函数。
function test(node, inx, depth) {depth += 1;var resultBody = [];while (1) {if (node.body.length === 1 && t.isExpressionStatement(node.body[0])) {return node.body}var curNode = node.body[1] || node.body[0];if (t.isReturnStatement(curNode)) {return node.body;}if (t.isIfStatement(curNode)) {if (t.isUnaryExpression(curNode.test)) {var real_val;if (t.isUnaryExpression(curNode.right)) {// 注意: 右边的值可能是负数, 例如 a += -1var val = curNode.consequent.body[0].expression.right.argument.value;var op = curNode.consequent.body[0].expression.right.operator;real_val = eval(`${op}${val}`);} else {real_val = curNode.consequent.body[0].expression.right.value;}// 获取操作符, += , -=var operator = curNode.consequent.body["0"].expression.operator;inx = eval(`${inx}${operator.replace('=', '')}${real_val}`);resultBody = resultBody.concat([t.ifStatement(curNode.test, t.blockStatement(test(initNode, inx + 1, 0)))])} else if (t.isBinaryExpression(curNode.test)) {if (arr[inx].value < curNode.test.right.value) {// 满足resultBody = resultBody.concat(test(curNode.consequent, inx, depth));} else {// 不满足resultBody = resultBody.concat(test(curNode.alternate, inx, depth));}}}if (t.isReturnStatement(resultBody[resultBody.length - 1])) {break;}if (depth > 1) {break;}inx += 1;}return resultBody;}
小结
ast这玩意儿折腾了一个星期,最烦人的部分还是写递归(算法学的差痛苦了。。。),在刚开始学习ast时,无法记住每一个节点的结构,所以需要多使用可视化工具,不清晰的结果就将代码丢进去瞧瞧。
还有一点,本文的平坦化方法不是最优解,多动手,多练习才是最有效的学习方式。
共勉!peace!
参考文章
[1] Js Ast一部曲:高完整度还原某V5的加密
[2] Js Ast二部曲:某V5 “绝对不可逆加密” 一探究竟
[3] 《JavaScript AST其实很简单》
小试牛刀-利用AST平坦化一段瑞数代码相关推荐
- 利用AST对抗js混淆(三) 控制流平坦化(Control Flow Flattening)的处理
控制流平坦化 参考:https://security.tencent.com/index.php/blog/msg/112 控制流平坦化,简单来讲就是将代码块之间的关系打断,由一个分发器来控制代码块的 ...
- AST还原技术专题:浅谈去控制流平坦化的思路及方法
一. while-switch结构的控制流 这类平坦化代码很简单,常见于经过obfuscator在线工具混淆后的控制流平坦化.一般代码段不会很长,常见的 switch-case 基本都在10个分支以内 ...
- 某酷ckey签名生成算法系列--(三)ast代码控制流平坦化
某酷ckey签名生成算法系列--(三)ast代码控制流平坦化 观察三个switch的值分别是Ci.mi和Ai.而这三个值又因为li的确定而确定的.也就是说已知li的值,就可以分别计算出Ci.mi和Ai ...
- AST混淆实战|仿obfuscator混淆控制流平坦化(超详细版)
之前写过一篇这样的文章 : JavaScript 代码混淆实战(六):仿obfuscator混淆控制流平坦化,但并没有写过程,在这篇文章里面说明下! 依然以文章里的代码来说明怎么进行控制流平坦化. 混 ...
- 利用符号执行去除控制流平坦化
1. 背景 1.1 控制流平坦化 控制流平坦化(control flow flattening)的基本思想主要是通过一个主分发器来控制程序基本块的执行流程,例如下图是正常的执行流程 经过控制流平坦化后 ...
- 跟着铁头干混淆4.1 ollvm控制流平坦化基本概念
ollvm 4.1 控制流平坦化基本概念 控制流平坦化基本概念 编译器参数:-mllvm -fla 英文全称 简称 编译参数 控制流平坦化 Control Flow Flattening fla -m ...
- 猿人学第二题,手撕OB混淆给你看(step06-控制流平坦化)
前情回顾: 猿人学第二题,手撕OB混淆给你看(Step1-开篇) 猿人学第二题,手撕OB混淆给你看(step2-字符串数字回填) 猿人学第二题,手撕OB混淆给你看(step3-函数调用还原) 猿人学第 ...
- 逆向进阶,利用 AST 技术还原 JavaScript 混淆代码
文章目录 什么是 AST AST 在编译中的位置 词法分析 语法分析 代码生成 Babel 简介 @babel/core @babel/parser @babel/generator @babel/t ...
- buu-[RoarCTF2019]polyre(控制流平坦化,虚假控制流程)
这题一开始拿到人看麻了(不会),写篇wp记录新题型 这么一大大大串的函数图,是经过OLLVM 的控制流平坦化混肴. 控制流平坦化(Control Flow Flattening)的基本思想主要是通过一 ...
最新文章
- python动态类型是如何实现的_Python 的动态类型系统
- TMSScripter演示
- oracle之数据同步:Oracle Sql Loader使用说明(大批量快速插入数据库记录)
- 提高sqlmap爆破效率
- 皮一皮:这是什么家族企业?
- Linux 终端部分重要快捷键
- HashSet集合介绍
- 曲线绕x轴旋转曲面方程_几何代数50 ----柱面及其方程
- @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) 注解的作用
- 5月8日——iOS中的3D Touch效果
- jaxp与dom4j遍历xml树
- java ipv4和ipv6通信_IPv6与IPv4连接负载
- 售票统计表表格模板_财务常用四大报表模板,共二十张,会计小白入职有它被高看一眼...
- 交叉熵损失的来源、说明、求导与pytorch实现
- 惠普HP ProDesk 400 G2 加装BCM94352HMB网卡
- 屏幕适配以及ui切图规范和命名标准
- jmeter工具进行性能测试
- 【计算机网络】计算机网络体系结构总结——基本知识要点汇总
- Python学习笔记(一)三步走安装pip
- Data-Mining试题