控制流是指代码执行时指令的执行顺序,通过编程语言中各种控制逻辑下可以逻辑执行,通过反编译工具可以将编程语言中的控制语句从上到下执行逻辑进行反编译,最终就可以将程序中的关键功能进行破解和修改。
控制流扁平化是一个代码级别的混淆手段,目前比较知名的用于控制流扁平化的开源软件就属于ollvm了,一般扁平化需要经过的几个步骤:将函数体拆分为多个语句块、构建流程图;将所有拆分的语句块用switch分支去处理;用一个状态变量来逻辑顺序控制。下面就进行分析下控制流扁平化。
控制流扁平化的目标是通过将代码中所有功能基本块放在同一级别上,进行隐藏功能的原始控制流,以达到对代码逻辑顺序进行混淆的效果。
以下是混淆传递之前,函数的逻辑流程图形如下所示:
通过ollvm进行控制流扁平化和虚假控制流进行混淆,根据ollvm使用的混淆器(及其配置),混淆函数的图形效果如下所示:
在ollvm中使用切换调度器方法, 通过函数级别Tigger Flattening进行混淆,混淆后的效果如下图所示:
通过使用Tigger进行混淆,使用间接调度程序的方法,进行对代码扁平化效果如下图所示:
在对so文件进行逆向分析的时候,这种控制流扁平化的混淆技术非常折磨人的,因为它会让我们无法快速理解一个函数流程。
例如,这里是使用 Tigress Flatten 混淆的函数的反编译代码的摘录(使用switch调度程序方法):
这里有以下几个情况会减慢对这个函数的分析速度:
1、很难找到语句块的真正执行顺序。例如,gettimeofday和strncmp的调用顺序?
2、很难识别出函数的真正逻辑结构。例如函数中的while(1)的死循环。
3、它可能会出现反编译的警告信息。例如,在第 24 行,可以看到变量v4和 v9是橙色的,因为 IDA 工具 识别出它们可能是未定义的变量。
通过前面描述的三种控制流扁平化技术混淆,混淆后函数图形状各自不同效果,但它们都有几个共同的元素:
1、状态变量用于跟踪应该执行哪个代码块
2、使用状态变量查找下一个要执行的块的调度程序
3、原始函数的每个块都被修改:不是使用分支指定要执行的下一个块,而是更新状态变量并跳转到调度程序
但是,根据每个人所使用的混淆器,这些元素的使用方式会发生变化:
1、在OLLVM中,状态变量是具有高熵的 32 位常数(Rolf Rolles将其用作启发式方法来识别扁平函数)。调度程序由多个跳转组成,这些跳转将此状态变量与其他 32 位常量进行比较。
2、对于带有switch dispatcher的Tigress Flatten,dispatcher是一个switch语句,其中每个case都是原始函数的一个块。这个swtich语句被包裹在一个无限循环中,以便每个块将更新状态变量以指定下一个应该执行switch语句的哪种情况。
3、对于带有间接调度程序的Tigress Flatten,每个功能基本块的地址都存储在一个全局跳转表中。状态变量是该跳转表中要执行的下一个块的索引。每个块都会更新状态变量,在跳转表中查找下一个块的地址并跳转到调度程序,这是对原始功能块的间接跳转。
总而言之,展开函数的策略相对简单:
我们必须识别到调度程序块和状态变量,接着,对于每个跳转到调度程序入口块的块,我们必须:
1、在这个块的末尾找到状态变量的值;
2、根据调度程序和状态变量的值找到下一个要执行的块的地址;
3、将跳转到调度程序替换为跳转到下一个块。
然而,在逆向分析实践中并不是那么简单,可能会发生多种问题:
1、找到 状态变量的值可能并不容易:可以使用常量混淆或不透明谓词来计算,因此我们需要动态计算它。
2、调度程序块可能有副作用,因此它们不能被完全删除。当我们将跳转到调度程序替换为跳转到下一个块时,我们需要有一种方法来检测这些指令并复制它们。
以上就是对控制流控制流扁平化简单梳理,后续将继续深入分析ollvm的代码保护以及过掉混淆或反混淆的思路。
参考学习
https://eshard.com/posts/D810-a-journey-into-control-flow-unflattening
【推荐阅读】
原文始发于微信公众号(Th0r安全):浅谈控制流扁平化的分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论