-
-
[原创]Polaris-Obfuscator中BogusControlFlow简要分析 反混淆
-
发表于: 10小时前 176
-
这个混淆也算是从OLLVM继承下来的老玩意了;简单来说就是对于一个basic block,在它之前创建一个相同的basic block,这个新的basic block包含一个opaque predicate(一个永远为真/永远为假的条件,即new block永远也不会被真正执行到);然后在这个新的block里面还可以相应的加入一些junk instructions,使得反编译器无法正确解析这个block。
具体可以看此篇:cfdK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6G2j5X3k6#2M7$3y4S2N6r3!0J5i4K6u0V1L8r3I4$3L8g2)9J5c8X3!0T1k6Y4g2K6j5$3q4@1L8%4u0Q4x3V1k6%4K9h3E0A6i4K6u0r3b7X3!0Y4N6i4y4Q4x3X3c8U0L8$3&6@1M7X3!0D9i4K6u0V1k6X3I4G2N6H3`.`.
f06K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6*7j5e0t1K6x3#2)9J5c8W2m8G2L8r3q4J5K9i4y4Q4x3X3c8a6j5X3k6#2M7$3y4S2N6r3!0J5i4K6u0r3j5X3I4G2j5W2)9J5c8X3#2S2K9h3&6Q4x3V1k6K6M7X3y4Q4x3V1k6D9L8s2k6E0i4K6u0r3L8r3W2T1i4K6u0r3g2s2u0S2L8Y4y4X3L8%4u0E0M7#2)9J5c8V1!0T1k6Y4g2K6j5$3q4@1K9h3!0F1i4K6u0r3b7X3!0Y4N6i4y4o6L8$3&6@1M7X3!0D9c8X3I4G2N6#2)9J5k6h3y4H3M7l9`.`.
我们逐个函数的来分析:
这个函数的功能就是克隆一个basic block。当然也进行了相应的变量重命名,以及metadata的克隆。但是并没有对其进行任何的修改,没有添加junk code。
这个函数的功能就是将函数中的每个基本块按每Size条指令拆分成多个较小的块。这样做的目的是为了增加基本块的数量,后面便可以插入更多的opaque predicates。
首先在函数头创建两个变量,Var和Var0。
紧接着,得到两个变量Mod和X:
Mod=0x100000000−rand32()
X=randPrime()modMod
然后Var和Var0都初始化成了X:
接下来的就是比较tricky的部分了。我先把代码贴上来,然后再进行分析:
首先来说说下面的这个IR到底是啥玩意:
转化成high level code的话就是:
那么这个公式到底算了啥玩意?从上面的计算公式可知:
B=rand32()modMod
A=((B∗Inv(X,Mod))modMod+1)modMod
这两个刻意构造的变量A和B满足如下的关系:
(A−1)∗X=BmodMod
其中,X可以是任何一个小于Mod的数。这个公式只需要将上面的A和B代入进去就可以轻松验证。
下面,只需要稍微变形一下上面的公式就可以得到:
A∗X=(X+B)modMod
将这个玩意带入到上面用于变换Var0的公式中就可以得到:

所以实际上Var0的值是永远不会改变的,一直和Var的值相等的。上面花里胡哨的计算其实只是为了让编译器和反编译器无法进行优化。
注意:这里的实现实际上有一个bug。 上面的推导在数学模运算下是正确的,但IR中使用的是sub i64(无符号64位减法)+ urem i64。当(A * X) % Mod < B时,sub会发生unsigned underflow,wrap到264附近的大数,后续的urem就不再等价于数学模运算了。例如取Mod=11, X=3, B=8,数学上(−8)mod11=3,但uint64下0 - 8 = 2^{64} - 8,(264−8)mod11=8=3。这会导致opaque predicate不再恒真,进而使程序陷入死循环。
接下来就是注入blocks和opaque predicates了:
具体的插入稍微有点复杂,涉及在graph上的变换。不过整体变化后的图像可以直接参考:

来源:2a1K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2S2M7s2u0A6L8%4u0A6N6q4)9J5k6h3y4G2L8g2)9J5c8X3c8W2N6W2)9J5k6r3u0D9L8$3N6Q4x3V1k6G2j5X3k6#2M7$3y4S2N6r3W2F1k6#2)9J5k6r3y4G2k6r3g2Q4x3X3c8@1L8#2)9J5k6s2y4W2j5%4g2J5k6g2)9J5k6r3q4F1k6s2u0G2K9h3c8Q4x3X3c8S2M7s2m8K6
不过核心点其实还是在于opaque predicate的构造上。只要能够把opaque predicate给优化掉,那么具体的插入方式实际上并不重要;反编译器会自动把dead block给剪枝掉。
刚刚快速看了一下源码,发现里面有两个都是BogusControlFlow的pass,分别是BogusControlFlow和BogusControlFlow2。前者就是我们上面分析的那个,而后者则是一个更为简单的版本?我没记错的话,以前OLLVM用的就是这个:
这里面生成的opaque predicate明显要简单很多,并没有涉及mod+invserse的计算。
在Pipeline.cpp里面,BogusControlFlow2才是那个被注册成pass的,而那个看上去更复杂的BogusControlFlow反而完全没有被使用上。
对一个csmith生成的测试样本进行bcf混淆后,得到的混淆片段如下:
注意,这里由于还是使用的旧的BogusControlFlow2, 所以生成的opaque predicate比较简单。
核心点在于对opaque predicate的判断。我认为可以分成如下流程:
不过在涉及到细节的时候,对这三个步骤的实现会复杂不少。具体而言:
下面简单post一下实现的代码:
首先,我们需要找到目标函数中所有的conditional branch指令。在VEX IR中,一个conditional branch对应的block会有Ijk_Boring类型的jumpkind,同时block内部会有一个Exit语句(也是Ijk_Boring类型)。
这里的逻辑很简单:遍历函数中的所有basic block,找到那些包含conditional branch的block(block的default exit是Ijk_Boring,且内部有一个Exit语句也是Ijk_Boring),然后把branch指令的地址记录下来。
对于每个branch,我们需要进行backward slicing来收集所有影响这个branch guard的指令。这里使用了angr的DDG(Data Dependency Graph)来进行backward slicing。
具体流程如下:
最终得到的slice_cls就是所有影响这个branch guard值的指令集合。
拿到backward slice之后,接下来就是通过符号执行来判断这个branch guard是不是一个opaque predicate。
首先,我们收集backward slice中涉及到的所有block地址并排序。然后从第一个block开始进行符号执行,逐步step到下一个block。在每次step之后,我们把那些跑到了"错误"地址的state移到deadended stash里——这样就保证我们只沿着slice中的路径执行。
到达branch block之后,我们再step一次,检查产生了多少个successor:
一旦确定了一个branch是opaque predicate,接下来就需要对binary进行patch。
Patch的策略:
最后,把上面的步骤串起来:
对于每个branch:
这是原来混淆的:

去混淆后:
