Saturday, November 7, 2026
HomeDeveloper Corner, APIs & Reverse EngineeringHow to Hook Android System APIs Using Native Inline C++ Hooks

How to Hook Android System APIs Using Native Inline C++ Hooks

How to Hook Android System APIs Using Native Inline C++ Hooks

Native hooks change what a system function does at the machine code level. An inline hook replaces the first instructions of a function with a jump to your code. Your code can log the arguments, change them, and then call the original function. The technique is powerful for testing your own native code, and it is dangerous when applied carelessly, so this guide starts with safe targets and works up to system APIs only after the basics are solid.

Use this technique only on apps and devices you control. Hooking a system function on a phone you do not own, or inside an app you did not write, can break the app, destabilise the system, and violate the terms of the software involved. Everything below assumes a rooted test device, your own test app, and a willingness to reboot into recovery when a mistake causes a bootloop.

I learned inline hooks the slow way, by crashing a test app a dozen times before the first hook worked. The turning point was understanding the trampoline: the small piece of code that runs the original instructions you replaced, then jumps back to the rest of the function. Once the trampoline made sense, the rest fell into place. This guide follows that same order, because the mechanics matter more than the code.

Understand inline hook mechanics first

An inline hook works at the instruction level. The processor reads the first instruction of a function and follows a jump instead. Your hook code runs first, and it can inspect arguments in registers. It can then call the original function through the trampoline, and after the original function returns, your hook can inspect the result. The hook saves the original instructions it replaces, writes a jump instruction at the start of the target function, and creates a trampoline that runs the saved instructions and then jumps back to the rest of the function.

Instruction size matters because a jump needs enough space. The hook must replace whole instructions and never part of an instruction. Replacing part of an instruction corrupts the code and causes a crash, often far from the hook itself, which makes the failure hard to diagnose. A good hook library handles instruction boundaries for you, and that is one of the strongest reasons to use a tested library rather than writing raw patch code by hand for your first hooks.

How a trampoline preserves the original function

The trampoline is a small block of executable memory that holds the instructions your hook replaced, followed by a jump back to the address just after the patched region. When your hook wants the original behaviour, it calls the trampoline instead of the original function. The trampoline runs the saved instructions, then jumps back into the middle of the original function, and execution continues as if nothing happened. Your hook wraps that call with whatever logging or argument changes you need.

Getting the trampoline right is the whole game. If the saved instructions include a branch that depends on its own address, the trampoline must fix up that branch, or the program will jump to the wrong place. Tested libraries such as Dobby and ShadowHook handle these fixups for ARM64, which is why starting with a library saves days of debugging.

Instruction boundaries on ARM64

ARM64 instructions are all four bytes wide, which makes boundary handling simpler than on x86, where instructions vary in length. An inline hook on ARM64 typically needs to replace at least four instructions to fit a long jump, or it can use a shorter branch when the target is close. Either way, the hook must replace whole four-byte instructions, and it must account for literal pools and PC-relative addressing near the patch site. A library that decodes ARM64 instructions properly will refuse to patch a function that is too short, and that refusal is a feature, not a bug.

Why you should use a tested hook library

Writing a hook from scratch means handling instruction decoding, relocation, cache flushing, and thread safety yourself. Each of those is a separate source of crashes. A library such as Dobby, which supports ARM64 and x86_64 and is widely used in Android research, already handles them. Start with the library, learn the mechanics through it, and only write raw patch code later if you have a specific reason to. For a different angle on runtime modification, dynamic code injection with Frida shows how a higher-level tool achieves similar goals without hand-written patches.

Choose a safe test target

A safe test target lives in your own native library. Build a test app with a native function that logs a value, hook that function, and confirm that the log shows the hook firing. Change the test function to return different values and confirm that the hook sees each value. A controlled target gives you a clear pass or fail signal, which is exactly what you need while you are learning.

System functions are harder targets because they run in many contexts. A system function may be called from many threads at once, and a hook that is not thread-safe can cause crashes in unrelated code. A system function may also be called during early startup, before your hook is ready. Test a system hook only after a custom function hook works reliably, and only on a device you can recover.

Hook your own native function first

