由于上述的三个pass是基于LLVM IR实现的, 因此从理论上来说, 这种稠浊器是支持天下上任何一种措辞和机器架构的。
关于每种pass的详细文档,可以查看下面的这三个链接:
Instructions Substitution(指令变换)

Bogus Control Flow(流程假造)
Control Flow Flattening(流程平坦化)
上面的这几个链接里面是各个pass的作者掩护的一份大略文档,如果你以为文档不足详尽,建议直接参考相应的源码即可,可能对你来说会又直不雅观又准确。
如果说看代码,实在是比较费劲的一个事情,紧张是LLVM Obfuscator的工程代码构造的缘故原由。现在github上,LLVM Obfuscator是按分支来掩护的,每个版本一个分支,也便是说你Clone下来的代码都杂在一起,直接上来就看代码很随意马虎迷失落在代码的海洋中。不过我们可以有目的的挑着看,比如我们Clone一份4.0的代码,然后直接在 lib/Transforms目录下的代码, 这里都是自定义的LLVM pass。
在我们这篇博文里面,我们只关注流程平坦化这一个主题,这个特性在我看来是比较有趣,并且稠浊效果也是比较空想的一个特性。
掌握流程平坦化总体来说,掌握流程平坦化这个特性,抽象下来,紧张是通过这几个步骤来实现的:
在全体代码流程中,剖析搜集出所有的基本代码块(Basic Block)(译者注:碰着条件分支就算是一个新的代码块了)
把基本代码块放到掌握流图的最底部,然后删除掉原来的基本块之间的跳转关系
添加稠浊器的流程掌握分发逻辑,通过新的繁芜分发逻辑还底本来程序块之间的逻辑关系
还是举个例子吧,为了形象一点,我这里给出两幅图来进行旁边比拟。
左边的图是IDA7.0(Demo版就行)对未稠浊程序天生的代码流程图,右图是同一个程序经由LLVM Obfuscator的流程平坦化处理之后IDA7.0剖析出的代码流程图。
在这两幅图里面,绿色的块表示函数里面的代码基本块,图中的蓝色的块便是稠浊器为了达到稠浊效果和保持原程序逻辑而添加的粘合代码,这里我们给这些蓝色块的代码起个名字好了,叫它 backbone(稠浊器运行框架)
对付右边这幅图,为了看起来更加的直不雅观,我们可以利用IDA的node分组的功能把流程图的显示办法优化一下,这里我直接把backbone代码合并成一个node,这样看起来就清晰了,看图:
虽然现在流程图大略了不少,但是通过和上面的左图进行比拟, 全体程序流程还是发生了很大的变革,并且各个基本块之间的逻辑关系也很难判断了,全体代码流程看上去更像是一个switch...case构造,每个基本块是case分支逻辑。
由此我们也可以这样想,全体逻辑流程变成了一个状态机架构,每次实行哪个代码块由状态机的值来决定,而每个代码块末了会更新状态机的值,然后backbone框架代码根据这个值,再来决定实行哪个基本代码块,以是一个代码块肯定要对应一个固定的状态机的值.。
流程平坦化的弱点从现在开始,我们开始借助 Binary Ninja这个平台来进行后续的剖析,选择这个平台紧张是基于这个平台里的几个特性(IDA中没有):
Medium-level IL
SSA Form
Value-set analysis
确定Backbone块(确定骨架代码)
为了搞清楚流程平坦化的弱点,我们通过一个例子来详细的剖析一下Backbone的代码,先看下我们的例子:
这个例子便是我们上面的那个经由稠浊处理的程序的一部分,其他部分的代码基本是相似的,因此这里我们就截取个中一部分代表就可以了。我们仔细不雅观察这段代码,这段代码会读取状态变量,然后把变量和某个值进行比较,如果比较相等,就跳转到某个基本块实行,如果不等,就跳转到下一个Backbone里面连续上述的过程
把稳,这里我们就创造了一个关键的薄弱点:给定一个状态变量,记为state_var,我们创造每个Backbone代码块至少包含一次对这个变量的引用,如果遍历出所有引用到这个变量的代码块,那我们就可以得到所有的Backbone块,下面我们通过Binary Ninja的medium-level IL特性来搜集所有的块,这里我直接给出代码:
这个算法可以找出所有对state_var进行过引用的Backbone块,包括程序的起始块(这个块是定义这个变量的块),起始块一样平常是这样的:
从起始块我们很随意马虎找到这个状态变量,然后通过def-use和use-def调用链,就能比较顺利的找到剩余的Backbone块了。
确定真实的程序逻辑块通过类似的思想,我们看看能不能找到什么特色,通过这个特色来找到所有的逻辑块。在我们的这个例子里面,一个真实基本块会包含一个或者多个实行出口,而实行出口一样平常都因此一个无条件跳转实现的,一个比较范例的真实块看起来大致是这样的:
看代码可以知道,先是修正一下状态变量state_var的值,然后跳转到骨架代码。看上面的代码,我们基本可以确定,下次实行的真实代码块对应的case值是0xfcbce33c,对付那种有多个出口的真实块会被拆分成多个块,看起来会大致像下面这样:
这里,原程序的一个条件语句实在被转换成了一个赋值语句,然后根据赋值的结果决定是不是要实行某个代码块,举个例子来说,比如原程序是这样的(^_^一起截图了,下面的是变革后的结果):
但是,我们的目标是找到所有的真实代码块,为了达到这个目标,我们须要利用LLVM Obfuscator的另一个关键弱点:所有逻辑基本块中,每个块至少包含一次state_var的定义动作(把稳是定义不是引用),就跟起始块有点类似。
乍一看,可能我们要基于深度优先来进行一次def-use类型的搜索,不过在Ninja上,这个事情被简化了不少,前面一个小节里面,我们查找了所有利用到了state_var的代码块,但是在这里我们只查找定义了这个变量的代码块就好了,代码如下:
上面的代码里面找到的每个包含了state_var变量定义的代码块都被认为是一个基本的逻辑代码块,包含起始块。后面的章节里面会创造,这种特色办法得到的结果还是很令人满意的。
还原代码流程到现在为止,我们基本上已经有了重构代码流程的所有信息,再梳理一下的话,我们现在对付一个经由稠浊过的程序,目前节制了以下两点信息:
所有的代码基本块
状态机值和基本代码块的映射关系
目前我们还差一步,那便是我们目前还不知道对付一个给定的基本代码块,它的下一个实行节点是什么,也便是我们须要确定一个基本代码块实行完毕的时候,它把状态机的值改成了什么。为了完成这个目标,我们就要借助Binary
Ninja的另一个很主要的特性,Value-Set剖析,通过这个剖析,可以知道某个寄存器或者内存位置里面的值是什么(译者注:相称于一个值跟踪系统,有点仿照实行的味道),有了这个,我们也可以确定出来末了状态机的值了。
前面提到过,一个基本代码块末了会把状态机state_var更新成一个固定值,现在我们就把这些值都找出来,这样整条链就串起来了:
对付有条件跳转的情形,处理的办法有点trick的味道,由于我们的目标是要确定基本块实行完毕的时候,出口的state_var的值是什么,也便是要确定条件跳转的时候,哪个是true,
哪个是false, 为了方便,我们利用Ninja的 SSA 图形视图来不雅观察一下,看下面这个例子:
在这个例子里面,函数实行完毕的时候,是有两种情形的的state_var的,在上图里面,凡事会影响到state_var干系的语句全都高亮了。为了更加的直不雅观,这里把上述的逻辑用 LLVM-IR再描述一下:
大致逻辑便是:
先把%next_state设置成%false_branch
如果%original_condition的结果为1, 在把%next_state设置成%true_branch
末了再把state结果保存到%state变量
转头不雅观察上图中的SSA-MLIL,
我们看下高亮的语句部分,实在便是在两个不同版本的nextState变量之间进行选择,而且每个不同版本的nextState后面随着一个数字作为版本号标志,再根据我们剖析的逻辑,版本号小的那个便是false,版本号大的是true.末了借助于Ninja的Value-Set剖析,我们就可以得出末了的nextState的终极值,以是就能确定下一个要实行的基本块是哪个了,还是上代码:
现在版本的API还不太方便,但是结果还是准确的,把上面的东西综合起来,就形成了一个比较完全的脚本了:
重构干净的二进制文件
目前来说,在Binary Ninja平台上patch程序还是比较麻烦的,但是彷佛也没有什么更好的替代品了。到现在为止,我们已经能够重构原始的掌握流程了,剩下的事情便是根据节制的信息来patch二进制代码了,让真实逻辑代码块之间直接相连,忽略掉Backbone代码。
构建代码原型在我们目前获取的所有原始代码块里面,还包含了当时LLVM
Obfuscator插入的一些框架代码,比如更新状态机的代码,这个代码现在对我们来说是垃圾代码了,因此须要清理掉这些代码了,恰好用这些代码的空闲位置来插入一些我们的代码块连接指令,我整理了一下,下面这三种类型的指令都可以删掉了:
跳转到Backbone分发器的代码(译者注:便是基本块末了的那种jump指令)
更新state_var的指令
用来打算state_var结果的干系指令
为了直不雅观一点,连续给例子吧,下图中凡是标红的指令都是要删除掉的了,没用了:
上面这些代码删除掉之后, 还能为我们后面修复流程的时候腾出来代码空间,一石二鸟。
修复掌握流程经由上面的步骤之后,在原始代码块里面腾出了一些空间,我们就利用这些空间来添加一些我们自己的修复指令,我们就用最大略的跳转指令来进行代码块之间的连接就好了,对付那种没有条件跳转的块,直接在末了街上 jmp next_block就好了
对付有条件分支的情形下,就须要确定是利用哪种jcc指令了,前面的小节里面我们知道,掌握流平坦化pass会用true状态覆盖false状态,如果true分支成立。在实际的代码中,一样平常是利用cmovne这样的语句来操作的,于是这里取一个巧,我们就干脆不管这个时候的状态,而是依葫芦画瓢,复用它的状态结果,直接做一个大略的映射关系,cmovne直接就更换成 jne,这样既大略又准确,以是末了的结果大致便是这样的:
有了上述的准备事情之后,下面的patch过程就大略了,对付每个基本代码块,按照下面的步骤来操作:
把除了垃圾指令之外的指令拷贝一份形成一个新的代码块
在末了追加一个jump跳转到下一个块
末了为了跟原来的程序大小保持同等,把剩余的空间用nop指令添补一下
这里分别给出一个无条件跳转和条件跳转情形下的修复例子:
到现在,全体掌握流程就修复好了
末了清理经由上述的修复之后,那些backbone代码和垃圾指令肯定是无法实行到了,但是在我们载入IDA剖析的时候,还是会在视图中涌现,还是在生理上造成滋扰,以是这里为了看起来干净一些,把这些没用的指令全部都用nop给添补一下(前面我们已经得到了backbone代码块凑集了)
成果展示为了展示一下我们的成果,我们再把还原的结果在 Ninja中的结果贴一下,先看没有经由稠浊的原始代码:
下面是经由稠浊过的代码:
按照我们上述的修复流程修复一下,终极我们得到这样的结果:
比拟第一张图和第三张图,实在已经非常靠近了,但是也有那么一点点不同(译者:但是都无伤大雅,HoHo~~):
部分代码块被拆分开了
插入了一些连接代码块的jump指令
上面这两点算是一点小遗憾,而且不是那么好修复
插件出炉上述的所有过程手工起来还是比较麻烦的,我们做成了插件,你可以在上找到源码,请把稳这里Binary Ninja插件,直接clone下来就可以用了。
利用插件来还愿代码只须要2步:
选择一条更新state_var的指令
实行插件
本文由看雪翻译小组 freakish 编译,来源rpis 转载请注明来自看雪社区