Skip to main content

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.

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()

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 means const char* → Kotlin String.
  • Java: Build a FunctionDescriptor using ValueLayout constants.

The Callback

The hook logic.

  • Kotlin: onCalled { ... }, with HookContext as receiver.
  • Java: Implement HookCallback, using raw Object[] args.

Argument Handling

  • Kotlin: arg<CString>(0) → Kotlin String.
  • 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.


Understanding the Foundation: Project Panama

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 structs have a strict, defined layout in memory, which our Struct 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