Write a small native function such as int add(int a, int b) that logs its arguments and returns the sum. Hook it, log the arguments from the hook, call the trampoline, and log the result. If the log shows the hook firing and the result stays correct, the mechanics work. If the app crashes, the bug is in your hook code, not in the target, and the small target makes the bug easy to find.

Why system functions are harder targets

A system function such as a libc call runs in every process, including system processes that your test app does not control. Hooking it from your app affects only your app’s process, which limits the damage, but the function may be called from signal handlers, from multiple threads, or during library loading. Each of those contexts adds a failure mode. Keep your first system hook to a rarely called function, and add logging so you can see exactly when it fires.

Thread safety in hook code

Your hook code runs in the caller’s thread, so it must be thread-safe. Use atomic operations or a mutex for shared state, keep logging lock-free where possible, and never allocate memory inside a hook that runs in a signal handler. A hook that deadlocks inside a system call will freeze the whole app, and the stack trace will point at innocent code rather than your hook.

Set up the native project with the NDK

Start with a clean Android Studio project that includes native C++ support. The NDK provides the compiler and the headers you need, and CMake wires the native library into the Gradle build. Target arm64-v8a first, since that is the architecture on nearly all modern Android phones, and add other architectures only after the hook works on one.

Put the hook library in your CMake build as a prebuilt static library or as a subdirectory with its own CMakeLists file. Keep the hook code in a separate source file from your test functions, so a crash in the hook does not take your test logic with it. Build the app, install it on your rooted test device, and confirm the native function runs before you add any hook code at all. A stable base makes later troubleshooting far easier, and Android NDK optimizations can help once the hook works, though speed matters less than correctness while you are learning.

CMake and the native library layout

A minimal CMakeLists file declares your library, links it against log and the hook library, and sets the C++ standard to 17 or newer. Keep the file small and explicit. A build system that hides its steps will hide your mistakes too, and hook development already produces enough confusing failures without the build adding more.

Building for arm64-v8a

Set abiFilters to arm64-v8a in your Gradle file while you develop. A single architecture builds faster and removes a whole class of confusion about which binary is actually running. Add armeabi-v7a later if you need it, and test each architecture separately, because instruction encoding and calling conventions differ between them.

Write your first inline hook step by step

Follow these steps in order, and test after each one. A hook that fails at step three is easy to fix. A hook built all at once from five untested pieces is not.

  1. Build the test app with a native function that logs its arguments and returns a known value. Run it and confirm the log appears.
  2. Add the hook library to the CMake build and link it. Rebuild and confirm the app still runs without any hook installed.
  3. Write the hook function. It should log the arguments, call the trampoline, log the result, and return the result unchanged.
  4. Install the hook from JNI_OnLoad or from a native init function called at startup, before the target runs. Confirm the log shows the hook firing.
  5. Change the hook to modify an argument, then confirm the result changes as expected. Change it back, and confirm the original behaviour returns.
  6. Move the hook to a system API only after steps one to five work reliably on your own function.

Saving original instructions before patching

Before writing the jump, the hook must copy the instructions it will replace into the trampoline. The number of instructions depends on the jump size, and the library calculates it by decoding the function prologue. Never patch by hand with a fixed byte count, because a function with a short prologue will not have enough room, and overwriting the next function in memory causes crashes that look unrelated to your hook.

Installing the jump and the trampoline

Installation has three parts: allocate executable memory for the trampoline, write the jump at the target address, and flush the instruction cache so the processor sees the new code. The order matters. Write the trampoline first, then the jump, then flush the cache. If you flush before writing the jump, the processor may execute the old instructions from cache and your hook will appear to do nothing.

Verifying the hook fired

Log from inside the hook function with a unique tag, and check the logcat output while you exercise the target. A hook that never logs either did not install or patched the wrong address. A hook that logs once and then crashes has a trampoline problem. Both failures are normal at this stage, and the log is your best diagnostic tool.

Hooking an Android system API carefully

Hooking a system API starts with finding its address. For exported functions in a loaded library, dlsym gives you the address directly. For internal functions that are not exported, you need the symbol table from an unstripped library, or you need to scan for the function by its byte pattern. Both approaches are fragile across Android versions, because a system library changes between releases and the address you found on Android 14 may not exist on Android 15.

