OpenRasp Java运行时修改字节码技术

本文介绍了Java运行时如何使用javassist库动态修改字节码,包括插入代码到方法中,以及验证修改位置的正确性。通过实例展示了在`toString()`方法前后插入代码的过程,并探讨了获取和操作类、方法的步骤。

Java运行时修改字节码技术

Java运行时动态修改字节码技术,常用的有javassist asm来实现。不过最近在分析openrasp-java这块时,程序使用的javassist来动态插桩关键类,达到监控某些程序的行为,OpenRasp使用这个技术来实现了监控程序的行为。为了分析OpenRasp和理解其使用的技术原理,先做一个java动态修改指令基础知识的补充。

第一个程序

有如下程序

package com.company;

import java.net.URL;
import  java.io.File;
import java.net.URLDecoder;
import java.util.Set;

public class Test1 {
    private String aa="heh";

    public Test1(){}

    @Override
    public String toString() {
        return "Test1{" +
                "aa='" + aa + '\'' +
                '}';
    }
}

正常情况下调用toString()方法,会得到如下

InsertCode:Test1{aa='heh'}

如果想要在toString()方法前插入某一个方法块,输出如下内容

方法调用前 ----->>aa
方法调用前 ----->>aaa 
方法调用后 ---->> bbbb
InsertCode:Test1{aa='heh'}

可以借用javassist工具类操作对应的字节码。

动态修改字节码–常用javassist类(这里是根据写的样例记录的,不是针对所有情况)

要想在动态修改程序行为,则需要使用javassist内的三个主要类

ClassPool --> 是CtClass的一个容器 要想获得一个类对象,必须
CtClass  -->和java的Class类似
CtMethod -->和java的Method类似
  • ClassPool --> 是CtClass的一个容器 要想获得一个类对象,必须通过这个对象获取
CtClass ctClzz =classPool.get("完整的类名,例如com.test.demn.A");
如果当前的classPool内没有这个类,则会报javassist.NotFoundException: com.company.Test1 后续会提到
#参考
http://javadox.com/org.javassist/javassist/3.18.1-GA/javassist/ClassPool.html
  • CtClass -->和java的Class类似代表了一个类对象,从ClassPool内获取到
参考
http://javadox.com/org.javassist/javassist/3.18.1-GA/javassist/CtClass.html
  • CtMethod -->和java的Method类似代表的一个方法对象,继承自CtBeHavior
参考
http://javadox.com/org.javassist/javassist/3.18.1-GA/javassist/CtMethod.html

在知道了这几个类之后,接着就是动手尝试修改com.company.Test1toString()方法。

插入代码到toString()方法

编写TestInsetOPs类

package com.company;

import javassist.*;
import org.junit.Test;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.LinkedList;

//测试插入程序
public class TestInsertOps {



    private static ClassPool insertCode() {

        try {

            //todo 两个方式回去ClassPool 1)ClassPool.getDefault(); 内部会自动调用appendSystemPath方法 2)可以直接new,不过要手动 appendSystemPath
            ClassPool pool = new ClassPool();// ClassPool.getDefault();
            pool.appendSystemPath();//如果添加到系统环境中内程序可以执行,否则会javassist.NotFoundException: com.company.Test1
            CtClass clazz = pool.get("com.company.Test1");
            CtMethod method = clazz.getDeclaredMethod("toString");
            method.insertBefore("{ System.out.println(\"方法调用前 ----->>aaa \"); }");
            method.insertAfter("{ System.out.println(\"方法调用后 ---->> bbbb\"); }");

            return pool;
        } catch (NotFoundException e) {
// TODO Auto-generated catch block
            e.printStackTrace();
        } catch (CannotCompileException e) {
// TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public static void testInsertCode() throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException {


        ClassPool classPool = insertCode();
        if (classPool == null) {
            System.out.println("程序添加失败 --> classPool is NullPointer");
            return;
        }



        // ClassPool pool =  new ClassPool()
        Test1 test1 = (Test1) classPool.get("com.company.Test1").toClass().newInstance();

        System.out.println("InsertCode:" + test1.toString());

    }
}

首先,我们需要获取一个ClassPool对象,通常有两个方法获取

  • 第一种 ClassPool.getDefault()
    这个方法的好处是调用getDefault()方法,其内部封装单例了方法,如下
 public static synchronized ClassPool getDefault() {
        if (defaultPool == null) {
            defaultPool = new ClassPool((ClassPool)null);
            defaultPool.appendSystemPath();
        }

        return defaultPool;
    }

可以看到其内部调用了new ClassPool()方法,因此,第二种方法就是

  • 第二种 new ClassPool(),构造函数如下
   public ClassPool() {
        this((ClassPool)null);
    }

    -->
      public ClassPool(ClassPool parent) {
        this.childFirstLookup = false;
        this.cflow = null;
        this.classes = new Hashtable(191);
        this.source = new ClassPoolTail();
        this.parent = parent;
        if (parent == null) {
            CtClass[] pt = CtClass.primitiveTypes;

            for(int i = 0; i < pt.length; ++i) {
                this.classes.put(pt[i].getName(), pt[i]);
            }
        }

        this.cflow = null;
        this.compressCount = 0;
        this.clearImportedPackages();
    }

两个不同的点在于,getDefault自动调用了

defaultPool.appendSystemPath();

如果这个方法不调用,则就是报错误javassist.NotFoundException:。如果使用的是第二种方法获取ClassPool对象,需要调用这个方法。

其次调用ClassPool.get(String className)获取一个CtClass对象,

CtClass clazz = pool.get("com.company.Test1");

接着获取一个CtMethod,可以通过getMethods()和getDeclareMethod()来获取,和java的反射类似使用,只是Method-->CtMethod

这里获取toString方法,使用getDeclareMethod方法并判断是否是预期的方法

