JDK1.5开始引入了Agent机制(即启动java程序时添加“-javaagent”参数,Java Agent机制允许用户在JVM加载class文件的时候先加载自己编写的Agent文件,通过修改JVM传入的字节码来实现注入自定义的代码。采用这种方式时,必须在容器启动时添加jvm参数,所以需要重启Web容器。
JDK1.6新增了attach方式,可以对运行中的java进程附加agent,提供了动态修改运行中已经被加载的类的途径。一般通过VirtualMachine的attach(pid)方法获得VirtualMachine实例,随后可调用loadagent方法将JavaAgent的jar包加载到目标JVM中。
下面一个章节笔者将通过两个demo案例说明JavaAgent技术的两种方式,让读者明白premain和agentmain的具体原理。
1、Premain
创建一个sayHello类,写一个say()方法。
public class sayHello { public String say() { return “hello,world!”; } } |
创建一个People类,运行say()方法,输出结果为:hello,world!
public class People { public static void main(String[] args) { System.out.println(new sayHello().say()); } } |
创建Transformer重写transformer方法,实现修改传入JVM的字节码。笔者这里通过javassist对类字节码进行处理。
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 Transformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { System.out.println(className); if (className.endsWith(“sayHello”)){ try { final ClassPool classPool = ClassPool.getDefault(); // 创建ClassPool对象 final CtClass ctClass = classPool.get(“org.example.sayHello”); CtMethod ctMethod = ctClass.getDeclaredMethod(“say”); // 获取成员方法 String methodBody = “return “hello premain”;”; ctMethod.setBody(methodBody); //替换方法体中所有内容 byte[] bytes = ctClass.toBytecode(); //使用类CtClass,生成类二进制 //调用CtClass对象的detach()方法 CtClass对象从ClassPool移除掉减少内存消耗 ctClass.detach(); return bytes; } catch (NotFoundException e) { e.printStackTrace(); } catch (CannotCompileException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } return null; } } |
定义Premain类的premain方法
package org.example; import java.lang.instrument.Instrumentation; public class Premain { public static void premain(String agentArgs, Instrumentation inst){ System.out.println(“premain agent run!”); inst.addTransformer(new Transformer()); } } |
使用Maven打包成TestPremain-1.0-SNAPSHOT.jar文件,需要如下修改pom.xml文件。把<Premain-class>设置为premain方法所在类。
<plugin> <artifactId>maven-jar-plugin</artifactId> <version>3.0.2</version> <configuration> <archive> <manifestEntries> <Premain-class>org.example.Premain</Premain-class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive> </configuration> </plugin> |
在运行配置中添加vm选项
图1
运行结果如图2所示,修改了say方法。
图2
2、Agentmain
同premain也创建一个People类循环打印字符串,代码如下所示。
package org.example;
public class People { public void sayHello(String name) { System.out.println(String.format(“%s say hello!”, name)); }
public static void main(String[] args) throws InterruptedException { People p = new People(); for (;;){ Thread.sleep(1000); p.sayHello(Thread.currentThread().getName()); } } } |
重写transform方法,注入进程后打印输出代理的类,代码如下所示。
package org.example;
import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain;
public class Transform implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { System.out.println(String.format(“agent run target class= %s”, className)); return classfileBuffer; } } |
新建Agent类实现agentmain方法,代码如下所示
public class Agent { public static void agentmain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException { inst.addTransformer(new Transform(),true); inst.retransformClasses(Class.forName(“org.example.People”)); } } |
将Agent设置为<Agent-Class>并打包成为jar文件。Pom.xml文件如下所示,值得注意的是如果需要修改已经被JVM加载过的类的字节码,那么还需要在MANIFEST.MF中添加Can-Retransform-Classes:true或Can-Redefine-Classes:true。
<Agent-Class>org.example.Agent</Agent-Class> <Can-Retransform-Classes>true</Can-Retransform-Classes> |
创建Attach类注入目标类的进程,代码如下所示。
public class Attach { public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { String agentPath = “F:\IdeaProjects\TestAgent\target\TestAgent-1.0-SNAPSHOT.jar”; List<VirtualMachineDescriptor> list = VirtualMachine.list(); //获取本机所有运行的Java进程 for (VirtualMachineDescriptor desc :list){ if (desc.displayName().endsWith(“People”)){ VirtualMachine vm = VirtualMachine.attach(desc.id()); vm.loadAgent(agentPath); vm.detach(); } } } } |
Attach捕获到类进程号如图3所示。
图3
先运行people类在运行attach,运行结果如图4所示。
图4
由上文可知Agentmain可实现最重要的三个类Agent Attach Transform,来分析冰蝎作者之前写的memshell实现原理,项目地址:https://github.com/rebeyond/memShell.gitMemshell中Transform类代码如图5所示。
图5
不同于前面章节的demo,这里除了使用ClassPool.getDefault()还使用ClassClassPath搜索class路径其原理是:ClassPool.getDefault()获取的ClassPool使用JVM的classpath。在Tomcat等Web服务器运行时,服务器会使用多个类加载器作为系统类加载器,这可能导致ClassPool可能无法找到用户的类。这时,ClassPool须添加额外的classpath才能搜索到用户的类。
CtClass cc = cp.get(“org.apache.catalina.core.ApplicationFilterChain”); CtMethod m = cc.getDeclaredMethod(“internalDoFilter”); m.addLocalVariable(“elapsedTime”, CtClass.longType); m.insertBefore(readSource()); |
如上代码:作者Hook了ApplicationFilterChain中的internalDoFilter方法,然后定义一个long类型的属性,elapsedTime,并通过insertBefore方法将source.txt中内容插入到方法内容的开始处。source.txt是url参数和agent交互的逻辑,如图6所示。
图6
笔者之前用此内存马时发现两个特点:第一是该内存马会自己删除jar包,实现代码如下。
图7
第二点是重启tomcat服务之后内存马还是存在,只有通过jps-l kill掉进程后启动服务才能删除内存马,其原理是使用了ShutdownHook机制。
图8
通过使用Runtime.addShutdownHook(Thread hook)方法注册JVM关闭的勾子,调用writeFiles方法把jar包落地磁盘,再通过Runtime.exec启动java-jar inject.jar。
由于Hook的关键函数ApplicationFilterChain.internalDoFilter是tomcat的方法,导致其他中间件不适用,在冰蝎3.0中的内存马作者更改了Hook点。(源码版本为V3.0 Beta11_t00ls)在agentmain中做了一个判断,如果是Tomcat选择hook javax.servlet.http.HttpServlet中的service方法,如果是weblogic选择hookweblogic.servlet.internal.ServletStubImpl中的execute方法。
代码如图9所示。
图9
在jdk9及以后的版本不允许SelfAttach(即无法attach自身的进程)。修改前面章节Attach demo,将jdk换成9之后的,attach自身的PID会报错提示Can not attach to current VM。代码如下,报错截图如图10所示。
public class Attach { public static void main(String[] args) throws Exception { List<VirtualMachineDescriptor> list = VirtualMachine.list(); for(VirtualMachineDescriptor desc : list){ System.out.println(“进程ID:” + desc.id() + “,进程名称:” + desc.displayName()); } Scanner myObj = new Scanner(System.in); System.out.println(“输入要注入的进程:”); String pid = myObj.nextLine(); String agentPath = “F:\IdeaProjects\TestAgent\target\TestAgent-1.0-SNAPSHOT.jar”; VirtualMachine vm = VirtualMachine.attach(pid); vm.loadAgent(agentPath); vm.detach(); }
} |
图10
看到Rebeyond师傅在《Java内存攻击技术漫谈》中提出一种方法,绕过allowAttachSelf。首先Debug attch执行流程,如图11所示。可以发现attach的时候会创建一个HotSpotVirtualMachine的父类对象,取键值对jdk.attach.allowAttachSelf的值计算后保存到ALLOW_ATTACH_SELF中,可通过反射修改该属性值。
图11
ALLOW_ATTACH_SELF字段有final修饰符,需要设置setAccessible(true);具体代码如下所示。
Class cls=Class.forName(“sun.tools.attach.HotSpotVirtualMachine”); Field field=cls.getDeclaredField(“ALLOW_ATTACH_SELF”); field.setAccessible(true); Field modifiersField=Field.class.getDeclaredField(“modifiers”); modifiersField.setAccessible(true); modifiersField.setInt(field,field.getModifiers()&~Modifier.FINAL); field.setBoolean(null,true); |
修改后会弹出警告信息如图12所示,成功注入结果如图13所示。
图12
图13
回到冰蝎3.0源码中,通过setProperty将jdk.attach.allowAttachSelf设置为true,实现绕过SelfAttach。
System.setProperty(“jdk.attach.allowAttachSelf”, “true”); |
本文从permain和agentmain两种实现JavaAgent的原理方法引入到java agent在内存马中的应用,通过分析memshell到冰蝎3.0内存马源码,加深了对agent型内存马Hook的关键函数、持久化方法以及绕过SelfAttach方法等内存马技术点的理解与学习,希望对读者有帮助。