JaveSec javaagent技术研究

一、前言

在学习java反序列化漏洞的时候,看到有采用 notsoserial 的黑白名单javaagent机制实现对Java反序列化漏洞的修复

java 在运行时候提供参数

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

在JDK1.5之后Java 提供java.lang.instrument支持,这是在rt.jar 中定义的一个包,该路径下有两个重要的类

  1. Instrumentation

    image-20201227185725035

    从函数的名称可以看出Instrumentation提供的功能:

    1.addTransformerremoveTransFromer:动态添加 or 移除ClassFileTranformer,功能是修改类字节码,具体实现将在demo体现

    2.appendToBootstrapClassLoaderSearchappendToSystemClassLoaderSearch:动态修改classpath 搜索结果

    3.getAllLoadedClasses:动态获取所有JVM已加载的类

    3.getInitiatedClasses: 动态获取某个类加载器已实例化的所有类

    4.redefineClasses: 重新定义已加载类

    5.retransFormClasses: 重新加载某个已经被JVM加载过的类字节码

    6.setNativeMethodPrefix: 动态设置JNI前缀,可以实现Hook native方法

  2. ClassFileTransformer

    只有一个方法tranformer,就是返回修改的字节码

    byte[]
        transform(  ClassLoader         loader,
                  String              className,
                  Class<?>            classBeingRedefined,
                  ProtectionDomain    protectionDomain,
                  byte[]              classfileBuffer)
        throws IllegalClassFormatException;

Instrumentation 的实现依赖JVMTI,JVMTI (JVM Tool Interface)是Java虚拟机对外提供的Native编程接口,通过JVMTI,外部进程可以获取到运行时JVM的诸多信息,比如线程、GC等。Agent是一个运行在目标JVM的特定程序,它的职责是负责从目标JVM中获取数据,然后将数据传递给外部进程。加载Agent的时机可以是目标JVM启动之时,也可以是在目标JVM运行时进行加载,而在目标JVM运行时进行Agent加载具备动态性,对于时机未知的Debug场景来说非常实用。

javaagent使用了 Instrumentation的技术,使用 javaagent 需要几个步骤:

  1. 定义一个 MANIFEST.MF 文件,必须包含 Premain-Class(JVM加载类后,执行main函数前,下面说的Agent模式) 选项或Agent-Class(执行main函数之后,下面说的Attach模式,类似与gdb的attach调试模式,在JDK1.6后支持),通常也会加入Can-Redefine-Classes 和 Can-Retransform-Classes 选项,表示允许修改Class字节码。
  2. 创建一个Premain-Class 指定的类,类中包含 premain 方法,方法逻辑由用户自己确定。
  3. 创建一个Agent-Class 指定的类,类中包含 agentmain 方法,方法逻辑由用户自己确定。
  4. 将 premain,agentmain 的类和 MANIFEST.MF 文件打成 jar 包。
  5. 使用参数 -javaagent: jar包路径 启动要代理的应用或者通过程序指定jvm pid附加agent运行。

其中著名的RASP(Runtime Application Self Protect)就是采用java agent技术,下面以一个crack license的demo讲解java agent的两种模式

二、Demo演示

demo采用maven的项目构建工具

JDK 1.8u66

maven 3.63

pom.xml依赖:

<dependencies>
    <!-- https://mvnrepository.com/artifact/org.javassist/javassist -->
    <dependency>
        <groupId>org.javassist</groupId>
        <artifactId>javassist</artifactId>
        <version>3.27.0-GA</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/com.sun/tools -->
    <dependency>
        <groupId>com.sun</groupId>
        <artifactId>tools</artifactId>
        <version>${java.version}</version>
        <scope>system</scope>
        <systemPath>${java.home}/../lib/tools.jar</systemPath>
    </dependency>

</dependencies>

其中javassist 如官网说明

Javassist (JAVA programming ASSISTant) makes Java bytecode manipulation simple. It is a class library for editing bytecodes in Java.

