Control Flow Flattening
简要来说,控制流平坦化是对程序控制流的变换,使得所有基本块都处于同一个层级,因此就无法简单推断出嵌套、顺序、条件分支的关系,从而干扰分析。经过控制流混淆后的代码,控制流图会变得比较“扁平”,因为原先的基本块的前驱和后继都被改成了派发器,原来的循环中的有层级关系的基本块经过处理后处于同一个层级了。下面是论文1的一张图:
基于OLLVM
的实现已经十分成熟,网上也有很多教程。
本文尝试基于soot
框架实现类似的控制流混淆。
尽管soot
有一个新的sootup
版本,而尝试起来确实比原来的soot
的API
更加友好,方便,但是其缺乏后端的支持,只能生成Jimple代码,因此本文还是选择原本的soot
进行开发。
Soot Implementation
主要的思路就是创建一个大的Switch
块,将随机生成的常量与基本块对应起来,最后写入派发的逻辑。如果是JGotoStmt
,那么把跳转的目标设置为bigswitch
,并且插入一个对x
的赋值。对于JIfStmt
,则需要获取到两个target
对应的x
值,并将跳转目标也都设置为bigswitch
。
Soot IR (Jimple)
Statements
Stmt
是Jimple
的一个基本单元。各种操作,例如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
来插入和修改Unit
。Transformer
是继承自类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
的,和llvm
的pass
类似,是对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
方法原来的控制流图如下:
下图是混淆后的控制流图:
然而,目前的实现无法通过java
的bytecode verifier
检查,运行时需要手动指定-noverify
参数。推测可能是由于asm
后端无法生成正确的stackmap
信息。
尽管输出了正确的结果,但只能算得上一个实现的原型,还无法应用于实际的代码保护。
Todo
- 修复字节码验证问题
- 研究已有的混淆器:
- yWorks/yGuard: The open-source Java obfuscation tool working with Ant and Gradle by yWorks - the diagramming experts
- Guardsquare/proguard: ProGuard, Java optimizer and obfuscator