diff --git a/tools/aegis4j/build.gradle b/tools/aegis4j/build.gradle index 379f168505..cc84995ae7 100644 --- a/tools/aegis4j/build.gradle +++ b/tools/aegis4j/build.gradle @@ -30,7 +30,7 @@ dependencies { testImplementation('log4j:apache-log4j-extras:1.2.17') { exclude group: 'log4j', module: 'log4j' } - testImplementation group: 'com.ea.agentloader', name: 'ea-agent-loader', version: '1.0.3' + // testImplementation group: 'com.ea.agentloader', name: 'ea-agent-loader', version: '1.0.3' testImplementation "com.google.guava:guava:$guava_version" } diff --git a/tools/aegis4j/src/main/java/net/gredler/aegis4j/AegisAgent.java b/tools/aegis4j/src/main/java/net/gredler/aegis4j/AegisAgent.java index e27ae8cab5..8f25162454 100644 --- a/tools/aegis4j/src/main/java/net/gredler/aegis4j/AegisAgent.java +++ b/tools/aegis4j/src/main/java/net/gredler/aegis4j/AegisAgent.java @@ -2,11 +2,20 @@ package net.gredler.aegis4j; +import java.io.IOException; +import java.io.InputStream; import java.lang.instrument.Instrumentation; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; import java.util.Set; +import java.util.TreeMap; import java.util.stream.Collectors; /** @@ -20,24 +29,48 @@ import java.util.stream.Collectors; */ public final class AegisAgent { + private static Instrumentation instrumentation; + /** * Supports static attach (via -javaagent parameter at JVM startup). * - * @param args agent arguments + * @param args agent arguments * @param instr instrumentation services */ public static void premain(String args, Instrumentation instr) { - Patcher.start(instr, toBlockList(args)); + instrumentation = instr; + if(args.trim().equalsIgnoreCase("dynamic")) return; + Path path = null; + boolean started = false; + for(String arg: args.split(";")) { + if(started) throw new IllegalArgumentException("ERROR: argument ordering means patching already started"); + String normalisedaArg = arg.trim().toLowerCase(); + if(normalisedaArg.isEmpty() || normalisedaArg.startsWith("block=") || normalisedaArg.startsWith("unblock=")) { + try { + Patcher.start(instr, toBlockList(normalisedaArg, path), getModificationsInputStream(path)); + started = true; + } catch (IOException e) { + e.printStackTrace(); + } + } else if(normalisedaArg.startsWith("path=")) { + path= Paths.get(arg.trim().substring(5)); + } + } + if(!started) throw new IllegalArgumentException("ERROR: patching not started"); } /** * Supports dynamic attach (via the com.sun.tools.attach.* API). * - * @param args agent arguments + * @param args agent arguments * @param instr instrumentation services */ public static void agentmain(String args, Instrumentation instr) { - Patcher.start(instr, toBlockList(args)); + premain(args, instr); + } + + static void dynamicLoad(String args) { + agentmain(args, instrumentation); } /** @@ -47,9 +80,8 @@ public final class AegisAgent { * @param args agent arguments * @return the block list derived from the agent arguments */ - protected static Set< String > toBlockList(String args) { - - Set< String > all = new HashSet(Arrays.asList("jndi", "rmi", "process", "httpserver", "serialization", "unsafe", "scripting", "jshell")); + protected static Set toBlockList(String args, Path path) { + Set all = loadFeaturesFromModifications(path); if (args == null || args.trim().isEmpty()) { // no arguments provided by user return all; @@ -70,8 +102,8 @@ public final class AegisAgent { return split(value, all); } else if ("unblock".equals(name)) { // user is modifying the default block list - Set< String > block = new HashSet<>(all); - Set< String > unblock = split(value, all); + Set block = new HashSet<>(all); + Set unblock = split(value, all); block.removeAll(unblock); return Collections.unmodifiableSet(block); } else { @@ -84,16 +116,15 @@ public final class AegisAgent { * Splits the specified comma-delimited feature list, validating that all specified features are valid. * * @param values the comma-delimited feature list - * @param all the list of valid features to validate against + * @param all the list of valid features to validate against * @return the feature list, split into individual (validated) feature names * @throws IllegalArgumentException if any unrecognized feature names are included in the comma-delimited feature list */ - private static Set< String > split(String values, Set< String > all) { - - Set< String > features = Arrays.asList(values.split(",")) - .stream() - .map(String::trim) - .collect(Collectors.toSet()); + private static Set split(String values, Set all) { + Set features = Arrays.asList(values.split(",")) + .stream() + .map(String::trim) + .collect(Collectors.toSet()); for (String feature : features) { if (!all.contains(feature)) { @@ -103,4 +134,25 @@ public final class AegisAgent { return Collections.unmodifiableSet(features); } + + private static Set loadFeaturesFromModifications(Path path) { + Properties props = new Properties(); + try { + props.load(getModificationsInputStream(path)); + } catch (IOException e) { + e.printStackTrace(); + } + Set features = new HashSet(); + for (String key : props.stringPropertyNames()) { + int first = key.indexOf('.'); + String feature = key.substring(0, first).toLowerCase(); + features.add(feature); + } + return Collections.unmodifiableSet(features); + } + + public static InputStream getModificationsInputStream(Path path) throws IOException { + if(path != null) return path.toUri().toURL().openStream(); + return AegisAgent.class.getResourceAsStream("mods.properties"); + } } diff --git a/tools/aegis4j/src/main/java/net/gredler/aegis4j/Patcher.java b/tools/aegis4j/src/main/java/net/gredler/aegis4j/Patcher.java index 6021a671ba..d450c6a02e 100644 --- a/tools/aegis4j/src/main/java/net/gredler/aegis4j/Patcher.java +++ b/tools/aegis4j/src/main/java/net/gredler/aegis4j/Patcher.java @@ -3,6 +3,7 @@ package net.gredler.aegis4j; import java.io.IOException; +import java.io.InputStream; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; import java.lang.instrument.UnmodifiableClassException; @@ -31,15 +32,15 @@ import javassist.NotFoundException; */ public final class Patcher implements ClassFileTransformer { - private final Map< String, List< Modification > > modifications; // class name -> modifications + private final Map> modifications; // class name -> modifications /** * Creates a new class patcher which blocks the specified features. * * @param block the features to block */ - public Patcher(Set< String > block) { - modifications = loadModifications(block); + public Patcher(Set block, InputStream inputStream) { + modifications = loadModifications(inputStream, block); } /** @@ -48,16 +49,15 @@ public final class Patcher implements ClassFileTransformer { * @param instr the instrumentation instance to add a new patcher to * @param block the features to block */ - public static void start(Instrumentation instr, Set< String > block) { - + public static void start(Instrumentation instr, Set block, InputStream inputStream) { System.out.println("Aegis4j patching starting"); - Patcher patcher = new Patcher(block); + Patcher patcher = new Patcher(block ,inputStream); instr.addTransformer(patcher, true); for (String className : patcher.modifications.keySet()) { try { System.out.println("Aegis4j patching " + className + "..."); - Class< ? > clazz = Class.forName(className); + Class clazz = Class.forName(className); instr.retransformClasses(clazz); } catch (ClassNotFoundException | UnmodifiableClassException e) { e.printStackTrace(); @@ -69,13 +69,12 @@ public final class Patcher implements ClassFileTransformer { } @Override - public byte[] transform(ClassLoader loader, String className, Class< ? > clazz, ProtectionDomain domain, byte[] classBytes) { + public byte[] transform(ClassLoader loader, String className, Class clazz, ProtectionDomain domain, byte[] classBytes) { return patch(className.replace('/', '.'), classBytes); } private byte[] patch(String className, byte[] classBytes) { - - List< Modification > mods = modifications.get(className); + List mods = modifications.get(className); if (mods == null || mods.isEmpty()) { return null; } @@ -109,16 +108,15 @@ public final class Patcher implements ClassFileTransformer { } } - private static Map< String, List< Modification > > loadModifications(Set< String > block) { - + private static Map> loadModifications(InputStream inputStream, Set block) { Properties props = new Properties(); try { - props.load(AegisAgent.class.getResourceAsStream("mods.properties")); + props.load(inputStream); } catch (IOException e) { e.printStackTrace(); } - List< Modification > mods = new ArrayList<>(); + List mods = new ArrayList<>(); for (String key : props.stringPropertyNames()) { int first = key.indexOf('.'); int last = key.lastIndexOf('.'); @@ -133,7 +131,7 @@ public final class Patcher implements ClassFileTransformer { } return Collections.unmodifiableMap(new TreeMap<>( - mods.stream().collect(Collectors.groupingBy(mod -> mod.className, Collectors.toList())) + mods.stream().collect(Collectors.groupingBy(mod -> mod.className, Collectors.toList())) )); } diff --git a/tools/aegis4j/src/test/java/net/gredler/aegis4j/AegisAgentCommandLineTest.java b/tools/aegis4j/src/test/java/net/gredler/aegis4j/AegisAgentCommandLineTest.java index 26156bfe18..4b2c313a93 100644 --- a/tools/aegis4j/src/test/java/net/gredler/aegis4j/AegisAgentCommandLineTest.java +++ b/tools/aegis4j/src/test/java/net/gredler/aegis4j/AegisAgentCommandLineTest.java @@ -33,6 +33,8 @@ public class AegisAgentCommandLineTest { testStaticAttach(jar, "block=jndi", ""); testStaticAttach(jar, "unblock=serialization", ""); testStaticAttach(jar, "block=serialization", "Java serialization blocked by aegis4j"); + testStaticAttach(jar, "block=serialization;", "Java serialization blocked by aegis4j"); + testStaticAttach(jar, ";block=serialization", "ERROR: argument ordering means patching already started"); } private static void testStaticAttach(String jar, String config, String expectedErr) throws Exception { diff --git a/tools/aegis4j/src/test/java/net/gredler/aegis4j/TestUtils.java b/tools/aegis4j/src/test/java/net/gredler/aegis4j/TestUtils.java index 84e1f8e6f2..5858700914 100644 --- a/tools/aegis4j/src/test/java/net/gredler/aegis4j/TestUtils.java +++ b/tools/aegis4j/src/test/java/net/gredler/aegis4j/TestUtils.java @@ -20,7 +20,6 @@ import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; -import com.ea.agentloader.AgentLoader; import com.google.common.base.Charsets; import com.google.common.io.ByteStreams; import org.junit.jupiter.api.function.Executable; @@ -136,7 +135,8 @@ public final class TestUtils { jvm.loadAgent(createAgentJar(), options); jvm.detach(); */ - AgentLoader.loadAgentClass(AegisAgent.class.getName(), options); + //AgentLoader.loadAgentClass(AegisAgent.class.getName(), options); + AegisAgent.dynamicLoad(options); } public static byte[] inputStreamReadAllBytes(InputStream stream) throws IOException {