Web,安全研究,Java

JavaAgent

Javassist基础

简介

可以理解为一个加强版的反射库

不仅可以反射获取各种类 方法 参数,还可以动态修改

原理是通过修改java字节码来实现的

API

Javassist为我们提供了类似于Java反射机制的API,如:CtClassCtConstructorCtMethodCtField与Java反射的ClassConstructorMethodField非常的类似。

Untitled

Javassist使用了内置的标识符来表示一些特定的含义,如:$_表示返回值。我们可以在动态插入类代码的时候使用这些特殊的标识符来表示对应的对象。

Untitled

准备动手

同样 maven项目 quickstart

<dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
</dependency>
<dependency>
      <groupId>org.javassist</groupId>
      <artifactId>javassist</artifactId>
      <version>3.26.0-GA</version>
</dependency>
<dependency>
      <groupId>commons-lang</groupId>
      <artifactId>commons-lang</artifactId>
      <version>2.6</version>
</dependency>

我们来实现一个User类,debug方法是为了测试方便,假设真实情况不存在debug方法

package org.example;

import java.util.Random;
import org.apache.commons.lang.RandomStringUtils;
public class User {

    private String secret;
    public String flag;

    public User(){
        this.secret = RandomStringUtils.randomAlphanumeric(32);
        this.flag = "flag{0xevoA}";
    }
    public String getFlag(String input){
        if(input.equals(this.secret)){

            System.out.println(this.flag);
            return this.flag;
        }
        else {
            System.out.println("nnnn");
            return "nnnn";
        }
    }
    public void debug(){
        System.out.println(this.secret);
        System.out.println(this.flag);
    }
}

假设这个类被加载到了内存中,类文件被删除,并且我们不知道flag的值,通过反射直接获取flag,如何通过javassist 修改获取flag呢?

  1. 直接将字节码写入成.class文件然后反编译
@Test
    public void jassist1() throws NotFoundException, IOException, CannotCompileException {
        ClassPool classPool = ClassPool.getDefault();
        CtClass user = classPool.getCtClass("User");
        user.defrost();
        byte[] bytecodes = user.toBytecode();
        FileOutputStream fileOutputStream = new FileOutputStream(new File(System.getProperty("user.dir") + "/src/test/User.class"));
        fileOutputStream.write(bytecodes);
        fileOutputStream.close();
    }

不用多说了8,看看代码就行了,动手敲一下

  1. 直接输出flag变量
@Test
public void jassist2() throws NotFoundException, IOException, CannotCompileException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
    ClassPool classPool = ClassPool.getDefault();
    CtClass cUser = classPool.getCtClass("User");
    cUser.defrost();
    // create method
    CtMethod ctMethod = CtMethod.make("public void cheatEngine(){System.out.println(this.flag);}", cUser);
    cUser.addMethod(ctMethod);
    Object user = cUser.toClass().newInstance();
    Method cheatEngine = user.getClass().getMethod("cheatEngine");
    cheatEngine.invoke(user);

}

新建了一个cheatEngine方法,直接输出this.flag

看代码就行,不说废话

  1. 如果flag是 private static
private String secret;
    private static String flag = "flag{0xeevoA}";

    public User(){
        this.secret = RandomStringUtils.randomAlphanumeric(32);
    }

static,private在这里没什么影响sout(this.flag)照样输出,当然,如果是static的话sout(flag)也可以

  1. 修改getFlag方法,在前面加入一行代码修改input

@Test
    public void jassist3() throws NotFoundException, IOException, CannotCompileException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        {
            ClassPool classPool = ClassPool.getDefault();
            CtClass cUser = classPool.getCtClass("User");
            cUser.defrost();

            CtMethod getFlag = cUser.getDeclaredMethod("getFlag");
            System.out.println(getFlag);
            getFlag.insertBefore("$0.secret=\"123456\";");
            System.out.println();
            Object o = cUser.toClass().newInstance();
            Method getFlag1 = o.getClass().getMethod("getFlag", String.class);
            getFlag1.invoke(o,"123456");
        }

$0 就是 this

具体看上面那个表

参考https://y4er.com/post/javassist-learn/

javaAgent

introduce

javaAgent, 可以理解为java自带的hook模式,有点类似php的so拓展,以及动态连接的LD_PROLOAD

Java Agent 支持两种方式进行加载:

  1. 实现 premain 方法,在启动时进行加载 (该特性在 jdk 1.5 之后才有)
  2. 实现 agentmain 方法,在启动后进行加载 (该特性在 jdk 1.6 之后才有)

我们可以使用JavaAgent实现一些Hook操作

为了生成jar包方便,下面用vscode

