An ARM64 Android userspace function hooking kernel module, with a small userspace controller. It uses the kernel hardware-breakpoint/perf APIs and does not patch the target process's text pages.
This project is for devices and processes you own or are explicitly authorized to instrument. It is an instrumentation primitive, not a general library injector or anti-detection package.
Thanks to Codex for assisting with this project.
The HWBP design and functionality in this project are based on and adapted
from xiaojianbang8888/xiaojianbang-stealth-hook,
the original APatch/KernelPatch KPM project. Credit for the original work goes
to xiaojianbang8888 and that repository. This repository reworks the HWBP
control path as a standalone ordinary kernel module with a /dev/ahwbp ioctl
interface.
- Per-thread AArch64 execution breakpoints.
- Capture
X0–X7, LR, PC and optionalX0return value. - Replace selected arguments before the original code runs.
- Skip a function and return a configured
X0value. - Skip initial hits or replace an integer return after the original runs.
- Conditional bypass rules such as
X0 == 42. - Enable, disable, query and unhook through
/dev/ahwbpioctl calls. - Live polling, new-thread coverage and optional pointer inspection.
- No KernelPatch/KPM dependency.
- ARM64 Android device with hardware-breakpoint and perf support.
- Root access to load the module and access
/dev/ahwbp. - Matching kernel source, prepared output, and LLVM/Clang toolchain.
- Android NDK AArch64 compiler for
ahwbpctl.
Set these to your prepared kernel source and output directories:
export KERNEL_SRC=/absolute/path/to/android-kernel-source
export KERNEL_OUT=/absolute/path/to/android-kernel-out
export LLVM_BIN=/absolute/path/to/matching-llvm/bin
export PATH="$LLVM_BIN:$PATH"
make module KERNEL_SRC="$KERNEL_SRC" KERNEL_OUT="$KERNEL_OUT" \
ARCH=arm64 LLVM=1This creates kernel/ahwbp.ko. The source, output (including Module.symvers
when CONFIG_MODVERSIONS=y), and LLVM toolchain must match the target kernel.
Build it with the Android NDK (or pass your cross-compiler as CC):
export NDK=/absolute/path/to/android-ndk
export NDK_BIN="$NDK/toolchains/llvm/prebuilt/<host>/bin"
"$NDK_BIN/aarch64-linux-android24-clang" \
-O2 -Wall -Wextra -Iinclude/uapi \
-o userspace/ahwbpctl userspace/ahwbpctl.cReplace <host> with the NDK directory for your machine, such as
darwin-x86_64 or linux-x86_64.
adb push kernel/ahwbp.ko /data/local/tmp/
adb shell su -c 'insmod /data/local/tmp/ahwbp.ko'Check load failures in dmesg:
adb shell su -c 'dmesg | tail -n 80'
adb shell su -c 'cat /proc/modules | grep ahwbp'Resolve an ELF offset relative to a loaded library:
ahwbpctl hook --pid 1234 --so libtarget.so --offset 0x12340Skip the original function and return zero:
ahwbpctl hook --pid 1234 --so libtarget.so --offset 0x12340 \
--replace-ret 0Return a chosen value only when X0 equals 42; otherwise the original
function runs:
ahwbpctl hook --pid 1234 --so libtarget.so --offset 0x12340 \
--replace-ret 0 --match-arg 0=42Modify arguments and let the original function run:
ahwbpctl hook --pid 1234 --so libtarget.so --offset 0x12340 \
--modify-arg 0=0 --modify-arg 1=100Query the most recent register snapshot:
ahwbpctl query --pid 1234 --so libtarget.so --offset 0x12340 \
--listen-retSkip the first ten calls (or start applying the policy at call ten):
ahwbpctl hook --pid 1234 --so libtarget.so --offset 0x12340 \
--skip-hits 10 --listen-ret
ahwbpctl hook --pid 1234 --so libtarget.so --offset 0x12340 \
--nth-hit 10 --replace-ret 0Replace the integer return value after the original function runs:
ahwbpctl hook --pid 1234 --so libtarget.so --offset 0x12340 \
--replace-ret-after 7With return capture enabled, RET is the original X0 and
RET_EFFECTIVE is the value returned to the caller.
Poll existing hooks until interrupted:
ahwbpctl query --pid 1234 --so libtarget.so --offset 0x12340 \
--live --listen-retInstall hooks, follow new threads, optionally inspect pointer arguments, and remove the nodes installed by this command when Ctrl-C is pressed:
ahwbpctl watch --pid 1234 --so libtarget.so --offset 0x12340 \
--listen-ret --dump-arg 0=64When running watch through ADB, allocate a remote PTY so Ctrl-C reaches the
controller:
adb shell -t su -c '/data/local/tmp/ahwbpctl watch --pid 1234 \
--so libtarget.so --offset 0x12340 --listen-ret'Without -t, Ctrl-C may stop only the local ADB client and leave the remote
hooks installed; use unhook manually in that case.
Change a policy without reinstalling the breakpoint:
ahwbpctl set --pid 1234 --so libtarget.so --offset 0x12340 \
--replace-ret 7 --match-arg 0=99hook reports how many per-thread nodes were installed. It does not stream
calls; invoke the target function after installation, then use query to read
the captured entry registers and (with --listen-ret) the return value. Use
watch when you want live output and new-thread polling.
Remove, disable or enable all per-thread nodes for an address:
ahwbpctl unhook --pid 1234 --so libtarget.so --offset 0x12340
ahwbpctl disable --pid 1234 --so libtarget.so --offset 0x12340
ahwbpctl enable --pid 1234 --so libtarget.so --offset 0x12340Absolute addresses are also accepted:
ahwbpctl hook --pid 1234 --address 0x7a0012340 --replace-ret 0- Hardware breakpoint slots are finite per thread; the controller currently accepts at most six addresses, matching common ARM64 Android hardware.
hookcovers current threads.watchpolls for new threads and removes the nodes it installed when interrupted.X0–X7are the supported integer/pointer argument registers. Floating point/SIMD arguments and returns are not decoded.- A bypass is safe only at a true function entry. Bypassing an instruction after a prologue may leave the stack/register state inconsistent.
- Return capture moves the breakpoint to LR and therefore does not observe recursive calls on the same thread while the first call is active.
--dump-arg N=SIZEreads up to 256 bytes from the address inXNthrough/proc/<pid>/mem; it requires permission to read the target.- The breakpoint callback runs in an exception/atomic context, so it does not
call
access_process_vm()or other potentially sleeping memory APIs. - No
/proc/mapshiding, ptrace spoofing, PTE manipulation or userspace library loading is included in this generic project.
This repository intentionally keeps the ordinary-module HWBP core and leaves the KPM-specific stealth and code-rewriting layers out:
| Capability | This project |
|---|---|
| Per-thread ARM64 execution HWBP | Implemented |
| Capture X0-X7, PC and LR | Implemented |
| Return-value capture via LR state machine | Implemented |
| Modify selected X0-X7 arguments | Implemented |
| Skip original and return a chosen X0 | Implemented |
| Conditional argument matching | Implemented here as a kernel policy |
| Hook/query/enable/disable/unhook | Implemented through /dev/ahwbp ioctls |
| Multiple offsets and existing-thread coverage | Implemented by ahwbpctl |
| Automatic coverage for newly created threads | Implemented by watch polling |
| Pointer memory dump and hex/ascii live output | Optional userspace /proc/<pid>/mem reads |
| Nth-hit/skip-count mode | Implemented with --nth-hit/--skip-hits |
| Continuous polling trace and Ctrl+C cleanup | Implemented by query --live and watch |
| Post-return integer X0 override | Implemented with --replace-ret-after |
| Inline callback/trampoline redirection | Not implemented |
| Full hook, DBI decoder/recompiler and clone page | Not implemented |
| PTE/UXN fault redirection | Not implemented |
| Ghost memory allocation/write/free | Not implemented |
/proc/maps hiding |
Not implemented |
ptrace hardware-register spoofing |
Not implemented |
| KPM syscall-285 bridge and supercall loading | Not implemented; this uses a misc device |
include/uapi/ahwbp.h Shared ioctl ABI
kernel/ahwbp.c Normal loadable kernel module
kernel/Kbuild Kbuild fragment
Makefile Optional local build wrapper
userspace/ahwbpctl.c Android userspace controller
userspace/Makefile Userspace build wrapper
LICENSE GNU GPL v2 or later