0

I have code to insert instructions into the constructor of a class from a third party library (okhttp3.OkHttpClient in this case). Disassembled class shows the added line. However, I am not sure how to make rest of the code to use this instrumented OkHttpClient class instead of using the original one. Once we have the modified class, do we need to install that in classpath or manipulate any class loaders? When the test method is run, I do not see "Constructor called" logged.

Here is the full code:

import java.io.FileOutputStream;
import java.io.IOException;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class ConstructorInjector {
    public static void main(String[] args) {
        String className = "okhttp3.OkHttpClient";

        byte[] modifiedBytecode = new byte[0];

        try {
            modifiedBytecode = ConstructorInjector.injectIntoConstructor(className);
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }

        try (FileOutputStream fos = new FileOutputStream("OkHttpClientModified.class")) {
            fos.write(modifiedBytecode);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        try {
            test();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    // Example method to inject into constructor
    public static void logConstructorCall() {
        System.out.println("Constructor called");
    }

    // Example method to modify a class
    public static byte[] injectIntoConstructor(String className) throws Exception {
        ClassReader classReader = new ClassReader(className);
        ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
        ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM7, classWriter) {

            // Override visitMethod to modify the constructor
            @Override
            public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
                MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);
                if (name.equals("<init>")) {
                    // Override the constructor
                    return new MethodVisitor(Opcodes.ASM7, methodVisitor) {
                        @Override
                        public void visitInsn(int opcode) {
                            // Find the last instruction in the constructor
                            if (opcode == Opcodes.RETURN) {
                                // Insert code before the return instruction
                                visitMethodInsn(Opcodes.INVOKESTATIC, "ConstructorInjector", "logConstructorCall", "()V", false);
                            }
                            super.visitInsn(opcode);
                        }
                    };
                }
                return methodVisitor;
            }
        };

        // Modify the class using the classVisitor
        classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);

        // Return the modified bytecode
        return classWriter.toByteArray();
    }

    private static void test() throws IOException {
        OkHttpClient client = new OkHttpClient.Builder()
                .build();

        Request request = new Request.Builder()
                .url("http://www.publicobject.com/helloworld.txt")
                .header("User-Agent", "OkHttp Example")
                .build();

        Response response = client.newCall(request).execute();
        String body = response.body().string();
        System.out.println("++++ BODY: " + body);
        //response.body().close();
    }
}
rysv
  • 2,416
  • 7
  • 30
  • 48
  • 1
    Either use the [instrumentation API](https://docs.oracle.com/en/java/javase/17/docs/api/java.instrument/java/lang/instrument/package-summary.html), or you have to replace the original `okhttp3/OkHttpClient.class` with your modified class. If everything is on the classpath, adding your own `okhttp3/OkHttpClient.class` before the okhttp library may work. It okhttp is loaded as module, you have to use the `--patch-module` command line argument. – Johannes Kuhn Mar 13 '23 at 00:25
  • 1
    `new FileOutputStream("OkHttpClientModified.class")` will just write into the current directory, which is not the location of the class file. You have to use the actual location of the class, to overwrite it. But when the class is in a jar file, you can’t just use a `FileOutputStream`. And keep in mind that when you overwrite the original class file, each time you run this program, another invocation of `logConstructorCall` will be injected. And, side note, there is no sense in initializing the `modifiedBytecode` variable with `new byte[0]`. Just declare it blank, `byte[] modifiedBytecode;` – Holger Mar 13 '23 at 08:55
  • @JohannesKuhn I am trying this on Android where I can't use Instrumentation API AFAIK. How do I add my own class implementation ahead of OkHttp library? – rysv Mar 14 '23 at 18:35
  • @Holger any idea how I can "overwrite" the original jar file, esp. on Android, - assuming it is possible and not a bad idea? – rysv Mar 14 '23 at 18:36
  • 2
    When you’re running on Android, it’s too late, as far as I know. You convert the class files to dex before deployment, hence, there is no way to perform bytecode manipulation in the target environment. – Holger Mar 14 '23 at 19:37

0 Answers0