Advanced Native Hooking
Simply observing function calls is useful, but the true power of hooking comes from modifying a program's behavior. In this section, we'll explore how to:
- Modify arguments before the original function is called.
- Block or skip the original function call entirely.
- Replace the return value of a function.
1. Modifying Arguments
Redirect any attempt to open a specific file to a different file.
Example: Redirect /data/local/tmp/config.txt
→ /data/local/tmp/redirected_config.txt
- Kotlin (Recommended)
- Java
import android.util.Log
import com.skiy.terminatorkt.*
fun setupFileRedirectHook() {
TerminatorNative.hook {
target("libc.so", "open")
returns<Int>()
params(CString::class, Int::class)
onCalled {
val originalPath = arg<CString>(0)
val flags = arg<Int>(1)
val targetPath = "/data/local/tmp/config.txt"
val redirectPath = "/data/local/tmp/redirected_config.txt"
if (originalPath == targetPath) {
Log.d("FileRedirect", "Redirecting open('$originalPath') to '$redirectPath'")
// IMPORTANT: callOriginal with MODIFIED arguments
// The new string is automatically converted to a native C-string for the call.
callOriginal(redirectPath, flags)
} else {
// For all other files, call the original function normally.
callOriginal()
}
}
}
}
import android.util.Log;
import com.skiy.terminator.hooks.cnative.TerminatorNativeHook;
import com.v7878.foreign.Arena;
import com.v7878.foreign.FunctionDescriptor;
import com.v7878.foreign.MemorySegment;
import com.v7878.foreign.ValueLayout;
public class MyAdvancedHooks {
public static void setupFileRedirectHook() {
FunctionDescriptor descriptor = FunctionDescriptor.of(
ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_INT);
TerminatorNativeHook.HookCallback callback = (original, args) -> {
MemorySegment pathPtr = (MemorySegment) args[0];
String originalPath = pathPtr.getUtf8String(0);
int flags = (int) args[1];
String targetPath = "/data/local/tmp/config.txt";
String redirectPath = "/data/local/tmp/redirected_config.txt";
try {
if (originalPath.equals(targetPath)) {
Log.d("FileRedirect", "Redirecting open(...)");
// To modify a string argument, we must allocate new native memory for it.
// A temporary Arena is perfect for this.
try (Arena tempArena = Arena.ofConfined()) {
MemorySegment newPathPtr = tempArena.allocateUtf8String(redirectPath);
// Call original with the new pointer.
return original.invoke(newPathPtr, flags);
}
} else {
// Pass original arguments through.
return original.invokeWithArguments(args);
}
} catch (Throwable e) {
Log.e("FileRedirect", "Error in hook", e);
return -1;
}
};
try {
TerminatorNativeHook.install("libc.so", "open", callback, descriptor);
} catch (Throwable e) {
Log.e("FileRedirect", "Failed to install hook", e);
}
}
}
Key Concepts
- Kotlin:
callOriginal(...)
can accept modified arguments. Redirect paths are converted to native strings automatically. - Java: You must allocate memory explicitly using
Arena.allocateUtf8String(...)
and pass the resulting pointer.
2. Skipping the Original Call
Block a function from executing. For example, prevent file deletion.
- Kotlin (Recommended)
import android.util.Log
import com.skiy.terminatorkt.*
// C function: int unlink(const char *pathname);
fun setupDeleteBlockHook() {
TerminatorNative.hook {
target("libc.so", "unlink") // 'unlink' is the C function for deleting files
returns<Int>()
params(CString::class)
onCalled {
val path = arg<CString>(0)
if (path.contains("important_file.db")) {
Log.w("DeleteBlocker", "Prevented deletion of '$path'")
// Don't call callOriginal().
// Return a value to the caller. 'unlink' returns 0 on success, -1 on error.
// Let's pretend it failed.
return@onCalled -1
}
// For all other files, allow deletion.
callOriginal()
}
}
}
3. Replacing the Return Value
Modify the return value after the original call.
- Kotlin (Recommended)
import android.util.Log
import com.skiy.terminatorkt.*
// C function: int is_feature_enabled(int featureId);
fun setupFeatureEnableHook() {
// Let's assume this function is at a specific offset in a library
TerminatorNative.hook {
target("libconfig.so", 0x1234) // Hooking by library + offset
returns<Int>()
params(Int::class)
onCalled {
val featureId = arg<Int>(0)
// Call the original function to let it run its course
val originalResult = callOriginal()
Log.d("FeatureHook", "Original result for feature $featureId was $originalResult")
// We can now ignore the original result and return our own value.
// Let's always return 1 to enable the feature.
return@onCalled 1
}
}
}
Hooking by Address or Offset
Use target("lib.so", 0xOFFSET)
for functions without exported names, or even target(0xABCDEF01)
for absolute addresses.
✅ What's Next?
You now know how to inspect, modify, block, and override function calls.
➡️ Next: Working with Pointers and Memory