Skip to main content

Advanced JVM Hooking

While beforeCall is great for observing and modifying arguments, you often need more control. Terminator provides a complete set of tools to manage the entire lifecycle of a method call, including:

  • Running logic after the original method has executed (afterCall)
  • Skipping the original method call entirely
  • Replacing the return value

afterCall: Reacting to a Method's Result

The afterCall infix function allows you to execute code immediately after the original method has run. This is perfect for inspecting or modifying the result.

Our Goal: Hook a hypothetical Auth.isPremiumUser() method that returns false and force it to return true.

import com.skiy.terminatorkt.afterCall

// Assume: class Auth { fun isPremiumUser(): Boolean { return false } }
fun setupPremiumHook() {
val target = Auth::class.functions.find { it.name == "isPremiumUser" }!!

target afterCall { methodCall ->
val originalResult = methodCall.result as Boolean
println("Original isPremiumUser() returned: $originalResult")

methodCall.setReturnValue(true)
}
}

The afterCall Execution Flow

  1. The original method is invoked.
  2. Its return value is captured and stored inside the MethodCall context.
  3. Your afterCall lambda is executed.
  4. methodCall.getResult() gives you the original return value.
  5. You call methodCall.setReturnValue(true).
  6. That value (true) is then returned to the caller.

Skipping Originals and Replacing Return Values

What if you want to prevent a method from running at all? This is crucial for blocking unwanted behavior, like an app trying to exit.

Use methodCall.setReturnValue():

The Power of setReturnValue

When you call methodCall.setReturnValue(newValue), it:

  • Sets the value returned to the caller.
  • Skips the original method call.

Our Goal: Prevent the app from closing when System.exit() is called.

import android.util.Log
import com.skiy.terminatorkt.beforeCall

fun setupExitBlockHook() {
val target = System::class.functions.find { it.name == "exit" }!!

target beforeCall { methodCall ->
val exitCode = methodCall.arguments[0] as Int
Log.w("ExitBlocker", "Blocked call to System.exit($exitCode)!")

methodCall.setReturnValue(null)
}
}

With this hook, any attempt to call System.exit() will be intercepted. The lambda logs a message and calls setReturnValue(). Since this also skips the original, the JVM does not exit.

Explicitly Skipping with setSkipOriginalMethodCall

For more fine-grained control, use methodCall.setSkipOriginalMethodCall(true). This skips the original method but returns a default value (e.g., null, 0, false).

Useful for void methods where the return value doesn't matter.

Summary of Control Flow

Your GoalRecommended HookAction in LambdaResult
Inspect/Modify ArgumentsbeforeCallmethodCall.getArguments() + args[i] = newValueOriginal method called with modified arguments
Inspect/Modify Return ValueafterCallmethodCall.getResult() + methodCall.setReturnValue()Original method runs; result is replaced
Block a call with fake returnbeforeCallmethodCall.setReturnValue(fakeVal)Original method is skipped; fake value is returned
Block a void methodbeforeCallmethodCall.setSkipOriginalMethodCall(true)Original method is skipped

✅ What's Next?

Congratulations! You now know how to hook and control both native and JVM methods. The final step is to put it all together.

➡️ Next: Conclusion and Further Reading