From cc28f1d5b596a3864c8ba924f9d4ef82d4785c2b Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 19 Dec 2021 12:59:46 +0800 Subject: [PATCH] Fix #1241: Use javaagent patch log4j --- .../jackhuang/hmcl/download/MaintainTask.java | 40 ++--------- .../hmcl/launch/DefaultLauncher.java | 59 +++++++++++---- minecraft/libraries/log4j-patch/build.gradle | 34 +++++---- .../glavo/log4j/patch/agent/Log4jAgent.java | 71 +++++++++++++++++++ 4 files changed, 144 insertions(+), 60 deletions(-) create mode 100644 minecraft/libraries/log4j-patch/src/main/agent/org/glavo/log4j/patch/agent/Log4jAgent.java diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MaintainTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MaintainTask.java index 290aaf61a..2179acbdc 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MaintainTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MaintainTask.java @@ -76,43 +76,17 @@ public class MaintainTask extends Task { version = maintainOptiFineLibrary(repository, unique(version), false); } - Library log4jPatch = null; - List libraries = version.getLibraries(); - for (Library library : libraries) { - if (library.is("org.apache.logging.log4j", "log4j-core")) { - if (library.getVersion().startsWith("2.0-beta")) { - if ("2.0-beta9".equals(library.getVersion())) { - log4jPatch = new Library(new Artifact("org.glavo", "log4j-patch-beta9", "1.0")); - } else { - Logging.LOG.warning("Log4j " + library.getVersion() + " cannot be patched"); - } - } else if (VersionNumber.VERSION_COMPARATOR.compare(library.getVersion(), "2.16") < 0) { - log4jPatch = new Library(new Artifact("org.glavo", "log4j-patch", "1.0")); - } - break; + if (libraries.size() > 0) { + Library library = libraries.get(0); + if ("org.glavo".equals(library.getGroupId()) + && ("log4j-patch".equals(library.getArtifactId()) || "log4j-patch-beta9".equals(library.getArtifactId())) + && "1.0".equals(library.getVersion()) + && library.getDownload() == null) { + version = version.setLibraries(libraries.subList(1, libraries.size())); } } - if (log4jPatch != null) { - ArrayList patchedLibraries = new ArrayList<>(libraries.size() + 1); - patchedLibraries.add(log4jPatch); - patchedLibraries.addAll(libraries); - version = version.setLibraries(patchedLibraries); - - Path log4jPatchPath = repository.getLibraryFile(version, log4jPatch).toPath(); - String patchName = log4jPatch.getArtifactId() + "-" + log4jPatch.getVersion(); - if (Files.notExists(log4jPatchPath)) { - try (InputStream input = MaintainTask.class.getResourceAsStream("/assets/game/" + patchName + ".jar")) { - Files.createDirectories(log4jPatchPath.getParent()); - Files.copy(input, log4jPatchPath, StandardCopyOption.REPLACE_EXISTING); - } catch (IOException e) { - Logging.LOG.log(Level.WARNING, "Unable to unpack " + patchName, e); - } - } - Logging.LOG.info("Apply patch " + patchName + " to log4j"); - } - return version; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java index d8e823bc4..6041483df 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java @@ -19,15 +19,10 @@ package org.jackhuang.hmcl.launch; import org.jackhuang.hmcl.auth.AuthInfo; import org.jackhuang.hmcl.download.LibraryAnalyzer; -import org.jackhuang.hmcl.game.Argument; -import org.jackhuang.hmcl.game.Arguments; -import org.jackhuang.hmcl.game.GameRepository; -import org.jackhuang.hmcl.game.LaunchOptions; -import org.jackhuang.hmcl.game.Library; -import org.jackhuang.hmcl.game.NativesDirectoryType; -import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.game.*; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Log4jLevel; +import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter; import org.jackhuang.hmcl.util.io.FileUtils; @@ -48,6 +43,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import java.util.*; import java.util.function.Supplier; import java.util.logging.Level; @@ -131,6 +127,8 @@ public class DefaultLauncher extends Launcher { } } + Optional log4j = findLog4j(); + // JVM Args if (!options.isNoGeneratedJVMArgs()) { appendJvmArgs(res); @@ -195,6 +193,13 @@ public class DefaultLauncher extends Launcher { res.addDefault("-Dfml.ignoreInvalidMinecraftCertificates=", "true"); res.addDefault("-Dfml.ignorePatchDiscrepancies=", "true"); + + if (log4j.isPresent()) { + String enableJndi = res.addDefault("-Dlog4j2.enableJndi=", "false"); + if (!"-Dlog4j2.enableJndi=true".equals(enableJndi)) { + res.add("-javaagent:" + repository.getLibraryFile(version, LOG4J_PATCH_AGENT).getAbsolutePath() + "=" + log4j.get().getVersion().startsWith("2.0-beta")); + } + } } // Fix RCE vulnerability of log4j2 @@ -203,7 +208,7 @@ public class DefaultLauncher extends Launcher { res.addDefault("-Dcom.sun.jndi.cosnaming.object.trustURLCodebase=", "false"); String formatMsgNoLookups = res.addDefault("-Dlog4j2.formatMsgNoLookups=", "true"); - if (!"-Dlog4j2.formatMsgNoLookups=false".equals(formatMsgNoLookups) && isUsingLog4j()) { + if (!"-Dlog4j2.formatMsgNoLookups=false".equals(formatMsgNoLookups) && log4j.isPresent()) { res.addDefault("-Dlog4j.configurationFile=", getLog4jConfigurationFile().getAbsolutePath()); } @@ -360,8 +365,17 @@ public class DefaultLauncher extends Launcher { } } - private boolean isUsingLog4j() { - return VersionNumber.VERSION_COMPARATOR.compare(repository.getGameVersion(version).orElse("Unknown"), "1.7") >= 0; + private static final Library LOG4J_PATCH_AGENT = new Library(new Artifact("org.glavo", "log4j-patch-agent", "1.0")); + + private Optional findLog4j() { + return version.getLibraries().stream() + .filter(it -> it.is("org.apache.logging.log4j", "log4j-core") + && (VersionNumber.VERSION_COMPARATOR.compare(it.getVersion(), "2.17") < 0 || "2.0-beta9".equals(it.getVersion()))) + .findFirst(); + } + + private boolean isLog4jUnsafe(Library log4j) { + return VersionNumber.VERSION_COMPARATOR.compare(log4j.getVersion(), "2.17") < 0; } public File getLog4jConfigurationFile() { @@ -371,7 +385,7 @@ public class DefaultLauncher extends Launcher { public void extractLog4jConfigurationFile() throws IOException { File targetFile = getLog4jConfigurationFile(); InputStream source; - if (VersionNumber.VERSION_COMPARATOR.compare(repository.getGameVersion(version).orElse("Unknown"), "1.12") < 0) { + if (VersionNumber.VERSION_COMPARATOR.compare(repository.getGameVersion(version).orElse("0.0"), "1.12") < 0) { source = DefaultLauncher.class.getResourceAsStream("/assets/game/log4j2-1.7.xml"); } else { source = DefaultLauncher.class.getResourceAsStream("/assets/game/log4j2-1.12.xml"); @@ -383,6 +397,19 @@ public class DefaultLauncher extends Launcher { } } + public void extractLog4jAgent() throws IOException { + Path log4jPatchPath = repository.getLibraryFile(version, LOG4J_PATCH_AGENT).toPath(); + String patchName = LOG4J_PATCH_AGENT.getArtifactId() + "-" + LOG4J_PATCH_AGENT.getVersion(); + if (Files.notExists(log4jPatchPath)) { + try (InputStream input = DefaultLauncher.class.getResourceAsStream("/assets/game/" + patchName + ".jar")) { + Files.createDirectories(log4jPatchPath.getParent()); + Files.copy(input, log4jPatchPath, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + Logging.LOG.log(Level.WARNING, "Unable to unpack " + patchName, e); + } + } + } + protected Map getConfigurations() { return mapOf( // defined by Minecraft official launcher @@ -450,8 +477,11 @@ public class DefaultLauncher extends Launcher { decompressNatives(nativeFolder); } - if (isUsingLog4j()) { + Optional log4j = findLog4j(); + if (log4j.isPresent()) { extractLog4jConfigurationFile(); + if (isLog4jUnsafe(log4j.get())) + extractLog4jAgent(); } File runDirectory = repository.getRunDirectory(version.getId()); @@ -528,8 +558,11 @@ public class DefaultLauncher extends Launcher { decompressNatives(nativeFolder); } - if (isUsingLog4j()) { + Optional log4j = findLog4j(); + if (log4j.isPresent()) { extractLog4jConfigurationFile(); + if (isLog4jUnsafe(log4j.get())) + extractLog4jAgent(); } String scriptExtension = FileUtils.getExtension(scriptFile); diff --git a/minecraft/libraries/log4j-patch/build.gradle b/minecraft/libraries/log4j-patch/build.gradle index 569399540..655c85bc7 100644 --- a/minecraft/libraries/log4j-patch/build.gradle +++ b/minecraft/libraries/log4j-patch/build.gradle @@ -1,15 +1,21 @@ version '1.0' +sourceSets.create("agent") { + java { + srcDir 'src/main/agent' + } +} + dependencies { compileOnly group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.0-beta9' } -tasks.compileJava { +tasks.withType(JavaCompile) { sourceCompatibility = 8 targetCompatibility = 8 doLast { - FileTree tree = fileTree('build/classes/java/main') + FileTree tree = fileTree('build/classes/java') tree.include '**/*.class' tree.each { RandomAccessFile rf = new RandomAccessFile(it, "rw") @@ -20,27 +26,27 @@ tasks.compileJava { } } -task patchJar(type: Jar) { +task agentJar(type: Jar) { dependsOn(tasks.compileJava) - baseName = 'log4j-patch' + baseName = 'log4j-patch-agent' - from(sourceSets.main.output) { - include('**/JndiLookup.class') + manifest { + attributes 'Premain-Class': 'org.glavo.log4j.patch.agent.Log4jAgent' } -} - -task patchBeta9Jar(type: Jar) { - dependsOn(tasks.compileJava) - - baseName = 'log4j-patch-beta9' + from(sourceSets.agent.output) from(sourceSets.main.output) { - include '**/Interpolator.class' + includeEmptyDirs = false + + eachFile { + it.path = "org/glavo/log4j/patch/agent/${it.name}.bin" + } } } tasks.jar { enabled = false - dependsOn patchJar, patchBeta9Jar + dependsOn agentJar } + diff --git a/minecraft/libraries/log4j-patch/src/main/agent/org/glavo/log4j/patch/agent/Log4jAgent.java b/minecraft/libraries/log4j-patch/src/main/agent/org/glavo/log4j/patch/agent/Log4jAgent.java new file mode 100644 index 000000000..1afea564b --- /dev/null +++ b/minecraft/libraries/log4j-patch/src/main/agent/org/glavo/log4j/patch/agent/Log4jAgent.java @@ -0,0 +1,71 @@ +package org.glavo.log4j.patch.agent; + +import java.io.InputStream; +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.lang.instrument.Instrumentation; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.ProtectionDomain; +import java.util.Arrays; + +public final class Log4jAgent { + static final String JNDI_LOOKUP_CLASS_NAME = "org/apache/logging/log4j/core/lookup/JndiLookup"; + static final String INTERPOLATOR_CLASS_NAME = "org/apache/logging/log4j/core/lookup/Interpolator"; + + static final byte[] INTERPOLATOR_CLASS_SHA = {53, 103, 16, 123, 51, 29, 65, -70, -32, 71, -11, 7, 114, -15, 72, 127, 40, -38, 35, 18}; + + static boolean isBeta = false; + + private static byte[] loadResource(String name) { + try { + try (InputStream input = Log4jAgent.class.getResourceAsStream(name)) { + if (input == null) { + throw new AssertionError(name + " not found"); + } + int available = input.available(); + if (available <= 0) { + throw new AssertionError(); + } + byte[] res = new byte[available]; + if (input.read(res) != available) { + throw new AssertionError(); + } + + return res; + } + } catch (Exception ex) { + throw new InternalError(ex); + } + } + + public static void premain(String agentArgs, Instrumentation inst) throws Exception { + if ("true".equals(agentArgs)) { + isBeta = true; + } + inst.addTransformer(new Transformer()); + } + + private static final class Transformer implements ClassFileTransformer { + + @Override + public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { + if (!isBeta && JNDI_LOOKUP_CLASS_NAME.equals(className)) { + return loadResource("JndiLookup.class.bin"); + } + if (isBeta && INTERPOLATOR_CLASS_NAME.equals(className)) { + try { + MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); + sha1.update(classfileBuffer); + if (Arrays.equals(INTERPOLATOR_CLASS_SHA, sha1.digest())) { + return loadResource("Interpolator.class.bin"); + } + } catch (NoSuchAlgorithmException e) { + throw new InternalError(e); + } + } + return null; + } + } + +}