是一个方便对字节流的class文件进行修改。

2.1 License.class:

每隔5s检查License是否过期,这里指定过期时间为 2020.01.01 12:00:00,以当前的时间比较总是超过日期,所以我们的工作是实现一个javaagent 修改License的检查逻辑。

package com.thonsun.sec.agent;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * @Project: java-agent
 * @Desc: 自动检测License合法性 --测试Javaagent 技术
 * @Author: thonsun
 * @Create: 2020/12/27 15:31
 **/
public class License {
    private static final SimpleDateFormat DATA_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    private static boolean checkLicense(String expireDate) throws ParseException {
        //最迟的过期日期
        Date date = DATA_FORMAT.parse(expireDate);

        if(new Date().before(date)){
            return false; //在过期日期前,有效License
        }
        return true;
    }

    public static void main(String[] args) {
        final String expireDate = "2020-01-01 12:00:00";

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    String time = "==当前时间:" + DATA_FORMAT.format(new Date()) + "==  ";

                    try {
                        if (checkLicense(expireDate)) {
                            System.err.println(time + "Licence 过期");
                        }else {
                            System.out.println(time + "Licence 正常 " + expireDate);
                        }
                        TimeUnit.SECONDS.sleep(5);
                    } catch (ParseException e) {
                        e.printStackTrace();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

}

2.2 Agent实现

package com.thonsun.sec.agent;

import javassist.*;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;

/**
 * @Project: java-agent
 * @Desc:
 * @Author: thonsun
 * @Create: 2020/12/27 15:54
 **/
public class CrackLicense {
    private static final String HOOK_CLASS = "com.thonsun.sec.agent.License";

    //java agent
    public static void premain(String args,final Instrumentation inst) {
        loadAgent(args,inst);
    }

    //java attach
    public static void agentmain(String args,final Instrumentation inst) {
        loadAgent(args,inst);
    }

    //load agent
    private static void loadAgent(String arg,final Instrumentation inst) {
        ClassFileTransformer classFileTransformer = createClassFileTransformer();

        // 添加自定义的Transformer,第二个参数true表示是否允许Agent Retransform,
        // 需配合MANIFEST.MF中的Can-Retransform-Classes: true配置
        inst.addTransformer(classFileTransformer,true);

        for (Class clazz : inst.getAllLoadedClasses()) {
            //重新加载HOOK_CLASS 类
            String name = clazz.getName();
            if (inst.isModifiableClass(clazz) && name.equals(HOOK_CLASS)) {
                try {
                    inst.retransformClasses(clazz);
                } catch (UnmodifiableClassException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    //create classfile transformer
    private static ClassFileTransformer createClassFileTransformer() {
        return new ClassFileTransformer() {
            @Override
            public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
                className = className.replace("/",".");
                if (className.equals(HOOK_CLASS)) {
                    ClassPool classPool = ClassPool.getDefault();

                    try {
                        CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));

                        CtMethod checkLicenseMethod = ctClass.getDeclaredMethod(
                                "checkLicense",
                                new CtClass[]{classPool.getCtClass("java.lang.String")}
                                );
                        //方法前插入
                        //System.out.println("[javagent] Liscense expire " + $1);
                        checkLicenseMethod.insertBefore("System.out.println(\"[javagent] Liscense expire \" + $1);");

                        //修改返回值
                        checkLicenseMethod.insertAfter("return false;");

                        classfileBuffer = ctClass.toBytecode();
                    } catch (IOException e) {
                        e.printStackTrace();
                    } catch (NotFoundException e) {
                        e.printStackTrace();
                    } catch (CannotCompileException e) {
                        e.printStackTrace();
                    }
                }

                return classfileBuffer;
            }
        };
    }
}

三、Agent模式

将agent打包到javasec-agent.jar

3.1 添加MANIFEST.MF

Premain-Class: com.thonsun.sec.agent.CrackLicense
Agent-Class: com.thonsun.sec.agent.CrackLicense
Can-Redefine-Classes: true
Can-Retransform-Classes: true

3.2 MAVEN配置

<build>
    <finalName>javasec-agent</finalName>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>${java.version}</source>
                <target>${java.version}</target>
                <encoding>UTF-8</encoding>
            </configuration>
        </plugin>

        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <version>${maven-jar-plugin.version}</version>

            <configuration>
                <archive>
                    <manifestFile>src/main/resources/${manifest-file.name}</manifestFile>
                </archive>
            </configuration>
        </plugin>

        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>${maven-shade-plugin.version}</version>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                    <configuration>
                        <filters>
                            <filter>
                                <artifact>*:*</artifact>
                                <excludes>
                                    <exclude>MANIFEST.MF</exclude>
                                    <exclude>META-INF/maven/</exclude>
                                </excludes>
                            </filter>
                        </filters>

                        <artifactSet>
                            <includes>
                                <include>org.javassist:javassist:jar:*</include>
                            </includes>
                        </artifactSet>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

整个项目结构

image-20201227193854469

3.3 打包

mvn clean install

3.4 运行效果

没有加载agent时License的运行效果:

image-20201227194311925

指定java 运行的参数加载上面打包好的javasec-agent.jar运行License程序

java -javaagent:target/javasec-agent.jar -classpath tartget/classes/ com.thonsun.sec.agent.License

可以看到License的逻辑已经改变

image-20201227194130799

四、Attach模式

attach模式可以将agent以附加的模式加载到指定pid的jvm中,获取jvm中所有运行的进程可以通过java提供的命令 jps -l

image-20201227194620549

在编程接口上jdk 提供了com.sun.tools.attach 支持,这个jar包在安装jdk就在本机的lib/tools.jar:

package com.thonsun.sec.agent;

import com.sun.tools.attach.*;

import java.io.IOException;

/**
 * @Project: java-agent
 * @Desc: 动态注入javagent 到 进程id
 * @Author: thonsun
 * @Create: 2020/12/27 16:55
 **/
public class AttachAgent {
    private static final String HOOK_NAME = "com.thonsun.sec.agent.License";
    private static final String AGENT_PATH = "D:/MyCode/JavaCode/java-agent/demo01/target/javasec-agent.jar";

    public static void main(String[] args) {
        for (VirtualMachineDescriptor desc : VirtualMachine.list()) {
            if (desc.displayName().equals(HOOK_NAME)) {
                System.out.println(desc.id() +"  "+ desc.displayName());
                try {
                    //attach pid jvm
                    VirtualMachine vm = VirtualMachine.attach(desc.id());

                    //load agent
                    vm.loadAgent(AGENT_PATH);

                    vm.detach();
                } catch (AttachNotSupportedException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (AgentLoadException e) {
                    e.printStackTrace();
                } catch (AgentInitializationException e) {
                    e.printStackTrace();
                }
            }

        }
    }
}

在运行着License程序之外在启动AttachAgent

image-20201227195335787

参考资料

  1. notsoserial
  2. openrasp
  3. 浅谈RASP技术攻防之基础

 上一篇
JavaSec jvm运行机制与类加载应用 JavaSec jvm运行机制与类加载应用
在这边文章将讲述: 1.Java语言的基础,从JVM角度讲述java程序运行的模型,如jvm内存模型,在脑海形成java语言编程到运行的概念; 2.类加载与应用:理解作为Java程序的基本元素Class字节码,Class对象,Class对
2020-12-29
下一篇 
CVE-2020-26945 Mybatis远程代码执行漏洞复现 CVE-2020-26945 Mybatis远程代码执行漏洞复现
一、简介MyBatis 本是Apache的一个开源项目iBatis, 2010年这个项目由Apache Software Foundation 迁移到了Google Code,并且改名为MyBatis。MyBatis是一款优秀的持久层框
2020-12-24
  目录