前言
这次要讲的是Agent型内存马,讲真我个人感觉Agent型内存马是内存马里面最好玩的一个了。在写这篇Agent内存马的时候,我搭建了一个shiro环境上传了一个冰蝎马然后上冰蝎进行注入,结果却失败了,这就让我很差异。将Agent内存马的笔记整理好后我总结出我们这篇文章的学习目的:
- 什么是JavaAgent?
- 什么是Instrumentation?
- 什么是Javassist?
- 如何通过JavaAgent实现内存马?
- 如何在实战中实现Agent内存马?
- 冰蝎是如何进行Agent内存马注入的?
- 我们如何手工注入Agent内存马?
以上就是我们这篇文章提出的问题,废话不多说我们直接开始进行Agent内存马的分析。
一、基础知识-JavaAgent
(1)JavaAgent简介
不知道大家对JavaAgent之前有没有了解,我最开始是看有些破解用的这玩意(づ ̄ 3 ̄)づ
在Java1.5以后引入了JavaAgent技术,Java Agent 能够在不影响正常编译的情况下来修改字节码,即动态修改已加载或者未加载的类,包括类的属性、方法。
JavaAgent其实也就是一个 Jar 包,只是启动方式和普通 Jar 包有所不同,对于普通的Jar包,通过指定类的 main 函数进行启动,但是 Java Agent 并不能单独启动,必须依附在一个 Java 应用程序运行。而它的启动方法共有两种,一种的方法是premain,一种是agentmain。
- jvm方式:实现 premain方法,在JVM启动前加载。// jvm 参数形式启动,运行此方法。
- attach方法:实现 agentmain方法,在JVM启动后加载。// 动态 attach 方式启动,运行此方法。
其中 jvm方式,也就是说要使用这个 agent 的目标应用,在启动的时候,需要指定 jvm 参数-javaagent:xxx.jar。而当目标应用程序启动之后,没有添加 -javaagent 加载我们的 agent,但我们希望目标程序使用我们的 agent,这时候就可以使用 attach 方式来使用。
(2)JavaAgent-premain方法
我们来做一个premain方法的实验看看它是什么样的。需要从头搭建环境小伙伴可以参考如下链接。
https://zhuanlan.zhihu.com/p/113523189
首先我们写一个premain的方法。
接下来我们在pom文件里添加配置。
通过mvn assembly:assembly 命令打包,生成出我们的jar包。
接下来我们新建一个项目,写一个测试类,运行查看一下。
接下来我们通过设置-javaagent:jar包绝对路径,重新运行。
可以看到我们成功执行了方法,且只执行了一次。
(2)JavaAgent-agentmain方法
刚才说的premain方法是在JVM启动前进行的,而这里说的agentmain方法却是在启动后进行的,我们继续进行实验,写一个agentmain方法并打包。
接下来我们就需要将需要思考如何将agentmain注入到JVM中,这里我们就引出了VirtualMachine,我们来看一下VirtualMachine。
什么是VirtualMachine?在JDK中com.sun.tools.attach.VirtualMachine提供了一些从外部进程attach到jvm上。并执行一些操作的功能,而我们就通过它来将agentmain注入到JVM中。
参考:https://xz.aliyun.com/t/9450#toc-3
https://nowjava.com/docs/java-api-11/jdk.jdi/com/sun/jdi/VirtualMachine.html
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) { ... } }
按照我们上面的说的方法,我们写一个简单的通过pid连接JVM并加载agent的代码。
package com.potato.memshell;
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 AttachMainTest {
public static void main(String[] args) throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException {
//attach方法参数为目标应用程序的进程号
VirtualMachine vm = VirtualMachine.attach("38060");
// 请用你自己的agent绝对地址,替换这个ps
vm.loadAgent("打包agnet的路径\\target\\memshellTest_Agent-1.0-SNAPSHOT-jar-with-dependencies.jar");
System.out.println("potato said stop!");
}
}
然后我们通过jps -l来获得我们目标应用的进程号。
可以看到我们成功输出了Potato巴拉巴拉,agent被成功注入进去了。
二、基础知识-Instrumentation
我们通过上面已经知道JavaAgent的使用方法了,那我们应该如何实现内存马呢?在刚才的实验中,我们得知premain方法是在启动前执行,这肯定不满足实现内存马的条件。而agentmain方法是在启动后执行的,所以agentmain就是我们实现内存马的方法。接下来我们来深入了解一下agentmain这个方法。我们回过头来看一下agentmain这个方法,可以看到它有两个方法参数分别是String agentArgs、Instrumentation inst。我们来一个一个看。
首先我们看String agentArgs,这个其实就是我们在实现方法的时候接收的一个参数。我们简单拿agentmain举个例子。
接下来我们就看第二个参数Instrumentation inst,这是java.lang.instrument.Instrumentation的实例。首先我们来学习一些基本的前置知识。
参考:https://www.apiref.com/java11-zh/java.instrument/java/lang/instrument/Instrumentation.html
http://wjlshare.com/archives/1582
https://xz.aliyun.com/t/9450#toc-5
首先JAVA Instrumentation 指的是可以用独立于应用程序之外的代理(agent)程序来监测和协助运行在JVM上的应用程序。Instrumentation 是 JVMTIAgent(JVM Tool Interface Agent)的一部分。
我们使用 Insrumentation 构建一个独立于应用程序的代理程序(Agent),监测和协助运行在 JVM 上的程序,甚至可以替换和修改某些类的定义。简单的来说 开发者使用Instrumentation 可以实现一种虚拟机级别的AOP实现。
说白了就是Java agent 通过这个类和目标JVM进行交互,从而达到修改数据的效果。
既然我们要学习这个类,我们接下来看看它都有哪些方法。
可以看到它还是存在不少方法的,我们这里只说接下来需要用到的方法。
addTransformer(ClassFileTransformer transformer, boolean canRetransform);
这个方法用来于注册 Transformer ,它有两个参数 transformer 和 canRetransform。
transformer - 要注册的变压器;
canRetransform - 是否重新转换此变换器的转换;
transformer是什么呢?(其实说到transformer我就想到在CC链里的链式调用/(ㄒoㄒ)/~~)
在 Instrumentation 中增加了名叫 transformer 的 Class 文件转换器,转换器可以改变二进制流的数据,Transformer 可以对未加载的类进行拦截,同时也可对已加载的类进行重新拦截,所以根据这个特性我们能够实现动态修改字节码。
我们去看一下它的接口类ClassFileTransformer,它是类的转换器,参数如下。
loader - 要转换的类的定义加载程序,如果引导加载程序可能是null
className - Java虚拟机规范中定义的完全限定类和接口名称的内部形式的类的名称。classBeingRedefined - 如果由重新定义或重新转换触发,则重新定义或重新转换类; 如果这是一个类加载为null
protectionDomain - 正在定义或重新定义的类的保护域classfileBuffer - 类文件格式的输入字节缓冲区 - 不得修改
参考:https://www.apiref.com/java11-zh/java.instrument/java/lang/instrument/ClassFileTransformer.html#transform(java.lang.ClassLoader,java.lang.String,java.lang.Class,java.security.ProtectionDomain,byte%5B%5D)
https://xz.aliyun.com/t/9450#toc-5
一旦变换器注册了addTransformer ,将为每个新定义的类和每个重新定义类调用变换器。里面的东西很多这翻译的也很阿巴阿巴阿巴。这里用cszeromirror的解释,如下。
使用Instrumentation.addTransformer()来加载一个转换器。
转换器的返回结果(transform()方法的返回值)将成为转换后的字节码。
对于没有加载的类,会使用ClassLoader.defineClass()定义它;对于已经加载的类,会使用ClassLoader.redefineClasses()重新定义,并配合Instrumentation.retransformClasses进行转换。
其实我看官方文档的我个人的理解就是对每个新定义的类和每个重新定义类调用转换器,让其变成转换后的字节码,而我们的内存马也就是在这个转换的过程进行注入。
getAllLoadedClasses:获取所有已经加载的类。
就,获取所有已经加载的类。
isModifiableClasses:判断某个类是否能被修改。
就,判断某个类是否能被修改。
retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
retransformClasses 方法能对已加载的 class 进行重新定义,也就是说如果我们的目标类已经被加载的话,我们可以调用该函数,来重新触发这个Transformer的拦截,以此达到对已加载的类进行字节码修改的效果。
三、基础知识-Javassist
我们已经初步学习了Instrumentation实例和它所的方法。其实addTransformer方法声明里对这个流程说的就很清晰,我们通过增加一个Class文件转换器来改变class文件数据,增加转换器之后,后续的类加载都会被Transformer拦截。而对已经类加载过的类,执行retransformClasses重新触发Transformer的拦截。
增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
清楚了上面的流程之后,我们可以知道内存马是通过转换器进行修改class文件数据的,可是我们怎么去修改class文件数据呢?此时就引出了我们的Javassist。
首先简单介绍一些javassist
Javassist (JAVA programming ASSISTant) 是在 Java 中编辑字节码的类库;它使 Java 程序能够在运行时定义一个新类, 并在 JVM 加载时修改类文件。
参考:
https://www.cnblogs.com/rickiyang/p/11336268.html
https://xz.aliyun.com/t/9450#toc-3
在第一个链接里面对javassist说的很详细,但是我们这里就只说我们接下来需要使用的,这里引用了很多第二个链接cszeromirror师傅文章里的解析,真的很清晰了。
ClassPool
ClassPool是CtClass对象的容器。CtClass对象必须从该对象获得。如果get()在此对象上调用,则它将搜索表示的各种源ClassPath 以查找类文件,然后创建一个CtClass表示该类文件的对象。创建的对象将返回给调用者。
获得方法:ClassPool cp = ClassPool.getDefault(); 。通过 ClassPool.getDefault() 获取的 ClassPool 使用 JVM 的类搜索路径。如果程序运行在 JBoss 或者 Tomcat 等 Web 服务器上,ClassPool 可能无法找到用户的类,因为 Web 服务器使用多个类加载器作为系统类加载器。在这种情况下,ClassPool 必须添加额外的类搜索路径。
cp.insertClassPath(new ClassClassPath(<Class>));
CtClass
类 Javaassit.CtClass 表示 class 文件。需要从ClassPool中获得。
ClassPool cp = ClassPool.getDefault();
CtClass ctc = cp.get(com.potato.memshell.hello);
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);
}
CtClass.detach()
在最后我们一定要避免内存溢出,如果 CtClass 对象的数量变得非常大,ClassPool 可能会导致巨大的内存消耗。 所以为了避免此问题,可以从 ClassPool 中显式删除不必要的 CtClass 对象。 如果对 CtClass 对象调用 detach(),那么该 CtClass 对象将被从 ClassPool 中删除。 当然了这也是我们注入内存马很小概率出现的问题。我们在写代码的时候还是稍微注意一下。
四、Agent内存马原理实验
根据我们上面所说的基础知识,大家应该已经了解了Agent内存马的实现原理,一句话就是利用JavaAgent的agentmain方法在Instrumentation转换器的过程中通过javassist修改class字节码。
接下来我们来做一个简单的内存马原理实验。
首先我们写一个程序,在执行hello方法后等待输入,当我们注入JavaAgent后在进行重新执行。
potato.java
package com.potato.memshell;
import java.util.Scanner;
public class potato {
public static void main(String[] args) {
hello h1 = new hello();
h1.hello();
// 等待输入
Scanner sc = new Scanner(System.in);
sc.nextInt();
// 重新执行
hello h2 = new hello();
h2.hello();
}
}
hello.java
package com.potato.memshell;
public class hello {
public void hello() {
System.out.println("hello world");
}
}
接下来我们开始写JavaAgent
参考:https://xz.aliyun.com/t/9450#toc-3
这段代码我们书写了一个agentmain,先获取所有已经加载的类,然后做一个循环判断类是否已经加载,之后添加Transformer,然后在通过retransformClasses触发过滤已加载的类。
JavassistTest.java(agentmain)
package com.potato.agentTest;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class JavassistTest {
public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
Class[] classes = inst.getAllLoadedClasses();
// 判断类是否已经加载
for (Class aClass : classes) {
if (aClass.getName().equals(JavassistTest_Transformer.editClassName)) {
// 添加 Transformer
inst.addTransformer(new JavassistTest_Transformer(), true);
// 触发 Transformer
inst.retransformClasses(aClass);
}
}
}
}
JavassistTest_Transformer.java(Transformer)
package com.potato.agentTest;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class JavassistTest_Transformer implements ClassFileTransformer {
// 如果在使用过程中找不到javassist包中的类,那么可以使用URLCLassLoader+反射的方式调用,这里我是通过pom.xml重新导了一个。
// 只需要修改这里就能修改别的函数
public static final String editClassName = "com.potato.memshell.hello";
public static final String editClassName2 = editClassName.replace('.', '/');
public static final String editMethod = "hello";
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try {
//使用 JVM 的类搜索路径
ClassPool cp = ClassPool.getDefault();
if (classBeingRedefined != null) {
//如果存在重定义的类,添加额外的类搜索路径
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
cp.insertClassPath(ccp);
}
//获取我们需要的Class对象
CtClass ctc = cp.get(editClassName);
//获取我们需要的Method对象
CtMethod method = ctc.getDeclaredMethod(editMethod);
String source = "{System.out.println(\"hello transformer\");}";
// setBody:设置方法体
// insertBefore:插入在方法体最前面
// insertAfter:插入在方法体最后面
// insertAt:在方法体的某一行插入内容
//修改方法体
method.setBody(source);
//获得修改过的类文件
byte[] bytes = ctc.toBytecode();
ctc.detach();
return bytes;
} catch (Exception e){
e.printStackTrace();
}
return null;
}
}
打包的时候别忘了修改pom.xml。
我们开始启动,运行第一次。
接下来通过jps -l获取进程,然后进行注入。
输入参数,进行第二次执行,发现成功修改。
这个其实就是Agent内存马最基本的原理,是不是很好玩。现在我们已经知道内存马的实现原理了,但是我们不可能都是通过jps -l获取进程了,假如有反序列化的话,我想之间把内存马打进去呀。所以我们接下来实现实战中如何打Agent内存马。
五、Agent内存马模拟实战
这里我搭了一个shiro框架,我们来做一个通过反序列化将内存马注入进去的实验。
首先我们来制作JavaAgent的jar包。
其实这里我们只需要把之前的实验代码进行简单的修改就可以了,这个很简单。但是我们是在哪里添加的内存马代码呢?
参考:http://wjlshare.com/archives/1582
这里看了木头师傅的文章,还记得我们的Filter内存马么,这里我们的容器都是Tomcat,而我们通过之前的文章可以知道,当我们在注册了Filter时,它就会将某个Servlet进行拦截。而Filter链一定会调用dofilter方法的。所以我搭建了一个springboot的环境对栈进行观察(为什么不用shiro观察,别问,问就是菜)
https://www.freebuf.com/articles/web/321975.html
我们打一个断点观察栈信息,我们进到ApplicationFilterchain#dofilter方法
XDM,这方法不是太香了么,request和response都在,我们只需要在这方法前将我们的马子打进去就OK了啊。
接下来我们开始制作JavaAent,这里我就不贴代码了,之间cszeromirror师傅和木头师傅的文章有很多代码进行参考,希望大家自己去写一个Agent内存马,上面的知识绝对足够写了。我注意说一下几个小的注意点。
- 注意如果存在重定义的类,添加额外的类搜索路径。
//使用 JVM 的类搜索路径
ClassPool cp = ClassPool.getDefault();
if (classBeingRedefined != null) {
//如果存在重定义的类,添加额外的类搜索路径
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
cp.insertClassPath(ccp);
}
- 注意别忘了修改pom.xml。
接下来我们需要只要在写一个注入类就OK了,这里木头师傅的文章提供了注入代码,大家可以去看一下,我就不贴了,这里我只说几个注意点。
http://wjlshare.com/archives/1582
- VirtualMachine需要通过URLCLassLoader+反射的形式调用。
- Tomcat进程名一般为org.apache.catalina.startup.Bootstrap,这个可以看我sevlet内存马的文章的部分内容。
- 反序列化需要将其做成对应利用链的执行格式。
将注入类编译后,接下来我们开始进行Shiro反序列化攻击,通过CB原生链进行攻击(这里不清楚的可以去看我之前写的CB链)。
这里我们先不考虑shiro利用Header头长度限制问题,这不是这篇文章的重点。如上图所示成功注入。
六、Agent内存马冰蝎真打
我们已经模拟了正常实战时候的内存马,不过我们真打实战注入个这种命令执行的马其实挺没意思的,我们都知道冰蝎可以注入Agent马,但是经过我自己的实战经验和朋友们的使用情况发现冰蝎的内存马注入其实不是那么特别好用,所以我就在shiro里面上了个冰蝎马然后注入试试。
可以看到我成功连接了冰蝎马
WHAT?为啥注入失败了?为了思考这个文件我去看了一下冰蝎的源码。
这段源码分析过后发现冰蝎的Agent内存马是注入到HttpServlet#service中的,它的流程其实本质上还是通过Javassist获取HttpServlet,然后在service方法注入内存马。我通过springboot打了一个断点分析到了注入的为止,哎,别说,这位置做内存马注入确实挺香啊。而且我们观察栈信息,该位置也是ApplicationFilterChain链下级执行到的地方,也就是说每次请求都会通过这里。OK
通过源码我们发现它的shellcode也在。我们格式化一下shellcode做个分析。这段代码我只截图了一部分,其实这段shellcode就是一个内存马版的冰蝎马。前面做了一下请求路径的比对,然后进行密钥的核对,如果都通过了就回传数据也就是连接上了冰蝎。
看到这里我就合计,那要不我就自己写一个吧,原理其实都一样。这里其实只有一个需要注意的地方。
我就贴这一段代码了,能认真看到这里的,懂的都懂。
CtClass request = pool.getCtClass("javax.servlet.ServletRequest");
CtClass response = pool.getCtClass("javax.servlet.ServletResponse");
依旧拿CB打一套注入,冰蝎马注入成功。
什么?那冰蝎马本身的Agent为啥没注入进去你分析出来了么,闭嘴。
关于冰蝎本身Agent马的注入原理可能就需要以后研究了,冰蝎源码分析这个问题等有时间一定会去学习的,神仙师傅们的工具yyds。
可爱小尾巴~~~
我只是个基础很差 技术很菜 脚本小子里面的小菜鸡,文章里面有什么写的不对的地方,望师傅们多加指正,我肯定狂奔加小跑的学。我个人觉得agent内存马是这几个内存马里面最好玩的,当然Filter内存马玩起来也不差。其实我了解到的其实还有一些内存马的打法但是我还没有怎么了解过,关于特定框架的例如spring-control这种的内存马我现阶段也还没有写的打算。所以这篇文章短期来说应该是内存马系列的最后一篇了,之后关于内存马我会开个专辑不定期更新。写到这里已经凌晨3点了,这篇文章真是从我下班回家一直写到现在。原计划3月份之前把这篇文章写出来的,比预计晚了二天。想学的东西真的是好多啊,之后看看情况更新吧,下个系列写点啥呢(づ ̄ 3 ̄)づ
参考链接
http://wjlshare.com/archives/1545
https://xz.aliyun.com/t/9450#toc-5
https://mp.weixin.qq.com/s/Whta6akjaZamc3nOY1Tvxg
https://www.jianshu.com/p/43424242846b
https://www.cnblogs.com/rickiyang/p/11336268.html
https://docs.oracle.com/javase/9/docs/api/java/lang/instrument/Instrumentation.html