Wisp provides runtime function hooking capabilities for ARM64 platforms, primarily designed for Android (aarch64-linux-android). It allows you to replace or intercept function calls at runtime by dynamically modifying executable code.
Warning: This library is still under development and cannot handle cases where instructions at the beginning of the target function contain PC-relative addressing instructions like
adrp. Do not use in production environments.
- Function Replacement: Replace target functions entirely with proxy implementations
- Function Hooking: Intercept function calls while preserving access to original implementation
- Dynamic Original Function: Retrieve original function pointer dynamically via
orig_fn!()macro - Function Interception: Modify function arguments at runtime via a callback before the original function executes
- Instruction Cache Synchronization: Ensures cache coherency after code modifications
Add this to your Cargo.toml:
[dependencies]
wisp = { git = "https://github.com/Mufanc/wisp" }Replace a target function entirely with a proxy function:
use wisp::Wisp;extern"C"fntarget_fn(a:i32,b:i32) -> i32{
a + b
}extern"C"fnproxy_fn(a:i32,b:i32) -> i32{
a * b
}unsafe{let stub = Wisp::replace_fn(target_fn as_, proxy_fn as_).expect("failed to replace function");// target_fn now executes proxy_fn's codeassert_eq!(target_fn(2,3),6);// 2 * 3// Save the stub and explicitly unhook when restoration is needed.
stub.unhook().expect("failed to unhook function");assert_eq!(target_fn(2,3),5);}Dropping a stub leaves the hook and its executable buffers installed until process exit. Call Stub::unhook explicitly before dropping it to restore the original function.
Hook a function while maintaining access to the original implementation:
use wisp::Wisp;use std::ffi::c_void;staticmutORIG_FN:*constc_void = std::ptr::null();extern"C"fntarget_fn(a:i32,b:i32) -> i32{
a + b
}extern"C"fnproxy_fn(a:i32,b:i32) -> i32{// Call original functionlet result = unsafe{
std::mem::transmute::<*constc_void,fn(i32,i32) -> i32>(ORIG_FN)(a, b)};// Modify behavior
result *2}unsafe{let _stub = Wisp::hook_fn(target_fn as_, proxy_fn as_,Some(&mutORIG_FN)).expect("failed to hook function");assert_eq!(target_fn(2,3),10);// (2 + 3) * 2}Use orig_fn!() macro to dynamically retrieve the original function without static variables:
use wisp::{Wisp, orig_fn};use std::ffi::c_void;use std::mem;extern"C"fntarget_fn(a:i32,b:i32) -> i32{
a + b
}extern"C"fnproxy_fn(a:i32,b:i32) -> i32{let orig_fn = orig_fn!();// Call original functionlet result = unsafe{
mem::transmute::<*constc_void,fn(i32,i32) -> i32>(orig_fn)(a, b)};// Modify behavior
result *2}unsafe{let _stub = Wisp::hook_fn(target_fn as_, proxy_fn as_,None).expect("failed to hook function");assert_eq!(target_fn(2,3),10);// (2 + 3) * 2}Intercept a function to modify its arguments at runtime via a callback before the original function executes:
use wisp::Wisp;use libc::c_long;extern"C"fntarget_fn(a:u32,b:u32) -> u32{
a + b
}extern"C"fncallback_fn(args:*mutc_long){unsafe{// args points to saved registers x0-x7 on the stack// Modify x0 (first argument)*args = 100;// Modify x1 (second argument)*args.add(1) = 200;}}unsafe{let _stub = Wisp::intercept_fn(target_fn as_, callback_fn).expect("failed to intercept function");// target_fn is called with modified arguments (100, 200) regardless of inputassert_eq!(target_fn(1,2),300);}The callback receives a pointer to the saved argument registers (x0-x7) on the stack, allowing you to read or modify any of the first 8 arguments before the original function executes.
Implement custom unhooking logic with the Unhooker trait:
use wisp::{CustomWisp,UnhookContext,Unhooker,WispResult};structMadviseUnhooker;unsafeimplUnhookerforMadviseUnhooker{fnunhook(context:UnhookContext<'_>) -> WispResult<()>{// Pseudocode:// let pages = page_range(context.target(), context.backup_insn().len());// madvise(pages, MADV_DONTNEED);// touch_pages(pages);todo!()}}typeMadviseWisp = CustomWisp<MadviseUnhooker>;Wisp: Main type alias forCustomWisp<SimpleUnhooker>CustomWisp<U>: Generic hooking interface with custom unhookerStub<U>: Handle used to explicitly unhook a functionUnhookContext: Read-only target information passed to an unhookerUnhooker: Unsafe extension point for custom unhook behaviorSimpleUnhooker: Default unhooker implementation
pubunsafefnreplace_fn(target_fn:*constc_void,proxy_fn:*constc_void,) -> WispResult<Stub<U>>Replaces the target function with a proxy function.
pubunsafefnhook_fn(target_fn:*constc_void,proxy_fn:*constc_void,backup_orig:Option<&mut*constc_void>,) -> WispResult<Stub<U>>Hooks the target function while preserving access to the original implementation. Pass Some(&mut ptr) to store the original function pointer, or None to skip storing.
pubunsafefnintercept_fn(target_fn:*constc_void,callback_fn:extern"C"fn(*mutc_long),) -> WispResult<Stub<U>>Intercepts the target function, invoking the callback with a mutable pointer to the saved arguments on the stack before executing the original function. The callback can read or modify the arguments.
pubunsafefnunhook(self) -> WispResult<()>Restores the target function and releases its executable buffers. Dropping the stub without calling this method leaves the hook installed.
Macro to dynamically retrieve the original function pointer within a proxy function. Must be called at the beginning of the proxy function.
- Recursive functions: Hooking functions that recursively call themselves is not supported
- Multiple hooks: Attaching multiple hooks to a single function is not supported
- Minimum instruction length: Target functions must have at least 4 ARM64 instructions (16 bytes)
- Concurrent operations: Simultaneous hook/unhook operations on the same function from multiple threads result in undefined behavior
- Internal library calls: Behavior is undefined when hooking functions that internally use library functions like
open,mmap, etc.
All hooking operations are inherently unsafe and require careful consideration:
- Target and proxy functions must be valid pointers to executable code
- Target functions must not be executed by other threads during patching to avoid race conditions
- Proper synchronization is the caller's responsibility
- Hooking functions whose first 4 instructions contain PC-relative addressing instructions (e.g.,
adrp) is not yet supported
Run tests on Android ARM64 target:
justThis requires:
- Android NDK installed
ANDROID_NDKenvironment variable setcargo-nextestinstalled
Currently supports:
- Architecture: ARM64/AArch64
- Target: aarch64-linux-android