shadowhook

repository·main·Indexed 25 days ago

https://github.com/bytedance/android-inline-hook

A high-performance, stable Android inline hook library designed for production environments. It supports function-level hooking and instruction-level interception on armeabi-v7a and arm64-v8a architectures. The library provides APIs to hook functions via absolute addresses, symbol addresses, or library and symbol names, and supports both Java and Native APIs for configuration and debugging.

Tokens
31.1K
Snippets
54
Records
93
Agent score
79%

What's inside shadowhook

  1. Overview of ShadowHook

    main

    shadowhook is an Android inline hook library designed for production environments. It focuses on stability, backward compatibility (API and ABI), and performance.

    Key features include:

    • Support for armeabi-v7a and arm64-v8a architectures.
    • Support for Android 4.1 to 17 (API level 16 to 37).
    • Support for both hook (function-level) and intercept (instruction-level) operations.
    • Ability to target locations via direct addresses or by using library name + function name.
    • Automatic hooking/intercepting of newly loaded ELFs with optional callbacks.
    • Automatic prevention of recursive calls between proxy functions.
    • Support for bypassing linker namespace restrictions to query symbols in all process ELFs.
    • Compatibility with CFI unwind and FP unwind in proxy/interceptor functions.
  2. Implement proxy functions in MULTI mode

    main

    In SHADOWHOOK_HOOK_WITH_MULTI_MODE, multiple hooks can be applied to the same function, creating a chain of proxy functions.

    Key Requirements:

    • The void **orig_addr parameter cannot be NULL. It is used by ShadowHook to maintain the chain of proxy functions.
    • The memory provided for orig_addr must be persistent and writable (e.g., a global variable, a static variable, or a member of a long-lived object).
    • To call the next function in the chain (which might be another proxy or the real original function), use the address stored in orig_addr.
    • Warning: Do not manually modify the value of orig_addr. ShadowHook modifies this value to manage the hook chain. Even after unhooking, do not set your orig_addr variable to NULL immediately, as other threads might still be executing the proxy and need that address to find the next link in the chain.
    void *orig; // global variable
    void *stub;
    
    typedef void *(*malloc_t)(size_t);
    
    void *my_malloc(size_t sz) {
        if(sz > 1024)
            return nullptr;
    
        // Call the next function in the chain
        return ((malloc_t)orig)(sz);
    }
    
    void do_hook(void) {
        // orig_addr must not be NULL
        stub = shadowhook_hook_sym_addr_2(malloc, my_malloc, &orig, SHADOWHOOK_HOOK_WITH_MULTI_MODE);
    }
    
    void do_unhook(void) {
        shadowhook_unhook(stub);
        stub = NULL;
    }
  3. How to hook a function

    main

    Hooking applies to an entire function. You must provide a proxy function that matches the signature (parameters and return type) of the original function. Inside the proxy, you can perform logic and then call the original function using the pointer returned by the hook mechanism.

    Key functions:

    • shadowhook_hook_sym_name: Hooks a function using its library name and symbol name.
    • shadowhook_unhook: Removes the hook using the returned stub pointer.
    • shadowhook_get_errno / shadowhook_to_errmsg: Used for error handling.
    void *orig = NULL;
    void *stub = NULL;
    
    // Type definition of the hooked function
    typedef void (*artmethod_invoke_func_type_t)(void *, void *, uint32_t *, uint32_t, void *, const char *);
    
    // Proxy function
    void artmethod_invoke_proxy(void *thiz, void *thread, uint32_t *args, uint32_t args_size, void *result, const char *shorty) {
        // do something
        ((artmethod_invoke_func_type_t)orig)(thiz, thread, args, args_size, result, shorty);
        // do something
    }
    
    void do_hook() {
        stub = shadowhook_hook_sym_name(
                   "libart.so",
                   "_ZN3art9ArtMethod6InvokeEPNS_6ThreadEPjjPNS_6JValueEPKc",
                   (void *)artmethod_invoke_proxy,
                   (void **)&orig);
        
        if(stub == NULL) {
            int err_num = shadowhook_get_errno();
            const char *err_msg = shadowhook_to_errmsg(err_num);
            LOG("hook error %d - %s", err_num, err_msg);
        }
    }
    
    void do_unhook() {
        int result = shadowhook_unhook(stub);
    
        if(result != 0) {
            int err_num = shadowhook_get_errno();
            const char *err_msg = shadowhook_to_errmsg(err_num);
            LOG("unhook error %d - %s", err_num, err_msg);
        }
    }
  4. How to Intercept and Unintercept instructions

    main

    Intercepting operates on specific instructions (either the first instruction of a function or an instruction in the middle). This is similar to a debugger breakpoint. When the instruction is reached, an interceptor function is called, allowing you to read or modify CPU registers. After the interceptor returns, execution continues at the intercepted instruction.

    Key functions:

    • shadowhook_dlopen / shadowhook_dlsym: Used to find the address of a symbol.
    • shadowhook_intercept_instr_addr: Intercepts an instruction at a specific address.
    • shadowhook_unintercept: Removes the intercept using the returned stub pointer.
    • SHADOWHOOK_INTERCEPT_WITH_FPSIMD_READ_WRITE: A flag used to specify intercept capabilities.

    Note: Intercept logic is architecture-specific (e.g., different register sets for aarch64 vs aarch32).

    #if defined(__aarch64__)
    void *stub;
    
    // The interceptor receives a CPU context to allow register manipulation
    void artmethod_invoke_interceptor(shadowhook_cpu_context_t *ctx, void *data) {
        // Example: Modify x20 and x21 if x19 is 0
        if (ctx->regs[19] == 0) {
            ctx->regs[20] = 1;
            ctx->regs[21] = 1000;
        }
    
        // Example: Modify SIMD registers
        if (ctx->vregs[0].q == 0) {
            ctx->vregs[0].q = 1;
            ctx->vregs[1].q = 0;
            ctx->vregs[2].q = 0;
            ctx->vregs[3].q = 0;
        }
    }
    
    void do_intercept(void) {
        void *handle = shadowhook_dlopen("libart.so");
        if (handle == NULL) return;
        
        void *sym_addr = shadowhook_dlsym(handle, "_ZN3art9ArtMethod6InvokeEPNS_6ThreadEPjjPNS_6JValueEPKc");
        shadowhook_dlclose(handle);
        if (sym_addr == NULL) return;
    
        // Calculate target instruction address (e.g., offset from symbol)
        void *instr_addr = (void *)((uintptr_t)sym_addr + 20);
        
        stub = shadowhook_intercept_instr_addr(
                   instr_addr,
                   artmethod_invoke_interceptor,
                   NULL,
                   SHADOWHOOK_INTERCEPT_WITH_FPSIMD_READ_WRITE);
    
        if(stub == NULL) {
            int err_num = shadowhook_get_errno();
            const char *err_msg = shadowhook_to_errmsg(err_num);
            LOG("intercept failed: %d - %s", err_num, err_msg);
        }
    }
    
    void do_unintercept() {
        int result = shadowhook_unintercept(stub);
        if (result != 0) {
            int err_num = shadowhook_get_errno();
            const char *err_msg = shadowhook_to_errmsg(err_num);
            LOG("unintercept failed: %d - %s", err_num, err_msg);
        }
    }
    #endif
  5. How intercept works in shadowhook

    main

    The intercept mechanism allows you to target a specific instruction address (either the start of a function or an instruction in the middle of a function). When the targeted instruction is executed, the system calls a user-provided interceptor function.

    Key behaviors:

    • Independence from Hook Mode: Intercepting works regardless of the current hook mode. Multiple interceptors can be added to the same address and will execute sequentially.
    • Register Access: The interceptor receives a shadowhook_cpu_context_t pointer containing all register values.
    • Register Modification: You can read all registers. You can modify all registers except SP (Stack Pointer) and PC (Program Counter); modifications to SP and PC are ineffective.
    • Performance (fpsimd): Accessing fpsimd (floating-point SIMD) registers is expensive. Use the flags parameter to control whether you need to read or write them. If you use the SHADOWHOOK_INTERCEPT_DEFAULT flag, fpsimd values will be random and modifications will not work.
  6. Understand the Operation Record format

    main

    Operation records are ASCII-based, one per line, with fields separated by commas. The fields are ordered as follows:

    1. TIMESTAMP: YYYY-MM-DDThh:mm:ss.sss+hh:mm
    2. CALLER_LIB_NAME: Basename of the caller dynamic library
    3. OP: Operation type (e.g., hook_func_addr, unhook, intercept_sym_name)
    4. LIB_NAME: Target function's library basename (not included for unhook/unintercept)
    5. SYM_NAME: Target function name (not included for unhook/unintercept)
    6. SYM_ADDR: Target instruction or function address (not included for unhook/unintercept)
    7. NEW_ADDR: Proxy or interceptor address (not included for unhook/unintercept)
    8. BACKUP_LEN: Length of instructions covered (in bytes)
    9. ERRNO: Error code
    10. STUB: Stub returned by hook/intercept (used to pair hook/unhook or intercept/unintercept)
    11. FLAGS: flags value
    12. TRACE: Trace data for debugging execution flow

    You can parse these records using the tools/record_parser.py script provided in the repository.

  7. Handle Thumb instructions in target addresses

    main

    When specifying a target address for either hook or intercept that points to a Thumb instruction, you must ensure the address value is odd. Shadowhook uses the parity of the address to distinguish between Thumb and ARM instructions.

    • Addresses obtained via shadowhook_dlsym(), dlsym(), or linker relocation are already odd and require no adjustment.
    • If you calculate the address manually or via memory scanning, you must explicitly ensure it is odd for Thumb instructions.
  8. Understand ShadowHook hook modes

    main

    ShadowHook uses "modes" to manage how proxy functions behave when multiple hooks are applied to the same address. Modes are configured per proxy function during the hook operation, rather than being a global setting. If no mode is specified, the "default hook mode" set during initialization is used.

    There are three modes:

    1. shared mode (Default):

      • Supports multiple independent hooks/unhooks on the same address.
      • Automatically avoids recursive calls between proxy functions.
      • Best for general use where recursion prevention is needed.
    2. multi mode:

      • Supports multiple independent hooks/unhooks on the same address.
      • Does NOT automatically avoid recursive calls.
      • Higher execution performance than shared mode.
      • shared and multi proxy functions can coexist on the same target address.
    3. unique mode:

      • Only one hook can exist at a specific address at a time (must be unhooked before re-hooking).
      • Cannot coexist with shared or multi modes at the same address.
      • Does NOT automatically avoid recursive calls.
      • Execution performance is identical to multi mode.

    Best Practices:

    • For performance-sensitive hook points, consider specifying multi mode during the hook call.
    • For security/privacy monitoring SDKs, evaluate if unique mode is necessary to ensure the proxy function is not interfered with.
  9. Best practices for hooking multiple symbols in a loaded library

    main

    If a target ELF (like libart.so) is already loaded and you need to hook multiple symbols, avoid calling shadowhook_hook_sym_name() repeatedly. Instead, follow this high-performance pattern:

    1. Use shadowhook_dlopen() $\rightarrow$ shadowhook_dlsym() $\rightarrow$ shadowhook_dlclose() to manually resolve the addresses of all required functions.
    2. Call shadowhook_hook_sym_addr_2() for each address.
    3. Use the SHADOWHOOK_HOOK_RECORD flag with the appropriate library and symbol names to ensure operation records are complete.

    Why?

    • Repeatedly calling dlopen/dlclose via the sym_name APIs is slow.
    • sym_name APIs may require reading the file or performing LZMA decompression of .gnu_debugdata for every call, whereas manual resolution is more efficient.
  10. How to Hook and Unhook functions

    main

    Hooking operates on an entire function. You must provide a proxy function that matches the signature (parameters and return type) of the original function. Once hooked, calling the original function requires using the orig pointer captured during the hook process.

    Key functions:

    • shadowhook_hook_sym_name: Hooks a function using its library name and mangled symbol name.
    • shadowhook_unhook: Removes the hook using the returned stub pointer.
    • shadowhook_get_errno / shadowhook_to_errmsg: Used for error handling.
    void *orig = NULL;
    void *stub = NULL;
    
    // Be sure the proxy matches the original function signature
    typedef void (*artmethod_invoke_func_type_t)(void *, void *, uint32_t *, uint32_t, void *, const char *);
    
    void artmethod_invoke_proxy(void *thiz, void *thread, uint32_t *args, uint32_t args_size, void *result, const char *shorty) {
        // do something
        ((artmethod_invoke_func_type_t)orig)(thiz, thread, args, args_size, result, shorty);
        // do something
    }
    
    void do_hook() {
        stub = shadowhook_hook_sym_name(
                   "libart.so",
                   "_ZN3art9ArtMethod6InvokeEPNS_6ThreadEPjjPNS_6JValueEPKc",
                   (void *)artmethod_invoke_proxy,
                   (void **)&orig);
        
        if(stub == NULL) {
            int err_num = shadowhook_get_errno();
            const char *err_msg = shadowhook_to_errmsg(err_num);
            LOG("hook error %d - %s", err_num, err_msg);
        }
    }
    
    void do_unhook() {
        int result = shadowhook_unhook(stub);
        if(result != 0) {
            int err_num = shadowhook_get_errno();
            const char *err_msg = shadowhook_to_errmsg(err_num);
            LOG("unhook error %d - %s", err_num, err_msg);
        }
    }
  11. Best practice for hooking multiple symbols in a loaded ELF

    main

    If you need to hook multiple symbols within a library that is already loaded (like libart.so), avoid calling shadowhook_hook_sym_name() repeatedly. Instead, follow this pattern for better performance:

    1. Use shadowhook_dlopen() $\rightarrow$ shadowshadowhook_dlsym() $\rightarrow$ shadowshadowhook_dlclose() to resolve all required symbol addresses first.
    2. Call shadowshadowhook_hook_sym_addr_2() for each address.
    3. Use the SHADOWHOOK_HOOK_RECORD flag in shadowshadowhook_hook_sym_addr_2() to provide the library name and symbol name for logging/record-keeping.

    Why this is faster: shadowhook_hook_sym_name() performs the dlopen/dlsym/dlclose cycle internally every time it is called. Repeatedly opening and closing libraries is expensive. Furthermore, if symbols are in .symtab, ShadowHook must read the file from disk and potentially perform LZMA decompression, which is significantly optimized by resolving addresses once manually.

  12. How hook and intercept interact

    main

    The interaction between hook and intercept depends on the target address:

    • Function Start Address: If the target is the start of a function, all intercept handlers run first, then the hook proxy runs.
    • Middle of Function: If an intercept targets an instruction in the middle of a function, the interceptor runs, then the original instruction continues. If no hook is present, the flow is direct.
    • End of Function (RET instruction): If an intercept targets the RET instruction at the end of a function, the interceptor runs, but the trampoline's branch to the original logic may not be executed as the function returns to the caller immediately after the RET instruction.

    Note on Hook Chains: In a hook, the proxy function is not strictly required to call the original function (the next link in the chain). The 'true' original function entry point is the trampoline at the end of the hook chain.