BlackObfuscator是基于dex2jar开发的Dex控制流混淆,开源地址:https://github.com/CodingGay/BlackObfuscator,现有的混淆一般只支持处理变量名、类名,这样做对抗是远远不够的。由于对于控制流此类资料较少,加上也没有现成方案,所以当时就自己研究了一下,现在出来分享一下设计思路。
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
我目前只做了
切入点是通过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)
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;
}
}
这里可以看到,我们控制流混淆的宗旨就是要用100句代码实现10句代码的功能。BlackObfuscator不足的地方就是太过单调,还可以进一步优化的地方可以是
看到此处比较简单,但是你别忘记了,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等库进行定制会更好一点,原理是一样的。