Call Indirection
| Plan | Platforms | MASVS |
|---|---|---|
| Team | Android | MASVS-RESILIENCE-3 |
Overview
Call Indirection replaces direct method calls within the protected application with indirect dispatch through generated trampoline classes. By routing calls through a central dispatcher that uses numeric IDs to select the target method via conditional branches, this control collapses the call graph into a star topology, making it extremely difficult for static analysis tools to trace control flow and understand inter-method relationships.
This control is implemented as a build-time bytecode rewrite. During the protected app's compilation, MobileDefender's build pipeline rewrites eligible invoke-static call sites in the app's smali code to redirect through generated dispatcher methods. No runtime overhead beyond the indirect call itself is added.
How It Works
Build-Time Smali Rewrite
The control operates during the AppTego Android protected build process, after the tenant app is decompiled to smali (bytecode) and before final reassembly. The CallIndirection pass:
- Scans all user smali files for
invoke-staticcalls to methods within the app (not framework classes). - Assigns each unique eligible call target a numeric dispatch ID (up to 200 targets per DEX file).
- Generates a dispatcher smali class with randomized package and class names (e.g.,
com/r8a3f1b2e/Rc94f2a817b), containing one trampoline method per dispatch target. - Replaces original call sites with calls to the corresponding dispatcher trampoline method.
Each dispatcher method is a simple forwarding stub:
.method public static m7af2e1(II)V
.locals 2
const/16 v1, 0x3a7c
xor-int/lit8 v1, v1, 0x4f
invoke-static {p0, p1}, Lcom/example/app/RealClass;->realMethod(II)V
return-void
.end method
The filler instructions (const/16, xor-int, etc.) use register v1 and introduce structural variation to resist pattern-based automated un-trampolining.
Eligibility and Safety Constraints
Eligible call sites:
- Static method calls to user classes (not framework classes like
android.*,androidx.*,com.google.*,kotlin.*). - Public static methods on public classes (the dispatcher is in a different package and cannot access package-private members).
- Methods with simple signatures (no
longordoubleparameters, which use register pairs and complicate the dispatcher). - Methods returning
void, primitives (exceptlong/double), or objects.
Skipped call sites:
- Constructors (
<init>) and class initializers (<clinit>). - Calls already using
invoke-static/range(complex parameter lists). - Complex control flow (flags register produced by arithmetic ops, non-const instructions, or cross-label jumps).
Probabilistic filtering: Only ~50% of eligible call sites are redirected. This mixing of direct and indirect calls increases analysis difficulty without exhausting method-ID headroom.
Per-DEX Method-ID Cap: The rewriter respects the 65,536 method-ID limit per DEX file, leaving 500 method-IDs of headroom. If a DEX file is near capacity, the pass is skipped for that DEX to prevent build failures.
How to Enable the Control
Navigate to Code Obfuscation from the AppTego portal, and expand the Control Flow Obfuscation section. Under this section you will find the Call Indirection control. Click Enable to apply it to the next protected build.
API Configuration Example
{
"CallIndirection": {
"protection": true
}
}
| Field | Purpose |
|---|---|
protection | Enables call indirection for protected builds. |
Threats Mitigated
- Call-Graph Extraction: Static analysis tools (jadx, Ghidra, Soot, WALA) build call graphs by following
invoke-*instructions. Call Indirection collapses the call graph into a star topology centered on the dispatcher, obscuring which methods call which and making it extremely difficult to trace execution paths or identify security-critical operations. - Cross-Reference Analysis: Reverse engineers rely on "Find Usages" and "Go to Definition" features in decompilers to navigate codebases. By routing calls through a dispatcher with numeric IDs and randomized method names, Call Indirection breaks these cross-references, forcing analysts to manually trace dispatcher logic and reconstruct call relationships.
- Control Flow Understanding: Automated program analysis (symbolic execution, data-flow analysis, vulnerability scanners) depends on accurate control flow graphs. The star-shaped call graph produced by Call Indirection causes these tools to over-approximate reachability and report excessive false positives, reducing their practical utility.
Caveats
Runtime Overhead
Each indirected call introduces a small overhead:
- One extra method invocation (dispatcher trampoline → real target).
- Filler instructions in the dispatcher method (1–3 no-op arithmetic operations).
For most applications, this overhead is negligible (sub-microsecond per call). However, apps with extremely tight performance constraints (game loops, real-time audio processing, sensor data pipelines) may experience measurable impact if hot-path methods are redirected.
Mitigation: The probabilistic filter ensures not all calls are redirected. If profiling reveals a performance issue, consider disabling Call Indirection or identifying specific hot methods and excluding them via allowlist (contact support for custom allowlist configuration).
Stack Traces
Stack traces will include the dispatcher class in the call chain:
at com.example.app.RealClass.realMethod(RealClass.java:42)
at com.r8a3f1b2e.Rc94f2a817b.m7af2e1(Unknown Source)
at com.example.app.Caller.doWork(Caller.java:15)
The (Unknown Source) entry is the generated dispatcher, which has no source file mapping. This does not prevent debugging or crash reporting, but analysts will see extra frames in traces.
Reflection
Call Indirection only affects static invoke-static bytecode. If the tenant app uses reflection to invoke methods (Method.invoke(...)), those calls are not redirected and remain visible in the call graph.
Compatibility with Other Obfuscation
Call Indirection runs after smali decompilation but before other obfuscation passes (Control Flow Obfuscation, Dead Code Injection, etc.). The dispatcher class itself is subject to subsequent obfuscation, which further obscures its structure.
R8/ProGuard code shrinking and optimization may inline some dispatcher methods or remove them if deemed unused (rare, as R8 runs before MobileDefender's build pipeline). If this occurs, the call is effectively restored to direct dispatch.
Support Matrix
| Platform | Minimum Version | Maximum Version | Notes |
|---|---|---|---|
| Android | API 21 (Android 5.0) | Latest | All smali bytecode versions supported |
| iOS | – | – | Not applicable |
Plan Requirement
TEAM or higher. Not available on FREE or BASIC plans.