A teaching-oriented function hooking framework for ARM64 Android. It exposes internals rather than hiding them β every component (trampoline allocation, instruction relocation, GOT patching) is readable and hackable.
What it does:
- Inline hook β patches function prologues with a branch to a replacement, relocates stolen instructions into a trampoline
- PLT/GOT hook β rewrites GOT entries to intercept dynamic symbol calls
- Unified dispatch β a single pipeline routes all hooked calls through before/replace/after handler chains
- Ring-buffer tracer β records enter/leave events with args, return values, and call duration, queryable on-demand
- UDS control channel β interactive
toyhookctlREPL for querying hook status, streaming trace events, and live watch with filtering - Re-entrant safe β recursive hook invocation (e.g. handler calls a function that triggers the same hook) is transparently handled by the dispatch pipeline
What it doesn't do:
- Multi-arch (ARM64 only)
- Thread safety during enable/disable
- Production hardening (no locks, no signal safety)
- Android NDK (set
ANDROID_NDK_ROOT) adbin PATH (for device deployment)- CMake 3.10+
bash scripts/build.sh --ndk $ANDROID_NDK_ROOT --pushadb shell toyhook inject <pid> /data/local/tmp/libtoyhook_payload.so# One-shot commands (for scripting)
adb shell toyhookctl status
adb shell toyhookctl count
adb shell toyhookctl dump
adb shell toyhookctl "watch hook=1"
# Interactive REPL
adb shell toyhookctl
toyhook> status
toyhook> watch hook=1
toyhook> dump
toyhook> quitbash scripts/test.shββββββββββββββββββββββββββββββββββββββββββββββββββββ
β user payload β
β toy_hook_on / toy_commit / ... β
ββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β core/toyhook.c (unified API) β
β session β hook β handler β dispatch pipeline β
β (includes re-entrant depth guard) β
ββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββ€
β inline_hook.c β plt_hook.c β
β (prologue patch) β (GOT rewrite) β
ββββββββββββββββββββββ΄ββββββββββββββββββββββββββββββ€
β arch/arm64/ (backend primitives) β
β emit.c β instruction encoding β
β asm.c β instruction relocation β
β dispatch.c β per-hook assembly stub β
ββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β core/trace.c (ring-buffer event recorder) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β core/server.c (UDS control server) β
β client/toyhookctl.c (CLI control client) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β utils/ mem.c (RWX allocation, near alloc) β
β elf.c (ELF section parsing) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β injector/ (ptrace dlopen injector) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
.
βββ arch/arm64/ ARM64 low-level primitives
β βββ emit.c/h Instruction encoding (STP, LDP_post, B, BL, LDR literal, ...)
β βββ asm.c/h Instruction relocation (PC-relative fixup)
β βββ dispatch.c/h Per-hook assembly stub (save x0-x7, call toy_dispatch)
βββ backend/
β βββ inline_hook.c Inline hook backend
β βββ plt_hook.c PLT/GOT hook backend
βββ core/
β βββ toyhook.c Unified API + dispatch pipeline (re-entrant guard)
β βββ trace.c Ring-buffer trace recorder
β βββ server.c UDS control server (abstract namespace)
βββ client/
β βββ toyhookctl.c CLI client for querying hooks at runtime
βββ include/
β βββ toyhook.h Public API header
β βββ trace.h Tracer API header
β βββ server.h Server context + socket name
β βββ inline_hook.h
β βββ plt_hook.h
βββ injector/
β βββ injector.c ptrace-based remote dlopen injector
βββ payload/
β βββ payload.c Example SO (PLT hook + inline hook + tracer + server)
βββ utils/
β βββ elf.c/h ELF .dynamic parsing (DT_JMPREL, DT_SYMTAB, ...)
β βββ mem.c/h RWX page allocation, near allocation
β βββ log.h Logging macros
βββ tests/ 119 host unit tests
β βββ test_inline_hook.c
β βββ test_plt_hook.c
β βββ test_toyhook.c
β βββ test_trace.c
β βββ android/log.h Mock for __android_log_print
βββ docs/
β βββ usage-guide.md Usage guide (Chinese)
β βββ usage-guide.en.md Usage guide (English)
βββ scripts/
βββ build.sh Cross-compile for Android + optional push
βββ test.sh Build and run host tests
βββ debug.sh Attach logcat for payload debugging
toy_session_t *sess = toy_session_create();
// ... add hooks, attach handlers, commit ...
toy_session_destroy(sess);toy_target_t target = {
.backend = TOY_BACKEND_INLINE,
.by_addr = { .addr = my_function },
};
toy_hook_t *hook = toy_hook_add(sess, &target);toy_target_t target = {
.backend = TOY_BACKEND_PLT,
.by_symbol = { .module = "libc.so", .symbol = "open" },
};
toy_hook_t *hook = toy_hook_add(sess, &target);static int log_args(toy_callctx_t *ctx, void *ud) {
printf("arg0 = 0x%lx\n", ctx->args[0]);
return 0;
}
static int deny_access(toy_callctx_t *ctx, void *ud) {
const char *path = (const char *)ctx->args[0];
if (path && strstr(path, "secret")) {
ctx->ret_val = (uint64_t)-1;
ctx->skip_original = 1;
}
return 0;
}
toy_handler_t h1 = {
.phases = TOY_PHASE_BEFORE,
.priority = 10,
.fn = log_args,
.name = "log_args",
};
toy_handler_t h2 = {
.phases = TOY_PHASE_BEFORE,
.priority = 20,
.fn = deny_access,
.name = "deny_access",
};
toy_hook_on(hook, &h1);
toy_hook_on(hook, &h2);
toy_hook_enable(hook);When a hooked function is called:
- Assembly stub saves
x0-x7, callstoy_dispatch(hook, args, 8) - Re-entrant check: if already dispatching on this thread, skip handlers and call original directly
- BEFORE handlers run (can inspect/modify arguments)
- REPLACE handlers run (can set
skip_originalandret_val) - If
skip_originalis not set, original function is called - AFTER handlers run (can inspect/modify return value)
- Return value goes back to caller
toy_tracer_t *tracer = toy_tracer_create(1024);
toy_tracer_attach(tracer, hook);Events are recorded to a lock-free ring buffer. Each LEAVE event carries a duration (elapsed ns from the matching ENTER). Query at runtime:
toyhookctl dump # dump all recorded events
toyhookctl count # show event count and dropped countThe payload starts a UDS server on an abstract namespace socket. Connect with toyhookctl:
Commands: dump | status | count | watch [hook=N] [tid=N] | help | quit
statusβ print all hooks and their statecountβ show tracer event/dropped countsdumpβ dump all recorded trace eventswatch [hook=N] [tid=N]β live-stream trace events, optional filter by hook id or thread idquitβ disconnect
toyhookctl can run as a one-shot command (toyhookctl dump) or as an interactive REPL (toyhookctl with no args).
uint64_t hits = toy_hook_get_hit_count(hook);
toy_hook_disable(hook);
toy_hook_remove(sess, hook);
toy_commit(sess); // enable all hooks with handlersInline hook trampolines must be within Β±128MB of the target function for ARM64 B/BL instructions. alloc_rwx_near() parses /proc/self/maps, sweeps outward from the target address, and uses mmap with MAP_FIXED_NOREPLACE to claim gaps between existing mappings.
Stolen instructions are not just memcpy'd to the trampoline. PC-relative instructions (ADRP, B, BL, CBZ, CBNZ, TBZ, TBNZ, B.cond, LDR literal) are decoded and their offsets rewritten for the new location.
ARM64 instructions are generated via small encoding functions (emit_stp_pre, emit_ldr_literal, emit_blr, etc.) instead of hardcoded hex. This makes the dispatch stub and trampoline code readable and maintainable.
Each hook gets its own small assembly page that saves argument registers (x0-x7) and frame registers (FP, LR), loads the hook pointer and toy_dispatch address via LDR literal, and calls into C. This avoids a global indirect branch table.
toy_dispatch uses a thread-local depth counter. If a handler (or the original function) triggers the same hook recursively, the nested invocation skips all handlers and calls the original function directly. Users never need to worry about recursive dispatch protection.
On Android, untrusted apps can't create TCP sockets (seccomp) or filesystem sockets (SELinux cross-UID). The control server uses abstract namespace UDS (AF_UNIX with sun_path[0] = '\0') which bypasses both restrictions, allowing adb shell to connect.
119 host-side unit tests covering:
- Instruction encoding and decoding
- PC-relative instruction relocation (all branch types, ADRP, LDR literal)
- Trampoline generation
- Dispatch pipeline (before/replace/after handlers, skip_original, hit count)
- Tracer (enter/leave events, duration tracking, ring buffer)
- PLT/GOT hooking (ELF parsing, GOT patching, roundtrip)
- Near allocation
Tests use a mock-based pattern: .c files are #include'd directly with stubs replacing arch-specific and platform-specific functions.
Educational / research use.