基于Block的控制流平坦化还原

前言

常见的控制流平坦化有 ollvm(对于二进制程序来说),而现在市面上大部分的二进制控制流平坦化去除工具,如 deflat,d810 等均为针对 ollvm 的特针的,例如分析条件跳转时针对 cmov 系列指令进行分析。然而,最近笔者遇到了一个新的控制流平坦化,其没有 ollvm 的特征,故对其进行分析

特征分析

初步探究

拿一个 demo 加上平坦化进行分析

demo 代码

#include 
#include 

int main() {char str1[] = "test123456";
    char str2[50];
    for (int i = 0; i < 9; ++i)
        str2[i] = (char)getchar();
    for (int i = 0; i < 9; ++i) {if (str1[i] != str2[i]) {printf("wrong");
            exit(1);
        }
    }
    printf("right");
    exit(0);
}

使用 clang 编译,将编译好的和混淆的程序分别拖到 ida 中查看

源程序:

基于 Block 的控制流平坦化还原

可以看到源程序的 cfg 非常的清晰美观

下面是加了控制流平坦化的程序:

基于 Block 的控制流平坦化还原

虽然但是,这个控制流一点都不平坦就是了。。。

可以看到,控制流中多了很多 cmp jnz 等,是很明显的分发结构。然而貌似由于没有明显的主分发器和预处理器,看起来控制流没有那么美观。还有个非常明显的特征就是里面多了很多对于 cf 寄存器和 rax 寄存器的操作,例如 pushf popf 等

详细分析

当然是直接 f5 康一下,然后再调一下啦

经过对比可以看出,并不像 ollvm 对 ir 修改之后再进行编译,原来的汇编代码没有经过任何的修改,只是被分割了储存在程序之中,

f5 之后发现 ida 的优化有着非常大的问题,对于平坦后栈中的内容处理很不到位,出现了很多针对 flag 寄存器的操作,导致代码可读性很差

进行对汇编代码的分析,可以看出,程序一样拥有序言部分,固定为

push rax
pushf
mov eax, 0

还有个更加明显的特征,就是在执行每一个真实快之前都会有一个恢复寄存器的操作,每一个真实块的后面都会接上一个保护寄存器的操作,究其原因,是因为其分发使用了 cmp 指令,并使用 eax 寄存器作为分发变量,因此在分发的过程中会破坏原来的 eax 值和 cf 寄存器,要将他们存在栈中,等待下次执行真实块的时候取出使用。并且序言的作用也是一样,是为了保护调用函数时的寄存器环境

结构如下图:
基于 Block 的控制流平坦化还原

可以看出,当没有出现条件分支的时候回先保存 rax 和 cf,然后再跳转到下一个分发结构。然而,由于没有主分发器和预处理器的存在,这下一个分发结构也是不确定的,这也就导致了 ida 显示的 cfg 并没有出现非常平整的样子而是乱七八糟的

去混淆思路

angr or unicorn?

angr 通过 supergragh 能够生成类似与 ida 的 cfg,便于接下来的处理。而这个样本却不一定要使用 angr

由于其独特的架构,导致没有 ollvm 那样明显的结构,不能通过“预处理器之前的块就是真实块”这样的结论来找出真实块进行连接。因此,我认为生成 cfg 不一定有用。而正是需要保护寄存器这样的特性,导致我们可以从另一个方面来找到真实块,即在“恢复寄存器环境”和“保护寄存器环境”之间的代码,是要执行的真实代码(而因为这个操作之后一定是一个 jmp 所以能直接获取真实块开头的地址)。因此,通过这样的方法就能获得真实代码,之后再通过 unicorn 执行来判断真实块的连接逻辑即可

条件跳转?

还有一个问题就是条件跳转。对于传统的 ollvm,条件跳转的指令需要通过修改 condition 再执行一次。但是本样本强度较弱,经过简单的分析就能发现,其实这个平坦化没有对原来的条件跳转做出任何处理,而是直接把原来的条件跳转当做一个基本块,按照执行顺序拼接即可

函数开头?

之所以要将函数的开头分开来处理是因为函数的开头也采用了保护寄存器(用于保护调用参数),因此其实也能将其认为是函数尾,照常处理即可

总体思路

首先获取并记录所有的真实块,我使用了 capstone 来进行反汇编获取指令

然后还要记录所有需要 patch 的地方,也就是所有做了保护寄存器操作的地方,因为我们其实只需要知道执行了这些保护寄存器操作之后,再接下来会执行到哪个真实块即可(因为原来的条件分发已经在保护寄存器操作之前就已经完成了,所以没有关系)

从这些地方开始,我们可以进行模拟执行,看看接下来会执行到哪个真实块

最后 patch 每个块中用于保护寄存器的位置为 jmp 到下一个真实块

之后函数开头 patch 成跳转到原来函数开头,其他的指令 nop 掉(感觉好像不 nop 掉也不影响。。。),就可以了

具体实现和最终效果

DeCatraz

最终成果:

本文章使用的 demo:

 

基于 Block 的控制流平坦化还原

 

可以看到,效果非常的好,几乎与原来的一模一样,完结撒花~

 

 

正文完
 0
评论(没有评论)