Saturday, November 7, 2026
HomeAPK Modding & Reverse Engineering BasicsReversing Native C/C++ Shared Libraries (.so) in Android Apps

Reversing Native C/C++ Shared Libraries (.so) in Android Apps

Native shared libraries hold compiled C and C++ code inside an Android app package for speed and reuse. Those libraries appear as files with a .so extension in folders named by device architecture inside the app. A Java method can call a native method through a bridge that links the two languages at run time. That bridge lets an app use fast native code for media and graphics and cryptography and games. Analysts who study only Java code miss the logic that lives in those native libraries. That missing logic can hold a key feature or a security check or a data transformation. For Java code that pairs with native study see DEX code and Smali layouts.

Native libraries differ from Java code in format and in tooling for analysis work. Java code arrives as DEX files that decompile into readable classes with names and methods. Native code arrives as machine code for a specific processor architecture like ARM or x86. That machine code needs a disassembler to show instructions in a readable form. It also needs a decompiler to rebuild a higher level view of functions in some cases. Analysts must choose tools that match the architecture of the target library file. A wrong architecture choice produces nonsense output that wastes time during analysis.

Finding Native Libraries In An APK

Finding native libraries starts with unpacking the APK file into a folder for study. The lib folder inside the unpacked app holds subfolders for each supported architecture. Each subfolder holds .so files for that architecture with names that often hint at their purpose. A library named for a media codec likely handles audio or video processing for the app. A library named for a crypto toolkit likely handles encryption and signing operations. A library with a generic name may hold custom app logic that the developer moved to native code. Analysts should list every library and its size and its architecture in project notes. That list becomes a map for deeper native study in later sessions.

Java code points to native libraries through load calls and native method declarations in classes. A load call names the library that the app loads at run time for a feature. A native method declaration names a function that lives in that library rather than in Java code. Analysts should search decoded Java code for load calls to find the libraries in use. They should also search for native declarations to find the functions that matter most. That search links Java features to native functions with a clear trail. A trail like that saves time when a native function needs close study.

Tools For Native Analysis

Disassemblers turn native machine code into assembly instructions that a person can read line by line. A disassembler shows functions and branches and calls in the order that the processor executes them. It also shows strings and data references that a function uses during its work. Analysts use a disassembler to follow a function from entry to exit with care. That close reading reveals logic that a high level view might hide in a complex function. A decompiler builds on disassembly to show a function in a C-like form that reads faster. That form helps an analyst grasp a large function without reading every assembly line.

Specialised native tools add features beyond basic disassembly for Android libraries. A tool can show exported symbols that other code can call from outside the library. A tool can show imported symbols that the library calls from the system or from other libraries. A tool can show the dynamic section that lists dependencies for the library at load time. Analysts should check exported symbols first to find entry points for a feature. They should check imported symbols next to see system services that the library uses. That pair of checks gives a fast overview of a library before deep reading begins.

Reading Native Functions

Reading a native function starts with its signature and its calling convention for the architecture. The calling convention states which registers hold arguments and which register holds the return value. The function prologue sets up a stack frame and saves registers that the function will change. The function body then performs work with registers and memory and calls to other functions. The function epilogue restores saved registers and returns control to the caller with a value. Analysts should learn the calling convention for ARM first because most Android phones use ARM processors. That knowledge turns a wall of assembly into a readable flow of data and decisions.

Control flow in native code appears as branches and jumps to labels within a function. A conditional branch jumps when a test passes and falls through when the test fails. A loop appears as a branch back to an earlier label after work on one item. A switch appears as a jump table that selects a target based on an index value. Analysts should follow branches in order and note the condition for each path. That method reveals the decisions that a function makes for different inputs. It also reveals error paths that a quick skim might miss in a long function.

Strings And Data In Native Code

Strings in native libraries often reveal the purpose of a function before code reading begins. A string that names a file path can point to a config file that the function reads. A string that names a network host can point to a service that the function contacts. A string that names an error message can point to a failure path in the function. Analysts should extract strings from a library and search for terms linked to a feature. That search narrows the code to a small set of functions for close study. A string search alone never proves a function purpose without code confirmation. Code reading must confirm that a function actually uses the string in a meaningful way.

Data structures in native code appear as memory layouts rather than as named classes in Java. A struct holds fields at fixed offsets from a base address in memory. A function reads a field by adding its offset to the base address. Analysts must infer field meanings from how a function uses each offset. That inference takes time and needs several examples from different functions. A field used as a length likely holds a size value for a buffer. A field used as a pointer likely holds an address for data or for another struct. Pattern recognition across functions builds a reliable picture of a struct over time.

unzip -l sample.apk | grep lib/
readelf -d lib/arm64-v8a/libsample.so
objdump -d lib/arm64-v8a/libsample.so | head -n 100
strings lib/arm64-v8a/libsample.so | head -n 100

Dynamic Study Of Native Code

Dynamic study complements static native analysis with live values from a running app. A debugger can pause a native function and show register values at that moment. It can show memory contents at an address that a function uses for data. It can show the call stack that led to the current function during a feature. That live view confirms static guesses about arguments and data flow. It also reveals values that static study cannot know without running the code. Analysts should use dynamic study for functions that resist static reading due to complexity. A complex function becomes clearer when live values show its real behaviour.

Dynamic instrumentation can also hook native functions at run time for observation. A hook can log arguments when a function is called during a feature. It can log the return value when the function finishes its work. It can change an argument to test how the function handles an edge case. That live control helps testers understand native logic without rewriting the library. It also helps testers find a native check that guards a feature. Hooking must stay on a test device with test data for safety. It must never target a live service or another user account during testing.

Choosing Between Static And Dynamic Native Study

Static native study works well when a library is small and its functions are clear in disassembly. A small library with few exported functions can be mapped in a single session of careful reading. A large library with many functions needs a question to focus the work on one feature. Without a question the analyst can spend days reading functions that do not matter for the feature. Set a question before opening a disassembler for a large library. That question keeps the work focused and useful for the project goal.

Dynamic native study works well when static reading stalls on a complex function during analysis. A debugger can show live register values that explain a function that looks opaque in disassembly. A hook can log arguments that reveal the purpose of a function during a feature run. That live evidence often unlocks a static reading that seemed impossible at first. Combine dynamic evidence with static reading for the best result in a hard case. That combination is the standard method for experienced native analysts.

Tool choice for native study should match the architecture and the size of the library file. A tool that supports ARM well is essential for most Android libraries in current phones. A tool that handles large files well is essential for a library with many functions. Test a tool on a small library before using it for a large project. That test reveals limits early and saves time later during the main work.

Readers who want a related topic can explore Ghidra decompiler for native binaries next in this series.

Conclusion

Native shared libraries hold important logic that Java analysis alone cannot explain for many Android apps. They appear in architecture folders inside an APK file and link to Java code through a native bridge. Analysts find them through lib folders and load calls in decoded Java code. They study them with disassemblers and decompilers and string searches in a careful order. They confirm static guesses with debugger and instrumentation work on a test device. That combined method turns machine code into a readable story about a feature. For a decompiler that helps native work see Ghidra decompiler for native binaries.

The steps above follow the approach described in the official developer documentation for Android applications and in widely used testing guidance for mobile software.

RELATED ARTICLES

Most Popular