Read this in other languages: English, 中文
Android linker bypass library — access private/internal symbols in system shared libraries (.so) by bypassing linker namespace restrictions.
Starting from Android 7.0 (Nougat), the system linker enforces namespace-based library isolation. Apps can no longer use dlopen/dlsym to access private symbols in system libraries like libc.so, libart.so, or linker64. Calling dlsym on these symbols returns NULL, even though the symbols exist in the library's ELF binary.
This restriction blocks legitimate use cases such as:
- Performance profiling — accessing ART internals to inspect method execution
- Security research — analyzing system behavior at the native level
- Compatibility workarounds — calling private APIs that have no public alternatives
- Reverse engineering — understanding system library behavior for debugging
AndLinker restores access to all symbols in any loaded .so file, regardless of linker namespace restrictions.
On Android 7.0+, the standard dlopen checks the caller's namespace and rejects libraries outside the app's allowed list. AndLinker bypasses this by:
- Locating the linker's internal
dlopenfunction — usesadlsymto find symbols like__loader_dlopen(API 28+),__dl__Z9do_dlopenPKciPK17android_dlextinfoPv(API 24-27), or__dl__Z20__android_dlopen_ext...(API 26-27) inside the linker binary itself - Calling it with a fake caller address — the linker determines namespace membership based on the caller's address; by passing an address from
libc(which belongs to the system namespace), the restriction is bypassed - Falling back to standard
dlopen— if the bypass fails (e.g., on future Android versions), it gracefully falls back
Standard dlsym only searches .dynsym (dynamic symbol table) and respects namespace visibility. AndLinker implements its own symbol lookup with three levels:
- Hash table lookup — parses
DT_GNU_HASHorDT_HASHfrom the library's dynamic section for O(1) symbol lookup in.dynsym(same algorithm as the system linker, but without namespace filtering) .dynsymlinear scan — falls back to iterating the dynamic symbol table if hash lookup fails.symtabfile-based scan — reads the full symbol table from the ELF file on disk viammap, which includes static/local symbols not in.dynsym(these are the "private" symbols that are completely invisible to standarddlsym)
C++ mangled names (e.g., _ZN3art9ArtMethod12PrettyMethodEb) vary across Android versions due to different compiler versions, overloads, or parameter types. adlsym_match solves this by:
- Trying exact match first
- Demangling each symbol using
__cxa_demangle(e.g., →art::ArtMethod::PrettyMethod(bool)), then substring matching against the demangled name - Falling back to raw mangled name substring matching
This allows searching with readable patterns like "ArtMethod::PrettyMethod" instead of exact mangled names.
All public APIs are protected by a recursive mutex, ensuring safe concurrent access from multiple threads.
adlopen/adlclose— Open and close shared libraries, bypassing linker restrictions on Android 7.0+adlsym— Resolve symbols (including private/internal ones) from loaded librariesadlvsym— Resolve versioned symbols (Android 7.0+)adlsym_match— Fuzzy symbol lookup with__cxa_demanglesupport (search by C++ readable name)adladdr— Get symbol information for a given address (likedladdr)adlerror— Get last error message (likedlerror)adl_iterate_phdr— Iterate over program headers of all loaded librariesadl_enum_symbols— Enumerate all symbols in a library (.dynsym + .symtab)
- Minimum SDK: API 21 (Android 5.0)
- Target SDK: API 34 (Android 14)
- Architectures: armeabi-v7a, arm64-v8a, x86, x86_64
- Tested on: Android 6.0 — 16 (API 23 — 36)
Add the modules to your project and declare dependencies:
dependencies {
implementation project(':andlinker') // symbol resolution, linker bypass
implementation project(':andhooker') // PLT hook + inline hook (depends on andlinker)
}#include <adl.h>
// Open a library (supports both full path and basename)
void *handle = adlopen("libc.so", 0);
// Resolve a private symbol
void *sym = adlsym(handle, "__openat");
// Fuzzy match by demangled C++ name
void *sym2 = adlsym_match(handle, "ArtMethod::PrettyMethod", NULL, NULL);
// Resolve a versioned symbol
void *sym_v = adlvsym(handle, "symbol_name", "LIBC");
// Enumerate all symbols
adl_enum_symbols(handle, [](const char *name, void *addr, size_t size,
int type, void *arg) -> int {
// process each symbol
return 0; // 0 = continue, non-zero = stop
}, NULL);
// Get symbol info by address
Dl_info info;
adladdr(some_addr, &info);
// Check errors
if (sym == NULL) {
const char *err = adlerror(); // returns error message, clears it
}
// Iterate loaded libraries
adl_iterate_phdr(callback, user_data);
// Close handle
adlclose(handle);andhooker is a companion module that provides PLT/GOT hook and inline hook capabilities, built on top of AndLinker's symbol resolution.
PLT hook intercepts function calls from a specific library by modifying its GOT (Global Offset Table) entries.
app calls strlen()
→ PLT stub in libsample.so
→ GOT entry (originally points to libc strlen)
→ [HOOKED] GOT entry now points to your proxy function
→ proxy calls original via saved pointer
Characteristics:
- Only affects calls from the specified library (other libraries still call the original)
- Safe for high-frequency functions (strlen, memcpy, etc.) — only one module affected
- Can modify parameters, return values, or block calls
- No instruction relocation needed — just a pointer swap
Inline hook patches the function entry directly, affecting all callers across the entire process.
Any code calls gettimeofday()
→ function entry (first 16 bytes replaced with jump)
→ Hub assembly trampoline
→ Check TLS recursion state
→ First call: jump to proxy function
→ Recursive call: jump to original (via trampoline)
→ proxy executes, calls orig via trampoline
→ Hub epilogue: pop recursion state, return to caller
Characteristics:
- Affects ALL callers in the process (global interception)
- Instruction relocation engine handles PC-relative instructions in trampoline
- Hub mechanism provides automatic recursion prevention (no user code needed)
- FORTIFY auto-detection: framework detects
__xxx_chkwrappers and adjusts target
adlopen(caller_lib)→ parse dynamic section viaadl_prelink_image- Iterate
.rela.plt/.rel.pltentries to find target symbol - Calculate GOT entry address:
load_bias + relocation.r_offset mprotectGOT page to writable → write new function pointer → restore protection- Save original pointer for unhook and
orig_funccallback
When user hooks strlen, the framework:
- Reverse-lookups symbol name via
adladdr - Checks if
__strlen_chkexists in the same library (pattern:__<name>_chkor__<name>_2) - If found, hooks the FORTIFY wrapper instead;
orig_funcpoints to the raw function
This prevents FORTIFY abort: __strlen_chk validates strlen's return value, so hooking raw strlen with a modified return triggers a security check. Hooking __strlen_chk directly bypasses this.
ARM64 security feature: CPU verifies branch targets have BTI instructions. If a function starts with HINT #34 (BTI), the hook patches after the BTI instruction, and the trampoline includes BTI at its entry.
The first 16 bytes of the target function are overwritten with a jump. The original instructions are moved to a trampoline with PC-relative fixups:
| ARM64 Instruction | Relocation Method |
|---|---|
| B / BL | → Absolute jump (LDR X17 + BR X17) |
| B.cond | → Invert condition skip + absolute jump |
| CBZ / CBNZ | → Same pattern |
| TBZ / TBNZ | → Same pattern |
| ADRP | → LDR Xd from literal pool |
| ADR | → LDR Xd from literal pool |
| LDR literal (all variants) | → Load address + indirect load |
| Other instructions | Direct copy (no relocation needed) |
ARM32 (ARM + Thumb) and x86/x86_64 relocators are also included.
The hub prevents infinite recursion when a proxy function indirectly re-enters the hooked function.
Architecture (ARM64):
The hub is an assembly template (adl_hub_arm64.S) compiled by the assembler for correct instruction encoding, then copied to mmap'd executable memory at runtime:
- Hub entry: Saves all parameter registers (x0-x8, q0-q7, LR), calls
adl_hub_push()in C - Push logic: Checks per-thread TLS frame stack — if
orig_addralready present, it's recursive → return trampoline address; otherwise push frame → return proxy address - Hub entry (cont): Restores all registers, sets LR to hub return address, jumps to decision result
- Proxy executes: User code runs normally, any re-entrant calls go through hub again (detected as recursive)
- Hub return: Proxy returns here, saves return values, calls
adl_hub_pop(), restores return values, returns to original caller
TLS Stack:
- Pre-allocated pool of 128 thread stacks (lock-free atomic allocation)
- Each thread stack holds up to 16 recursion frames
pthread_key_tfor automatic cleanup on thread exit
Shared Hub Pages:
- Multiple hub slots share a single 4KB mmap page (256 bytes per slot, 16 slots per page)
- Reduces memory from 4KB per hook to ~256 bytes per hook
Note: Hub is currently implemented for ARM64 only. On ARM32/x86/x86_64, inline hooks fall back to direct proxy jumps without automatic recursion prevention.
The same function can be hooked multiple times by different modules. Each hook adds a proxy to the chain:
function entry → hub → proxy_C (newest)
↓ orig_C calls
proxy_B
↓ orig_B calls
proxy_A (oldest)
↓ orig_A calls
trampoline → original function
Each adl_inline_hook call on an already-hooked function adds a new proxy to the head of the chain. The orig_func returned to each caller points to the next proxy (or trampoline for the first hook).
By default, the hub blocks recursive calls to prevent infinite loops. For scenarios where legitimate recursion is needed (e.g., thread pools, recursive algorithms), reentrant mode can be enabled per hook point:
adl_inline_hook(target, my_proxy, &orig);
adl_inline_hook_allow_reentrant(target); // recursive calls now go through proxy
adl_inline_hook_disallow_reentrant(target); // back to default (block recursion)Hook installation uses ordered writes with memory barriers to prevent crashes from concurrent execution:
- Write target address (bytes 8-15) first
- Memory barrier (
dmb ishon ARM,mfenceon x86) - Write jump instruction (bytes 0-7) — atomically activates the hook
Result: concurrent threads either execute complete old code or complete new hook, never partial/corrupted instructions.
#include <adl_hook.h>
// --- PLT Hook ---
// Hook close() calls from libsample.so only
static int (*orig_close)(int) = NULL;
int my_close(int fd) {
log("closing fd=%d", fd);
return orig_close(fd);
}
adl_plt_hook("libsample.so", "close", my_close, &orig_close);
adl_plt_unhook("libsample.so", "close");
// --- Inline Hook ---
// Hook gettimeofday() globally (all callers affected)
static int (*orig_gettimeofday)(struct timeval*, struct timezone*) = NULL;
int my_gettimeofday(struct timeval *tv, struct timezone *tz) {
int ret = orig_gettimeofday(tv, tz); // call original
if (ret == 0) tv->tv_sec += 86400; // add 1 day
return ret;
}
void *target = adlsym(adlopen("libc.so", 0), "gettimeofday");
adl_inline_hook(target, my_gettimeofday, (void**)&orig_gettimeofday);
adl_inline_unhook(target);Safe to modify return values for:
- Low-frequency functions:
gettimeofday,localtime,atoi,access, etc. - Functions not called by system infrastructure (JIT, GC, malloc)
Unsafe to modify return values for (via raw function inline hook):
strlen,memcpy,memset,malloc,free— called by every thread including JIT/GC; modified return values corrupt heap across the entire process- Note: hooking
__strlen_chkdirectly is safe because you replace the FORTIFY check itself
For high-frequency global functions:
- Use PLT hook (only affects one module) to safely modify return values
- Use inline hook in observe-only mode (transparent pass-through) for global interception
- Or hook the
__xxx_chkFORTIFY wrapper directly (bypasses FORTIFY validation)
Android's _FORTIFY_SOURCE replaces many libc functions with checked versions at compile time:
| Function | FORTIFY Wrapper | PLT Hook Target |
|---|---|---|
strlen |
__strlen_chk |
__strlen_chk |
strcpy |
__strcpy_chk |
__strcpy_chk |
memcpy |
__memcpy_chk |
__memcpy_chk |
sprintf |
__sprintf_chk |
__sprintf_chk |
snprintf |
__vsnprintf_chk |
__vsnprintf_chk |
open |
__open_2 |
__open_2 |
adl_inline_hook auto-detects FORTIFY wrappers at runtime. When you pass strlen's address, the framework automatically hooks __strlen_chk instead, and orig_func returns the raw strlen pointer (matching the original signature).
ARM64 inline hook overwrites 16 bytes at the function entry. Functions shorter than 16 bytes (e.g., atol 12 bytes, strptime 8 bytes, getopt_long_only 8 bytes) risk overwriting adjacent functions.
In practice, ARM64 compilers align function entries to 16-byte boundaries, so short functions are followed by padding bytes (udf #0 or nop). This makes the overflow land on padding rather than real code. Testing confirms atol (12 bytes + 4 bytes padding) can be hooked successfully.
However, the framework conservatively rejects inline hooks on short functions (adl_inline_hook returns -1) because:
- Custom
.sofiles or non-standard linker scripts may not have 16-byte alignment - Different compilers or optimization levels may eliminate padding
- Safety is preferred over relying on alignment assumptions
For short functions, use PLT hook instead (no size restriction — only modifies a GOT pointer).
MIT License. See LICENSE for details.
Requires Android NDK and CMake 3.10.2+.
# Build all modules
./gradlew assembleRelease
# Build individually
./gradlew :andlinker:assembleRelease
./gradlew :andhooker:assembleRelease
./gradlew :sample:assembleDebug