Skip to main content

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

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

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.

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.

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