JavaAgent型内存马基础

2021-10-27

Java Instrumentation

​ java Instrumentation指的是可以用独立于应用程序之外的代理(agent)程序来监测和协助运行在JVM上的应用程序。这种监测和协助包括但不限于获取JVM运行时状态,替换和修改类定义等。简单一句话概括下:Java Instrumentation可以在JVM启动后,动态修改已加载或者未加载的类,包括类的属性、方法。

java agent技术原理及简单实现 - kokov - 博客园 (cnblogs.com)

什么是java agent?

IDEA + maven 零基础构建 java agent 项目 - 一灰灰Blog - 博客园 (cnblogs.com)

java agent本质上可以理解为一个插件,该插件就是一个精心提供的jar包,这个jar包通过JVMTI(JVM Tool Interface)完成加载,最终借助JPLISAgent(Java Programming Language Instrumentation Services Agent)完成对目标代码的修改。

java agent技术的主要功能如下:

  • 可以在加载java文件之前做拦截把字节码做修改
  • 可以在运行期将已经加载的类的字节码做变更
  • 还有其他的一些小众的功能
    • 获取所有已经被加载过的类
    • 获取所有已经被初始化过了的类
    • 获取某个对象的大小
    • 将某个jar加入到bootstrapclasspath里作为高优先级被bootstrapClassloader加载
    • 将某个jar加入到classpath里供AppClassloard去加载
    • 设置某些native方法的前缀,主要在查找native方法的时候做规则匹配

Instrument

(32条消息) ClassPool CtClass浅析_罗小辉的专栏-CSDN博客

​ instrument是JVM提供的一个可以修改已加载类的类库,专门为Java语言编写的插桩服务提供支持。它需要依赖JVMTI的Attach API机制实现。在JDK 1.6以前,instrument只能在JVM刚启动开始加载类时生效,而在JDK 1.6之后,instrument支持了在运行时对类定义的修改。要使用instrument的类修改功能,我们需要实现它提供的ClassFileTransformer接口,定义一个类文件转换器。接口中的transform()方法会在类文件被加载时调用,而在transform方法里,我们可以利用ASM或Javassist对传入的字节码进行改写或替换,生成新的字节码数组后返回。

image

​ 总之,transform返回值为需要替换的class的字节码。有两种方法获取字节码,一种使用文件读取的方式,直接读取相应class文件的字节码,还有一种使用Javaassist包,结合反射机制进行字节码的替换。

我们来看一下第二种的示例代码

SimpleAgent.java 作为Javagent去注入目标程序

import java.lang.instrument.Instrumentation;
import java.lang.reflect.Method;
import java.util.LinkedList;
import java.util.List;

public class SimpleAgent {

    /**
     * jvm 参数形式启动,运行此方法
     *
     * @param agentArgs
     * @param inst
     */
    private static String className = "com.company.BaseMain";
    private static String methodName = "print";
    public static void premain(String agentArgs, Instrumentation instrumentation) {
        System.out.println("premain");
        //instrumentation.addTransformer(new TestTransformer(className, methodName));
    }

    /**
     * 动态 attach 方式启动,运行此方法
     *
     * @param agentArgs
     * @param instrumentation
     */
    public static void agentmain(String agentArgs, Instrumentation instrumentation) {
        System.out.println("agentmain");
        instrumentation.addTransformer(new TestTransformer(className, methodName),true);
        try {
            List<Class> needRetransFormClasses = new LinkedList<>();
            Class[] loadedClass = instrumentation.getAllLoadedClasses();//获取所有加载的类
            for (Class c : loadedClass) {
                //System.out.println(loadedClass[i].getName());
                if (c.getName().equals(className)) {
                    System.out.println("---find!!!---");
                    Method[] methods = c.getDeclaredMethods();
                    for(Method method : methods)
                    {System.out.println(method.getName());}
                    instrumentation.retransformClasses(c);
                }
            }


        } catch (Exception e) {

        }

    }
}

TestTransformer.java 替换目标类的函数

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.Modifier;
public class TestTransformer implements ClassFileTransformer {
    //目标类名称,  .分隔
    private String targetClassName;
    //目标类名称,  /分隔
    private String targetVMClassName;
    private String targetMethodName;