   CtMethod method = clazz.getDeclaredMethod("toString");

在得到了方法体之后,调用insertBefore(String method)在方法入口处插入第一段方法


接着调用insertAfter(String method)在return之前添加程序

 method.insertAfter("{ System.out.println(\"方法调用后 ---->> bbbb\"); }");

insertAfter和insertBefore的 method参数是一个字符串,并且是一个完整的方法调用,这个在openrasp中有体现

最后调用toString方法

Test1 test1 = (Test1) classPool.get("com.company.Test1").toClass().newInstance();
System.out.println("InsertCode:" + test1.toString());

输入结果为

方法调用前 ----->>aaa 
方法调用后 ---->> bbbb
InsertCode:Test1{aa='heh'}

验证程序写入的位置

为了验证程序添加到正确的位置,这里修改了TestInsertOps的程序,在InsertCode内添加了如下内容

package com.company;

import javassist.*;
import org.junit.Test;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.LinkedList;

//测试插入程序
public class TestInsertOps {


    private static void save(byte[] data,String filename) throws IOException {
        String path = System.getProperty("user.dir");
        File cache = new File(path+File.separator+"cache");
        if(!cache.isDirectory()){
            cache.mkdirs();
        }
        path = cache+File.separator+filename;
        File file = new File(path);
        if(file.exists()){
            file.delete();
        }

        FileOutputStream fileOutputStream  = new FileOutputStream(file);
        fileOutputStream.write(data);
        fileOutputStream.flush();
        fileOutputStream.close();
        System.out.println("保存成功 "+file.getAbsolutePath());


    }

    private static ClassPool insertCode() {

        try {

            //todo 两个方式回去ClassPool 1)ClassPool.getDefault(); 内部会自动调用appendSystemPath方法 2)可以直接new,不过要手动 appendSystemPath

            ClassPool.getDefault();
            ClassPool pool = new ClassPool();// ClassPool.getDefault();
            pool.appendSystemPath();//如果添加到系统环境中内程序可以执行,否则会javassist.NotFoundException: com.company.Test1
            CtClass clazz = pool.get("com.company.Test1");
            //
            byte[] origin_clzz=clazz.toBytecode();
            save(origin_clzz,"origin_clzz.class");
            if (clazz.isFrozen())
            {
                System.out.println("Frozen ...");
                clazz.defrost();

            }
            CtMethod method = clazz.getDeclaredMethod("toString");
            method.insertBefore("{ System.out.println(\"方法调用前 ----->>aaa \"); }");
            method.insertAfter("{ System.out.println(\"方法调用后 ---->> bbbb\"); }");
//            clazz.writeFile();

            byte[] mode_clzz=clazz.toBytecode();
            save(mode_clzz,"mode_clzz.class");


            return pool;
        } catch (NotFoundException e) {
// TODO Auto-generated catch block
            e.printStackTrace();
        } catch (CannotCompileException e) {
// TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public static void testInsertCode() throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException {


        ClassPool classPool = insertCode();
        if (classPool == null) {
            System.out.println("程序添加失败 --> classPool is NullPointer");
            return;
        }



        // ClassPool pool =  new ClassPool()
        Test1 test1 = (Test1) classPool.get("com.company.Test1").toClass().newInstance();

        System.out.println("InsertCode:" + test1.toString());

    }
}

程序运行后可以得到两个类origin_clzz.class和mode_clzz.class。使用Idea的反编译功能,可以看到被修改的类Test1前后差别

Before :

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.company;

import java.io.File;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Set;

public class Test1 {
    private String aa = "heh";

    public Test1() {
    }

    public String getLocalPath() {
        URL url = this.getClass().getProtectionDomain().getCodeSource().getLocation();
        String decode = URLDecoder.decode(url.getFile());
        System.out.println("Before " + url.getFile() + "decodeUrl " + decode + " replace " + decode.replace("+", "%2B") + "parent " + (new File(decode)).getParent());
        return decode;
    }

    public void testParam(Set<String> result) {
        String[] objec = new String[]{"AAA", "BBB", "CCC", "DDD"};
        String[] var3 = objec;
        int var4 = objec.length;

        for(int var5 = 0; var5 < var4; ++var5) {
            String a = var3[var5];
            result.add(a);
        }

    }

    public String toString() {
        return "Test1{aa='" + this.aa + '\'' + '}';
    }
}

After:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.company;

import java.io.File;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Set;

public class Test1 {
    private String aa = "heh";

    public Test1() {
    }

    public String getLocalPath() {
        URL url = this.getClass().getProtectionDomain().getCodeSource().getLocation();
        String decode = URLDecoder.decode(url.getFile());
        System.out.println("Before " + url.getFile() + "decodeUrl " + decode + " replace " + decode.replace("+", "%2B") + "parent " + (new File(decode)).getParent());
        return decode;
    }

    public void testParam(Set<String> result) {
        String[] objec = new String[]{"AAA", "BBB", "CCC", "DDD"};
        String[] var3 = objec;
        int var4 = objec.length;

        for(int var5 = 0; var5 < var4; ++var5) {
            String a = var3[var5];
            result.add(a);
        }

    }

    public String toString() {
        System.out.println("方法调用前 ----->>aaa ");
        String var2 = "Test1{aa='" + this.aa + '\'' + '}';
        System.out.println("方法调用后 ---->> bbbb");
        return var2;
    }
}

完。

总结

java可以在运行时修改程序,通过javaasist工具类来实现,通过ClassPool,CtClass,CtMethod来实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值