Indirect Branch

Static analysis tools rely on explicit branch targets when attempting to reconstruct the boundaries of a function and its control-flow, as accurately as possible. IndirectBranch pass aims at severing the control-flow edges: each jump successor is added to a table, which is later indexed in order to compute the jump target at runtime.

When to use it?

As for the other passes, it is recommended to employ IndirectBranch on sensible functions. The pass comes with a high runtime performance overhead and extra code size, hence, it is advisable to enable it with a low probability, or on a handful of selected routines. For increased security, it is suggested to enable the pass in conjunction with other passes.

How to use it?

In the Python configuration callback, the pass is suggested to be enabled for selective usage by leveraging the default_config method as follows:

def indirect_branch(self, mod: omvll.Module, func: omvll.Function):
  # Skip obfuscating third-party modules and apply the pass with a 5% likelihood.
  return omvll.ObfuscationConfig.default_config(self, mod, fun, ["third-party/"], [], [], 5)

Implementation

First off, branch and switch instructions terminators are collected. These are those which delimits a basic block. Likewise, all the basic block successors are gathered, and a per-function global array is created with all the basic block addresses (a LLVM BlockAddress), sparsed randomly.

Next, each previously gathered branch is visited and is replaced into an indirect branch by locating and loading the actual successor target from the jump table. More specifically, the following br IR instruction performing a conditional branch depending on whether the integer %value is zero or not:

  %cmp = icmp eq i32 %value, 0
  br i1 %cmp, label %true, label %false

Is rewritten as follows:

  %idx.bb.true  = getelementptr inbounds ([8 x i64], ptr @.indbr.block_addresses, i64 0, i64 %idx)
  %idx.bb.false = getelementptr inbounds ([8 x i64], ptr @.indbr.block_addresses, i64 0, i64 %idx2)
  %bb.true_or_bb.false = select i1 %cmp, ptr %idx.bb.true, ptr %idx.bb.false
  %bb.address = load ptr, ptr %bb.true_or_bb.false
  indirectbr ptr %bb.address, [label %true, label %false]

As it can be seen, the direct jump has been translated via a indirectbr, whose target address derives from a blockaddress stored in the @.indbr.block_addresses global array. Depending on condition %cmp, the basic block address corresponding to the true label or the false one is loaded and passed as operand to the indirectbr.

As such, the jump address label is reconstructed and computed at runtime.

Limitations

  • The pass currently has known limitations on iOS applications built in Release mode.

  • Functions annotated with alwaysinline are skipped as indirectbr instructions may prevent inlining from occurring.

  • The pass currently attempts to translate LLVM critical edges too, although this may break invariants that later code-motion optimizations expect.

  • One may incur high performance penalties due to the overhead coming from the further layer of indirection.