Keep the hook minimal. Log the arguments, call the original, log the result, and return. Resist the urge to add complex logic inside a system hook, because every line of hook code runs in the context of the caller, and a bug there affects the whole process. If you need complex processing, pass the logged data to another thread and process it there.

Finding the function address

Use dlsym(RTLD_DEFAULT, "function_name") for exported symbols, and check the return value before patching. A null result means the symbol is not visible in your process, and patching a null address will crash the app instantly. For hidden symbols, extract the symbol table from the library file on your device with a tool such as nm or readelf, and verify the offset against the loaded base address at runtime.

Dealing with stripped symbols

Most system libraries on production devices are stripped, so their internal functions have no names. In that case, identify the function by its behaviour: call it with known inputs, observe the output, and match the address to a pattern from a matching firmware image. This is slow, version-specific work, and it is the main reason system hooks are harder than hooking your own functions. Document the Android version and the device model for every address you find, because the same address will be wrong on the next update.

Debugging crashes and common mistakes

Crashes during hook development usually come from a small set of causes. The most common is replacing part of an instruction, which corrupts the code stream and crashes at an address that has nothing to do with your hook. The second is forgetting to flush the instruction cache, which makes the processor run stale code. The third is a trampoline that does not fix up a PC-relative branch, which jumps to a wrong address the first time the original function runs.

When a crash happens, read the tombstone. It shows the fault address, the registers, and the backtrace. A fault address inside your trampoline points at the saved instructions. A fault address at the target function points at the patch. A fault address far from both points at a broken branch fixup. That distinction narrows the search quickly, and debugging memory leaks with Profiler can help when the crash is actually a slow leak rather than a bad patch, since a hook that allocates on every call will exhaust memory over time.

Partial instruction replacement

Never assume a function starts with a fixed number of instructions. Decode the prologue properly, or let the library do it. On ARM64, a jump may need four instructions of space, and a function whose prologue is shorter than that cannot be hooked inline. In that case, hook the caller instead, or use a different technique entirely. Forcing the patch anyway is the fastest way to a crash you cannot explain.

Forgetting to flush the instruction cache

ARM processors keep separate caches for instructions and data. Writing a jump updates the data cache, but the processor may still execute the old instructions from the instruction cache. Call __builtin___clear_cache after writing the patch, covering the patched region and the trampoline. Forgetting this step produces the most confusing symptom in hook development: the code looks right, the patch is in memory, and the hook simply never fires.

Frequently asked questions

Is hooking system APIs legal on my own device?

Hooking code on a device you own, for testing software you control, is generally accepted for research and development, but the rules depend on your country and on the software licence involved. Never hook an app to bypass its licence checks, to cheat in a game, or to intercept data that belongs to someone else. If you are unsure, keep your experiments inside your own test app.

Do I need root to install an inline hook?

Inside your own app, no. An app can hook functions in its own process without root, because it already owns that memory. Hooking functions in another process, or hooking a system service, needs root or a framework that provides injection. Start inside your own app, where no root is needed, and add root only when your target demands it.

Why does my hook crash only on some phones?

Different phones run different Android versions and different system libraries, so a function address or a prologue layout that works on one phone can be wrong on another. Test on each Android version you support, and never hardcode an address you found on a single device. Version checks and runtime symbol lookup are not optional extras; they are part of a working hook.

Can I hook a Java method with a native hook?

Not directly. Java methods run in the ART runtime, and their machine code is generated at runtime, so there is no stable function address to patch. For Java methods, use the runtime’s own instrumentation, or a framework built for that purpose. Native inline hooks are for native functions, and mixing the two approaches without understanding ART internals leads to crashes.

What to build next

Once your first hook works on your own function, the next step is a small tracing tool: hook the file and network functions your app uses, log their arguments, and watch how your app actually behaves. That tool will teach you more about your app than any amount of reading, and it stays inside your own process, so it is safe to run every day.

After that, try hooking a system API on your rooted test device, with logging only and no behaviour changes. Watch it fire, check the arguments, and confirm the trampoline preserves the original result. When that works reliably across reboots, you understand inline hooks well enough to use them as a real debugging tool rather than a curiosity.

RELATED ARTICLES

Most Popular