免责声明:
本公众号致力于安全研究和红队攻防技术分享等内容,本文中所有涉及的内容均不针对任何厂商或个人,同时由于传播、利用本公众号所发布的技术或工具造成的任何直接或者间接的后果及损失,均由使用者本人承担。请遵守中华人民共和国相关法律法规,切勿利用本公众号发布的技术或工具从事违法犯罪活动。最后,文中提及的图文若无意间导致了侵权问题,请在公众号后台私信联系作者,进行删除操作。
在Java安全中,Agent的利用愈发重要,例如内存马技术、IAST灰盒扫描、RASP防御技术等都利用了java agent的特性,本系列将分两部分详细介绍Java agent的两种代理原理,分别为静态代理和动态代理,同时将用详细的代码Demo来进行实验。文中使用的完整实验代码将放到小密圈供下载,感兴趣的同学可加小密圈下载。
在Java中,Agent是一种可以在Java应用程序运行时修改或监视类和字节码的工具。Java Agent可以通过两种方式加载:静态Agent加载和动态Agent加载。本文先从静态加载说起,静态Agent加载是一种将Agent注入到Java应用程序中的一种方式,它在应用程序启动之前加载Agent。这种加载方式通常需要在应用程序启动命令中指定一些特殊参数,以便 JVM 在启动时加载Agent。
-
静态Agent加载是在Java应用程序启动之前加载Agent的一种方式。它需要在启动应用程序的命令行中指定特殊的参数。
-
以下是一个使用静态Agent加载的示例命令行:
java -javaagent:/path/to/agent.jar -jar myapp.jar
-
-javaagent参数后面跟着代理程序的JAR文件路径。
-
-jar参数后面是要运行的主应用程序的JAR文件。
-
JVM在启动时会加载指定的Agent,并在应用程序运行期间执行Agent的代理操作
-
Agent的作用分为下面几类
要写一个静态Agent,就必须实现一个公共的premain方法,该方法用于在Java应用程序启动时加载代理,并进行初始化。
public static void premain(String agentArgs, Instrumentation inst)
premain方法是Java代理程序的入口点,它必须满足以下要求:
-
方法必须是public、static、void类型的。
-
方法名称必须是premain。
-
方法必须接受一个参数,类型为String。这个参数包含了代理程序的启动参数,通常是代理程序的JAR文件路径。
-
参数agentArgs :这是一个字符串参数,它允许你向代理程序传递配置或其他参数。你可以在代理程序中解析这个参数,并根据需要使用它来配置代理行为。
-
参数Instrumentation inst :这是一个 Instrumentation 对象,它是 Java Instrumentation API 的一部分。它允许代理程序与 Java 虚拟机(JVM)交互,进行字节码操作、类加载的监控等操作。
-
使用 Instrumentation 对象,你可以在类加载之前或之后对类进行修改,比如在类加载之前添加字节码、修改类的字段、方法等。
-
通过 inst.addTransformer() 方法,你可以注册一个字节码转换器(ClassFileTransformer),用于在类加载时转换字节码。这允许你对指定的类进行字节码操作。
-
你可以使用 inst.retransformClasses() 方法来重新加载已经加载的类,以便重新转换它们的字节码。
-
通过 inst.getAllLoadedClasses() 方法,你可以获取当前已加载的所有类的列表,以便监控类加载情况。
基于上述premain的介绍本次Demo中可以有下面两种写法
写法一:
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("agentArgs: " + agentArgs);
inst.addTransformer(new DefineTransformer(), true);
}
static class DefineTransformer implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("premain load class: " + className);
return classfileBuffer;
}
}
写法二:
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("agentArgs: " + agentArgs);
inst.addTransformer(new MyClassTransformer(), true);
}
有了上述的基础知识介绍后,我们需要写一个Demo来进行实验。本次使用的是IDEA创建的maven项目来管理的。
首先我们需要写一个Java主进程,该进程用来加载外部的agent。
我们写的是一个简单的加法运算过程,其中调用了add方法进行运算并打印。
主进程:
public static void main( String[] args )
{
try{
System.out.println( "main start!" );
myTester test = new myTester();
Date d = new Date();
System.out.println(d.getTime());
int x1 = 1;
int x2 = 2;
while(true){
System.out.println(Integer.toString(test.add(x1, x2)));
Thread.sleep(2000);
}
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("main end");
}
}
private int add(int x1, int x2){
return x1+x2;
}
接下来我们需要写一个Agent,在上面的介绍中我们提到了premain函数,我们需要在premain函数使用Instrumentation增加一个transformer。当监控的java应用每次加载class的时候都会调用transformer。下面例子中的MyClassTransformer是一个transformer,是ClassFileTransformer的实现。在它的transform函数的入参中会给出当前加载的类名,类加载器等信息。
我们在加载时判断当前加载的类是否是我们的测试类,如果是的话,在上述主进程中修改add函数,将其中的相加过程修改为{return $1 + $2 + 100;}也就是多加100。
public byte[] transform(final ClassLoader loader,
final String className,
final Class<?> classBeingRedefined,
final ProtectionDomain protectionDomain,
final byte[] classfileBuffer) {
if ("com/test/agentTester/myTester".equals(className)) {
try {
// 从ClassPool获得CtClass对象
System.out.println("start modify class bytes !!!!!!!!!!!!");
final ClassPool classPool = ClassPool.getDefault();
final CtClass clazz = classPool.get("com.test.agentTester.myTester");
System.out.println(clazz);
//打印App类中的所有成员函数
CtMethod[] methodList = clazz.getDeclaredMethods();
for(CtMethod method: methodList){
System.out.println("premain method: "+ method.getName());
}
// 获取add函数并替换,$1表示函数的第一个入参
CtMethod convertToAbbr = clazz.getDeclaredMethod("add");
String methodBody = "{return $1 + $2 + 100;}";
convertToAbbr.setBody(methodBody);
// 在add函数体之前增加一段代码,同理也可以在函数尾部添加
methodBody = "System.out.println(Integer.toString($1));nSystem.out.println(Integer.toString($2));";
convertToAbbr.insertBefore(methodBody);
// 返回字节码,并且detachCtClass对象
byte[] byteCode = clazz.toBytecode();
//detach的意思是将内存中曾经被javassist加载过的Date对象移除,如果下次有需要在内存中找不到会重新走javassist加载
clazz.detach();
return byteCode;
} catch (Throwable ex) {
System.out.println("something wrong??????????");
System.out.println(ex.getMessage());
ex.printStackTrace();
}
}
// 如果返回null则字节码不会被修改
return null;
}
同时由于我们使用的是pom.xml进行配置,所以在编译前下面的配置非常重要,否则无法进行字节修改。
<manifestEntries>
<bootclasspath>lib/javassist.jar</bootclasspath>
//这个配置项指定了代理程序的入口类,即包含 premain 方法的类。在启动 Java 应用程序时,JVM 将加载并调用这个类的 premain 方法,从而加载代理程序。
<Premain-Class>com.test.myagent.MyAgent</Premain-Class>
//这两个配置项分别用于启用代理程序对类的重新定义(Redefine)和类的重新转换(Retransform)
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
随后我们先将Agent的jar打包
最后在Demo主进程中配置agent启动路径,指向该agent jar包
启动主进程,下图中虽然打印了传入的参数是1和2,但是最后计算结果是103,证明我们之前添加的多加100已经成功注入。
以上就是静态Agent注入的全过程,完整Demo已经放小密圈,需要的小伙伴扫描下方二维码获取吧。
后台回复“加群”或“小助手”,或扫描下方二维码加入我们的付费圈子,一起进步吧
原文始发于微信公众号(Lambda小队):静态代理——JavaAgent详解(一)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论