Control Flow Flattening

简要来说,控制流平坦化是对程序控制流的变换,使得所有基本块都处于同一个层级,因此就无法简单推断出嵌套、顺序、条件分支的关系,从而干扰分析。经过控制流混淆后的代码,控制流图会变得比较“扁平”,因为原先的基本块的前驱和后继都被改成了派发器,原来的循环中的有层级关系的基本块经过处理后处于同一个层级了。下面是论文1的一张图:

基于OLLVM的实现已经十分成熟,网上也有很多教程。

本文尝试基于soot框架实现类似的控制流混淆。

尽管soot有一个新的sootup版本,而尝试起来确实比原来的sootAPI更加友好,方便,但是其缺乏后端的支持,只能生成Jimple代码,因此本文还是选择原本的soot进行开发。

Soot Implementation

主要的思路就是创建一个大的Switch块,将随机生成的常量与基本块对应起来,最后写入派发的逻辑。如果是JGotoStmt,那么把跳转的目标设置为bigswitch,并且插入一个对x的赋值。对于JIfStmt,则需要获取到两个target对应的x值,并将跳转目标也都设置为bigswitch

Soot IR (Jimple)

Statements

StmtJimple的一个基本单元。各种操作,例如JAssignStmt是赋值操作,前面提到的bigswitch可以用下面的代码实现:

        var big_switch = Jimple.v().newLookupSwitchStmt(x, keys, key_targets, assignStmt);
 

Basic Block

Basic Block是连续的不包含跳转的代码块。soot中可以通过BriefBlockGraph来得到。

var bbg = new BriefBlockGraph(body);
var bbs = bbg.getBlocks();

绘图使用CFGToDotGraph类。

        UnitGraph unitGraph = new ClassicCompleteUnitGraph(body);
        unitGraph.size();
 
        CFGToDotGraph dotGraph = new CFGToDotGraph();
        var res = dotGraph.drawCFG(unitGraph, body);
        String filename = body.getMethod().getName() + ".dot";
        res.plot(filename);
 

Implementation

soot的初始化部分较为复杂,因此笔者直接参考了下面的代码:

混淆主要是用transformer来插入和修改UnitTransformer是继承自类BodyTransformer,或者SceneTransformer,根据粒度的不同。

  • 方法body (e.g., BodyTransformer).
    public class MyBodyTransformer extends BodyTransformer {
        @Override
        protected void internalTransform(Body body, String phaseName, Map<String, String> options) {
            // Analyze or transform the body
            for (Unit unit : body.getUnits()) {
                System.out.println("Statement: " + unit);
            }
        }
    }
  • 全程序 (e.g., SceneTransformer).
    public class MySceneTransformer extends SceneTransformer {
        @Override
        protected void internalTransform(String phaseName, Map<String, String> options) {
            // Analyze or transform the program's scene
            Scene scene = Scene.v();
            for (SootClass clazz : scene.getClasses()) {
                System.out.println("Class: " + clazz.getName());
            }
        }
    }

soot的执行是基于phases的,和llvmpass类似,是对IR代码的分析和转换,transformer也在这里被执行。一些pack列举如下:

  • jtp (Java Transformation Pack): 对于方法执行BodyTransformer
  • wjtp (Whole Java Transformation Pack): 对于整个程序执行 SceneTransformer
  • bb (Basic Block Pack): Operates at the basic block level.

例如,在本文中,使用下面的代码执行BodyTransformer

PackManager.v().getPack("jtp").add(new Transform("jtp.xorTransformer", new Flatten()));

Result

https://magjac.com/graphviz-visual-editor/

测试类使用了一个RC4的加密。

public class Test {
 
    private static void test(byte[] key, byte[] plaintext) {
        // Initialize S-box
        int[] S = new int[256];
        for (int i = 0; i < 256; i++) {
            S[i] = i;
        }
 
        // Key scheduling algorithm (KSA)
        int j = 0;
        for (int i = 0; i < 256; i++) {
            j = (j + S[i] + (key[i % key.length] & 0xFF)) % 256;
            // Swap S[i] and S
            int temp = S[i];
            S[i] = S[j];
            S[j] = temp;
        }
 
        // Pseudo-random generation algorithm (PRGA)
        int i = 0;
        j = 0;
        byte[] ciphertext = new byte[plaintext.length];
 
        for (int k = 0; k < plaintext.length; k++) {
            i = (i + 1) % 256;
            j = (j + S[i]) % 256;
 
            // Swap S[i] and S[j]
            int temp = S[i];
            S[i] = S[j];
            S[j] = temp;
 
            int t = (S[i] + S[j]) % 256;
            int keyStream = S[t];
 
            // XOR the keystream with plaintext to get ciphertext
            ciphertext[k] = (byte) (plaintext[k] ^ keyStream);
        }
 
        System.out.println("Plaintext: " + new String(plaintext));
        System.out.println("Ciphertext (hex): " + bytesToHex(ciphertext));
    }
 
    // Helper method to convert bytes to hexadecimal string
    private static String bytesToHex(byte[] bytes) {
        StringBuilder hex = new StringBuilder();
        for (byte b : bytes) {
            hex.append(String.format("%02X ", b & 0xFF));
        }
        return hex.toString();
    }
 
    public static void main(String[] strArr) {
        byte[] r1 = "SecretKey123".getBytes();
        byte[] r3 = "Hello World!".getBytes();
        test(r1, r3);
    }
 
}

test方法原来的控制流图如下:

下图是混淆后的控制流图:

然而,目前的实现无法通过javabytecode verifier检查,运行时需要手动指定-noverify参数。推测可能是由于asm后端无法生成正确的stackmap信息。

尽管输出了正确的结果,但只能算得上一个实现的原型,还无法应用于实际的代码保护。

Todo

  • 修复字节码验证问题

研究已有的混淆器:

Footnotes

  1. http://ac.inf.elte.hu/Vol_030_2009/003.pdf#page=1.40