浅谈BlackObfuscator控制流混淆的设计思路

0x1 关于BlackObfuscator

BlackObfuscator是基于dex2jar开发的Dex控制流混淆,开源地址:https://github.com/CodingGay/BlackObfuscator,现有的混淆一般只支持处理变量名、类名,这样做对抗是远远不够的。由于对于控制流此类资料较少,加上也没有现成方案,所以当时就自己研究了一下,现在出来分享一下设计思路。

0x2 源码

BlackObfuscator的核心模块为dex-obfuscator,源码目录:

├── IRObfuscator.java
├── LBlock.java
├── ObfDic.java
├── ObfuscatorConfiguration.java
├── RebuildIfResult.java
└── chain
    ├── FlowObfuscator.java
    ├── IfObfuscator.java
    ├── SubObfuscator.java
    └── base
        ├── BaseObfuscatorChain.java
        └── ObfuscatorChain.java

我目前只做了

  1. 控制流混淆
  2. if块的混淆
  3. 指令运算混淆
    对应的是源码chain目录下的三个混淆器。

切入点是通过dex2jar会将指令转换为ir指令,通过转换出来的ir指令进行混淆。

指令运算混淆

switch (v1.vt) {
    case LOCAL:
        if (v1.valueType.charAt(0) == 'I') {
            if (v2.vt == Value.VT.ADD) {
                if (v2.getOp2().vt == Value.VT.CONSTANT) {
                    // a=b+c    a=b-(-c)
                    int increment = (Integer) ((Constant) v2.getOp2()).value;
                    v2.vt = Value.VT.SUB;
                    v2.setOp2(Exprs.nInt(-increment));
                }
            } else if (v2.vt == Value.VT.SUB) {
                if (v2.getOp2().vt == Value.VT.CONSTANT) {
                    // a=b-c    a=b+(-c)
                    int increment = (Integer) ((Constant) v2.getOp2()).value;
                    v2.vt = Value.VT.ADD;
                    v2.setOp2(Exprs.nInt(-increment));
                }
            } else if (v2.vt == Value.VT.XOR) {
                // a=a^b    (a ^ r) ^ (b ^ r)

                // seed
                Local local = newLocal("xor", "I");
                newStmts.add(Stmts.nAssign(local, Exprs.nInt(new Random().nextInt(1000))));

                Local left = newLocal("xor_left", "I");
                newStmts.add(Stmts.nAssign(left, Exprs.nXor(v2.getOp1(), local, "I")));
                v2.setOp1(left);

                Local right = newLocal("xor_right", "I");
                newStmts.add(Stmts.nAssign(right, Exprs.nXor(v2.getOp2(), local, "I")));
                v2.setOp2(right);
            }
        }
        break;
}

可以看到核心就是将简单的计算,转换为一些不容易看出的计算方式并且结果不变
例:
a=b+c 转换为 a=b-(-c)
a=b-c 转换为 a=b+(-c)
a=a^b 转换为 (a ^ r) ^ (b ^ r)

if块混淆

top.niunaijun.obfuscator.chain.IfObfuscator#reBuild0
这一块代码比较长,并且不是很好理解,我只讲设计思路。

if(i < 0) {
    Log.d(TAG, "条件成立")
} else {
    Log.d(TAG, "条件不成立")
}

可以将其拓展

int i = 0;
int index = 0;
while(true) {
    switch(index) {
        case 0:
            index = 1;
            break;
        case 1:
            if(i < 0) {
                index = 2;
            } else {
                index = 3;
            }
            break;
        case 2:
            Log.d(TAG, "条件成立");
            break;
        case 3:
            Log.d(TAG, "条件不成立");
            break;
    }
}

这是一种丐版的方式,实际上BlackObfuscator还增加了隐性的index,通过index为字符串,然后通过字符串的hashCode进行switch。

控制流混淆

一样的,代码比较难理解,我们直接看结果。代码可以自己去慢慢感受。

int a = 10;
int b = 20;
a = a + 5;
b = b + 5;
Log.d(TAG, "这里是a : " + a);
Log.d(TAG, "这里是B : " + b);

可以将它拓展为

int a = 0;
int b = 0;

int index = 0;
while(true) {
    switch(index) {
        case 0:
            a = 10;
            index = 1;
            break;
        case 1:
            b = 20;
            index = 2;
            break;
        case 2:
            a = a + 5;
            index = 3;
            break;
        case 3:
            b = b + 5;
            index = 4;
            break;
        case 4:
            Log.d(TAG, "这里是a : " + a);
            index = 5;
            break;
        case 5:
            Log.d(TAG, "这里是B : " + b);
            index = 6;
            break;
        default:
            return;
    }
}

这也是一种丐版,实际上可能是这样

