Skip to content

Mufanc/wisp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

23 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Wisp

wisp

Overview

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.

Features

  • 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

Installation

Add this to your Cargo.toml:

[dependencies]
wisp = { git = "https://github.com/Mufanc/wisp" }

Usage

Function Replacement

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
}

Function Hooking

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
}

Dynamic Original Function

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
}

Function Interception

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.

Custom Unhook Behavior

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>;

API

Core Types

  • Wisp: Main type alias for CustomWisp<SimpleUnhooker>
  • CustomWisp<U>: Generic hooking interface with custom unhooker
  • Stub<U>: Represents a hooked function, automatically unhooks on drop
  • SimpleUnhooker: Default unhooker implementation

Methods

Wisp::replace_fn

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.

Wisp::hook_fn

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.

Wisp::intercept_fn

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.

orig_fn!()

Macro to dynamically retrieve the original function pointer within a proxy function. Must be called at the beginning of the proxy function.

Limitations

  • 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.

Safety

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

Testing

Run tests on Android ARM64 target:

just

This requires:

  • Android NDK installed
  • ANDROID_NDK environment variable set
  • cargo-nextest installed

Platform Support

Currently supports:

  • Architecture: ARM64/AArch64
  • Target: aarch64-linux-android

About

A lightweight Rust library for inline hooking on Android

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages