ByteHook Documentation

repository·main·Indexed 25 days ago

https://github.com/bytedance/bhook

A high-performance, stable Android PLT (Procedure Linkage Table) hook library for production use. ByteHook allows developers to intercept function calls across dynamic libraries by modifying the Global Offset Table (GOT) of the caller. It provides a C API and Java wrapper with support for single, partial, and global hooking, as well as Automatic and Manual operation modes. The library depends on ShadowHook and includes features for managing debug logs, operation records, and custom library loaders via ConfigBuilder.

Tokens
11.4K
Snippets
24
Records
45
Agent score
82%

What's inside ByteHook

  1. What is ByteHook and its key features?

    main

    ByteHook is an Android PLT (Procedure Linkage Table) hook library designed for production environments. Unlike inline hook libraries (like shadowhook), ByteHook focuses on stability, backward compatibility, and performance.

    Key Features:

    • Compatibility: Supports Android 4.1 to 17 (API 16 - 37) and architectures armeabi-v7a, arm64-v8a, x86, and x86_64.
    • Non-conflicting Hooks: Multiple hooks and unhooks on the same function do not conflict.
    • Granular Control: Can hook single, partial, or all dynamic libraries in a process, including automatically hooking newly loaded libraries.
    • Safety: Automatically avoids recursive or circular calls between proxy functions.
    • Observability: Supports stack trace retrieval within proxy functions.
  2. Understand the operation record data format

    main

    Operation records are returned as a string where each line represents a single operation. Fields are separated by commas (,) and lines end with a newline (\n).

    Record Types

    1. hook: A successful hook operation.
    2. unhook: A successful unhook operation.
    3. error: Indicates the ByteHook recording module itself encountered an issue and cannot continue.

    Data Field Mapping

    #Field NameDescription
    1TimestampYYYY-MM-DDThh:mm:ss.sss+hh:mm
    2Caller LibBasename of the library performing the operation
    3Op Typehook, unhook, or error
    4Target Lib(Hook only) Basename of the target library
    5Sym Name(Hook only) Name of the target function
    6New Addr(Hook only) Address of the proxy function
    7ErrnoError code
    8StubPointer value used to pair hook/unhook operations

    Example Output

    2021-11-05T15:20:27.767+08:00,libbytehooksystest.so,hook,libappfuse.so,writev,78ace73fb0,0,76891db690
    2021-11-05T15:21:40.226+08:00,libbytehooksystest.so,unhook,0,76891db690
    9999-99-99T00:00:00.000+00:00,error,error,0,0
  3. Call the original function in a proxy using BYTEHOOK_CALL_PREV

    main

    When implementing a proxy function, use the BYTEHOOK_CALL_PREV macro to call the original function. Do not attempt to call the original function directly by its name, as this bypasses the hook chain.

    Behavior for multiple hooks: ByteHook executes proxy functions in reverse order of registration for the same function in the same ELF. If you register Hook A then Hook B, Hook B's proxy will execute first.

    Note on skipping the original function: You can choose not to call the original function (e.g., to block a call). However, doing so prevents other SDKs registered at the same hook point from receiving the call. If multiple SDKs need to coexist, ensure the one that might skip the original function is registered first.

    // C++ usage: first argument is the current proxy function address, followed by arguments
    size_t my_strlen(const char* const str)
    {
        BYTEHOOK_STACK_SCOPE();
        size_t result = BYTEHOOK_CALL_PREV(my_strlen, str);
        return result;
    }
    
    // C usage: first argument is proxy address, second is the function signature/type, followed by arguments
    typedef size_t (*strlen_t)(const char* const);
    size_t my_strlen(const char* const str)
    {
        size_t result = BYTEHOOK_CALL_PREV(my_strlen, strlen_t, str);
        BYTEHOOK_POP_STACK();
        return result;
    }
  4. How Android PLT Hook works

    main

    PLT Hooking works by intercepting the relocation process performed by the dynamic linker. Instead of modifying the ELF file on disk, it modifies the data in memory at runtime.

    The Process:

    1. Identify Symbol: Use the symbol name to find corresponding symbol information in the hash table (e.g., .hash or .gnu.hash) and the .dynsym table.
    2. Locate PLT/Relocation Info: Find the relevant relocation information in sections like .rel.plt, .rela.plt, .rel.dyn, or .rela.dyn.aps2.
    3. Find Absolute Address: Locate the actual address storage in the GOT table (.got.plt) or data sections (.data, .data.rel.ro).
    4. Modify Address: Replace the original absolute address with the address of your "proxy function."

    Critical Implementation Details:

    • Memory Permissions: Since the linker sets .got.plt and .data.rel.ro to read-only after relocation, you must use mprotect to set the memory page to "writable" before modification.
    • Cache Coherency: After modifying the address, you must call __builtin___clear_cache to clear the CPU cache so the change takes effect immediately.
  5. How ByteHook monitors dynamic library loading (DL monitor)

    main

    ByteHook uses a DL monitor module to track the loading and unloading of shared libraries (.so files) in Android. It hooks dlopen, android_dlopen_ext, and dlclose to achieve the following:

    1. Immediate Hooking: As soon as a new .so is loaded into memory, ByteHook can immediately execute pre-defined hook tasks.
    2. Safe Unloading: When a .so is being unloaded via dlclose, ByteHook uses internal read-write locks to synchronize with the "ELF cache and hook execution module." This ensures that a library currently being hooked is not unloaded mid-process.

    Android Version Compatibility

    ByteHook handles the evolution of Android's linker and library loading restrictions:

    • Android 7.0: To bypass restrictions preventing apps from calling dlopen on system libraries, ByteHook calls internal linker functions instead of the standard dlopen/android_dlopen_ext.
    • Android 8.0+: ByteHook hooks functions directly within libdl.so (__loader_dlopen and __loader_android_dlopen_ext), allowing it to call the original functions directly.
    // Internal linker functions used for Android 7.0 bypass:
    __dl__ZL10dlopen_extPKciPK17android_dlextinfoPv
    __dl__Z9do_dlopenPKciPK17android_dlextinfoPv
    __dl__Z23linker_get_error_bufferv
    __dl__ZL23__bionic_format_dlerrorPKcS0_
    
    // Functions hooked in libdl.so for Android 8.0+:
    __loader_dlopen
    __loader_android_dlopen_ext
  6. How Automatic and Manual modes work

    main

    ByteHook provides two modes for managing hook points:

    1. Automatic Mode (ByteHook.Mode.AUTOMATIC):

      • Mechanism: Uses a combination of trampolines and a proxy list to manage multiple proxy functions for the same hook point.
      • Benefits: Unhooking one proxy function does not affect others. It automatically prevents recursive calls and circular calls between proxy functions.
      • Recommendation: Always use Automatic mode for production SDKs.
    2. Manual Mode (ByteHook.Mode.MANUAL):

      • Mechanism: Directly modifies the GOT (Global Offset Table) of the hook point to return the original function address to the caller.
      • Risks: If another SDK hooks the same point, unhooking can lead to "proxy function loss."
      • Recommendation: Use only for local testing in special circumstances.
  7. How ByteHook's trampoline mechanism works

    main

    Unlike simple PLT hook solutions (like xHook) that replace addresses in the .got.plt table, ByteHook uses a trampoline mechanism to support multiple proxy functions for a single hook point and safe unhooking.

    Key Features

    • Multiple Proxies: Instead of a chain of calls where unhooking one proxy breaks the chain, ByteHook writes the address of a management entry function into the GOT table. This entry function maintains a list of all active proxy functions and iterates through them.
    • Trampoline (Shellcode): ByteHook uses mmap and mprotect to create a "trampoline" (shellcode) that performs the actual jump to the proxy functions at runtime.
    • Circular Call Detection: To prevent infinite loops, the trampoline records the execution stack of proxy functions. If a proxy function is detected to be already in the current execution stack, the system identifies a "circular call," ignores the rest of the chain, and executes the original function directly.

    Performance Considerations

    Because the proxy list is frequently traversed in multi-threaded environments, the implementation is designed to be lock-free to minimize performance overhead.

  8. Implement a proxy function and handle stack cleanup

    main

    When hooking a function, you must provide a proxy function.

    Crucial Requirements:

    1. Stack Cleanup: You MUST perform stack cleanup to prevent memory corruption.
      • In C++: Call BYTEHOOK_STACK_SCOPE(); at the beginning of the function.
      • In C: Call BYTEHOOK_POP_STACK(); at every return branch.
    2. Calling the Original Function: Use the BYTEHOOK_CALL_PREV macro to call the original function. Do not call the function by its original name directly.

    C++ Example:

    size_t my_strlen(const char* const str) {
        BYTEHOOK_STACK_SCOPE(); // Mandatory
        // ... pre-logic ...
        size_t result = BYTEHOOK_CALL_PREV(my_strlen, str);
        // ... post-logic ...
        return result;
    }

    C Example:

    size_t my_strlen(const char* const str) {
        size_t result;
        if(condition) {
            BYTEHOOK_POP_STACK(); // Mandatory for every return
            return 0;
        }
        result = BYTEHOOK_CALL_PREV(my_strlen, strlen_t, str);
        BYTEHOOK_POP_STACK(); // Mandatory
        return result;
    }
    size_t my_strlen(const char* const str)
    {
        BYTEHOOK_STACK_SCOPE();
    
        __android_log_print(ANDROID_LOG_DEBUG, "tag", "pre strlen");
    
        size_t result = BYTEHOOK_CALL_PREV(my_strlen, str);
    
        __android_log_print(ANDROID_LOG_DEBUG, "tag", "post strlen");
    
        return result;
    }
  9. Understand the difference between Inline Hook and PLT Hook

    main

    ByteHook supports or discusses two primary methods for native hooking on Android:

    1. Inline Hook:

      • Pros: Extremely powerful; can hook almost any location.
      • Cons: Can have stability issues and lacks large-scale online verification in many open-source implementations.
    2. PLT Hook:

      • Pros: High stability and controllable; suitable for full-scale production use.
      • Cons: Limited to function calls that jump through the Procedure Linkage Table (PLT).

    In production environments, it is common to use both techniques simultaneously to leverage their respective strengths.

  10. How ByteHook's hook mechanism works

    main

    ByteHook is a "caller-based" hook solution. It modifies the Global Offset Table (GOT) of the caller (the library making the call) rather than the callee (the library being called).

    Key Concepts:

    • Task-based management: Every hook request is a "task". Tasks are managed via a bytehook_stub_t returned upon successful creation, which is used for later unhook operations.
    • Synchronous execution: All hook functions are synchronous; ByteHook does not create additional threads.
    • Automatic Task Completion:
      • hook_single: If the caller is already loaded, it hooks immediately. If not, ByteHook waits and hooks it automatically when it is loaded.
      • hook_partial: Uses a filter to match callers. It automatically hooks any new ELF that matches the filter upon loading.
      • hook_all: Automatically hooks every currently loaded ELF and every ELF loaded in the future.
    • Pathname vs Basename:
      • pathname: Absolute path starting with / (e.g., /system/lib/libc.so).
      • basename: Filename only (e.g., libc.so).
      • Warning: On Android 5.x/6.x, dl_iterate_phdr() may return the basename. Users must handle both pathname and basename in filters and callbacks to ensure compatibility.
  11. Understand CFI (Control Flow Integrity) compatibility

    main

    On Android 8.1 and later, some arm64 and x86_64 system libraries use LLVM's CFI mechanism. This mechanism validates the legitimacy of addresses in the GOT (Global Offset Table) at runtime. Modifying GOT values directly can cause the program to crash.

    ByteHook attempts to bypass CFI detection when encountering such ELFs. If this bypass fails (returning status code 22), ByteHook will not be able to proceed with hooking that specific ELF to prevent a crash.

  12. Initialize ByteHook in Native code

    main

    To use ByteHook in a pure Native process, call bytehook_init(). In most cases, you should instead use the Java-layer initialization function, which calls this native function automatically.

    Modes:

    • BYTEHOOK_MODE_AUTOMATIC (0): Automatic mode.
    • BYTEHOOK_MODE_MANUAL (1): Manual mode.
    #include "bytehook.h"
    
    #define BYTEHOOK_MODE_AUTOMATIC 0
    #define BYTEHOOK_MODE_MANUAL    1
    
    // Initialize ByteHook
    int bytehook_init(int mode, bool debug);