Search code examples
javahookbyte-buddy

ByteBuddy intercepted method not getting called


private void orsc.ORSCApplet.draw() draws every frame of a video game. The goal is to intercept the draw() method so that we can ourselves draw over the currently drawn frame, so we can display information not normally visible to the user.

Below, I use ByteBuddy to create a new subclass of orsc.ORSCApplet, and then intercept the draw() function so that it will call draw() normally and also call public void PaintCallback.paint().

        ClassLoader myLoader = this.getClass().getClassLoader();
        
        new ByteBuddy().subclass(orsc.ORSCApplet.class)
                       .method(ElementMatchers.named("draw"))
                       .intercept(SuperMethodCall.INSTANCE.andThen(MethodCall.invoke(PaintCallback.class.getMethod("paint"))))
                       .make()
                       .load(myLoader); //I have also tried fromInstalledAgent (BuddyByteAgent.install() ran) and INJECTION
        

The main() for the video game is inside orsc.OpenRSC. Hence, we will create a new orsc.OpenRSC object using the class loader with the intercepted method:

        Class loadedMyClass = myLoader.loadClass("orsc.OpenRSC");
        Constructor constructor = loadedMyClass.getConstructor();
        Object myClassObject = constructor.newInstance();
        return myClassObject; //later on, we call orsc.OpenRSC.main() with this object.

However, I am not receiving any calls to PaintCallback.paint(). Why isn't my intercept working? I have three theories:

  1. Despite using the myLoader class loader, the class is being loaded elsewhere. However, I have tried using the ByteBuddyAgent.install() and specified this in .load(myLoader, ClassLoadingStrategy.fromInstalledAgent()).

  2. Instead of doing a subclass, should I be attempting to do redefine? If so, if I intercept with redefine, can I call the original function? If so, how? I have attempted this with a lot of different methods, and ended up spinning my wheels in the mud pretty hard.

  3. Instead of subclass or redefine, should I be doing rebase? I attempted this with the below code, and received an exception from the instrumentation library:

             new ByteBuddy().rebase(orsc.ORSCApplet.class)
                        .method(ElementMatchers.named("draw"))
                        .intercept(SuperMethodCall.INSTANCE.andThen(MethodCall.invoke(PaintCallback.class.getMethod("paint"))))
                        .make()
                        .load(myLoader, 
                                ClassReloadingStrategy.fromInstalledAgent());
    
     java.lang.IllegalStateException: Error invoking java.lang.instrument.Instrumentation#retransformClasses
         at net.bytebuddy.dynamic.loading.ClassReloadingStrategy$Dispatcher$ForJava6CapableVm.retransformClasses(ClassReloadingStrategy.java:503)
         at net.bytebuddy.dynamic.loading.ClassReloadingStrategy$Strategy$2.apply(ClassReloadingStrategy.java:568)
         at net.bytebuddy.dynamic.loading.ClassReloadingStrategy.load(ClassReloadingStrategy.java:225)
         at net.bytebuddy.dynamic.TypeResolutionStrategy$Passive.initialize(TypeResolutionStrategy.java:100)
         at net.bytebuddy.dynamic.DynamicType$Default$Unloaded.load(DynamicType.java:6292)
         at reflector.Reflector.injectCallbacksAndReturnClient(Reflector.java:319)
         at reflector.Reflector.createClient(Reflector.java:50)
         at bot.Main.main(Main.java:164)
     Caused by: java.lang.UnsupportedOperationException: class redefinition failed: attempted to add a method
         at java.instrument/sun.instrument.InstrumentationImpl.retransformClasses0(Native Method)
         at java.instrument/sun.instrument.InstrumentationImpl.retransformClasses(InstrumentationImpl.java:167)
         at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
         at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:64)
         at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
         at java.base/java.lang.reflect.Method.invoke(Method.java:564)
         at net.bytebuddy.dynamic.loading.ClassReloadingStrategy$Dispatcher$ForJava6CapableVm.retransformClasses(ClassReloadingStrategy.java:495)
         ... 7 more
    

Solution

  • When you are creating a subclass, you have to make sure that this subclass is returned where it is created, typically from some sort of factory. Otherwise, your subclass will exist, but never be used. It's not clear to me that you are doing this in your code.

    Rebasing can be seen as subclassing where the original class is extended within itself, to avoid this restriction of subclassing from agents where creating factories is often difficult. However, this often requires adding methods or fields what is not possible when a class is already loaded, even with a Java agent. In your case however, if you avoid referencing the orsc.ORSCApplet.class, you can create a description with a TypePool.Default and provide this to Byte Buddy. If you then load the instrumented class via injection, it will precedent the original class file and be used in your application.

    The most elegant approach however would be to use redefinition, which can be applied for loaded classes. Normally, you would use an AgentBuilder for this. If you want to add code to an existing method, you would ideally use Advice, not delegation here, as advice adds code to the beginning and/or end of a method. Make sure to apply the advice as decoration using DynamicType.Builder::visit and not the regular interception.