好久前弄的,补个档。

概述

R1mao👴在2022年为Pluto-Obfuscator加了个新的控制流平坦化算法,旨在对抗一般的静态/动态去混淆脚本。然而他的算法可以被”动态模拟执行+BFS探索真实块“所破解。

不过这个去混淆算法没什么实用价值,因为Pluto-Obfuscator已经长时间无人维护了,R1mao👴的这个算法也没人用力,顶多就CTF偶尔有人炒冷饭吧,破它就当图个乐。

修改后的qiling脚本可以在这里找到:https://github.com/JANlittle/qiling/tree/dev

R1mao👴的算法干了啥?

一般的OLLVM平坦化中的真实块会对状态变量直接赋新值来确定它的后继。那么一般的静态对抗手法就是定位真实块对状态变量的赋值,并查找这个状态变量对应的真实块,从而确定真实块后继;一般的动态对抗手法就是遍历并模拟执行每个真实块,探索它的后继块。

R1mao👴的算法改进如下:

  1. 每个真实块都会被分配一个key变量和访问标志位。
  2. 每个真实块第一次被访问时,都会修改其后继所有块(包括后继的后继的后继…)的key变量。
  3. 只有以正确顺序访问真实块,才能依次正确获得后继块key变量,计算正确的后继块,保证控制流的正确。

这样有什么好处?

  • 对于静态分析去平坦化,其实只要把状态变量的修改方式改一下就够了,静态分析去平坦化方案通用性基本很低。这个增强型算法很大程度上增加了状态变量修改方式的复杂性,所以目前的静态分析去平坦化脚本应该都没办法去除,需要手动修改。
  • 对于模拟执行去平坦化,因为目前的模拟执行去平坦化就是单纯遍历真实块,探索每个真实块的后继块而已,没有考虑真实块间的执行顺序,所以是无法更新出正确的key变量,从而导致控制流分析出错。

如何对抗?

其实很简单,顺着R1mao的思路就可以了:如果不按真实的控制流执行真实块的话就无法获取正确的后继块,那我模拟执行的时候就模拟真实块的执行顺序不就好了!

我们可以像BFS遍历图那样,使用BFS遍历访问每个真实块:

  1. 从函数的起始块开始,探索真实块的后继块,将未访问过的后继块推入队列中,同时保存其运行时状态。
  2. 从队列中取出新的真实块并重复1,直到每个块都被访问过。
  3. 我们已经获取到所有真实块及其后继块的关系,直接重建控制流就好了!

在访问和执行块时,我们每次都会保存其运行状态,这样我们就可以完整维护每一条执行路径,保存key变量,恢复出真正的控制流。

在具体实现上,本来想用qiling框架自己写一个的,但没想到qiling的IDA插件已经实现了一个去平坦化方案,所以我就直接在它的基础上改进了,主要就是修改了_search_path_guide_hook方法:https://github.com/JANlittle/qiling/blob/dev/qiling/extensions/idaplugin/qilingida.py

此外,我还修改了下qiling去平坦化的一些东西:

  • 修改了分配块、预分配快、虚假块、真实块的识别方式等。用户需要自行决定虚假块的识别函数。
  • 去混淆前需要指定是否要先执行整个程序到某个位置,这主要是为了初始化动态库和对抗Pluto-Obfuscator的全局加密。
  • 去混淆前需要指定哪些函数在执行过程中不要跳过,因为去混淆时碰到函数调用默认是跳过的,但遇到例如这个增强版平坦化时,有些函数还是不能跳过的。
  • 去混淆前需要指定哪些函数调用需要patch掉,例如这个增强版平坦化会为每个真实块添加更新key变量的函数,patch的时候将其去掉可以让最终结果更好看。patch的范围是由IDA微码决定的。

效果

Pluto-Obfuscator给的官方例子,只开Control Flow Flattening Enhanced

image-20240705215827414

去混淆后:

image-20240705222308406

对于ACTF 2023的obfuse也是轻松秒,只要先处理掉indirect call和indirect jump就行:

image-20240705223008439