Some modifications to specify custom patching file and some work towards dynamic init with view to re-enabling tests

This commit is contained in:
rick.parker 2023-04-04 11:53:54 +01:00
parent 11aaa018c0
commit e09c6473ad
5 changed files with 86 additions and 34 deletions

View File

@ -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"
}

View File

@ -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<String>(Arrays.<String>asList("jndi", "rmi", "process", "httpserver", "serialization", "unsafe", "scripting", "jshell"));
protected static Set<String> toBlockList(String args, Path path) {
Set<String> 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<String> block = new HashSet<>(all);
Set<String> 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<String> split(String values, Set<String> all) {
Set<String> 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<String> loadFeaturesFromModifications(Path path) {
Properties props = new Properties();
try {
props.load(getModificationsInputStream(path));
} catch (IOException e) {
e.printStackTrace();
}
Set<String> features = new HashSet<String>();
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");
}
}

View File

@ -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<String, List<Modification>> 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<String> 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<String> 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<Modification> 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<String, List<Modification>> loadModifications(InputStream inputStream, Set<String> 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<Modification> 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()))
));
}

View File

@ -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 {

View File

@ -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 {