我最近听说了“控制流扁平化”混淆,它似乎用于破坏二进制程序的 CFG 结构(参见暗黑混淆模块和符号执行和 CFG 扁平化)。
有人可以解释一下它的基本原理是什么,以及如何产生这种混淆(工具、编程技术……)?而且,很高兴知道是否有办法提取程序控制流的真实形状。
我最近听说了“控制流扁平化”混淆,它似乎用于破坏二进制程序的 CFG 结构(参见暗黑混淆模块和符号执行和 CFG 扁平化)。
有人可以解释一下它的基本原理是什么,以及如何产生这种混淆(工具、编程技术……)?而且,很高兴知道是否有办法提取程序控制流的真实形状。
来自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
将用于跳转到下一个块的值。
在大多数情况下,恢复它应该不会太困难 - 只需跟踪控制变量更新并将跳转到调度程序节点替换为跳转到与新控制变量值对应的下一个块。