Control Flow Obfuscation
Protection ID: controlFlowObfuscation
Control Flow Obfuscation restructures the logic within methods by modifying the code execution paths, inserting additional branches, and creating complex non-linear flows. This makes static analysis tools unable to follow the actual execution path through decompiled code.
Configuration
shield {
protections {
controlFlowObfuscation = true
}
}shield {
protections {
controlFlowObfuscation = true
}
}How It Works
Control flow obfuscation modifies the structure of your methods to create "spaghetti code" patterns that prevent decompiler interpretation. The protection rearranges code blocks into a non-linear order, injects conditions that always resolve to the same value at runtime but appear ambiguous to static analysis, and inserts dead code branches that are never actually executed.
The result is that decompilers cannot determine which branches are real and which are dead, forcing an attacker to manually analyze every possible path.
Before Shield
public PaymentResult processOrder(Order order, User user) {
if (!user.isVerified()) {
return PaymentResult.rejected("User not verified");
}
double total = order.calculateTotal();
double tax = total * 0.21;
double finalAmount = total + tax;
if (user.hasDiscount()) {
finalAmount = finalAmount * 0.9;
}
return gateway.charge(user.getPaymentMethod(), finalAmount);
}public PaymentResult processOrder(Order order, User user) {
if (!user.isVerified()) {
return PaymentResult.rejected("User not verified");
}
double total = order.calculateTotal();
double tax = total * 0.21;
double finalAmount = total + tax;
if (user.hasDiscount()) {
finalAmount = finalAmount * 0.9;
}
return gateway.charge(user.getPaymentMethod(), finalAmount);
}After Shield (Simplified Representation)
public PaymentResult processOrder(Order order, User user) {
int state = 0;
PaymentResult result = null;
double v1, v2, v3;
while (true) {
switch (state) {
case 0:
if (!user.isVerified()) { state = 4; break; }
state = 1; break;
case 1:
v1 = order.calculateTotal();
v2 = v1 * 0.21;
v3 = v1 + v2;
state = user.hasDiscount() ? 2 : 3; break;
case 2:
v3 = v3 * 0.9;
state = 3; break;
case 3:
return gateway.charge(user.getPaymentMethod(), v3);
case 4:
return PaymentResult.rejected("User not verified");
default:
state = 0; break;
}
}
}public PaymentResult processOrder(Order order, User user) {
int state = 0;
PaymentResult result = null;
double v1, v2, v3;
while (true) {
switch (state) {
case 0:
if (!user.isVerified()) { state = 4; break; }
state = 1; break;
case 1:
v1 = order.calculateTotal();
v2 = v1 * 0.21;
v3 = v1 + v2;
state = user.hasDiscount() ? 2 : 3; break;
case 2:
v3 = v3 * 0.9;
state = 3; break;
case 3:
return gateway.charge(user.getPaymentMethod(), v3);
case 4:
return PaymentResult.rejected("User not verified");
default:
state = 0; break;
}
}
}Decompilers cannot reconstruct the original clean logic from the flattened switch-based structure.
Performance Impact
Control flow obfuscation adds runtime overhead because the JVM must evaluate the additional expressions and follow the restructured flow. The impact depends on the method's complexity and how often it is called.
Light methods (getters, simple logic) see negligible impact. Initialization code that runs once has no practical impact. For hot loops and performance-critical methods, consider excluding them with @Exclude:
@Exclude(protections = {"ControlFlowMatrix"})
public void renderFrame() {
// Performance-critical rendering code
}@Exclude(protections = {"ControlFlowMatrix"})
public void renderFrame() {
// Performance-critical rendering code
}When to Use
Control flow obfuscation is recommended for security-critical code, algorithm implementations, license validation logic, payment processing flows, and any business logic you want to protect from reverse engineering. It is most effective when combined with Name Obfuscation and Reference Proxy.
Related
- Name Obfuscation - Rename identifiers
- Reference Proxy - Add call indirection
- Protections Overview - All available protections