JavaAgent型内存马基础
Java Instrumentation
java Instrumentation指的是可以用独立于应用程序之外的代理(agent)程序来监测和协助运行在JVM上的应用程序。这种监测和协助包括但不限于获取JVM运行时状态,替换和修改类定义等。简单一句话概括下:Java Instrumentation可以在JVM启动后,动态修改已加载或者未加载的类,包括类的属性、方法。
java agent技术原理及简单实现 - kokov - 博客园 (cnblogs.com)
什么是java agent?
IDEA + maven 零基础构建 java agent 项目 - 一灰灰Blog - 博客园 (cnblogs.com)
java agent本质上可以理解为一个插件,该插件就是一个精心提供的jar包,这个jar包通过JVMTI(JVM Tool Interface)完成加载,最终借助JPLISAgent(Java Programming Language Instrumentation Services Agent)完成对目标代码的修改。
java agent技术的主要功能如下:
- 可以在加载java文件之前做拦截把字节码做修改
- 可以在运行期将已经加载的类的字节码做变更
- 还有其他的一些小众的功能
- 获取所有已经被加载过的类
- 获取所有已经被初始化过了的类
- 获取某个对象的大小
- 将某个jar加入到bootstrapclasspath里作为高优先级被bootstrapClassloader加载
- 将某个jar加入到classpath里供AppClassloard去加载
- 设置某些native方法的前缀,主要在查找native方法的时候做规则匹配
Instrument
(32条消息) ClassPool CtClass浅析_罗小辉的专栏-CSDN博客
instrument是JVM提供的一个可以修改已加载类的类库,专门为Java语言编写的插桩服务提供支持。它需要依赖JVMTI的Attach API机制实现。在JDK 1.6以前,instrument只能在JVM刚启动开始加载类时生效,而在JDK 1.6之后,instrument支持了在运行时对类定义的修改。要使用instrument的类修改功能,我们需要实现它提供的ClassFileTransformer接口,定义一个类文件转换器。接口中的transform()方法会在类文件被加载时调用,而在transform方法里,我们可以利用ASM或Javassist对传入的字节码进行改写或替换,生成新的字节码数组后返回。
总之,transform返回值为需要替换的class的字节码。有两种方法获取字节码,一种使用文件读取的方式,直接读取相应class文件的字节码,还有一种使用Javaassist包,结合反射机制进行字节码的替换。
我们来看一下第二种的示例代码
SimpleAgent.java 作为Javagent去注入目标程序
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Method;
import java.util.LinkedList;
import java.util.List;
public class SimpleAgent {
/**
* jvm 参数形式启动,运行此方法
*
* @param agentArgs
* @param inst
*/
private static String className = "com.company.BaseMain";
private static String methodName = "print";
public static void premain(String agentArgs, Instrumentation instrumentation) {
System.out.println("premain");
//instrumentation.addTransformer(new TestTransformer(className, methodName));
}
/**
* 动态 attach 方式启动,运行此方法
*
* @param agentArgs
* @param instrumentation
*/
public static void agentmain(String agentArgs, Instrumentation instrumentation) {
System.out.println("agentmain");
instrumentation.addTransformer(new TestTransformer(className, methodName),true);
try {
List<Class> needRetransFormClasses = new LinkedList<>();
Class[] loadedClass = instrumentation.getAllLoadedClasses();//获取所有加载的类
for (Class c : loadedClass) {
//System.out.println(loadedClass[i].getName());
if (c.getName().equals(className)) {
System.out.println("---find!!!---");
Method[] methods = c.getDeclaredMethods();
for(Method method : methods)
{System.out.println(method.getName());}
instrumentation.retransformClasses(c);
}
}
} catch (Exception e) {
}
}
}
TestTransformer.java 替换目标类的函数
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.Modifier;
public class TestTransformer implements ClassFileTransformer {
//目标类名称, .分隔
private String targetClassName;
//目标类名称, /分隔
private String targetVMClassName;
private String targetMethodName;
public TestTransformer(String className,String methodName){
this.targetVMClassName = new String(className).replaceAll("\\.","\\/");
this.targetMethodName = methodName;
this.targetClassName=className;
}
//类加载时会执行该函数,其中参数 classfileBuffer为类原始字节码,返回值为目标字节码,className为/分隔
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
//判断类名是否为目标类名
if(!className.equals(targetVMClassName)){
System.out.println("not do transform");
return classfileBuffer;
}
try {
System.out.println("do transform");
ClassPool classPool = ClassPool.getDefault();
CtClass cls = classPool.get(this.targetClassName);
System.out.println(cls.getName());
CtMethod ctMethod = cls.getDeclaredMethod(this.targetMethodName);
System.out.println(ctMethod.getName());
ctMethod.insertBefore("{ System.out.println(\"start\"); }");
ctMethod.insertAfter("{ System.out.println(\"end\"); }");
return cls.toBytecode();
} catch (Exception e) {
}
return classfileBuffer;
}
}
参考链接IDEA + maven 零基础构建 java agent 项目 - 一灰灰Blog - 博客园 (cnblogs.com),将他们打包。
编写测试程序
BaseMain.java
package com.company;
public class BaseMain {
public int print(int i) {
System.out.println("i: " + i);
return i + 2;
}
public void run() {
int i = 1;
while (true) {
i = print(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
BaseMain main = new BaseMain();
main.run();
Thread.sleep(1000 * 60 * 60);
}
}
编写注入程序 attachwithjps.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 attachwithjps {
public static void main(String[] args)
throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException {
// attach方法参数为目标应用程序的进程号,命令行使用jps -l可以查看相关jvm的进程号
VirtualMachine vm = VirtualMachine.attach(目标应用程序的进程号);
// 请用你自己的agent绝对地址,替换这个
vm.loadAgent("E:/内存马/java-agent/target/java-agent-1.0-SNAPSHOT-jar-with-dependencies.jar");
vm.detach();
}
}
注入步骤:
- 运行被测试程序
- cmd 输入jps -l 查找目标进程号
- 运行attach程序
运行结果
web应用注入--tomcat
要在tomcat中选择类进行替换实现webshell,需要降低对url的依赖,在tomcat处理请求流程中选择最通用的类。
如internalDoFilter,调用了dofilter,在此之前可以插入代码对request和response作出操作。
具体代码参考rebeyond师傅的
利用“进程注入”实现无文件复活 WebShell - FreeBuf网络安全行业门户
但是,一旦重启tomcat,内存马就会消失,失去目标服务器的权限。要实现服务器重启后,仍能够维持权限,必须要在服务器关闭前将相关代码保存下来,在重启时自动加载。这里rebeyond师傅使用了ShutdownHook技术.
ShutdownHook是JDK提供的一个用来在JVM关掉时清理现场的机制,这个钩子可以在如下场景中被JVM调用:
1.程序正常退出
2.使用System.exit()退出
3.用户使用Ctrl+C触发的中断导致的退出
4.用户注销或者系统关机
5.OutofMemory导致的退出
6.Kill pid命令导致的退出所以ShutdownHook可以很好的保证在tomcat关闭时,我们有机会埋下复活的种子
相关代码
public static void persist() {
try {
Thread t = new Thread() {
public void run() {
try {
writeFiles("inject.jar",Agent.injectFileBytes);
writeFiles("agent.jar",Agent.agentFileBytes);
startInject();
} catch (Exception e) {
}
}
};
t.setName("shutdown Thread");
Runtime.getRuntime().addShutdownHook(t);
} catch (Throwable t) {
}
JVM关闭前,会先调用writeFiles把inject.jar和agent.jar写到磁盘上,然后调用startInject,startInject通过Runtime.exec启动java -jar inject.jar。
应用:在有能够进行命令执行的情况下,上传agent.jar与需要注入的jar。而后运行agent.jar对其进行注入即可。