    public TestTransformer(String className,String methodName){
        this.targetVMClassName = new String(className).replaceAll("\\.","\\/");
        this.targetMethodName = methodName;
        this.targetClassName=className;
    }
    //类加载时会执行该函数,其中参数 classfileBuffer为类原始字节码,返回值为目标字节码,className为/分隔
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        //判断类名是否为目标类名
        if(!className.equals(targetVMClassName)){
            System.out.println("not do transform");
            return classfileBuffer;
        }
        try {
            System.out.println("do transform");
            ClassPool classPool = ClassPool.getDefault();
            CtClass cls = classPool.get(this.targetClassName);
            System.out.println(cls.getName());
            CtMethod ctMethod = cls.getDeclaredMethod(this.targetMethodName);
            System.out.println(ctMethod.getName());
            ctMethod.insertBefore("{ System.out.println(\"start\"); }");
            ctMethod.insertAfter("{ System.out.println(\"end\"); }");
            return cls.toBytecode();
        } catch (Exception e) {

        }
        return classfileBuffer;
    }


}

参考链接IDEA + maven 零基础构建 java agent 项目 - 一灰灰Blog - 博客园 (cnblogs.com),将他们打包。

编写测试程序

BaseMain.java

package com.company;

public class BaseMain {

    public int print(int i) {
        System.out.println("i: " + i);
        return i + 2;
    }

    public void run() {
        int i = 1;
        while (true) {
            i = print(i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        BaseMain main = new BaseMain();
        main.run();
        Thread.sleep(1000 * 60 * 60);
    }
}

编写注入程序 attachwithjps.java

import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;

import java.io.IOException;

public class attachwithjps {
    public static void main(String[] args)
            throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException {
        // attach方法参数为目标应用程序的进程号,命令行使用jps -l可以查看相关jvm的进程号
        VirtualMachine vm = VirtualMachine.attach(目标应用程序的进程号);
        // 请用你自己的agent绝对地址,替换这个
       vm.loadAgent("E:/内存马/java-agent/target/java-agent-1.0-SNAPSHOT-jar-with-dependencies.jar");
       vm.detach();
    }
}

注入步骤:

  • ​ 运行被测试程序
  • ​ cmd 输入jps -l 查找目标进程号
  • ​ 运行attach程序

运行结果

image

web应用注入--tomcat

要在tomcat中选择类进行替换实现webshell,需要降低对url的依赖,在tomcat处理请求流程中选择最通用的类。

如internalDoFilter,调用了dofilter,在此之前可以插入代码对request和response作出操作。

具体代码参考rebeyond师傅的
利用“进程注入”实现无文件复活 WebShell - FreeBuf网络安全行业门户

但是,一旦重启tomcat,内存马就会消失,失去目标服务器的权限。要实现服务器重启后,仍能够维持权限,必须要在服务器关闭前将相关代码保存下来,在重启时自动加载。这里rebeyond师傅使用了ShutdownHook技术.

ShutdownHook是JDK提供的一个用来在JVM关掉时清理现场的机制,这个钩子可以在如下场景中被JVM调用:

1.程序正常退出

2.使用System.exit()退出

3.用户使用Ctrl+C触发的中断导致的退出

4.用户注销或者系统关机

5.OutofMemory导致的退出

6.Kill pid命令导致的退出所以ShutdownHook可以很好的保证在tomcat关闭时,我们有机会埋下复活的种子

相关代码

  public static void persist() {
      try {
          Thread t = new Thread() {
              public void run() {
                  try {
                      writeFiles("inject.jar",Agent.injectFileBytes);
                      writeFiles("agent.jar",Agent.agentFileBytes);
                      startInject();
                  } catch (Exception e) {
                  }
              }
          };
          t.setName("shutdown Thread");
          Runtime.getRuntime().addShutdownHook(t);
      } catch (Throwable t) {
      }

JVM关闭前,会先调用writeFiles把inject.jar和agent.jar写到磁盘上,然后调用startInject,startInject通过Runtime.exec启动java -jar inject.jar。

应用:在有能够进行命令执行的情况下,上传agent.jar与需要注入的jar。而后运行agent.jar对其进行注入即可。