How to hook Java method with JNI during runtime?

Viewed 265

Other questions I saw about this topic were not about while the Java program is running or were unclear to me, so I'm asking a separate question

I'd like to know if there's a way to hook/override methods with JNI during runtime

It would work as Mixins (like in Fabric API), which "injects" code at the beginning or at the end of the method but with C++ and JNI

For example: there's a getter method that returns a field of a class but I want to change the return value to something I want instead of the expected value

I've ran into some topics that said about JVM Handles but I have no idea where to start searching it

3 Answers

Thank you all for the answers but I found exactly what I wanted (not sure if it's what everyone is looking for but this was my goal with this question)

Basically I found out that jmethodIDs are pointers, which I thought of actually being the address of the method on the stack

To check if this would work I debugged the jvm and found out that it is actually it! With some testing I also found out that some methods have specific offsets in order to be hooked... you could get this with trial and error but also debugging the jvm (its mostly 0x40)

With the address of the function we can hook it with simple hooking as we would hook any other process method

Here's an example using MinHook and jni (obviously) that actually works with Minecraft with the onTick method (used to keep track of when a tick on the game happens):

typedef void(__cdecl* runTick) (void**, void**);

runTick pRunTick;
runTick pRunTickTarget;

void __cdecl tickHook(void** p1, void** p2) {
    sdk::instance->hasTick = true;
    return pRunTick(p1, p2);
}

bool  cheat::hooks::jhooks::apply_jtickhook(bool create) {
    jclass mc_class = machip::instance->get_env()->FindClass("ave");
    jmethodID method = machip::instance->get_env()->GetMethodID(mc_class, "s", "()V");
    pRunTickTarget = reinterpret_cast<runTick>(*(unsigned __int64*)(*(unsigned __int64*)method + 0x40));

    if (create)
    {
        MH_STATUS status = MH_CreateHook(reinterpret_cast<void**>(pRunTickTarget), &tickHook, reinterpret_cast<void**>(&pRunTick));
        if (status != MH_OK) {
            wrapper::output("failed to hook onTick");
            wrapper::output(MH_StatusToString(status));
            return false;
        }
    }
    

    if (MH_EnableHook(reinterpret_cast<void**>(pRunTickTarget)) != MH_OK) {
        wrapper::output("failed to enable onTick hook");
        return false;
    }

    machip::instance->get_env()->DeleteLocalRef(mc_class);

    wrapper::output("onTick hooked");

    return true;
}

For more about hooking/detour I saw this video to understand better and also this one

Steps for the creation of native methods are as follows, according to GeeksForGeeks:

  1. Write java code
  2. Compile the java code.
  3. Create C header(.h file)
  4. Create C stubs file (using tool: Java HEdge)
  5. Write C code
  6. Create a shared code library (DLL)
  7. Run application
     // Native method
    public native void test()
    {
 
        static
        {
 
            // We will be loading body from DLL file
            // It has to be present in DLL file
            System.loadLibrary("NameOfDLLFile");
 
            // Above C code in loaded in the JVM
        }
    }
// C++ Program to Be Shared In DLL to Illustrate
// Native Method in Java
 
// Importing required libraries
#include <iostream>
 
using namespace std;
// Method 2
// Native
void test(int var)
{
    cout << var;
}
 
// Method 1
// Main driver method
int main()
{
 
    test(10);
 
    return 1;
}

**Syntax: Declaring Native Methods

private native String getLine(String prompt); Syntax: From the Native Language Side

javah -jni Prompt JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv *, jobject, jstring);

Additional source.

based on this...

For example: there's a getter method that returns a field of a class but I want to change the return value to something I want instead of the expected value

you may be confusing JNI with reflection. If say you want to invoke some method in a dll somewhere inside your own jvm, you'll have to use JNI to envelope java runtime calls around some c++ code.

If you, on the other hand, want to intercept fields and/or methods in your own runtime, you'll have to use the reflection api.

The reflection API applies to pretty much everything in java, including methods, fields, annotations, etc. https://docs.oracle.com/javase/tutorial/reflect/

e.g.

class someClass{
    String myMethod(){
        return "somethingSomething";
    }
}

may be called during runtime with

new someClass().myMethod() // returns somethingSomething

... and using the reflection API with

someClass importantObject = new someClass();
someClass.getClass().getMethod("myMethod").invoke(importantObject);

in which case, evidently you can intercept the return value of that method and change it how you want... but doing things this way is not the best of ideas.

e.g.

String vader = String.class.cast(someClass.getClass().getMethod("myMethod").invoke(importantObject)).concat("Darkside");
System.out.println(vader);

should yield (if i didn't mess up the code)

somethingSomethingDarkside

which is something you've technically mutated and behaves differently as opposed to whatever the initial method should return.

but be advised... this is kinda dumb.

reflection is usually kind of a last resort type of deal as it can completely screw up jvms.... so I'd advise caution since in most cases the code you'll have to run must be access-(de)controlled using method.setAccessible(true/false)

Related