Skip to content

Rprop/JHook

Repository files navigation

JHook

JHook is an Android library for ARM64 (AArch64) inline hooks on native code.

The hook engine is implemented entirely in Java (rprop.java.jhook): JCodeGen / JAssembler generate AArch64 machine code at runtime, JPatch patches the process via /proc/self/mem, and JMem executes stub pages from mmap.

Package rprop.java.jhook
Gradle namespace rprop.java.jhook
minSdk 24
compileSdk 36
ABI ARM64 only (hooks native code in the same process)
Implementation Pure Java; no bundled native library

Capabilities

  • JHook.install(target, JCallback) — inline patch at target (see Hook site below).
  • onHook returns false — restore saved registers, run trampoline (relocated overwritten insns), then continuePC (rest of the function).
  • onHook returns true — restore and RET to the caller (skip trampoline and tail); set return value with ctx.setX0(...) (and setLR if needed).
  • Hook site — any 4-byte-aligned address inside a native function, not only the symbol entry.
  • BTI / PAC — if the first overwritten word at target is BTI/PAC, the patch skips it; trampoline NOPs those slots and relocates PC-relative insns in the backed-up range.
  • Patch — 4-byte B when within ±128 MB of the bridge; else MOVZ/MOVK + BR x16 (16 bytes).
  • SymbolsJElf.findSymbol / dlopen via /proc/self/maps; RTLD_FILE (on-disk ELF) or RTLD_MEMORY (PT_DYNAMIC in the mapping).
  • MapsJMaps.forEach / findFirst with a callback over /proc/self/maps lines.
  • Bridge saves (optional) — GPR (256 B), D0–D31, Q0–Q31 (VR, exclusive with FPR), NZCV.

Hook site

target passed to install is the virtual address of the instruction to patch, not necessarily the function entry from JElf.findSymbol.

long target = JElf.findSymbol("libc.so", "open");
// target += 4;   // same function, one instruction later (must be 4-byte aligned)
JHook.install(target, callback);

What happens:

  1. JPatch.peekBytes reads up to MAX_PEEK bytes from target.
  2. If the first word is BTI/PAC (typical at function entry), it is left in place; the branch patch starts after it.
  3. Otherwise the patch starts exactly at target (mid-function hook).
  4. originalBytesSize bytes are copied out, relocated into the trampoline, then execution continues at continuePC = target + originalBytesSize.

Requirements and limits:

Topic Detail
Alignment ARM64 instructions are 4 bytes — do not hook inside an instruction.
PC-relative insns In the overwritten range must be relocatable; otherwise install throws.
Trampoline size Relocated code must fit in 512 B (TRAMPOLINE_SIZE).
Register context Mid-function hooks see CPU state at that point (not necessarily “fresh” entry arguments).
BTI/PAC Only auto-skipped when they are the first word at target.

Source layout

jhook/
├── jhook/
│   ├── build.gradle
│   ├── consumer-rules.pro
│   └── src/main/java/rprop/java/jhook/
│       ├── JHook.java
│       ├── JCallback.java
│       ├── JContext.java
│       ├── JCodeGen.java
│       ├── JAssembler.java
│       ├── JPatch.java
│       ├── JMem.java
│       ├── JElf.java
│       ├── JMaps.java
│       ├── JArtMethod.java
│       ├── JDisassembler.java
│       └── JLog.java
├── demo/          # sample app (may be gitignored locally)
└── tools/

Execution flow

  caller
    ▼
  … instructions …
    ▼
  hook site [BTI/PAC?] + B or MOV+BR → bridge
    ▼
  bridge: save (optional) → JNI → JHook.callback → CBNZ x0
         → false: restore → trampoline → continuePC → rest of function
         → true:  restore → RET (intercept)

RX page per hook

Region Offset Size (max) Role
Bridge page.base 1280 B Save/restore, JNI, intercept branch
Trampoline base + 1280 512 B Relocated overwritten insns → continuePC

mmap hint is near target so a 4-byte B can reach the bridge when possible.

RW page after JHook.init()

