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.Test1的toString()方法。
插入代码到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来实现。

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

被折叠的 条评论
为什么被折叠?



