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