Skip to content

Commit

Permalink
Overhauled Project Structure and add Java Agent/ModLauncher Support (#2)
Browse files Browse the repository at this point in the history
* feat: split project into subprojects - still wip
also added support for ModLauncher (Forge 1.13+) and JVM agents

* feat: change compile target to Java 7 to also support older minecraft versions

* chore(actions): bump jdk version to 16 since it's the required version for modlauncher

* chore: merge manifests of subprojects in bundleJar task

* chore: compile modlauncher module with Java 8 to fix startup in 1.13-1.16

* fix: do not throw exception on attempted reinitialization

* feat: prevent double-patching of mods when the agent is active

* fix(modlauncher): add class locator so patched mods can find the patched OIS correctly

* feat: add more patches to default config

---------

Co-authored-by: Dogboy21 <[email protected]>
  • Loading branch information
dogboy21 and dogboy21 authored Jul 29, 2023
1 parent d840a75 commit 51943f0
Show file tree
Hide file tree
Showing 29 changed files with 679 additions and 172 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ jobs:

steps:
- uses: actions/checkout@v3
- name: Set up JDK 8
- name: Set up JDK 16
uses: actions/setup-java@v3
with:
java-version: '8'
java-version: '16'
distribution: 'temurin'
- name: Validate Gradle wrapper
uses: gradle/wrapper-validation-action@v1
Expand Down
12 changes: 12 additions & 0 deletions agent/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
dependencies {
implementation project(':core')
}

jar {
manifest {
attributes([
"Premain-Class": "io.dogboy.serializationisbad.agent.SerializationIsBadAgent",
"Can-Redefine-Classes": "true",
])
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.dogboy.serializationisbad.agent;

import io.dogboy.serializationisbad.core.Patches;
import io.dogboy.serializationisbad.core.SerializationIsBad;
import org.objectweb.asm.tree.ClassNode;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class SIBTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
String classNameDots = className.replace('/', '.');

if (Patches.getPatchModuleForClass(classNameDots) == null) return classfileBuffer;

SerializationIsBad.logger.info("Applying patches to " + classNameDots);

ClassNode classNode = Patches.readClassNode(classfileBuffer);
Patches.applyPatches(classNameDots, classNode);
return Patches.writeClassNode(classNode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.dogboy.serializationisbad.agent;

import io.dogboy.serializationisbad.core.SerializationIsBad;

import java.io.File;
import java.lang.instrument.Instrumentation;

public class SerializationIsBadAgent {

public static void premain(String agentArgs, Instrumentation inst) {
SerializationIsBad.init(new File("."));
inst.addTransformer(new SIBTransformer());
}

}
64 changes: 18 additions & 46 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,60 +1,32 @@
buildscript {
repositories {
maven { url = 'https://maven.minecraftforge.net/' }
mavenCentral()
}
dependencies {
classpath 'net.minecraftforge.gradle:ForgeGradle:3.+'
}
plugins {
id 'java'
}

apply plugin: 'net.minecraftforge.gradle'
apply plugin: 'eclipse'
apply plugin: 'maven-publish'

version = '1.1.1'
group = 'io.dogboy.serializationisbad'
archivesBaseName = 'serializationisbad'

sourceCompatibility = targetCompatibility = compileJava.sourceCompatibility = compileJava.targetCompatibility = '1.8'

minecraft {
mappings channel: 'snapshot', version: '20171003-1.12'
subprojects {
apply plugin: 'java'

runs {
client {
workingDirectory project.file('run')
sourceCompatibility = targetCompatibility = compileJava.sourceCompatibility = compileJava.targetCompatibility = '1.7'

property 'forge.logging.markers', 'SCAN,REGISTRIES,REGISTRYDUMP'
property 'forge.logging.console.level', 'debug'
repositories {
maven {
url = 'https://maven.minecraftforge.net'
}

server {
property 'forge.logging.markers', 'SCAN,REGISTRIES,REGISTRYDUMP'
property 'forge.logging.console.level', 'debug'
maven {
url = 'https://libraries.minecraft.net'
}
}
}

dependencies {
minecraft 'net.minecraftforge:forge:1.12.2-14.23.5.2860'
}

jar {
exclude("cpw/mods/fml/relauncher/FMLInjectionData.class")
task bundleJar(type: Jar, dependsOn: subprojects.assemble) {
archivesBaseName = rootProject.name
from project(':core').configurations.archives.allArtifacts.files.collect { zipTree(it) }
from project(':legacyforge').configurations.archives.allArtifacts.files.collect { zipTree(it) }
from project(':modlauncher').configurations.archives.allArtifacts.files.collect { zipTree(it) }
from project(':agent').configurations.archives.allArtifacts.files.collect { zipTree(it) }

manifest {
attributes([
"Specification-Title": project.name,
"Specification-Vendor": "dogboy21",
"Specification-Version": "1",
"Implementation-Title": project.name,
"Implementation-Version": "${version}",
"Implementation-Vendor" :"dogboy21",
"Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ"),
"FMLCorePlugin": "io.dogboy.serializationisbad.SerializationIsBad",
])
from subprojects.collect{ it.jar.manifest }
}
}

jar.finalizedBy('reobfJar')
build.dependsOn bundleJar
21 changes: 21 additions & 0 deletions core/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
apply plugin: 'java-library'

dependencies {
implementation 'com.google.code.gson:gson:2.10.1'
api 'org.apache.logging.log4j:log4j-api:2.20.0'
api 'org.ow2.asm:asm-tree:9.5'
}

jar {
manifest {
attributes([
"Specification-Title": rootProject.name,
"Specification-Vendor": "dogboy21",
"Specification-Version": "1",
"Implementation-Title": rootProject.name,
"Implementation-Version": "${rootProject.version}",
"Implementation-Vendor" : "dogboy21",
"Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ"),
])
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.dogboy.serializationisbad;
package io.dogboy.serializationisbad.core;

import io.dogboy.serializationisbad.config.PatchModule;
import io.dogboy.serializationisbad.core.config.PatchModule;

import java.io.IOException;
import java.io.InputStream;
Expand All @@ -24,12 +24,12 @@ private boolean isClassAllowed(String className) {
className = className.substring(1, className.length() - 1);
}

if (SerializationIsBad.config.getClassAllowlist().contains(className)
if (SerializationIsBad.getInstance().getConfig().getClassAllowlist().contains(className)
|| this.patchModule.getClassAllowlist().contains(className)) {
return true;
}

Set<String> allowedPackages = new HashSet<>(SerializationIsBad.config.getPackageAllowlist());
Set<String> allowedPackages = new HashSet<>(SerializationIsBad.getInstance().getConfig().getPackageAllowlist());
allowedPackages.addAll(this.patchModule.getPackageAllowlist());

for (String allowedPackage : allowedPackages) {
Expand All @@ -47,7 +47,8 @@ protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, Clas

if (!this.isClassAllowed(desc.getName())) {
SerializationIsBad.logger.warn("Tried to resolve class " + desc.getName() + ", which is not allowed to be deserialized");
if (SerializationIsBad.config.isExecuteBlocking()) throw new ClassNotFoundException("Class " + desc.getName() + " is not allowed to be deserialized");
if (SerializationIsBad.getInstance().getConfig().isExecuteBlocking())
throw new ClassNotFoundException("Class " + desc.getName() + " is not allowed to be deserialized");
}

return super.resolveClass(desc);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.dogboy.serializationisbad;
package io.dogboy.serializationisbad.core;

import net.minecraft.launchwrapper.IClassTransformer;
import io.dogboy.serializationisbad.core.config.PatchModule;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
Expand All @@ -12,46 +12,58 @@
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TypeInsnNode;

public class SIBTransformer implements IClassTransformer {
@Override
public byte[] transform(String name, String transformedName, byte[] basicClass) {
if (Patches.getPatchModuleForClass(transformedName) == null) return basicClass;
public class Patches {

SerializationIsBad.logger.info("Applying patches to " + transformedName);
public static PatchModule getPatchModuleForClass(String className) {
for (PatchModule patchModule : SerializationIsBad.getInstance().getConfig().getPatchModules()) {
if (patchModule.getClassesToPatch().contains(className)) {
return patchModule;
}
}

return null;
}

public static ClassNode readClassNode(byte[] classBytecode) {
ClassNode classNode = new ClassNode();
ClassReader classReader = new ClassReader(basicClass);
ClassReader classReader = new ClassReader(classBytecode);
classReader.accept(classNode, 0);
return classNode;
}

public static byte[] writeClassNode(ClassNode classNode) {
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
classNode.accept(writer);
return writer.toByteArray();
}

public static void applyPatches(String className, ClassNode classNode) {
for (MethodNode methodNode : classNode.methods) {
InsnList instructions = methodNode.instructions;
for (int i = 0; i < instructions.size(); i++) {
AbstractInsnNode instruction = instructions.get(i);
if (instruction.getOpcode() == Opcodes.NEW
&& instruction instanceof TypeInsnNode && "java/io/ObjectInputStream".equals(((TypeInsnNode) instruction).desc)) {
((TypeInsnNode) instruction).desc = "io/dogboy/serializationisbad/ClassFilteringObjectInputStream";
((TypeInsnNode) instruction).desc = "io/dogboy/serializationisbad/core/ClassFilteringObjectInputStream";

SerializationIsBad.logger.info("(1/2) Redirecting ObjectInputStream to ClassFilteringObjectInputStream in method " + methodNode.name);
SerializationIsBad.logger.info(" (1/2) Redirecting ObjectInputStream to ClassFilteringObjectInputStream in method " + methodNode.name);
} else if (instruction.getOpcode() == Opcodes.INVOKESPECIAL
&& instruction instanceof MethodInsnNode && "java/io/ObjectInputStream".equals(((MethodInsnNode) instruction).owner)
&& "<init>".equals(((MethodInsnNode) instruction).name)) {
((MethodInsnNode) instruction).owner = "io/dogboy/serializationisbad/ClassFilteringObjectInputStream";
((MethodInsnNode) instruction).desc = "(Ljava/io/InputStream;Lio/dogboy/serializationisbad/config/PatchModule;)V";
((MethodInsnNode) instruction).owner = "io/dogboy/serializationisbad/core/ClassFilteringObjectInputStream";
((MethodInsnNode) instruction).desc = "(Ljava/io/InputStream;Lio/dogboy/serializationisbad/core/config/PatchModule;)V";

InsnList additionalInstructions = new InsnList();
additionalInstructions.add(new LdcInsnNode(transformedName));
additionalInstructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "io/dogboy/serializationisbad/Patches",
"getPatchModuleForClass", "(Ljava/lang/String;)Lio/dogboy/serializationisbad/config/PatchModule;", false));
additionalInstructions.add(new LdcInsnNode(className));
additionalInstructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "io/dogboy/serializationisbad/core/Patches",
"getPatchModuleForClass", "(Ljava/lang/String;)Lio/dogboy/serializationisbad/core/config/PatchModule;", false));

instructions.insertBefore(instruction, additionalInstructions);

SerializationIsBad.logger.info("(2/2) Redirecting ObjectInputStream to ClassFilteringObjectInputStream in method " + methodNode.name);
SerializationIsBad.logger.info(" (2/2) Redirecting ObjectInputStream to ClassFilteringObjectInputStream in method " + methodNode.name);
}
}
}

ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
classNode.accept(writer);
return writer.toByteArray();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package io.dogboy.serializationisbad.core;

import com.google.gson.Gson;
import io.dogboy.serializationisbad.core.config.SIBConfig;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

public class SerializationIsBad {
public static final Logger logger = LogManager.getLogger(SerializationIsBad.class);
private static SerializationIsBad instance;
private static boolean agentActive = false;

public static SerializationIsBad getInstance() {
return SerializationIsBad.instance;
}

public static void init(File minecraftDir) {
if (SerializationIsBad.instance != null) {
SerializationIsBad.logger.warn("Attempted to initialize SerializationIsBad twice, skipping");
return;
}

String implementationType = SerializationIsBad.getImplementationType();
if (implementationType.equals("agent")) {
SerializationIsBad.agentActive = true;
}
SerializationIsBad.logger.info("Initializing SerializationIsBad, implementation type: " + implementationType);
SerializationIsBad.instance = new SerializationIsBad(minecraftDir);
}

private final SIBConfig config;

private SerializationIsBad(File minecraftDir) {
this.config = SerializationIsBad.readConfig(minecraftDir);

SerializationIsBad.logger.info("Loaded config file");
SerializationIsBad.logger.info(" Blocking Enabled: " + this.config.isExecuteBlocking());
SerializationIsBad.logger.info(" Loaded Patch Modules: " + this.config.getPatchModules().size());
}

public SIBConfig getConfig() {
return this.config;
}

private static SIBConfig readConfig(File minecraftDir) {
File configFile = new File(new File(minecraftDir, "config"), "serializationisbad.json");

if (configFile.isFile()) {
Gson gson = new Gson();
try (FileInputStream fileInputStream = new FileInputStream(configFile)) {
return gson.fromJson(new InputStreamReader(fileInputStream, StandardCharsets.UTF_8),
SIBConfig.class);
} catch (Exception e) {
SerializationIsBad.logger.error("Failed to load config file", e);
}
}

return new SIBConfig();
}

private static String getImplementationType() {
for (StackTraceElement stackTraceElement : Thread.currentThread().getStackTrace()) {
if (stackTraceElement.getClassName().startsWith("io.dogboy.serializationisbad.")
&& !stackTraceElement.getClassName().startsWith("io.dogboy.serializationisbad.core.")) {
return stackTraceElement.getClassName().split("[.]")[3];
}
}

return "unknown";
}

public static boolean isAgentActive() {
return SerializationIsBad.agentActive;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.dogboy.serializationisbad.config;
package io.dogboy.serializationisbad.core.config;

import java.util.HashSet;
import java.util.Set;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.dogboy.serializationisbad.config;
package io.dogboy.serializationisbad.core.config;

import java.util.ArrayList;
import java.util.HashSet;
Expand Down
7 changes: 3 additions & 4 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# Sets default memory used for gradle commands. Can be overridden by user or command line properties.
# This is required to provide enough memory for the Minecraft decompilation process.
org.gradle.jvmargs=-Xmx3G
org.gradle.daemon=false
group=io.dogboy.serializationisbad
name=serializationisbad
version=1.2
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-bin.zip
Loading

0 comments on commit 51943f0

Please sign in to comment.