文章目录

  • 前言
  • 第一式:鬼影迷踪
  • 第二式:森罗万象
    • 处理逻辑
    • 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;}}}
}

先简单介绍上方代码的怎么执行的。

  1. 有一个整数数组和下标起始值(在本例中,起始值是_$ZZ=0

  2. 进入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");

处理逻辑

先把大致逻辑写出来:

  1. if...else if...else 转为if...else语句;

  2. 补全if...else...的大括号;

  3. 平坦化;

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 {// 不满足}
}

nodeBlockStatement 节点,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并且testUnaryExpression

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平坦化一段瑞数代码相关推荐

  1. 利用AST对抗js混淆(三) 控制流平坦化(Control Flow Flattening)的处理

    控制流平坦化 参考:https://security.tencent.com/index.php/blog/msg/112 控制流平坦化,简单来讲就是将代码块之间的关系打断,由一个分发器来控制代码块的 ...

  2. AST还原技术专题:浅谈去控制流平坦化的思路及方法

    一. while-switch结构的控制流 这类平坦化代码很简单,常见于经过obfuscator在线工具混淆后的控制流平坦化.一般代码段不会很长,常见的 switch-case 基本都在10个分支以内 ...

  3. 某酷ckey签名生成算法系列--(三)ast代码控制流平坦化

    某酷ckey签名生成算法系列--(三)ast代码控制流平坦化 观察三个switch的值分别是Ci.mi和Ai.而这三个值又因为li的确定而确定的.也就是说已知li的值,就可以分别计算出Ci.mi和Ai ...

  4. AST混淆实战|仿obfuscator混淆控制流平坦化(超详细版)

    之前写过一篇这样的文章 : JavaScript 代码混淆实战(六):仿obfuscator混淆控制流平坦化,但并没有写过程,在这篇文章里面说明下! 依然以文章里的代码来说明怎么进行控制流平坦化. 混 ...

  5. 利用符号执行去除控制流平坦化

    1. 背景 1.1 控制流平坦化 控制流平坦化(control flow flattening)的基本思想主要是通过一个主分发器来控制程序基本块的执行流程,例如下图是正常的执行流程 经过控制流平坦化后 ...

  6. 跟着铁头干混淆4.1 ollvm控制流平坦化基本概念

    ollvm 4.1 控制流平坦化基本概念 控制流平坦化基本概念 编译器参数:-mllvm -fla 英文全称 简称 编译参数 控制流平坦化 Control Flow Flattening fla -m ...

  7. 猿人学第二题,手撕OB混淆给你看(step06-控制流平坦化)

    前情回顾: 猿人学第二题,手撕OB混淆给你看(Step1-开篇) 猿人学第二题,手撕OB混淆给你看(step2-字符串数字回填) 猿人学第二题,手撕OB混淆给你看(step3-函数调用还原) 猿人学第 ...

  8. 逆向进阶,利用 AST 技术还原 JavaScript 混淆代码

    文章目录 什么是 AST AST 在编译中的位置 词法分析 语法分析 代码生成 Babel 简介 @babel/core @babel/parser @babel/generator @babel/t ...

  9. buu-[RoarCTF2019]polyre(控制流平坦化,虚假控制流程)

    这题一开始拿到人看麻了(不会),写篇wp记录新题型 这么一大大大串的函数图,是经过OLLVM 的控制流平坦化混肴. 控制流平坦化(Control Flow Flattening)的基本思想主要是通过一 ...

最新文章

  1. python动态类型是如何实现的_Python 的动态类型系统
  2. TMSScripter演示
  3. oracle之数据同步:Oracle Sql Loader使用说明(大批量快速插入数据库记录)
  4. 提高sqlmap爆破效率
  5. 皮一皮:这是什么家族企业?
  6. Linux 终端部分重要快捷键
  7. HashSet集合介绍
  8. 曲线绕x轴旋转曲面方程_几何代数50 ----柱面及其方程
  9. @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) 注解的作用
  10. 5月8日——iOS中的3D Touch效果
  11. jaxp与dom4j遍历xml树
  12. java ipv4和ipv6通信_IPv6与IPv4连接负载
  13. 售票统计表表格模板_财务常用四大报表模板,共二十张,会计小白入职有它被高看一眼...
  14. 交叉熵损失的来源、说明、求导与pytorch实现
  15. 惠普HP ProDesk 400 G2 加装BCM94352HMB网卡
  16. 屏幕适配以及ui切图规范和命名标准
  17. jmeter工具进行性能测试
  18. 【计算机网络】计算机网络体系结构总结——基本知识要点汇总
  19. Python学习笔记(一)三步走安装pip
  20. Data-Mining试题

热门文章

  1. Unity3d Ugui 10 Toggle ToggleGroup
  2. 纽约摄影教材Tips
  3. 给定字符串A和B,输出A和B中的最大公共子串。
  4. 2014 WOT全球软件技术峰会深圳站
  5. 使用PyCharm进行接口测试
  6. Linux 基本操作 看完这篇Linux基本的操作就会了
  7. HTTP请求的交互过程和常见的相应状态码
  8. 计算机及通讯公司转正申请(范文)
  9. nod-1631-小鲨鱼在51nod小学
  10. ECSHOP去版权教程