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 - Automatic Unhooking: Automatically restores original function code when stub is dropped
- 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" fn target_fn(a: i32, b: i32) -> i32 {
a + b
}
extern "C" fn proxy_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 code
assert_eq!(target_fn(2, 3), 6); // 2 * 3
// When _stub is dropped, original behavior is restored
}Hook a function while maintaining access to the original implementation:
use wisp::Wisp;
use std::ffi::c_void;
static mut ORIG_FN: *const c_void = std::ptr::null();
extern "C" fn target_fn(a: i32, b: i32) -> i32 {
a + b
}
extern "C" fn proxy_fn(a: i32, b: i32) -> i32 {
// Call original function
let result = unsafe {
std::mem::transmute::<*const c_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(&mut ORIG_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" fn target_fn(a: i32, b: i32) -> i32 {
a + b
}
extern "C" fn proxy_fn(a: i32, b: i32) -> i32 {
let orig_fn = orig_fn!();
// Call original function
let result = unsafe {
mem::transmute::<*const c_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
}Implement custom unhooking logic with the Unhooker trait:
use wisp::{CustomWisp, Unhooker, Stub};
use wisp::result::WispResult;
struct MyUnhooker;
impl Unhooker for MyUnhooker {
fn unhook(stub: &Stub<Self>) -> WispResult<()> {
// Custom unhook logic
Ok(())
}
}
type MyWisp = CustomWisp<MyUnhooker>;Wisp: Main type alias forCustomWisp<SimpleUnhooker>CustomWisp<U>: Generic hooking interface with custom unhookerStub<U>: Represents a hooked function, automatically unhooks on dropSimpleUnhooker: Default unhooker implementation
pub unsafe fn replace_fn(
target_fn: *const c_void,
proxy_fn: *const c_void,
) -> WispResult<Stub<U>>Replaces the target function with a proxy function.
pub unsafe fn hook_fn(
target_fn: *const c_void,
proxy_fn: *const c_void,
backup_orig: Option<&mut *const c_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.
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