String str = "۫ۨ۬ۤۤۡ۬۬ۦۛۥۧ۠ۘۧۢۨ۟ۧۛۜۘۢۘۦۘ۫ۥۧ";
while (true) {
    switch ((str.hashCode() ^ 213) ^ 0x5e9d8e28) {
        case -2112984833:
            a = null;
            str = "۠ۨۜۘ۫ۨ۟ۙۦ۠۟ۡۗ۬ۜۨ۬ۘۦۘ";
            break;
        case -1764939362:
            return;
        case -1465172033:
            needX86Bridge = false;
            str = "ۜۢۗۚۢۧ۬ۙۛۢ۬۟ۨۚۦۚۦۖ";
            break;
        case -1331622716:
            f = null;
            str = "ۜۨۜۥۛۥ۠ۦۡۤ۫ۥۧۛۜۘ";
            break;
        case -187618826:
            returnIntern = true;
            str = "ۙۡ۠ۤۨۤۤ۠۠ۥ۬ۨۜۛۗ۬ۘۘۡۧۜ۫ۜۘ";
            break;
        case 97430221:
            i = new ConcurrentHashMap();
            str = "ۖۡۖۘۙۗۘۖۚ۬ۛ۬ۥۤۨۡۘ۟ۙۚۚۡۨۘ";
            break;
        case 165757335:
            h = null;
            str = "۫۬ۖۘۙۜ۠ۘۢۙۧۢۛ۠ۡۙۜۖۗ";
            break;
        case 265781367:
            d = null;
            str = "ۙۙ۫ۚۛ۠ۙۜ۫ۦۙۜۘ۬ۚۥۦۦۥۘۜۤۦۘۙۛ۟";
            break;
        case 780928495:
            g = null;
            str = "ۡۧۛ۬ۘۖۛۗۛۥ۟ۨۗۦۤۨۦۢۛۙ۠ۚۙ۟۠ۛۖ";
            break;
        case 864246795:
            b = "libjiagu";
            str = "ۖۚۘۘۗۡۘۜۨۖ۠۫ۘۜۤ۬ۘۗۜ۬ۛۡۘۜۚ۠";
            break;
        case 1015028737:
            e = null;
            str = "ۢ۫ۘۘۧۨۧۗۥۛۡۤ۟ۤۧۨ";
            break;
        case 1129108171:
            loadFromLib = false;
            str = "ۘ۬۟۫۟ۧ۟ۨۢ۟ۢ۠ۚۥۡ۫ۡۘۖۨۜۘۗۖۧ";
            break;
    }
}

0x3 总结

这里可以看到,我们控制流混淆的宗旨就是要用100句代码实现10句代码的功能。BlackObfuscator不足的地方就是太过单调,还可以进一步优化的地方可以是

  1. 在switch块中插入无用代码,使代码更加难以阅读。
  2. 将代码抽离出改switch块,移至别的地方执行

看到此处比较简单,但是你别忘记了,BlackObfuscator可是支持深度设定的,何为深度设定?说大白话就是套娃混淆。

一句指令混淆 a=a^b 转换为 (a ^ r) ^ (b ^ r)。在下一次混淆时将会变成(a ^ r) ^ (b ^ r)(a ^ r) ^ (b ^ r)(a ^ r) ^ (b ^ r),再下一次则是(a ^ r) ^ (b ^ r)(a ^ r) ^ (b ^ r)(a ^ r) ^ (b ^ r)(a ^ r) ^ (b ^ r)(a ^ r) ^ (b ^ r)(a ^ r) ^ (b ^ r)(a ^ r) ^ (b ^ r)(a ^ r) ^ (b ^ r)(a ^ r) ^ (b ^ r)

上面这个举例只是做演示,展示混淆的套娃能力。

以上就是BlackObfuscator的设计思路和原理。由于dex2jar本身是有问题的,所以本库也不会再怎么跟进了,有兴趣的同学可以尝试用dex2lib等库进行定制会更好一点,原理是一样的。

  1. 万里星河说道:

    额 啊这 不会吧 还真的只是浅谈啊?😅😅😅

    1. 牛奶君说道:

      代码的话自己对照看就行了,都是某个库API的操作。讲起来没什么意义

      1. 万里星河说道:

        那个对ir的statement进行重构的操作是调用的库的api吗?请问具体是哪个库啊?还有就是进行if混淆好像要画控制流图啥的 那个具体大概是怎么个操作法呀?

        1. 牛奶君说道:

          首先你可以看看比如自己加一句代码,用statement怎么操作。后面的就是看上面我列的拓展思路一句一句拼起来就好了。因为里面全都是ir的操作,所以列出来没有实际用处。思路就是上面的思路。按照思路一句一句把代码拼起来就可以了。

          1. 万里星河说道:

            请问ir操作用的库叫什么名字呀 我好先搜搜教程系统学学.还有就是if语句混淆成while-switch结构的控制流程图是怎么设计的呀 能简要说说不?

          2. 牛奶君说道:

            你写一个,然后看就知道了,其实就是一个goto 又回到上面的语句,就是个while。至于你说ir这个东西,是dex2jar原本项目里的。自己照葫芦画瓢即可。

          3. 万里星河说道:

            ......

          4. 牛奶君说道:

            就是你在我提到那几个控制器里面,打个断点,就能知道其中原理了。代码就是用来实现思路的过程而已,核心还是你的思想。

发表评论

电子邮件地址不会被公开。 必填项已用*标注