Offset Field
0 JavaVM*
8 JavaVM->functions
16 jmethodIDJHook.callback(long, long)
24 global ref — JHook class

Init RX stub is unmapped after bootstrap.


Usage

Dependency

dependencies {
    implementation project(':jhook')
}

init()

Call once before install(). Internally the library builds a temporary init stub, runs it through JArtMethod to fill the RW page (JavaVM, jmethodID for JHook.callback, global ref), then unmaps the stub.

import rprop.java.jhook.JHook;
import rprop.java.jhook.JElf;
import rprop.java.jhook.JCallback;
import rprop.java.jhook.JContext;

JHook.init();

install()

long target = JElf.findSymbol("libc.so", "open");
if (target == 0) {
    throw new UnsatisfiedLinkError("symbol not found");
}

JCallback hook = new JCallback() {
    @Override
    public boolean onHook(final JContext ctx) {
        // return false → run original via trampoline
        // return true  → intercept; ctx.setX0(...) for return value
        return false;
    }
};

JHook.HookInfo info = JHook.install(target, hook);
if (info == null) {
    throw new IllegalStateException("install failed");
}

Flagsinstall(target, callback, preserveGPR, preserveFPR, VR, preserveCSR):

Flag Effect
preserveGPR 256-byte x0x28, LR, SP snapshot (JContext)
preserveFPR save/restore D0–D31
VR save/restore Q0–Q31 (not with preserveFPR)
preserveCSR save/restore NZCV

Default: install(target, callback) → GPR only.

Same target twice returns the existing HookInfo.

uninstall()

JHook.uninstall(target);

Restores HookInfo.layout.originalBytes and munmap the hook RX page.


Demo

The demo module (rprop.java.jhook.MainActivity) shows:

  1. JHook.init()
  2. Resolve libc.so open, optional target += 4 for mid-function hook
  3. install with return true and setX0(0) to intercept open
  4. FileInputStream("/proc/self/maps") to trigger the hook
  5. uninstall
./gradlew :demo:installDebug

JCallback / JContext

public abstract boolean onHook(final JContext context);

JHook.callback(long targetAddress, long snapshotSp) looks up HookInfo and returns onHook(...); the bridge CBNZ uses that boolean for the intercept path.

JContext Description
info JHook.HookInfo
registerBuffer JMem.wrap(snapshotSp, …) when preserveGPR is enabled

Registers: X0X28, FP, LR, SPgetLong/setLong, getXn/setXn, or named accessors.


API reference

JHook

Method Description
init() Bootstrap RW page; register JNI callback
install(target, callback) Hook with GPR snapshot
install(…, preserveGPR, preserveFPR, VR, preserveCSR) Hook with optional save/restore
uninstall(target) Restore bytes; free RX page
isHooked(target) Registered?
getInfo(target) HookInfo or null
callback(target, sp) JNI entry

HookInfo

targetAddress, bridge, trampoline, callback, page, layout (JCodeGen.TargetPatch).

Utilities

Class Role
JPatch peekBytes, pokeBytes, pokeAllBytes (/proc/self/mem)
JMem allocateRX / allocateRW, wrap, readCString
JElf findSymbol, dlopen, dlopenAt, dlsym; flags RTLD_FILE, RTLD_MEMORY
JMaps forEach, findFirst, pathMatches
JCodeGen buildBridgeCode, buildTrampoline, buildInitStub, …
JAssembler AArch64 encoder
JDisassembler disassemble(ptr, size)
JLog d (debug), e

install does not flush I/D cache after poke; flush before executing patched code if required on your device.


Build

./gradlew :jhook:assembleRelease
  • Java 21
  • hiddenapibypass for DirectByteBuffer on API 28+
  • Library release build may use R8; see consumer-rules.pro

R8 / ProGuard

consumer-rules.pro keeps public classes/interfaces and native methods under rprop.java.jhook.**.

App subclasses of JCallback are not in that package — add in the app, for example:

-keep class * extends rprop.java.jhook.JCallback {
    public boolean onHook(rprop.java.jhook.JContext);
}

License

MIT License

About

Android ARM64 (AArch64) inline hooks implemented entirely in Java

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors