What you are loading
Native libraries in an APK live under lib with a subdirectory per ABI and arrive as ELF shared objects. Release builds commonly strip the symbol table, which removes most function and variable names while leaving dynamic symbols in place. Those dynamic symbols are the ones the loader and the Java runtime need, and for a library that registers its methods statically they include entries named Java_ followed by the mangled class and method name.
Ghidra reads ELF files directly and recovers the code, the relocation tables and the import list. The import list is often the most informative part of a first pass, because a library that links against libcrypto or BoringSSL exposes which primitives it uses even when the surrounding application code has no names left.
Load the file with the right processor
Import the library into a Ghidra project and set the processor to match the ABI directory it came from. The 64-bit directories use AArch64 and the 32-bit ones use ARM little endian. Accept the default analyzers, then let auto analysis finish before reading anything. Disassembly that looks corrupted in the first seconds usually means analysis is still running or the wrong processor was selected.
When analysis completes, work from three views. The symbol tree shows what survived stripping, the defined strings view shows literals, and the function list shows how they connect. Sorting functions by size is a quick way to find a hand-written crypto primitive, because such implementations are usually far larger than the functions around them.
Find how Java reaches the code
JNI reaches native code in two ways, and each leaves a different trail.
Static registration produces exported functions named Java_package_Class_method with a JNIEnv pointer and a jobject as the first two arguments. Reading those signatures gives you the Java method name and the native parameter types straight from the mangled symbol.
Dynamic registration hides the same information. JNI_OnLoad calls RegisterNatives with a table of structures that pair a Java method name, a signature string and a function pointer. Find JNI_OnLoad in the exports, follow the call to RegisterNatives, and read the table to recover the mapping. The function pointers in that table are the real entry points, and they are rarely named.
Recover request signing and crypto
When a signature is produced in native code, the routine usually reveals itself through constants. Look for a large substitution table, a round constant array, or a block of bytes referenced as a key. Long runs of data in the read-only section that no code writes to are candidates for key material or lookup tables.
Libraries that link the platform crypto often give away more than the app intends. Imports such as HMAC, EVP_DigestSign or SHA256 show which primitive is in use, and the arguments at the call site show the key and the message layout. When an application statically links its own copy of a crypto library, the same primitives appear as internal functions with no names, and the fastest way to identify them is by their constants and their call shape rather than by their names.
Handle encoded strings and flattened control flow
Strings in a protected library are often stored encoded and decoded on first use. The decoder is a small function that takes a pointer and a length, and every encoded literal calls it. Once you find that function, rename it and use its cross references as a map of every hidden string in the binary. Decoding is usually a byte-wise operation against a key derived from a constant, which is easy to reimplement in a script and run over the data section.
Control flow flattening replaces ordinary branches with a dispatcher that reads a state variable and jumps through a table. The code stays readable in Ghidra, but the order of execution does not, so record the state transitions or run the blocks through an emulator. Anti-debugging checks that call ptrace or inspect process status appear as small functions near the entry point and can be ignored while reading statically.
Confirm the reading against the running app
Static reading produces a hypothesis about the algorithm. Instrumentation confirms it. Attach to the process, locate the exported or registered function, and print its arguments and return value while the app makes a real request. Comparing the returned bytes with the signature on the wire closes the loop, and it also captures the exact input the app passes, which is frequently a differently ordered concatenation than the one you assumed.
That check matters most for derived values. A key can be recovered correctly and still be assembled in the wrong order, and only a byte-for-byte comparison settles the question.
Where native analysis fits the API work
Native code is where mobile apps put the parts they want to keep: signing, encryption, integrity checks and key derivation. Reading it is what allows a client to reproduce a request instead of replaying one. SReverse reads native request signing and crypto and folds the result into a working client. When the signing routine turns out to be called from a JNI method, the surrounding chain is covered under APK to callable API.
Related work
Reviewed 28 September 2026 · SReverse research desk