A self-contained C++17 static library for hooking Java methods at the ART
(Android Runtime) level. Manipulates ArtMethod structures directly via
runtime-derived offsets — no Frida, no Xposed, no LSPlant, no YAHFA, no
Pine, no SandHook. Standard system libraries only (libc, libdl,
liblog, and ART symbols resolved at runtime).
Compatible with Android 8.0 (API 26) through the latest Android version without per-version hardcoded offset tables.
- Replace any Java method's implementation with a C function of matching JNI signature.
- Hand back a callable "backup"
jmethodIDthat invokes the original unmodified method. - Install and remove hooks at runtime, thread-safely.
- Survive ART layout changes across Android releases by discovering
ArtMethodfield offsets at initialization time rather than hardcoding them.
- Inject the
.sointo another process. You're expected to have already done that (zygisk, ptrace, or whatever else); arthook just provides the hooking primitive once you're inside. - Restore methods that have been inlined into hot AOT-compiled callers. Those callers are not deoptimized — they keep running the inlined copy.
- Provide a scripting layer, argument-logger, or libffi-based generic dispatch. Your replacement function must match the original's ABI.
Requires NDK r25+ and CMake 3.18+.
cmake -B build/arm64 \
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a \
-DANDROID_PLATFORM=android-26 \
-DCMAKE_BUILD_TYPE=Release
cmake --build build/arm64This produces build/arm64/libarthook.a.
Supported ABIs: arm64-v8a, armeabi-v7a, x86_64, x86.
add_subdirectory(third_party/arthook)
add_library(my_payload SHARED my_payload.cpp)
target_link_libraries(my_payload PRIVATE arthook::arthook)// my_payload.cpp — hooks Object.toString() (a non-native Java method).
#include <arthook/Hooked.h>
#include <android/log.h>
static arthook::Hooked g_toString;
extern "C" JNIEXPORT jstring JNICALL
HookedToString(JNIEnv* env, jobject self) {
__android_log_print(ANDROID_LOG_INFO, "demo", "toString called");
return g_toString.invoke<jstring>(env, self);
}
extern "C" JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM* vm, void*) {
JNIEnv* env = nullptr;
vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6);
if (arthook::Initialize(env) != arthook::Status::kOk) return JNI_ERR;
jclass obj = env->FindClass("java/lang/Object");
g_toString.install(env, obj, "toString", "()Ljava/lang/String;",
reinterpret_cast<void*>(&HookedToString));
env->DeleteLocalRef(obj);
return JNI_VERSION_1_6;
}Hooked::invoke<Ret>(env, thiz, args...) works the same for native and
non-native targets, instance and static. For static methods pass nullptr
as thiz — it's ignored. Call g_toString.release(env) before the handle
goes out of scope (e.g. from JNI_OnUnload); the destructor cannot do it
because no JNIEnv is available there.
The lower-level arthook::Hook / Unhook API in ArtHook.h remains
available if you'd rather manage the backup jmethodID and declaring-class
GlobalRef yourself.
If your .so is loaded by an injector (zygisk, ptrace, another .so
calling dlopen) instead of System.loadLibrary, there's no JNI_OnLoad
callback to hand you a JavaVM*. Use arthook::AttachToJavaVM — it
locates the running JavaVM via JNI_GetCreatedJavaVMs, attaches the
calling thread if needed, and detaches automatically on scope exit:
#include <arthook/ArtHook.h>
#include <arthook/Hooked.h>
static arthook::Hooked g_toString;
// Called by your injector at some entry point of its choosing.
extern "C" void arthook_payload_start() {
arthook::AttachToJavaVM([](JNIEnv* env) {
if (arthook::Initialize(env) != arthook::Status::kOk) return;
jclass obj = env->FindClass("java/lang/Object");
g_toString.install(env, obj, "toString", "()Ljava/lang/String;",
reinterpret_cast<void*>(&HookedToString));
env->DeleteLocalRef(obj);
});
}The hooks installed inside the lambda persist after detach — the installation is recorded in ART's method tables, not in the env.
The full surface is in include/arthook/ArtHook.h:
namespace arthook {
enum class Status { kOk, kNotInitialized, kLayoutDiscoveryFailed,
kMethodNotFound, kTrampolineAllocFailed,
kAlreadyHooked, kNotHooked, kInternalError };
Status Initialize(JNIEnv* env);
Status Hook(JNIEnv* env, jclass clazz, const char* name, const char* sig,
void* replacement, void** backup_out);
Status HookReflected(JNIEnv* env, jobject reflected_method,
void* replacement, void** backup_out);
Status Unhook(JNIEnv* env, jclass clazz, const char* name, const char* sig);
bool IsInitialized();
const char* StatusToString(Status s);
}The replacement function follows the standard JNI calling convention: first
arg JNIEnv*, second arg jobject (instance) or jclass (static), then
the Java parameters mapped to JNI types.
We never hardcode ArtMethod offsets per Android version. Instead, at
Initialize() time we derive four numbers from the live runtime:
If any of these checks fails or returns an ambiguous result, Initialize()
returns kLayoutDiscoveryFailed and the library refuses to install hooks.
The implementation lives in src/art/Layout.cpp and
is the most safety-critical part of the codebase.
include/arthook/ArtHook.h -- public API (the only header consumers see)
src/
art/ -- ArtMethod offset accessors + runtime layout discovery
elf/ -- libart.so dynamic + on-disk symbol resolver
trampoline/ -- per-arch RWX trampoline pages + .S templates
probe/ -- runtime-built probe dex + InMemoryDexClassLoader injection
hook/ -- install/remove hooks; jmethodID ↔ ArtMethod mapping
util/ -- Log macros + safe memcpy-based offset reads
A hook installation does the following:
- Resolve target
ArtMethod*viaGetMethodID/FromReflectedMethod. - Snapshot the current access flags + both entry points (used to undo).
- Allocate a backup ArtMethod and
memcpythe original into it. Return that to the caller as a callablejmethodID. - Rewrite the access flags: set
kAccPrivate | kAccNative | kAccCompileDontBother, clear the cluster of dispatch-shortcut bits listed insrc/art/AccessFlags.h(kAccFastNative,kAccCriticalNative,kAccPreCompiled,kAccIntrinsified, etc.). This forces ART onto the generic JNI dispatch path that consults the entry-point fields. - Build an RX trampoline pointing at the user's replacement and install
it as
entry_point_from_jni_. For non-native targets also install the captured generic-JNI bridge asentry_point_from_quick_compiled_code_so quick callers go through it before reaching JNI.
Unhook() reverses steps 4–5 and frees the trampoline.
Initialize, Hook, HookReflected, and Unhook take a single global
mutex. They are safe to call from any thread.
Hooked method invocation is lock-free. The trampoline is a few
bytes of ldr+br (ARM64), ldr+bx (ARM), jmp [rip+0] (x86_64), or
push+ret (x86) — no synchronization, no extra branches. ART thinks it
called the original method and dispatches normally.
- On Android 11+ default ART builds
jmethodIDis(index << 1) | 1(thekSwapablePointerJNI-IDs mode), not a directArtMethod*. We transparently round-trip throughenv->ToReflectedMethodand read the raw pointer out of theExecutable.artMethodlong field — seesrc/art/ArtMethod.cpp. - AOT-inlined call sites bypass the hook. JIT-tiered callers eventually
redispatch through the entry points once
kAccCompileDontBothercauses the inlined copy to fall out of cache, but there is no immediate deoptimization. If you need that, you'll need to add a deopt path; this library deliberately stays out of that complexity. - Interface
defaultmethods are not currently hookable on Android 13+: ART dispatches past the per-class copiedArtMethodwe patch, going directly to the interface's original. Would require hooking the interface'sArtMethoditself (different code path; not exposed).
A comprehensive test suite lives under tests/ — 56 tests across
10 categories (method kinds, modifiers, concurrency, backup, args,
lifecycle, failure, resources, diagnostics, SSL). Open it as an Android
Studio project, run on a device/emulator with API 26+, tap Run all.
See tests/TESTING.md for what each category covers.
examples/ssl_bypass/ is a complete payload that
neutralizes the certificate-pinning checks of OkHttp, Trustkit, Conscrypt
TrustManagerImpl, WebView, Apache HttpClient, and a handful of legacy
libraries — a port of Maurizio Siddu's frida_multiple_unpinning script.
It demonstrates the recommended consumption pattern (deferred init via a
worker thread to avoid the early-startup GC race) and uses the shared
prebuilt arthook static lib from examples/arthook/.
See CONTRIBUTING.md for build, style, and PR guidelines.
