什么是“控制流扁平化”混淆技术?

逆向工程 混淆
2021-06-10 01:37:15

我最近听说了“控制流扁平化”混淆,它似乎用于破坏二进制程序的 CFG 结构(参见暗黑混淆模块符号执行和 CFG 扁平化)。

有人可以解释一下它的基本原理是什么,以及如何产生这种混淆(工具、编程技术……)?而且,很高兴知道是否有办法提取程序控制流的真实形状。

2个回答

来自Timea Laszlo 和 Akos Kiss 的这篇论文

扁平化函数的基本方法如下。

首先,我们将函数体分解为基本块,然后将所有这些块(最初位于不同的嵌套级别)并排放置。

现在同等级别的基本块被封装在一个选择结构(C++语言中的switch语句)中,每个块在一个单独的案例中,选择被依次封装在一个循环中。

最后,正确的控制流由代表程序状态的控制变量确保,该变量设置在每个基本块的末尾,并用于封闭循环和选择的谓词中。

Image showing how control-flow flattening obfuscation alters code that contains loop structures.
在此处输入图片说明

A simple example:

int original()
{
    print "Do"
    print "you"
    print "like"
    print "milk?"
}


int obfuscated()
{
    int ctrFlowVar = 1;

    while(ctrFlowVar != 0)
    {
        switch(ctrFlowVar)
        {
            case 1:
                print "do"
                ctrFlowVar = 2;
                break;
            
            case 2:
                print "you"
                ctrFlowVar = 3;
                break;
            
            case 3:
                print "like"
                ctrFlowVar = 4;
                break;
            
            case 4:
                print "milk?"
                ctrFlowVar = 0;
                break;
        }
    }
}

如果您熟悉switch语句的编写方式assembly(我知道2种方式,if 样式跳转表),那么上面的示例很容易去混淆。break;指令是一个jmp。您可以让它跳转到块应该是下一个。

有关这种混淆的一个很好的例子,请查看 Apple 的 FairPlay 代码,例如 iTunes 或 iOS 库。这是应用了这种混淆的函数的典型图形:

在此处输入图片说明

如您所见,基本块之间的所有边——条件和无条件的——都被重定向到一个调度器节点,该节点使用一个新的人工变量来决定下一个应该跳转到哪个块。该变量在每个分离的基本块的末尾更新。

这是调度程序节点:

LDR    R3, =0xF26A85D2
ADD    R3, R2, R3
CMP    R3, #0x40 ; switch 65 cases
ADDLS  PC, PC, R3,LSL#2 ; switch jump

R2用作控制值。

这是基本块之一:

LDR  R2, =0x853FD863 ; jumptable 00532EFC case 33
LDR  R1, [SP,#0x130+var_108]
STR  R2, [SP,#0x130+var_134]
LDR  R2, =0xD957A31
STR  R1, [SP,#0x130+var_44]
B    loc_532ED0

它更新R2将用于跳转到下一个块的值。

在大多数情况下,恢复它应该不会困难 - 只需跟踪控制变量更新并将跳转到调度程序节点替换为跳转到与新控制变量值对应的下一个块。