0x00:前言

学习一下Java Agent技术,很多技术都会去采用Java Agent该技术去做实现,比方说RASP和内存马(其中一种方式)、包括IDEA的这些破解都是基于Java Agent去做实现。下面来领略该技术的微妙所在。

0x01:Java Agent 机制

在JDK1.5版本开始,Java增加了Instrumentation(Java Agent API)和JVMTI(JVM Tool Interface)功能,该功能可以实现JVM再加载某个class文件对其字节码进行修改,也可以对已经加载的字节码进行一个重新的加载。Java Agent可以去实现字节码插桩、动态跟踪分析等。

Java Aget运行模式

  1. 启动Java程序的时候添加-javaagent(Instrumentation API实现方式)或-agentpath/-agentlib(JVMTI的实现方式)参数
  2. 在1.6版本新增了attach(附加方式)方式,可以对运行中的Java进程插入Agent

方式一中只能在启动前去指定需要加载的Agent文件,而方式二可以在Java程序运行后根据进程ID进行动态注入Agent到JVM里面去。

0x02:Java Agent 概念

Java Agent是一个Java中的命令参数,该参数内容可以指定一个jar包,该jar包内容有一定的规范

  1. jar包中的MANIFEST.MF 文件必须指定 Premain-Class 项
  2. Premain-Class 指定的那个类必须实现 premain() 方法

上面说到的这个premain方法会在运行main方法前被调用,也就是说在运行main方法前会去加载-javaagent指定的jar包里面的Premain-Class类中的premain方法。那么其实Java agent本质上就是一个Java的类,但是普通的Java类是以main方法作为程序入口点,而Java Agent则将premain(Agent模式)和agentmain(Attach模式)作为了Agent程序的入口。

如果需要修改已经被JVM加载过的类的字节码,那么还需要设置在MANIFEST.MF中添加Can-Retransform-Classes: true或Can-Redefine-Classes: true。

先来看看命令参数

命令参数:

-agentlib:<libname>[=<选项>] 加载本机代理库 <libname>, 例如 -agentlib:hprof
另请参阅 -agentlib:jdwp=help 和 -agentlib:hprof=help
-agentpath:<pathname>[=<选项>]
按完整路径名加载本机代理库
-javaagent:<jarpath>[=<选项>]
加载 Java 编程语言代理, 请参阅 java.lang.instrument

上面说到的 java.lang.instrument 提供允许 Java 编程语言代理监测运行在 JVM 上的程序的服务。监测的机制是对方法的字节码的修改,在启动 JVM 时,通过指示代理类 及其代理选项 启动一个代理程序。

该代理类必须实现公共的静态 premain方法,该方法原理上类似于 main 应用程序入口点,并且premain方法的前面也会有一定的要求,签名必须满足一下两种格式:


public static void premain(String agentArgs, Instrumentation inst)
public static void premain(String agentArgs)

JVM会去优先加载带 Instrumentation 签名的方法,加载成功忽略第二种,如果第一种没有,则加载第二种方法。这个逻辑在sun.instrument.InstrumentationImpl类中实现,可以来审计一下该代码

例:


public static void premain(String agentArgs, Instrumentation inst);

参数详细说明:


-javaagent:jarpath&#91;=options]
    jarpath 是指向代理程序 JAR 文件的路径。options 是代理选项。此开关可以在同一命令行上多次使用,从而创建多个代理程序。多个代 理程序可以使用同一 jarpath。代理 JAR 文件必须符合 JAR 文件规范。下面的清单属性是针对代理 JAR 文件定义的:
Premain-Class
    代理类。即包含 premain 方法的类。此属性是必需的,如果它不存在,JVM 将中止。注:这是类名,而不是文件名或路径。
Boot-Class-Path
    由引导类加载器搜索的路径列表。路径表示目录或库(在许多平台上通常作为 jar 或 zip 库被引用)。查找类的特定于平台的机制出现故障之后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。路径使用分层 URI 的路径组件的语法。如果该路径以斜杠字符(“/”)开头,则为绝对路径,否则为相对路径。相对路径根据代理 JAR 文件的绝对路径解析。忽略格式不正确的路径和不存在的路径。此属性是可选的。
Can-Redefine-Classes
    布尔值(true 或 false,与大小写无关)。能够重定义此代理所需的类。值如果不是 true,则被认为是 false。此属性是可选的,默认值为 false。
代理 JAR 文件附加到类路径之后。

在JDK里面有个rt.jar包中存在一个java.lang.instrument的包,这个包提供了Java运行时,动态修改系统中的Class类型的功能。但最关键的还是javaagent 。它可以在运行时重新接收外部请求,对class类型进行一个修改。

这里面有2个重要的接口Instrumentation和 ClassFileTransformer

Instrumentation接口

先来看看Instrumentation接口中的内容

来看到上图,这是java.lang.instrument.Instrumentation中的一些方法。借鉴一下javasec里面的一张图,该图片描述了各种方法的一个作用

java.lang.instrument.Instrumentation的作用是用来监测运行在JVM中的Java API,利用该类可以实现如下功能:

  1. 动态添加或移除自定义的ClassFileTransformer(addTransformer/removeTransformer),JVM会在类加载时调用Agent中注册的ClassFileTransformer;
  2. 动态修改classpath(appendToBootstrapClassLoaderSearch、appendToSystemClassLoaderSearch),将Agent程序添加到BootstrapClassLoader和SystemClassLoaderSearch(对应的是ClassLoader类的getSystemClassLoader方法,默认是sun.misc.Launcher$AppClassLoader)中搜索;
  3. 动态获取所有JVM已加载的类(getAllLoadedClasses);
  4. 动态获取某个类加载器已实例化的所有类(getInitiatedClasses)。
  5. 重定义某个已加载的类的字节码(redefineClasses)。
  6. 动态设置JNI前缀(setNativeMethodPrefix),可以实现Hook native方法。
  7. 重新加载某个已经被JVM加载过的类字节码retransformClasses)。