public class User {

    private String secret;
    private static String flag = "flag{0xeevoA}";

    public User(){
        this.secret = "secret";
    }
    public String getFlag(String input){
        if(input.equals(this.secret)){

            System.out.println(this.flag);
            return this.flag;
        }
        else {
            System.out.println("nnnn");
            return "nnnn";
        }
    }
    public void debug(){
        System.out.println(this.secret);
        System.out.println(this.flag);
    }

    public static void main(String[] args) {
        User user = new User();
        if(args[0].equals("1")){
            user.debug();
        }
        if(args[0].equals("2")){
            user.getFlag(args[1]);
        }
    }
}

javac User.java 生成User.class

premain

public static void premain(String agentArgs, Instrumentation inst)

premain有两个参数,第一个agentArgs没什么用,主要关注第二个,Instrumentation

是与JVM交互的类,可以动态修改JVM加载的类字节码,结合javassist,我们可以hook所有JVM加载过的类并且为所欲为

声明

public interface Instrumentation {

    // 增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 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();

    ......
}

Instrumentation主要关注三个方法addTransformer getAllLoadedClasses retransformClasses

  1. addTransformer

增加一个transformer(类似php-parse的NodeTraverser)

  1. getAllLoadedClasses

获取所有已经load的类

  1. retransformClasses

新建一个自定义transformer类 Agent.class

输出所有加载过的类

Agent.class

import java.lang.instrument.Instrumentation;

public class Agent {
    public static void premain(String agentArgs, Instrumentation inst) throws Exception{
        Class[] cs = inst.getAllLoadedClasses(); 
        for(Class c: cs){
            System.out.println(c.getName());
        }
    }
}

新建一个agent.mf

Manifest-Version: 1.0
Premain-Class: Agent
Agent-Class: Agent

运行

javac .\Agent.java 
jar cvfm agent.jar .\Agent.mf .\Agent.class
java -javaagent:agent.jar

即可输出所有load类

然后我们把javassit 和 javaagent连起来,实现

java maven项目(因为要引入javassit)

目录结构

├── pom.xml
├── src
│   ├── main
│   │   ├── java
│   │   │   └── com
│   │   │       ├── Agent.java
│   │   │       └── AgentTransformer.java
│   │   └── resources
│   │       └── META-INF
│   │           └── MANIFEST.MF
│   └── test
│       └── java
│           ├── com
│           │   └── AppTest.java

Agent.java

package com;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;

public class Agent {
    public static void premain(String agentArgs, Instrumentation inst) throws Exception{

        ClassFileTransformer transformer = new AgentTransformer();
        inst.addTransformer(transformer);

    }
}

AgentTransformer.java

package com;
import java.lang.instrument.ClassFileTransformer;
import javassist.*;

import java.lang.reflect.Method;
import java.security.ProtectionDomain;

public class AgentTransformer implements ClassFileTransformer {
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,ProtectionDomain protectionDomain, byte[] classfileBuffer){

        className = className.replace("/", ".");
        if(className.equals("User")){
            try {
                ClassPool classPool = ClassPool.getDefault();
                CtClass cUser = classPool.get("User");
                cUser.defrost();
                CtMethod ctMethod = CtMethod.make("public void cheatEngine(){System.out.println(this.flag);}", cUser);
                cUser.addMethod(ctMethod);
                Object user = cUser.toClass().newInstance();
                Method cheatEngine = user.getClass().getMethod("cheatEngine");
                cheatEngine.invoke(user);
            } catch (Exception e) {
                //TODO: handle exception
            }
        }
        return new byte[0];
    }

}
Manifest-Version: 1.0
Premain-Class: com.Agent

打包成jar

选择Project Structure -> Artifacts -> JAR -> From modules with dependencies

https://xzfile.aliyuncs.com/media/upload/picture/20210416111859-7c5a3b50-9e62-1.png

默认的配置就行。

https://xzfile.aliyuncs.com/media/upload/picture/20210416111859-7c81b400-9e62-1.png

选择Build -> Build Artifacts -> Build

https://xzfile.aliyuncs.com/media/upload/picture/20210416111900-7cb3a460-9e62-1.png

之后产生out/artifacts/agent_jar/agent.jar

└── out
    └── artifacts
        └── agent_jar
            └── agent.jar

然后运行User.class

java -javaagent:.\artifacts\javaagent_jar\javaagent.jar User 2 password

成功打印出flag,虽然后续报错,但目的已经达到

参考

http://wjlshare.com/archives/1582
https://xz.aliyun.com/t/9450#toc-12
https://zhishihezi.net/

评论

This is just a placeholder img.