Your First Native Hook
Let's dive in by creating our first hook. The "Hello, World!" of hooking is often to intercept a common function from Android's standard C library, libc.so
. We'll target the open
function, which is called every time a process needs to open a file.
Our Goal: Log the path of every file that the application opens.
The C signature for open
looks like this:
int open(const char *pathname, int flags, ...);
We are interested in the first argument, pathname
, which is the path to the file.
- Kotlin (Recommended)
- Java
The Kotlin DSL provides the most elegant and type-safe way to define hooks.
// In your Application class or another early-initialization point
import android.util.Log
import com.skiy.terminatorkt.CString
import com.skiy.terminatorkt.TerminatorNative
import com.skiy.terminatorkt.returns
fun setupFileMonitorHook() {
TerminatorNative.hook {
target("libc.so", "open")
returns<Int>()
params(CString::class, Int::class)
onCalled {
val path = arg<CString>(0)
val flags = arg<Int>(1)
Log.d("FileMonitor", "Intercepted call to open: path='$path', flags=$flags")
callOriginal()
}
}
}
// In your Application's onCreate method:
// setupFileMonitorHook()
While the Kotlin DSL is recommended, you can achieve the same result in pure Java.
import android.util.Log;
import com.skiy.terminator.hooks.cnative.TerminatorNativeHook;
import com.v7878.foreign.FunctionDescriptor;
import com.v7878.foreign.MemorySegment;
import com.v7878.foreign.ValueLayout;
public class MyHooks {
public static void setupFileMonitorHook() {
FunctionDescriptor descriptor = FunctionDescriptor.of(
ValueLayout.JAVA_INT,
ValueLayout.ADDRESS,
ValueLayout.JAVA_INT
);
TerminatorNativeHook.HookCallback callback = (original, args) -> {
MemorySegment pathPtr = (MemorySegment) args[0];
String path = pathPtr.getUtf8String(0);
int flags = (int) args[1];
Log.d("FileMonitor", "Intercepted call (from Java): " + path);
try {
return original.invokeWithArguments(args);
} catch (Throwable e) {
Log.e("FileMonitor", "Error calling original", e);
return -1;
}
};
try {
TerminatorNativeHook.install("libc.so", "open", callback, descriptor);
} catch (Throwable e) {
Log.e("FileMonitor", "Failed to install hook", e);
}
}
}
// In your Application's onCreate method:
// MyHooks.setupFileMonitorHook();
In-Depth Breakdown
TerminatorNative.hook { ... }
(Kotlin) / TerminatorNativeHook.install(...)
(Java)
This is the entry point for creating a global, persistent native hook. Kotlin uses a DSL builder, Java uses a static method.
target("libc.so", "open")
(Kotlin)
This tells Terminator what function to intercept. The framework finds the address automatically. In Java, you pass the strings directly to install
.
Signature Definition
This describes the function signature.
- Kotlin:
returns<Int>()
,params(CString::class, ...)
.CString
meansconst char*
→ KotlinString
. - Java: Build a
FunctionDescriptor
usingValueLayout
constants.
The Callback
The hook logic.
- Kotlin:
onCalled { ... }
, withHookContext
as receiver. - Java: Implement
HookCallback
, using rawObject[]
args.
Argument Handling
- Kotlin:
arg<CString>(0)
→ KotlinString
. - Java:
(MemorySegment) args[0]
→getUtf8String(0)
.
Calling the Original Function
- Kotlin:
callOriginal()
- Java:
original.invokeWithArguments(args)
You must call the original, or the app may crash.
Terminator is a powerful abstraction layer built on top of a port of Java's modern Project Panama (Foreign Function & Memory API).
While you can be highly productive without knowing the low-level details, having a basic grasp of Panama's core concepts will be immensely helpful, especially when debugging. Key ideas to keep in mind are:
- Direct Memory Access: Unlike JNI, you are working directly with memory addresses (
Pointer
). - Memory Layouts: Native
struct
s have a strict, defined layout in memory, which ourStruct
system models. - Arenas: Memory for native data (like strings or new structs) is managed within an
Arena
to prevent memory leaks.
You don't need to be an expert in the FFM API, but knowing these concepts exist will make you a much more powerful user.
✅ What's Next?
Now that you've intercepted a function and inspected its arguments, let's explore how to actively modify the function's behavior.
➡️ Next: Advanced Native Hooking