SReverseby Simpa Labs

Control flow

Analyzing control-flow obfuscation in Android apps

A flattened method still computes the same result, and it can still be observed at its boundaries. That is often enough to rebuild the request it produces.

What the transformation does

A normal method compiles into basic blocks connected by branches and loops, and a decompiler rebuilds if statements and while loops from that structure. Control-flow obfuscation breaks the correspondence. The original blocks are lifted into a dispatcher, usually a loop containing a switch, and a state variable decides which block runs next. Execution order is preserved at run time and lost in the source a decompiler produces.

The transformation comes from commercial protectors. Applying it to every method would give up a great deal of size and speed, so it tends to be applied to selected classes. That leaves a build where some classes read normally and others decompile into a state machine, which is itself a signal about what the developer treated as sensitive.

What flattened output looks like

  • A single loop wrapping a switch whose cases are the original basic blocks.
  • A local variable assigned in each case that selects the next case.
  • Reused local slots, so one variable holds unrelated values at different points.
  • Conditions that are constant in practice, such as a comparison between two values the code just computed.
  • Blocks that no input can reach, kept in the method to enlarge it.

Opaque predicates and dead branches

Flattening is often combined with predicates that always evaluate the same way. A condition comparing two expressions that both fold to constants looks meaningful and is not. When such a predicate guards a block, one side never runs, and the decompiled method contains code that can never execute.

Evaluating these predicates by hand is mechanical work, and it pays off because it removes large parts of a method from consideration. A constant folding pass over the decompiled output, or a small script that simplifies the arithmetic branches, reduces the method to the paths that matter.

Instruction substitution is worth checking at the same time. A simple addition can be rewritten as a chain of shifts, multiplications and masks that evaluates to the same integer, which makes the code look busier without adding a real branch. Folding those expressions back to their results removes most of the noise before you attempt anything else.

Use execution to recover the order

Static reconstruction of the original control flow is not necessary for most API work. The useful question is what the method produces, and that is observable. Run the app, set a breakpoint or a trace at the dispatcher, and record the sequence of states for the target action. The order you observe is the path the app actually takes, and it usually covers a small subset of the cases present in the method.

Trace at the boundaries rather than inside the dispatcher when the goal is request reconstruction. Hooking the HTTP client, the serializer or the signing helper gives you inputs and outputs without needing to understand the flattened code between them. The flattened method becomes a component with a known API specification, and the API specification is what the deliverable implements.

A hybrid pass works well. Rebuild the basic blocks statically from the dispatcher cases, then use one trace to order them and to mark the cases that never run. What remains is a short sequence of operations with known inputs, and that sequence is usually small enough to reimplement in the deliverable language without decompiling the method back to its original shape.

When the interpreter has to be read

Some builds go further and replace the method with a program for a custom interpreter. The dex then contains the interpreter and a stream of custom opcodes, and there is no dispatcher in the original method to trace in a useful way. This is the expensive case. It usually requires reading the interpreter's dispatch loop, identifying what each opcode does to the virtual machine state, and then decoding the instruction stream for the methods in scope.

The cost depends on how much of the app is virtualized. A protector that virtualizes everything would produce an unreasonably large and slow build, so the technique is normally limited to selected classes, and the reconstruction can often stop once the full application flow's inputs and outputs are known even if the internals stay opaque.

Deciding where to stop

Control-flow work has no natural end point, because a flattened method can always be restored further. Tie the effort to a testable outcome: the target call runs, returns the expected response, and survives the error paths the server sends. Beyond that, additional reconstruction adds documentation value rather than capability, and it can be scheduled separately if the client wants a readable version of a specific method.

Reviewed 28 September 2026 · SReverse research desk

Start your full APK reconstruction

Send the full APK

Projects start at $120. Choose WhatsApp or email, then attach the APK in the app that opens. We reply within one hour with the next step and send the fixed quote after review.

Want us to contact you?