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
.
- Kotlin (Recommended)
- Java
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)
}
}
import static com.skiy.terminatorkt.TerminatorJVMExtensions.afterCall;
import com.skiy.terminator.hooks.jvm.MethodCall;
import java.lang.reflect.Method;
public class MyAdvancedJvmHooks {
public static void setupPremiumHook() throws NoSuchMethodException {
Method target = Auth.class.getMethod("isPremiumUser");
afterCall(target, (methodCall) -> {
boolean originalResult = (boolean) methodCall.getResult();
System.out.println("Original isPremiumUser() returned: " + originalResult);
methodCall.setReturnValue(true);
return null;
});
}
}
The afterCall
Execution Flow
- The original method is invoked.
- Its return value is captured and stored inside the
MethodCall
context. - Your
afterCall
lambda is executed. methodCall.getResult()
gives you the original return value.- You call
methodCall.setReturnValue(true)
. - 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()
:
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.
- Kotlin (Recommended)
- Java
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)
}
}
import android.util.Log;
import static com.skiy.terminatorkt.TerminatorJVMExtensions.beforeCall;
import java.lang.reflect.Method;
public class MyAdvancedJvmHooks {
public static void setupExitBlockHook() throws NoSuchMethodException {
Method target = System.class.getMethod("exit", int.class);
beforeCall(target, (methodCall) -> {
int exitCode = (int) methodCall.getArguments()[0];
Log.w("ExitBlocker", "Blocked call to System.exit(" + exitCode + ")!");
methodCall.setReturnValue(null);
return 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 Goal | Recommended Hook | Action in Lambda | Result |
---|---|---|---|
Inspect/Modify Arguments | beforeCall | methodCall.getArguments() + args[i] = newValue | Original method called with modified arguments |
Inspect/Modify Return Value | afterCall | methodCall.getResult() + methodCall.setReturnValue() | Original method runs; result is replaced |
Block a call with fake return | beforeCall | methodCall.setReturnValue(fakeVal) | Original method is skipped; fake value is returned |
Block a void method | beforeCall | methodCall.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