这里已经表明各大实现功能所对应的方法了。

0x03:Java Agent 技术实现

上面说的都是一些概念性的问题,现在去做一个Java agent的实现

来看一下实现的大致几个步骤

  1. 定义一个 MANIFEST.MF 文件,必须包含 Premain-Class 选项,通常也会加入Can-Redefine-Classes 和 Can-Retransform-Classes 选项。
  2. 创建指定的Premain-Class类,并且里面包含premain 方法,方法逻辑由用户自己确定
  3. 把premain 和MANIFEST.MF文件打包成一个jar包
  4. 使用 -javaagent: jar参数包路径 启动要代理的方法。

完成以上步骤后,启动程序的时候会去执行premain 方法,当然这个肯定是优先于main方法执行的。但是不免会有一些系统类优先于javaagent进行执行。但是用户类这些肯定是会被javaagent给拦截下来的。这么这时候拦截下来后就可以进行一个重写类等操作,例如使用ASM、javassist,cglib等等来改写实现类。在实现里面需要去些2个项目,一个是javaAgent的类,一个是需要JavaAagent需要去代理的类。在mian方法执行前去执行的一些代码。

JVM运行前运行

1、首先写一个agent程序

package com.test;

import java.lang.instrument.Instrumentation;

public class TestAgent {
    public static void premain(String agentArgs, Instrumentation inst){
        System.out.println("premain start");
        System.out.println("agentArgs"+agentArgs);
    }
}

代码很简单,只有一个premain方法,顾名思义它代表着他将在主程序的main方法之前运行,agentArgs代表传递过来的参数,inst则是agent技术主要使用的API,我们可以使用它来改变和重新定义类的行为,这里我们简单的进行一下参数打印。

2、编写MANIFEST.MF文件

MANIFEST.MF文件用于描述Jar包的信息,例如指定入口函数等。我们需要在该文件中加入如下配置,指定我们编写的含有premain方法类的全路径,然后将agent类打成Jar包。

Manifest-Version: 1.0
Premain-Class: com.test.TestAgent

将TestAgent类编译为jar包

编译

3、新建一个项目

在配置中增加-javaagent参数配置agent

-javaagent:out\Agent1.jar 后面不能有空格

4、编写一个main方法

将jar包放到out下

package com.test;

import java.io.IOException;
import java.io.InputStream;

public class test {


    public static void main(String[] args) throws IOException {
        System.out.println("main");


    }

}

可见这里先执行了jar包中的代码后执行test.java

JVM运行前运行

在主程序运行之前的agent模式有一些缺陷,例如需要在主程序运行前就指定javaagent参数,这里在实战注入内存马的时候相当于没用,premain方法中代码出现异常会导致主程序启动失败等,为了解决这些问题,JDK1.6以后提供了在程序运行之后改变程序的能力。它的实现步骤和之前的模式类似

1、编写agent类

我们复用上面的类,将premain方法修改为agentmain方法,由于是在主程序运行后再执行,意味着我们可以获取主程序运行时的信息,这里我们打印出来主程序中加载的类名。

package com.test;

import java.lang.instrument.Instrumentation;

public class TestAgent {
    public static void agentmain(String args, Instrumentation inst){
        System.out.println("loadagent after main run.args="+args);
        Class<?>[] classes = inst.getAllLoadedClasses();
        for (Class<?> cls : classes){
            System.out.println(cls.getName());
        }
        System.out.println("agent run completely");
    }
}

2、修改MANIFEST.MF文件并打包

Manifest-Version: 1.0
Agent-Class: com.test.TestAgent

3、启动主程序,编写加载agent类的程序

在程序运行后加载,我们不可能在主程序中编写加载的代码,只能另写程序,那么另写程序如何与主程序进行通信?这里用到的机制就是attach机制,它可以将JVM A连接至JVM B,并发送指令给JVM B执行,JDK自带常用工具如jstack,jps等就是使用该机制来实现的。这里我们先用tomcat启动一个程序用作主程序B,再来写A程序代码

package com.test;


import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import sun.tools.attach.WindowsVirtualMachine;

import java.io.IOException;

public class test {
    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {

        WindowsVirtualMachine wvm = (WindowsVirtualMachine) WindowsVirtualMachine.attach("3384");
        wvm.loadAgent("E://Public/Agent1/out/artifacts/Agent1_jar/Agent1.jar");
    }
}

我们使用VirtualMachine attach到目标进程,其中3384为tomcat进程的PID,可以使用jps命令获得,也可以使用WindowsVirtualMachine.list方法获取本机上所有的Java进程,再来判断tomcat进程,loadAgent方法第一个参数为Jar包在本机中的路径,第二个参数为传入agentmain的args参数,此处为null,运行程序

然而什么都没有打印啊!是不是什么地方写错了呢?仔细想想就会发现,我们是将进程attach到了tomcat进程上,agent其实是在主程序B中运行的,所以程序A中自然就不会进行打印,我们跳回tomcat程序的控制台,查看结果。

可以看到,agentmain方法中的代码已经在主程序中顺利运行了,并且打印出了程序中加载的类!

0x04:参考

https://www.cnblogs.com/nice0e3/p/14086165.html

https://www.jianshu.com/p/63c328ca208d

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注