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
- 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
}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" fn target_fn(a: u32, b: u32) -> u32 {
a + b
}
extern "C" fn callback_fn(args: *mut c_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 input
assert_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, 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.
pub unsafe fn intercept_fn(
target_fn: *const c_void,
callback_fn: extern "C" fn(*mut c_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.
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