What native obfuscation changes
Native code compiles to machine instructions, so obfuscation works on a different surface than the DEX transformations applied to Java and Kotlin. A protector usually changes a small set of things.
- Symbol stripping removes names. The dynamic symbol table keeps only the entries the loader needs, and the local symbols that labelled internal functions, file names and source paths are gone, so the listing arrives without an index.
- Control flow stops matching the source shape. A protector inserts dispatchers, opaque predicates and reordered blocks so that a decompiler emits branches that never execute and misses branches that do. The computation stays correct because the application still has to work.
- String material is encrypted at rest. Literals are stored in an encoded form and rebuilt in memory when the code reaches them, so a search of the file for an endpoint, a key or a message finds nothing useful.
- Import and call tables are altered. Some libraries resolve the functions they call at runtime through a lookup rather than a direct entry, which hides from the import table who they talk to.
What the ABI and calling convention fix in place
The application binary interface is the part obfuscation cannot move, because the operating system and the linker enforce it. On ARM, integer arguments travel in the first registers and further arguments go on the stack, the return value arrives in a defined register, and every call has to respect stack alignment and preserve a defined set of registers. A tool that follows those rules can reconstruct the argument and return types of a function even when no name is attached to it, because the shape of the code around a call is constrained.
The loader imposes another fixed point. The library has to declare the version of its interfaces, expose the functions other code calls, and state which external functions it needs before it can run. A file that violated any of those would not load, so a protector treats them as given rather than as things it can rewrite.
The JNI export table stays an anchor
JNI is the strongest of those constraints and the most useful in practice. When Java declares a native method, the runtime resolves it by name and signature, and the library has to export a symbol whose spelling is fixed at link time and derived mechanically from the Java package, class and method name. Renaming classes in the DEX pass changes the expected spelling, and the two sides have to agree or the call fails at runtime.
That gives an analysis a reliable list of entry points even in a stripped library. Each exported JNI function accepts the VM pointer and the object or class, then the declared arguments, and returns a value the Java side already knows how to consume. Reading one of them tells you what data crosses the boundary in each direction, and locating the ones that carry interesting arguments tells you where the application's hidden work begins. Libraries that route everything through a single registered native entry reduce the list, and the registration call still exists in the library's startup path, so the JNI side stays observable.
Narrowing the code that must be read
A shared library in a mobile application usually matters for a small part of one flow: a signing routine, a key schedule, a serializer, or a piece of request assembly. Mapping the JNI surface first converts an unbounded reading problem into a defined one.
The method is to attach to the running process, hook the exported functions, and record what each one receives and returns. The result is a set of functions with known inputs and outputs, sorted by how often they are called on the path you care about. Strings resolved at runtime add a second type of evidence: a hook on the routine that rebuilds a literal, or on the memory allocation that receives it, reveals the plaintext that the file withheld.
From there the reading effort goes to a handful of functions rather than the whole binary. When a function is short enough to follow by hand, the instruction listing is sufficient. When it is large and flattened, the input and output values recover the API specification without recovering the body, which is what a reproduction needs. The goal is a client that produces the same output as the app, and the values crossing the JNI boundary define that output precisely.
Related work
Reviewed 28 September 2026 · SReverse research desk