From 7927ec7f5a079c438fd8a787fc4c368c5a38abe3 Mon Sep 17 00:00:00 2001 From: Col-E Date: Mon, 31 Jul 2023 23:18:46 -0400 Subject: [PATCH 01/11] Most of the dynamic scanning model fleshed out --- .../java/info/mmpa/concoction/Concoction.java | 2 +- .../input/model/ApplicationModel.java | 1 - .../input/model/impl/BasicModelBuilder.java | 6 +- .../info/mmpa/concoction/output/SusLevel.java | 42 +++--- .../scan/dynamic/DynamicScanner.java | 3 +- .../concoction/scan/dynamic/EntryPoint.java | 2 +- .../concoction/scan/dynamic/SsvmContext.java | 31 +++- .../scan/insn/InstructionScanner.java | 2 +- .../concoction/scan/model/MultiMatchMode.java | 65 ++++++++ .../scan/model/dynamic/DynamicMatchEntry.java | 36 ----- .../model/dynamic/DynamicMatchingModel.java | 48 +++++- ...icMatchingModelDeserializingConverter.java | 1 + ...amicMatchingModelSerializingConverter.java | 1 + .../model/dynamic/entry/AllMultiDynamic.java | 33 ++++ .../model/dynamic/entry/AnyMultiDynamic.java | 32 ++++ .../scan/model/dynamic/entry/Condition.java | 27 ++++ .../dynamic/entry/ConditionDeserializer.java | 36 +++++ .../dynamic/entry/ConditionSerializer.java | 26 ++++ .../dynamic/entry/DynamicMatchEntry.java | 30 ++++ .../entry/DynamicMatchEntryDeserializer.java | 76 ++++++++++ .../entry/DynamicMatchEntrySerializer.java | 49 ++++++ .../model/dynamic/entry/MethodLocation.java | 141 ++++++++++++++++++ .../entry/MethodLocationDeserializer.java | 61 ++++++++ .../entry/MethodLocationSerializer.java | 30 ++++ .../model/dynamic/entry/MultiDynamic.java | 67 +++++++++ .../model/dynamic/entry/NoneMultiDynamic.java | 38 +++++ .../entry/SingleConditionCheckingDynamic.java | 91 +++++++++++ .../scan/model/dynamic/entry/When.java | 12 ++ .../model/insn/InstructionMatchingList.java | 3 + .../model/insn/InstructionsMatchingModel.java | 1 + ...nsMatchingModelDeserializingConverter.java | 1 + ...ionsMatchingModelSerializingConverter.java | 1 + .../insn/{ => entry}/AllMultiInstruction.java | 6 +- .../insn/{ => entry}/AnyMultiInstruction.java | 6 +- .../model/insn/{ => entry}/Instruction.java | 2 +- .../{ => entry}/InstructionMatchEntry.java | 2 +- .../InstructionMatchEntryDeserializer.java | 19 +-- .../InstructionMatchEntrySerializer.java | 2 +- .../insn/{ => entry}/InstructionWildcard.java | 2 +- .../{ => entry}/InstructionWildcardMulti.java | 2 +- .../insn/{ => entry}/MultiInstruction.java | 42 +----- .../{ => entry}/NoneMultiInstruction.java | 6 +- .../info/mmpa/concoction/util/JsonUtil.java | 20 +++ .../mmpa/concoction/util/Serialization.java | 40 ++++- .../model/DynamicMatchSerializationTests.java | 27 ++++ .../InstructionMatchSerializationTests.java | 35 ++--- .../concoction/util/TestSerialization.java | 20 ++- 47 files changed, 1067 insertions(+), 159 deletions(-) create mode 100644 concoction-lib/src/main/java/info/mmpa/concoction/scan/model/MultiMatchMode.java delete mode 100644 concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/DynamicMatchEntry.java create mode 100644 concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/AllMultiDynamic.java create mode 100644 concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/AnyMultiDynamic.java create mode 100644 concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/Condition.java create mode 100644 concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/ConditionDeserializer.java create mode 100644 concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/ConditionSerializer.java create mode 100644 concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/DynamicMatchEntry.java create mode 100644 concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/DynamicMatchEntryDeserializer.java create mode 100644 concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/DynamicMatchEntrySerializer.java create mode 100644 concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MethodLocation.java create mode 100644 concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MethodLocationDeserializer.java create mode 100644 concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MethodLocationSerializer.java create mode 100644 concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MultiDynamic.java create mode 100644 concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/NoneMultiDynamic.java create mode 100644 concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/SingleConditionCheckingDynamic.java create mode 100644 concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/When.java rename concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/{ => entry}/AllMultiInstruction.java (82%) rename concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/{ => entry}/AnyMultiInstruction.java (82%) rename concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/{ => entry}/Instruction.java (98%) rename concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/{ => entry}/InstructionMatchEntry.java (93%) rename concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/{ => entry}/InstructionMatchEntryDeserializer.java (81%) rename concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/{ => entry}/InstructionMatchEntrySerializer.java (97%) rename concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/{ => entry}/InstructionWildcard.java (93%) rename concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/{ => entry}/InstructionWildcardMulti.java (97%) rename concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/{ => entry}/MultiInstruction.java (65%) rename concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/{ => entry}/NoneMultiInstruction.java (83%) create mode 100644 concoction-lib/src/main/java/info/mmpa/concoction/util/JsonUtil.java create mode 100644 concoction-lib/src/test/java/info/mmpa/concoction/scan/model/DynamicMatchSerializationTests.java diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/Concoction.java b/concoction-lib/src/main/java/info/mmpa/concoction/Concoction.java index bf9c9bc..d30fa83 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/Concoction.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/Concoction.java @@ -5,9 +5,9 @@ import info.mmpa.concoction.input.model.InvalidModelException; import info.mmpa.concoction.input.model.ModelBuilder; import info.mmpa.concoction.output.Results; +import info.mmpa.concoction.scan.dynamic.CoverageEntryPointSupplier; import info.mmpa.concoction.scan.dynamic.DynamicScanException; import info.mmpa.concoction.scan.dynamic.DynamicScanner; -import info.mmpa.concoction.scan.dynamic.CoverageEntryPointSupplier; import info.mmpa.concoction.scan.dynamic.EntryPointDiscovery; import info.mmpa.concoction.scan.insn.InstructionScanner; import info.mmpa.concoction.scan.model.ScanModel; diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/input/model/ApplicationModel.java b/concoction-lib/src/main/java/info/mmpa/concoction/input/model/ApplicationModel.java index ef6cf18..64cd421 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/input/model/ApplicationModel.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/input/model/ApplicationModel.java @@ -6,7 +6,6 @@ import java.util.stream.Stream; import static java.util.stream.Stream.concat; -import static java.util.stream.Stream.of; /** * Full application model, comprised of one or more units. diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/input/model/impl/BasicModelBuilder.java b/concoction-lib/src/main/java/info/mmpa/concoction/input/model/impl/BasicModelBuilder.java index 8076ce7..0926d87 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/input/model/impl/BasicModelBuilder.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/input/model/impl/BasicModelBuilder.java @@ -28,7 +28,7 @@ public class BasicModelBuilder implements ModelBuilder { @Nonnull @Override public ModelBuilder addSource(@Nonnull ArchiveLoadContext context, @Nonnull Path path) throws IOException { - ClassesAndFiles input = pathReader.from(context,path); + ClassesAndFiles input = pathReader.from(context, path); String identifier = path.getFileName().toString(); sources.add(new BasicModelSource(identifier, input.getClasses(), input.getFiles())); return this; @@ -36,8 +36,8 @@ public ModelBuilder addSource(@Nonnull ArchiveLoadContext context, @Nonnull Path @Nonnull @Override - public ModelBuilder addSource(@Nonnull ArchiveLoadContext context,@Nonnull String identifier, @Nonnull byte[] raw) throws IOException { - ClassesAndFiles input = rawReader.from(context,raw); + public ModelBuilder addSource(@Nonnull ArchiveLoadContext context, @Nonnull String identifier, @Nonnull byte[] raw) throws IOException { + ClassesAndFiles input = rawReader.from(context, raw); sources.add(new BasicModelSource(identifier, input.getClasses(), input.getFiles())); return this; } diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/output/SusLevel.java b/concoction-lib/src/main/java/info/mmpa/concoction/output/SusLevel.java index 4d0aa1d..4d8ea47 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/output/SusLevel.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/output/SusLevel.java @@ -4,25 +4,25 @@ * Levels of suspicions for report items. */ public enum SusLevel { - /** - * Almost guaranteed to be malicious. - */ - MAXIMUM, - /** - * Very likely to be malicious, but in some cases it may just be benign shoddy code. - */ - STRONG, - /** - * Typically these are strictly context based. For instance, deleting a file is usually not a concern. - * Deleting core operating system files is a concern though. - */ - MEDIUM, - /** - * Unlikely to be malicious in most circumstances. - */ - WEAK, - /** - * Incredibly unlikely to be malicious in any circumstance. - */ - NOTHING_BURGER + /** + * Almost guaranteed to be malicious. + */ + MAXIMUM, + /** + * Very likely to be malicious, but in some cases it may just be benign shoddy code. + */ + STRONG, + /** + * Typically these are strictly context based. For instance, deleting a file is usually not a concern. + * Deleting core operating system files is a concern though. + */ + MEDIUM, + /** + * Unlikely to be malicious in most circumstances. + */ + WEAK, + /** + * Incredibly unlikely to be malicious in any circumstance. + */ + NOTHING_BURGER } diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/dynamic/DynamicScanner.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/dynamic/DynamicScanner.java index c72473a..e1f8817 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/dynamic/DynamicScanner.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/dynamic/DynamicScanner.java @@ -2,6 +2,7 @@ import dev.xdark.ssvm.execution.VMException; import info.mmpa.concoction.input.model.ApplicationModel; +import info.mmpa.concoction.input.model.path.SourcePathElement; import info.mmpa.concoction.output.Results; import info.mmpa.concoction.output.ResultsSink; import info.mmpa.concoction.scan.model.ScanModel; @@ -50,7 +51,7 @@ public DynamicScanner(@Nonnull EntryPointDiscovery entryPointDiscovery, @SuppressWarnings("UnnecessaryLocalVariable") public Results accept(@Nonnull ApplicationModel model) throws DynamicScanException { ResultsSink sink = new ResultsSink(); - SsvmContext context = new SsvmContext(model, scanModels); + SsvmContext context = new SsvmContext(model, sink, new SourcePathElement(model.primarySource()), scanModels); // Visit initial entry points List initialEntryPoints = entryPointDiscovery.createEntryPoints(model, context); diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/dynamic/EntryPoint.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/dynamic/EntryPoint.java index 42a09df..c8192f2 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/dynamic/EntryPoint.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/dynamic/EntryPoint.java @@ -140,7 +140,7 @@ public String toString() { @Override public int compareTo(@Nonnull EntryPoint o) { int cmp = className.compareTo(o.className); - if (cmp != 0){ + if (cmp != 0) { cmp = methodName.compareTo(o.methodName); if (cmp != 0) cmp = methodDescriptor.compareTo(o.methodDescriptor); } diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/dynamic/SsvmContext.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/dynamic/SsvmContext.java index 67208c8..e7af246 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/dynamic/SsvmContext.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/dynamic/SsvmContext.java @@ -10,6 +10,9 @@ import dev.xdark.ssvm.thread.OSThread; import info.mmpa.concoction.input.model.ApplicationModel; import info.mmpa.concoction.input.model.ModelSource; +import info.mmpa.concoction.input.model.path.SourcePathElement; +import info.mmpa.concoction.output.DetectionArchetype; +import info.mmpa.concoction.output.ResultsSink; import info.mmpa.concoction.scan.model.ScanModel; import org.objectweb.asm.Opcodes; @@ -36,15 +39,18 @@ public class SsvmContext { /** * @param model * Model to pass to the VM. + * @param sink + * Sink to feed match results into. + * @param sourcePath + * Current method path to the containing input source. + * SSVM will pass along the rest of the path details. * @param scanModel * List of detection models to scan for. */ - public SsvmContext(@Nonnull ApplicationModel model, @Nonnull Collection scanModel) { - // TODO: Supply dynamic models to match against - // - Tweak VM initialization to track information that is needed for the models to match against. - // - Method enter/exit listeners - // - Method instruction interceptors for some edge cases perhaps? - + public SsvmContext(@Nonnull ApplicationModel model, + @Nonnull ResultsSink sink, + @Nonnull SourcePathElement sourcePath, + @Nonnull Collection scanModel) { // Create and initialize the VM. VirtualMachine vm = new VirtualMachine() { @Override @@ -60,14 +66,23 @@ protected FileManager createFileManager() { vmi.registerMethodEnterListener(ctx -> { OSThread thread = vm.currentJavaThread().getOsThread(); Stack stack = threadFrameMap.computeIfAbsent(thread, t -> new Stack<>()); - stack.push(new CallStackFrame(ctx)); + CallStackFrame frame = new CallStackFrame(ctx); + stack.push(frame); + for (ScanModel modelEntry : scanModel) { + DetectionArchetype archetype = modelEntry.getDetectionArchetype(); + modelEntry.getDynamicMatchingModel().matchOnEnter(sink, archetype, sourcePath, frame); + } }); vmi.registerMethodExitListener(ctx -> { OSThread thread = vm.currentJavaThread().getOsThread(); Stack stack = threadFrameMap.get(thread); if (stack == null || stack.isEmpty()) throw new IllegalArgumentException("Cannot pop call stack frame from thread with no prior stack history"); - stack.pop(); + CallStackFrame frame = stack.pop(); + for (ScanModel modelEntry : scanModel) { + DetectionArchetype archetype = modelEntry.getDetectionArchetype(); + modelEntry.getDynamicMatchingModel().matchOnExit(sink, archetype, sourcePath, frame); + } }); // Some patches to circumvent bugs arising from VM implementation changes in later versions diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/insn/InstructionScanner.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/insn/InstructionScanner.java index 53b472a..bbbddb5 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/insn/InstructionScanner.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/insn/InstructionScanner.java @@ -50,7 +50,7 @@ public Results accept(@Nonnull ApplicationModel model) { try { ClassNode classNode = AsmUtil.node(classEntry.getValue()); - // TODO #4: Class structure matchers + // TODO #4: Class structure matchers, then refactor this class name to 'StaticScanner' // Run per-method matchers (instruction matching models) for (MethodNode methodNode : classNode.methods) { diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/MultiMatchMode.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/MultiMatchMode.java new file mode 100644 index 0000000..a564db5 --- /dev/null +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/MultiMatchMode.java @@ -0,0 +1,65 @@ +package info.mmpa.concoction.scan.model; + +import info.mmpa.concoction.scan.model.dynamic.entry.*; +import info.mmpa.concoction.scan.model.insn.entry.*; + +import javax.annotation.Nonnull; +import java.util.List; + +import static java.util.Collections.unmodifiableList; + +/** + * Mode for subtypes of multi-instruction matchers. + */ +public enum MultiMatchMode { + /** + * @see AllMultiInstruction + */ + ALL, + /** + * @see AnyMultiInstruction + */ + ANY, + /** + * @see NoneMultiInstruction + */ + NONE; + + /** + * @param entries + * Entries to wrap into a multi-instruction matcher subtype. + * + * @return Instance of multi-instruction matcher subtype. + */ + @Nonnull + public MultiInstruction createMultiInsn(@Nonnull List entries) { + switch (this) { + case NONE: + return new NoneMultiInstruction(unmodifiableList(entries)); + case ALL: + return new AllMultiInstruction(unmodifiableList(entries)); + case ANY: + default: + return new AnyMultiInstruction(unmodifiableList(entries)); + } + } + + /** + * @param entries + * Entries to wrap into a multi-dynamic matcher subtype. + * + * @return Instance of multi-dynamic matcher subtype. + */ + @Nonnull + public MultiDynamic createMultiDynamic(@Nonnull List entries) { + switch (this) { + case NONE: + return new NoneMultiDynamic(unmodifiableList(entries)); + case ALL: + return new AllMultiDynamic(unmodifiableList(entries)); + case ANY: + default: + return new AnyMultiDynamic(unmodifiableList(entries)); + } + } +} diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/DynamicMatchEntry.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/DynamicMatchEntry.java deleted file mode 100644 index 25abf07..0000000 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/DynamicMatchEntry.java +++ /dev/null @@ -1,36 +0,0 @@ -package info.mmpa.concoction.scan.model.dynamic; - -import info.mmpa.concoction.scan.dynamic.CallStackFrame; - -import javax.annotation.Nonnull; - -/** - * Outline of dynamic matching. - */ -// TODO: Custom serializers for short-hand -// TODO: Sub-types -// - Method in class (enter/exit) -// - with optional parameter matching -// - Primitive math matching -// - Object null/not-null -// - String TextMatchMode matching -// - Also for char[] and other similar types (StringBuilder, StringBuffer, non-string CharSequence) -// - byte[] matching helpers -// - with optional parent calling context predicate -public interface DynamicMatchEntry { - /** - * @param frame - * Method call stack reference. - * - * @return {@code true} on match. - */ - boolean matchOnEnter(@Nonnull CallStackFrame frame); - - /** - * @param frame - * Method call stack reference. - * - * @return {@code true} on match. - */ - boolean matchOnExit(@Nonnull CallStackFrame frame); -} diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/DynamicMatchingModel.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/DynamicMatchingModel.java index fde4964..68b6873 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/DynamicMatchingModel.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/DynamicMatchingModel.java @@ -2,7 +2,13 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import info.mmpa.concoction.input.model.path.MethodPathElement; +import info.mmpa.concoction.input.model.path.SourcePathElement; +import info.mmpa.concoction.output.Detection; import info.mmpa.concoction.output.DetectionArchetype; +import info.mmpa.concoction.output.ResultsSink; +import info.mmpa.concoction.scan.dynamic.CallStackFrame; +import info.mmpa.concoction.scan.model.dynamic.entry.DynamicMatchEntry; import javax.annotation.Nonnull; import java.util.Map; @@ -26,7 +32,47 @@ public DynamicMatchingModel(@Nonnull Map variants) { this.variants = variants; } - // TODO: Implement scanning + /** + * @param sink + * Sink to feed match results into. + * @param archetype + * Information about what the signature being matched. + * @param sourcePath + * Current method path to the containing input source. SSVM holds the rest of the details. + * @param frame + * SSVM frame of method entered. + */ + public void matchOnEnter(@Nonnull ResultsSink sink, @Nonnull DetectionArchetype archetype, + @Nonnull SourcePathElement sourcePath, @Nonnull CallStackFrame frame) { + for (DynamicMatchEntry entry : variants.values()) + if (entry.matchOnEnter(frame)) { + MethodPathElement path = sourcePath + .child(frame.getOwnerName()) + .child(frame.getMethodName(), frame.getMethodDesc()); + sink.add(path, archetype, new Detection(archetype, path)); + } + } + + /** + * @param sink + * Sink to feed match results into. + * @param archetype + * Information about what the signature being matched. + * @param sourcePath + * Current method path to the containing input source. SSVM holds the rest of the details. + * @param frame + * SSVM frame of method exited. + */ + public void matchOnExit(@Nonnull ResultsSink sink, @Nonnull DetectionArchetype archetype, + @Nonnull SourcePathElement sourcePath, @Nonnull CallStackFrame frame) { + for (DynamicMatchEntry entry : variants.values()) + if (entry.matchOnExit(frame)) { + MethodPathElement path = sourcePath + .child(frame.getOwnerName()) + .child(frame.getMethodName(), frame.getMethodDesc()); + sink.add(path, archetype, new Detection(archetype, path)); + } + } /** * @return Map of variants to detect the pattern. diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/DynamicMatchingModelDeserializingConverter.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/DynamicMatchingModelDeserializingConverter.java index 8ec4e48..2e354bd 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/DynamicMatchingModelDeserializingConverter.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/DynamicMatchingModelDeserializingConverter.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.type.TypeFactory; import com.fasterxml.jackson.databind.util.Converter; +import info.mmpa.concoction.scan.model.dynamic.entry.DynamicMatchEntry; import java.util.Collections; import java.util.Map; diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/DynamicMatchingModelSerializingConverter.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/DynamicMatchingModelSerializingConverter.java index b492afb..c5d4386 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/DynamicMatchingModelSerializingConverter.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/DynamicMatchingModelSerializingConverter.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.type.TypeFactory; import com.fasterxml.jackson.databind.util.Converter; +import info.mmpa.concoction.scan.model.dynamic.entry.DynamicMatchEntry; import java.util.Map; diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/AllMultiDynamic.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/AllMultiDynamic.java new file mode 100644 index 0000000..6badb82 --- /dev/null +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/AllMultiDynamic.java @@ -0,0 +1,33 @@ +package info.mmpa.concoction.scan.model.dynamic.entry; + +import info.mmpa.concoction.scan.dynamic.CallStackFrame; +import info.mmpa.concoction.scan.model.MultiMatchMode; +import info.mmpa.concoction.scan.model.TextMatchMode; +import info.mmpa.concoction.scan.model.insn.entry.Instruction; + +import javax.annotation.Nonnull; +import java.util.List; + +/** + * A dynamic matcher that defines multiple sub-matchers. + * All the sub-matchers must match the given input all at once. + */ +public class AllMultiDynamic extends MultiDynamic { + /** + * @param entries + * Sub-matchers which must all match inputs in order to pass. + */ + public AllMultiDynamic(@Nonnull List entries) { + super(MultiMatchMode.ALL, entries); + } + + @Override + public boolean matchOnEnter(@Nonnull CallStackFrame frame) { + return entries.stream().allMatch(e -> e.matchOnEnter(frame)); + } + + @Override + public boolean matchOnExit(@Nonnull CallStackFrame frame) { + return entries.stream().allMatch(e -> e.matchOnExit(frame)); + } +} diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/AnyMultiDynamic.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/AnyMultiDynamic.java new file mode 100644 index 0000000..7d9e28e --- /dev/null +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/AnyMultiDynamic.java @@ -0,0 +1,32 @@ +package info.mmpa.concoction.scan.model.dynamic.entry; + +import info.mmpa.concoction.scan.dynamic.CallStackFrame; +import info.mmpa.concoction.scan.model.MultiMatchMode; +import info.mmpa.concoction.scan.model.TextMatchMode; + +import javax.annotation.Nonnull; +import java.util.List; + +/** + * A dynamic matcher that defines multiple sub-matchers. + * Any single one of the sub-matchers must match the given input. + */ +public class AnyMultiDynamic extends MultiDynamic { + /** + * @param entries + * Sub-matchers where any single input must match in order to pass. + */ + public AnyMultiDynamic(@Nonnull List entries) { + super(MultiMatchMode.ANY, entries); + } + + @Override + public boolean matchOnEnter(@Nonnull CallStackFrame frame) { + return entries.stream().anyMatch(e -> e.matchOnEnter(frame)); + } + + @Override + public boolean matchOnExit(@Nonnull CallStackFrame frame) { + return entries.stream().anyMatch(e -> e.matchOnExit(frame)); + } +} diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/Condition.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/Condition.java new file mode 100644 index 0000000..b397ed3 --- /dev/null +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/Condition.java @@ -0,0 +1,27 @@ +package info.mmpa.concoction.scan.model.dynamic.entry; + +import info.mmpa.concoction.scan.dynamic.CallStackFrame; + +import javax.annotation.Nonnull; + +// TODO: Sub-types +// - Any (wildcard, useful where any execution of a 'MethodLocation' is flag-worthy) +// - Value matching for (Parameter N), (Field $NAME/$DESC) +// - Primitive math matching +// - Object null/not-null +// - String TextMatchMode matching +// - Also for char[] and other similar types (StringBuilder, StringBuffer, non-string CharSequence) +// - byte[] matching helpers + +/** + * Outline for condition matching in a given stack-frame. + */ +public interface Condition { + /** + * @param frame + * Frame to check for conditions within. + * + * @return {@code true} when condition matches. + */ + boolean match(@Nonnull CallStackFrame frame); +} diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/ConditionDeserializer.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/ConditionDeserializer.java new file mode 100644 index 0000000..5d6d0e5 --- /dev/null +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/ConditionDeserializer.java @@ -0,0 +1,36 @@ +package info.mmpa.concoction.scan.model.dynamic.entry; + +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; + +import javax.annotation.Nonnull; +import java.io.IOException; + +/** + * Deserializes {@link Condition} shorthand JSON into instances. + * + * @see ConditionSerializer + */ +public class ConditionDeserializer extends StdDeserializer { + /** + * New deserializer instance. + */ + public ConditionDeserializer() { + super(Condition.class); + } + + @Override + public Condition deserialize(JsonParser jp, DeserializationContext ctx) throws IOException { + JsonNode node = jp.getCodec().readTree(jp); + return deserializeNode(jp, node); + } + + @Nonnull + private Condition deserializeNode(JsonParser jp, JsonNode node) throws JacksonException { + // TODO: Implement when condition sub-types are fleshed out + throw new UnsupportedOperationException("implement condition deserialization"); + } +} diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/ConditionSerializer.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/ConditionSerializer.java new file mode 100644 index 0000000..bc59fe5 --- /dev/null +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/ConditionSerializer.java @@ -0,0 +1,26 @@ +package info.mmpa.concoction.scan.model.dynamic.entry; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import java.io.IOException; + +/** + * Serializes {@link Condition} subtypes into shorthand JSON. + * + * @see ConditionDeserializer + */ +public class ConditionSerializer extends StdSerializer { + /** + * New serializer instance. + */ + public ConditionSerializer() { + super(Condition.class); + } + + @Override + public void serialize(Condition entry, JsonGenerator jgen, SerializerProvider provider) throws IOException { + // TODO: Implement when condition sub-types are fleshed out + } +} diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/DynamicMatchEntry.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/DynamicMatchEntry.java new file mode 100644 index 0000000..1701b1b --- /dev/null +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/DynamicMatchEntry.java @@ -0,0 +1,30 @@ +package info.mmpa.concoction.scan.model.dynamic.entry; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import info.mmpa.concoction.scan.dynamic.CallStackFrame; + +import javax.annotation.Nonnull; + +/** + * Outline of dynamic matching. + */ +@JsonDeserialize(using = DynamicMatchEntryDeserializer.class) +@JsonSerialize(using = DynamicMatchEntrySerializer.class) +public interface DynamicMatchEntry { + /** + * @param frame + * Method call stack reference. + * + * @return {@code true} on match. + */ + boolean matchOnEnter(@Nonnull CallStackFrame frame); + + /** + * @param frame + * Method call stack reference. + * + * @return {@code true} on match. + */ + boolean matchOnExit(@Nonnull CallStackFrame frame); +} diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/DynamicMatchEntryDeserializer.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/DynamicMatchEntryDeserializer.java new file mode 100644 index 0000000..068ab51 --- /dev/null +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/DynamicMatchEntryDeserializer.java @@ -0,0 +1,76 @@ +package info.mmpa.concoction.scan.model.dynamic.entry; + +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import info.mmpa.concoction.scan.model.MultiMatchMode; +import info.mmpa.concoction.scan.model.TextMatchMode; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static info.mmpa.concoction.util.JsonUtil.breakByFirstSpace; + +/** + * Deserializes {@link DynamicMatchEntry} shorthand JSON into instances. + * + * @see DynamicMatchEntrySerializer + */ +public class DynamicMatchEntryDeserializer extends StdDeserializer { + /** + * New deserializer instance. + */ + public DynamicMatchEntryDeserializer() { + super(DynamicMatchEntry.class); + } + + @Override + public DynamicMatchEntry deserialize(JsonParser jp, DeserializationContext ctx) throws IOException { + JsonNode node = jp.getCodec().readTree(jp); + return deserializeNode(jp, node); + } + + @Nonnull + private DynamicMatchEntry deserializeNode(JsonParser jp, JsonNode node) throws JacksonException { + if (node.isObject()) { + // If location exists, it's a condition matcher + JsonNode locationNode = node.get("location"); + if (locationNode != null) { + // Construct the location + MethodLocation location = jp.getCodec().treeToValue(locationNode, MethodLocation.class); + + // Construct the condition + JsonNode conditionNode = node.get("condition"); + if (conditionNode == null) + throw new JsonMappingException(jp, "Dynamic match entry missing 'condition' value"); + Condition condition = jp.getCodec().treeToValue(conditionNode, Condition.class); + + // Construct the when + JsonNode whenNode = node.get("when"); + When when = whenNode == null ? When.ENTRY : When.valueOf(whenNode.asText()); + + return new SingleConditionCheckingDynamic(location, condition, when); + } else { + // Should be a multi-dynamic if no other case applies. + // Determine which mode by its name. + for (MultiMatchMode mode : MultiMatchMode.values()) { + JsonNode modeNode = node.get(mode.name()); + if (modeNode != null && modeNode.isArray()) { + // Mode found, now extract the entries and create the multi-matcher. + List entries = new ArrayList<>(modeNode.size()); + for (JsonNode arrayItem : modeNode) { + entries.add(deserializeNode(jp, arrayItem)); + } + return mode.createMultiDynamic(entries); + } + } + } + } + throw new JsonMappingException(jp, "Dynamic match entry expects a JSON object, or '*' literal for wildcards"); + } +} diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/DynamicMatchEntrySerializer.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/DynamicMatchEntrySerializer.java new file mode 100644 index 0000000..61b2718 --- /dev/null +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/DynamicMatchEntrySerializer.java @@ -0,0 +1,49 @@ +package info.mmpa.concoction.scan.model.dynamic.entry; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import java.io.IOException; + +/** + * Serializes {@link DynamicMatchEntry} subtypes into shorthand JSON. + * + * @see DynamicMatchEntryDeserializer + */ +public class DynamicMatchEntrySerializer extends StdSerializer { + /** + * New serializer instance. + */ + public DynamicMatchEntrySerializer() { + super(DynamicMatchEntry.class); + } + + @Override + public void serialize(DynamicMatchEntry entry, JsonGenerator jgen, SerializerProvider provider) throws IOException { + if (entry instanceof SingleConditionCheckingDynamic) { + SingleConditionCheckingDynamic conditionCheck = (SingleConditionCheckingDynamic) entry; + jgen.writeStartObject(); + jgen.writeObjectField("location", conditionCheck.getLocation()); + if (conditionCheck.getWhen() != When.ENTRY) { + // ENTRY is the default value for when, so if that's our value we can omit it. + jgen.writeStringField("when", conditionCheck.getWhen().name()); + } + jgen.writeObjectField("condition", conditionCheck.getCondition()); + jgen.writeEndObject(); + } else if (entry instanceof MultiDynamic) { + // Multi-match + MultiDynamic multiInstruction = (MultiDynamic) entry; + jgen.writeStartObject(); + jgen.writeFieldName(multiInstruction.getMode().name()); + jgen.writeStartArray(); + for (DynamicMatchEntry subEntry : multiInstruction.getEntries()) { + serialize(subEntry, jgen, provider); + } + jgen.writeEndArray(); + jgen.writeEndObject(); + } else { + throw new IOException("Unknown dynamic match entry type: " + entry.getClass().getName()); + } + } +} diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MethodLocation.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MethodLocation.java new file mode 100644 index 0000000..c564460 --- /dev/null +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MethodLocation.java @@ -0,0 +1,141 @@ +package info.mmpa.concoction.scan.model.dynamic.entry; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import info.mmpa.concoction.scan.dynamic.CallStackFrame; +import info.mmpa.concoction.scan.model.TextMatchMode; + +import javax.annotation.Nonnull; + +/** + * Outline of a method location. + */ +@JsonDeserialize(using = MethodLocationDeserializer.class) +@JsonSerialize(using = MethodLocationSerializer.class) +public class MethodLocation { + private final String className; + private final String methodName; + private final String methodDesc; + private final TextMatchMode classMatchMode; + private final TextMatchMode methodNameMatchMode; + private final TextMatchMode methodDescMatchMode; + + /** + * @param className + * Declaring class of method. + * @param methodName + * Method name. + * @param methodDesc + * Method descriptor. + * @param classMatchMode + * Class name text matching technique. + * @param methodNameMatchMode + * Method name text matching technique. + * @param methodDescMatchMode + * Method descriptor text matching technique. + */ + public MethodLocation(@Nonnull String className, + @Nonnull String methodName, + @Nonnull String methodDesc, + @Nonnull TextMatchMode classMatchMode, + @Nonnull TextMatchMode methodNameMatchMode, + @Nonnull TextMatchMode methodDescMatchMode) { + this.className = className; + this.methodName = methodName; + this.methodDesc = methodDesc; + this.classMatchMode = classMatchMode; + this.methodNameMatchMode = methodNameMatchMode; + this.methodDescMatchMode = methodDescMatchMode; + } + + /** + * @param frame + * Frame to check. + * + * @return {@code true} when this location matches the given frame's location. + */ + public boolean match(@Nonnull CallStackFrame frame) { + if (!classMatchMode.matches(className, frame.getOwnerName())) return false; + if (!methodNameMatchMode.matches(methodName, frame.getMethodName())) return false; + return methodDescMatchMode.matches(methodDesc, frame.getMethodDesc()); + } + + /** + * @return Declaring class of method. + */ + @Nonnull + public String getClassName() { + return className; + } + + /** + * @return Method name. + */ + @Nonnull + public String getMethodName() { + return methodName; + } + + /** + * @return Method descriptor. + */ + @Nonnull + public String getMethodDesc() { + return methodDesc; + } + + /** + * @return Class name text matching technique. + */ + @Nonnull + public TextMatchMode getClassMatchMode() { + return classMatchMode; + } + + /** + * @return Method name text matching technique. + */ + @Nonnull + public TextMatchMode getMethodNameMatchMode() { + return methodNameMatchMode; + } + + /** + * @return Method descriptor text matching technique. + */ + @Nonnull + public TextMatchMode getMethodDescMatchMode() { + return methodDescMatchMode; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + MethodLocation that = (MethodLocation) o; + + if (!className.equals(that.className)) return false; + if (!methodName.equals(that.methodName)) return false; + if (!methodDesc.equals(that.methodDesc)) return false; + if (classMatchMode != that.classMatchMode) return false; + if (methodNameMatchMode != that.methodNameMatchMode) return false; + return methodDescMatchMode == that.methodDescMatchMode; + } + + @Override + public int hashCode() { + int result = className.hashCode(); + result = 31 * result + methodName.hashCode(); + result = 31 * result + methodDesc.hashCode(); + result = 31 * result + (classMatchMode != null ? classMatchMode.hashCode() : 0); + result = 31 * result + (methodNameMatchMode != null ? methodNameMatchMode.hashCode() : 0); + result = 31 * result + (methodDescMatchMode != null ? methodDescMatchMode.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return className + "." + methodName + methodDesc; + } +} diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MethodLocationDeserializer.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MethodLocationDeserializer.java new file mode 100644 index 0000000..8f69d79 --- /dev/null +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MethodLocationDeserializer.java @@ -0,0 +1,61 @@ +package info.mmpa.concoction.scan.model.dynamic.entry; + +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import info.mmpa.concoction.scan.model.TextMatchMode; + +import javax.annotation.Nonnull; +import java.io.IOException; + +import static info.mmpa.concoction.util.JsonUtil.breakByFirstSpace; + +/** + * Deserializes {@link MethodLocation} shorthand JSON into instances. + * + * @see MethodLocationSerializer + */ +public class MethodLocationDeserializer extends StdDeserializer { + /** + * New deserializer instance. + */ + public MethodLocationDeserializer() { + super(MethodLocation.class); + } + + @Override + public MethodLocation deserialize(JsonParser jp, DeserializationContext ctx) throws IOException { + JsonNode node = jp.getCodec().readTree(jp); + return deserializeNode(jp, node); + } + + @Nonnull + private MethodLocation deserializeNode(JsonParser jp, JsonNode node) throws JacksonException { + if (node.isObject()) { + JsonNode classNode = node.get("class"); + if (classNode == null) + throw new JsonMappingException(jp, "Dynamic match entry location missing 'class' value"); + JsonNode methodNameNode = node.get("mname"); + if (methodNameNode == null) + throw new JsonMappingException(jp, "Dynamic match entry location missing 'mname' value"); + JsonNode methodDescNode = node.get("mdesc"); + if (methodDescNode == null) + throw new JsonMappingException(jp, "Dynamic match entry location missing 'mdesc' value"); + String[] classInputs = breakByFirstSpace(jp, classNode.asText()); + String[] methodNameInputs = breakByFirstSpace(jp, methodNameNode.asText()); + String[] methodDescInputs = breakByFirstSpace(jp, methodDescNode.asText()); + return new MethodLocation( + classInputs[1], + methodNameInputs[1], + methodDescInputs[1], + TextMatchMode.valueOf(classInputs[0]), + TextMatchMode.valueOf(methodNameInputs[0]), + TextMatchMode.valueOf(methodDescInputs[0]) + ); + } + throw new JsonMappingException(jp, "Dynamic match entry expects a JSON object, or '*' literal for wildcards"); + } +} diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MethodLocationSerializer.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MethodLocationSerializer.java new file mode 100644 index 0000000..3e15385 --- /dev/null +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MethodLocationSerializer.java @@ -0,0 +1,30 @@ +package info.mmpa.concoction.scan.model.dynamic.entry; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import java.io.IOException; + +/** + * Serializes {@link MethodLocation} subtypes into shorthand JSON. + * + * @see MethodLocationDeserializer + */ +public class MethodLocationSerializer extends StdSerializer { + /** + * New serializer instance. + */ + public MethodLocationSerializer() { + super(MethodLocation.class); + } + + @Override + public void serialize(MethodLocation entry, JsonGenerator jgen, SerializerProvider provider) throws IOException { + jgen.writeStartObject(); + jgen.writeStringField("class", entry.getClassMatchMode().name() + " " + entry.getClassName()); + jgen.writeStringField("mname", entry.getMethodNameMatchMode().name() + " " + entry.getMethodName()); + jgen.writeStringField("mdesc", entry.getMethodDescMatchMode().name() + " " + entry.getMethodDesc()); + jgen.writeEndObject(); + } +} diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MultiDynamic.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MultiDynamic.java new file mode 100644 index 0000000..ae6499f --- /dev/null +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MultiDynamic.java @@ -0,0 +1,67 @@ +package info.mmpa.concoction.scan.model.dynamic.entry; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import info.mmpa.concoction.scan.model.MultiMatchMode; + +import javax.annotation.Nonnull; +import java.util.List; + +/** + * Base multi-dynamic outline. + * + * @see AllMultiDynamic + * @see AnyMultiDynamic + * @see NoneMultiDynamic + */ +public abstract class MultiDynamic implements DynamicMatchEntry { + @JsonDeserialize(contentUsing = DynamicMatchEntryDeserializer.class) + @JsonSerialize(contentUsing = DynamicMatchEntrySerializer.class) + protected final List entries; + protected final MultiMatchMode mode; + + /** + * @param mode + * Mode which determines the subtype. + * @param entries + * Dynamic matchers to wrap. Behavior changes based on the {@link #getMode() mode}. + */ + protected MultiDynamic(@Nonnull MultiMatchMode mode, @Nonnull List entries) { + this.mode = mode; + this.entries = entries; + } + + /** + * @return Dynamic matchers to wrap. Behavior changes based on the {@link #getMode() mode}. + */ + @Nonnull + public List getEntries() { + return entries; + } + + /** + * @return Mode which determines the subtype. + */ + @Nonnull + public MultiMatchMode getMode() { + return mode; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + MultiDynamic that = (MultiDynamic) o; + + if (!entries.equals(that.entries)) return false; + return mode == that.mode; + } + + @Override + public int hashCode() { + int result = entries.hashCode(); + result = 31 * result + mode.hashCode(); + return result; + } +} diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/NoneMultiDynamic.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/NoneMultiDynamic.java new file mode 100644 index 0000000..9cb6101 --- /dev/null +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/NoneMultiDynamic.java @@ -0,0 +1,38 @@ +package info.mmpa.concoction.scan.model.dynamic.entry; + +import info.mmpa.concoction.scan.dynamic.CallStackFrame; +import info.mmpa.concoction.scan.model.MultiMatchMode; +import info.mmpa.concoction.scan.model.insn.entry.Instruction; + +import javax.annotation.Nonnull; +import java.util.List; + +/** + * A dynamic matcher that defines multiple sub-matchers. + * None the sub-matchers must match the given input. + *

+ * This is mostly useful as a blacklist modifier. + *
+ * For example, if there is some API that is used fairly often for malicious purposes, except maybe one or two cases, + * you can declare the desired cases as {@link SingleConditionCheckingDynamic condition matches} and then invert the + * matching condition by wrapping those matchers with this type. + */ +public class NoneMultiDynamic extends MultiDynamic { + /** + * @param entries + * Sub-matchers which must all not match inputs in order to pass. + */ + public NoneMultiDynamic(@Nonnull List entries) { + super(MultiMatchMode.NONE, entries); + } + + @Override + public boolean matchOnEnter(@Nonnull CallStackFrame frame) { + return entries.stream().noneMatch(e -> e.matchOnEnter(frame)); + } + + @Override + public boolean matchOnExit(@Nonnull CallStackFrame frame) { + return entries.stream().noneMatch(e -> e.matchOnExit(frame)); + } +} diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/SingleConditionCheckingDynamic.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/SingleConditionCheckingDynamic.java new file mode 100644 index 0000000..fec84cf --- /dev/null +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/SingleConditionCheckingDynamic.java @@ -0,0 +1,91 @@ +package info.mmpa.concoction.scan.model.dynamic.entry; + +import info.mmpa.concoction.scan.dynamic.CallStackFrame; + +import javax.annotation.Nonnull; + +/** + * A dynamic matcher that matches a {@link Condition} in a given {@link MethodLocation}. + */ +public class SingleConditionCheckingDynamic implements DynamicMatchEntry { + private final MethodLocation location; + private final Condition condition; + private final When when; + + /** + * @param location + * Where this match applies to. + * @param condition + * Condition to match against. + * @param when + * When this condition should be checked. + */ + public SingleConditionCheckingDynamic(@Nonnull MethodLocation location, @Nonnull Condition condition, + @Nonnull When when) { + this.location = location; + this.condition = condition; + this.when = when; + } + + @Override + public boolean matchOnEnter(@Nonnull CallStackFrame frame) { + if (when == When.ENTRY) { + if (!location.match(frame)) return false; + return condition.match(frame); + } + return false; + } + + @Override + public boolean matchOnExit(@Nonnull CallStackFrame frame) { + if (when == When.RETURN) { + if (!location.match(frame)) return false; + return condition.match(frame); + } + return false; + } + + /** + * @return Where this match applies to. + */ + @Nonnull + public MethodLocation getLocation() { + return location; + } + + /** + * @return Condition to match against. + */ + @Nonnull + public Condition getCondition() { + return condition; + } + + /** + * @return When this condition should be checked. + */ + @Nonnull + public When getWhen() { + return when; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SingleConditionCheckingDynamic that = (SingleConditionCheckingDynamic) o; + + if (!location.equals(that.location)) return false; + if (!condition.equals(that.condition)) return false; + return when == that.when; + } + + @Override + public int hashCode() { + int result = location.hashCode(); + result = 31 * result + condition.hashCode(); + result = 31 * result + (when != null ? when.hashCode() : 0); + return result; + } +} diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/When.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/When.java new file mode 100644 index 0000000..f29dee1 --- /dev/null +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/When.java @@ -0,0 +1,12 @@ +package info.mmpa.concoction.scan.model.dynamic.entry; + +import info.mmpa.concoction.scan.dynamic.CallStackFrame; + +/** + * Enum for handling {@link DynamicMatchEntry#matchOnEnter(CallStackFrame)} vs + * {@link DynamicMatchEntry#matchOnExit(CallStackFrame)}. + */ +public enum When { + ENTRY, + RETURN +} diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionMatchingList.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionMatchingList.java index 0c4559e..e61bee8 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionMatchingList.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionMatchingList.java @@ -2,6 +2,9 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import info.mmpa.concoction.scan.model.insn.entry.InstructionMatchEntry; +import info.mmpa.concoction.scan.model.insn.entry.InstructionMatchEntryDeserializer; +import info.mmpa.concoction.scan.model.insn.entry.InstructionMatchEntrySerializer; import software.coley.collections.delegate.DelegatingList; import javax.annotation.Nonnull; diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionsMatchingModel.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionsMatchingModel.java index a4dec9b..04e49d1 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionsMatchingModel.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionsMatchingModel.java @@ -6,6 +6,7 @@ import info.mmpa.concoction.output.Detection; import info.mmpa.concoction.output.DetectionArchetype; import info.mmpa.concoction.output.ResultsSink; +import info.mmpa.concoction.scan.model.insn.entry.*; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.MethodNode; diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionsMatchingModelDeserializingConverter.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionsMatchingModelDeserializingConverter.java index 38f1a42..698cdca 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionsMatchingModelDeserializingConverter.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionsMatchingModelDeserializingConverter.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.type.CollectionLikeType; import com.fasterxml.jackson.databind.type.TypeFactory; import com.fasterxml.jackson.databind.util.Converter; +import info.mmpa.concoction.scan.model.insn.entry.InstructionMatchEntry; import java.util.Collections; import java.util.List; diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionsMatchingModelSerializingConverter.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionsMatchingModelSerializingConverter.java index 8e2de32..f19de92 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionsMatchingModelSerializingConverter.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionsMatchingModelSerializingConverter.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.type.CollectionLikeType; import com.fasterxml.jackson.databind.type.TypeFactory; import com.fasterxml.jackson.databind.util.Converter; +import info.mmpa.concoction.scan.model.insn.entry.InstructionMatchEntry; import java.util.List; import java.util.Map; diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/AllMultiInstruction.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/AllMultiInstruction.java similarity index 82% rename from concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/AllMultiInstruction.java rename to concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/AllMultiInstruction.java index ec853af..a52b265 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/AllMultiInstruction.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/AllMultiInstruction.java @@ -1,5 +1,6 @@ -package info.mmpa.concoction.scan.model.insn; +package info.mmpa.concoction.scan.model.insn.entry; +import info.mmpa.concoction.scan.model.MultiMatchMode; import info.mmpa.concoction.scan.model.TextMatchMode; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.MethodNode; @@ -16,7 +17,8 @@ */ public class AllMultiInstruction extends MultiInstruction { /** - * @param entries Sub-matchers which must all match inputs in order to pass. + * @param entries + * Sub-matchers which must all match inputs in order to pass. */ public AllMultiInstruction(@Nonnull List entries) { super(MultiMatchMode.ALL, entries); diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/AnyMultiInstruction.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/AnyMultiInstruction.java similarity index 82% rename from concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/AnyMultiInstruction.java rename to concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/AnyMultiInstruction.java index dfff24e..9b9b025 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/AnyMultiInstruction.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/AnyMultiInstruction.java @@ -1,5 +1,6 @@ -package info.mmpa.concoction.scan.model.insn; +package info.mmpa.concoction.scan.model.insn.entry; +import info.mmpa.concoction.scan.model.MultiMatchMode; import info.mmpa.concoction.scan.model.TextMatchMode; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.MethodNode; @@ -16,7 +17,8 @@ */ public class AnyMultiInstruction extends MultiInstruction { /** - * @param entries Sub-matchers where any single input must match in order to pass. + * @param entries + * Sub-matchers where any single input must match in order to pass. */ public AnyMultiInstruction(@Nonnull List entries) { super(MultiMatchMode.ANY, entries); diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/Instruction.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/Instruction.java similarity index 98% rename from concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/Instruction.java rename to concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/Instruction.java index 0f11ebf..553a9f1 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/Instruction.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/Instruction.java @@ -1,4 +1,4 @@ -package info.mmpa.concoction.scan.model.insn; +package info.mmpa.concoction.scan.model.insn.entry; import info.mmpa.concoction.scan.model.TextMatchMode; import info.mmpa.concoction.util.InstructionStrings; diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionMatchEntry.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/InstructionMatchEntry.java similarity index 93% rename from concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionMatchEntry.java rename to concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/InstructionMatchEntry.java index 736a49e..9dc5503 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionMatchEntry.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/InstructionMatchEntry.java @@ -1,4 +1,4 @@ -package info.mmpa.concoction.scan.model.insn; +package info.mmpa.concoction.scan.model.insn.entry; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionMatchEntryDeserializer.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/InstructionMatchEntryDeserializer.java similarity index 81% rename from concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionMatchEntryDeserializer.java rename to concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/InstructionMatchEntryDeserializer.java index 44c791b..16e3958 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionMatchEntryDeserializer.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/InstructionMatchEntryDeserializer.java @@ -1,4 +1,4 @@ -package info.mmpa.concoction.scan.model.insn; +package info.mmpa.concoction.scan.model.insn.entry; import com.fasterxml.jackson.core.JacksonException; import com.fasterxml.jackson.core.JsonParser; @@ -7,7 +7,9 @@ import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import info.mmpa.concoction.scan.model.MultiMatchMode; import info.mmpa.concoction.scan.model.TextMatchMode; +import static info.mmpa.concoction.util.JsonUtil.*; import javax.annotation.Nonnull; import java.io.IOException; @@ -64,7 +66,7 @@ private InstructionMatchEntry deserializeNode(JsonParser jp, JsonNode node) thro } else { // Should be a multi-instruction if no other case applies. // Determine which mode by its name. - for (MultiInstruction.MultiMatchMode mode : MultiInstruction.MultiMatchMode.values()) { + for (MultiMatchMode mode : MultiMatchMode.values()) { JsonNode modeNode = node.get(mode.name()); if (modeNode != null && modeNode.isArray()) { // Mode found, now extract the entries and create the multi-matcher. @@ -72,22 +74,11 @@ private InstructionMatchEntry deserializeNode(JsonParser jp, JsonNode node) thro for (JsonNode arrayItem : modeNode) { entries.add(deserializeNode(jp, arrayItem)); } - return mode.createMulti(entries); + return mode.createMultiInsn(entries); } } } } throw new JsonMappingException(jp, "Instruction match entry expects a JSON object, or '*' literal for wildcards"); } - - @Nonnull - private static String[] breakByFirstSpace(@Nonnull JsonParser jp, @Nonnull String input) throws JsonProcessingException { - String[] split = new String[2]; - int splitIndex = input.indexOf(' '); - if (splitIndex <= 0) - throw new JsonMappingException(jp, "opcode or argument was not in expected format of: ' "); - split[0] = input.substring(0, splitIndex); // text match mode - split[1] = input.substring(splitIndex + 1); // text input - return split; - } } diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionMatchEntrySerializer.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/InstructionMatchEntrySerializer.java similarity index 97% rename from concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionMatchEntrySerializer.java rename to concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/InstructionMatchEntrySerializer.java index c400f6a..9fe7077 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionMatchEntrySerializer.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/InstructionMatchEntrySerializer.java @@ -1,4 +1,4 @@ -package info.mmpa.concoction.scan.model.insn; +package info.mmpa.concoction.scan.model.insn.entry; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.SerializerProvider; diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionWildcard.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/InstructionWildcard.java similarity index 93% rename from concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionWildcard.java rename to concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/InstructionWildcard.java index b8eb2ec..8825ce6 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionWildcard.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/InstructionWildcard.java @@ -1,4 +1,4 @@ -package info.mmpa.concoction.scan.model.insn; +package info.mmpa.concoction.scan.model.insn.entry; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.MethodNode; diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionWildcardMulti.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/InstructionWildcardMulti.java similarity index 97% rename from concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionWildcardMulti.java rename to concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/InstructionWildcardMulti.java index 76745e0..f342b87 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/InstructionWildcardMulti.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/InstructionWildcardMulti.java @@ -1,4 +1,4 @@ -package info.mmpa.concoction.scan.model.insn; +package info.mmpa.concoction.scan.model.insn.entry; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.MethodNode; diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/MultiInstruction.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/MultiInstruction.java similarity index 65% rename from concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/MultiInstruction.java rename to concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/MultiInstruction.java index 84b8f0f..910ce49 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/MultiInstruction.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/MultiInstruction.java @@ -1,13 +1,12 @@ -package info.mmpa.concoction.scan.model.insn; +package info.mmpa.concoction.scan.model.insn.entry; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import info.mmpa.concoction.scan.model.MultiMatchMode; import javax.annotation.Nonnull; import java.util.List; -import static java.util.Collections.unmodifiableList; - /** * Base multi-instruction outline. * @@ -65,41 +64,4 @@ public int hashCode() { result = 31 * result + mode.hashCode(); return result; } - - /** - * Mode for subtypes of multi-instruction matchers. - */ - public enum MultiMatchMode { - /** - * @see AllMultiInstruction - */ - ALL, - /** - * @see AnyMultiInstruction - */ - ANY, - /** - * @see NoneMultiInstruction - */ - NONE; - - /** - * @param entries - * Entries to wrap into a multi-instruction matcher subtype. - * - * @return Instance of multi-instruction matcher subtype. - */ - @Nonnull - public MultiInstruction createMulti(List entries) { - switch (this) { - case NONE: - return new NoneMultiInstruction(unmodifiableList(entries)); - case ALL: - return new AllMultiInstruction(unmodifiableList(entries)); - case ANY: - default: - return new AnyMultiInstruction(unmodifiableList(entries)); - } - } - } } diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/NoneMultiInstruction.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/NoneMultiInstruction.java similarity index 83% rename from concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/NoneMultiInstruction.java rename to concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/NoneMultiInstruction.java index 658b710..262e75f 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/NoneMultiInstruction.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/NoneMultiInstruction.java @@ -1,5 +1,6 @@ -package info.mmpa.concoction.scan.model.insn; +package info.mmpa.concoction.scan.model.insn.entry; +import info.mmpa.concoction.scan.model.MultiMatchMode; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.MethodNode; @@ -18,7 +19,8 @@ */ public class NoneMultiInstruction extends MultiInstruction { /** - * @param entries Sub-matchers which must all not match inputs in order to pass. + * @param entries + * Sub-matchers which must all not match inputs in order to pass. */ public NoneMultiInstruction(@Nonnull List entries) { super(MultiMatchMode.NONE, entries); diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/util/JsonUtil.java b/concoction-lib/src/main/java/info/mmpa/concoction/util/JsonUtil.java new file mode 100644 index 0000000..0b565dd --- /dev/null +++ b/concoction-lib/src/main/java/info/mmpa/concoction/util/JsonUtil.java @@ -0,0 +1,20 @@ +package info.mmpa.concoction.util; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; + +import javax.annotation.Nonnull; + +public class JsonUtil { + @Nonnull + public static String[] breakByFirstSpace(@Nonnull JsonParser jp, @Nonnull String input) throws JsonProcessingException { + String[] split = new String[2]; + int splitIndex = input.indexOf(' '); + if (splitIndex <= 0) + throw new JsonMappingException(jp, "opcode or argument was not in expected format of: ' "); + split[0] = input.substring(0, splitIndex); // text match mode + split[1] = input.substring(splitIndex + 1); // text input + return split; + } +} diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/util/Serialization.java b/concoction-lib/src/main/java/info/mmpa/concoction/util/Serialization.java index df0faeb..90e48a8 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/util/Serialization.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/util/Serialization.java @@ -4,8 +4,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; import info.mmpa.concoction.scan.model.ScanModel; -import info.mmpa.concoction.scan.model.insn.InstructionMatchEntry; -import info.mmpa.concoction.scan.model.insn.InstructionMatchEntryDeserializer; +import info.mmpa.concoction.scan.model.dynamic.entry.Condition; +import info.mmpa.concoction.scan.model.dynamic.entry.DynamicMatchEntry; +import info.mmpa.concoction.scan.model.dynamic.entry.MethodLocation; +import info.mmpa.concoction.scan.model.insn.entry.InstructionMatchEntry; +import info.mmpa.concoction.scan.model.insn.entry.InstructionMatchEntryDeserializer; import javax.annotation.Nonnull; @@ -58,6 +61,39 @@ public static InstructionMatchEntry deserializeInsnEntry(@Nonnull String text) t return deserialize(InstructionMatchEntry.class, text); } + /** + * @param text + * Text representing an {@link DynamicMatchEntry}. + * + * @return Deserialized value. + */ + @Nonnull + public static DynamicMatchEntry deserializeDynamicEntry(@Nonnull String text) throws JsonProcessingException { + return deserialize(DynamicMatchEntry.class, text); + } + + /** + * @param text + * Text representing an {@link MethodLocation}. + * + * @return Deserialized value. + */ + @Nonnull + public static MethodLocation deserializeMethodLocation(@Nonnull String text) throws JsonProcessingException { + return deserialize(MethodLocation.class, text); + } + + /** + * @param text + * Text representing an {@link Condition}. + * + * @return Deserialized value. + */ + @Nonnull + public static Condition deserializeCondition(@Nonnull String text) throws JsonProcessingException { + return deserialize(Condition.class, text); + } + /** * @param type * Type to deserialize into. diff --git a/concoction-lib/src/test/java/info/mmpa/concoction/scan/model/DynamicMatchSerializationTests.java b/concoction-lib/src/test/java/info/mmpa/concoction/scan/model/DynamicMatchSerializationTests.java new file mode 100644 index 0000000..a49e0fa --- /dev/null +++ b/concoction-lib/src/test/java/info/mmpa/concoction/scan/model/DynamicMatchSerializationTests.java @@ -0,0 +1,27 @@ +package info.mmpa.concoction.scan.model; + +import info.mmpa.concoction.output.DetectionArchetype; +import info.mmpa.concoction.output.SusLevel; +import info.mmpa.concoction.scan.model.dynamic.DynamicMatchingModel; +import info.mmpa.concoction.scan.model.dynamic.entry.Condition; +import info.mmpa.concoction.scan.model.dynamic.entry.DynamicMatchEntry; +import info.mmpa.concoction.scan.model.insn.*; +import info.mmpa.concoction.scan.model.insn.entry.*; +import org.junit.jupiter.api.Test; +import software.coley.collections.Maps; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static info.mmpa.concoction.scan.model.TextMatchMode.EQUALS; +import static info.mmpa.concoction.util.TestSerialization.*; +import static org.junit.jupiter.api.Assertions.*; + + +/** + * Tests for {@link DynamicMatchingModel}, {@link DynamicMatchEntry} and {@link Condition} serialization. + */ +public class DynamicMatchSerializationTests { + +} diff --git a/concoction-lib/src/test/java/info/mmpa/concoction/scan/model/InstructionMatchSerializationTests.java b/concoction-lib/src/test/java/info/mmpa/concoction/scan/model/InstructionMatchSerializationTests.java index 200000d..a956f68 100644 --- a/concoction-lib/src/test/java/info/mmpa/concoction/scan/model/InstructionMatchSerializationTests.java +++ b/concoction-lib/src/test/java/info/mmpa/concoction/scan/model/InstructionMatchSerializationTests.java @@ -3,7 +3,8 @@ import info.mmpa.concoction.output.DetectionArchetype; import info.mmpa.concoction.output.SusLevel; import info.mmpa.concoction.scan.model.dynamic.DynamicMatchingModel; -import info.mmpa.concoction.scan.model.insn.*; +import info.mmpa.concoction.scan.model.insn.InstructionsMatchingModel; +import info.mmpa.concoction.scan.model.insn.entry.*; import org.junit.jupiter.api.Test; import software.coley.collections.Maps; @@ -16,7 +17,7 @@ import static org.junit.jupiter.api.Assertions.*; /** - * Tests for {@link InstructionMatchEntry} serialization. + * Tests for {@link InstructionsMatchingModel} and {@link InstructionMatchEntry} serialization. */ public class InstructionMatchSerializationTests { @Test @@ -57,39 +58,39 @@ void wildcardMultiCount() { @Test void insnOpcodeOnly() { - Instruction instruction = new Instruction("NOP", null, TextMatchMode.EQUALS, null); + Instruction instruction = new Instruction("nop", null, TextMatchMode.EQUALS, null); String serialized = serialize(instruction); - assertEquals("{\"op\":\"EQUALS NOP\"}", serialized); + assertEquals("{\"op\":\"EQUALS nop\"}", serialized); - InstructionMatchEntry entry = deserializeInsnEntry("{\"op\":\"EQUALS NOP\"}"); + InstructionMatchEntry entry = deserializeInsnEntry("{\"op\":\"EQUALS nop\"}"); assertEquals(instruction, entry); } @Test void insnOpcodeAndArgs() { - Instruction instruction = new Instruction("INVOKESTATIC", "java/lang/Runtime", TextMatchMode.EQUALS, TextMatchMode.STARTS_WITH); + Instruction instruction = new Instruction("invokestatic", "java/lang/Runtime", TextMatchMode.EQUALS, TextMatchMode.STARTS_WITH); String serialized = serialize(instruction); - assertEquals("{\"op\":\"EQUALS INVOKESTATIC\",\"args\":\"STARTS_WITH java/lang/Runtime\"}", serialized); + assertEquals("{\"op\":\"EQUALS invokestatic\",\"args\":\"STARTS_WITH java/lang/Runtime\"}", serialized); - InstructionMatchEntry entry = deserializeInsnEntry("{\"op\":\"EQUALS INVOKESTATIC\",\"args\":\"STARTS_WITH java/lang/Runtime\"}"); + InstructionMatchEntry entry = deserializeInsnEntry("{\"op\":\"EQUALS invokestatic\",\"args\":\"STARTS_WITH java/lang/Runtime\"}"); assertEquals(instruction, entry); } @Test void multiInsn() { InstructionWildcard instruction1 = InstructionWildcard.INSTANCE; - Instruction instruction2 = new Instruction("NOP", null, TextMatchMode.EQUALS, null); - Instruction instruction3 = new Instruction("INVOKESTATIC", "java/lang/Runtime", TextMatchMode.EQUALS, TextMatchMode.STARTS_WITH); + Instruction instruction2 = new Instruction("nop", null, TextMatchMode.EQUALS, null); + Instruction instruction3 = new Instruction("invokestatic", "java/lang/Runtime", TextMatchMode.EQUALS, TextMatchMode.STARTS_WITH); MultiInstruction multiInstruction = new AnyMultiInstruction(Arrays.asList(instruction1, instruction2, instruction3)); String serialized = serialize(multiInstruction); - assertEquals("{\"ANY\":[\"*\",{\"op\":\"EQUALS NOP\"},{\"op\":\"EQUALS INVOKESTATIC\",\"args\":\"STARTS_WITH java/lang/Runtime\"}]}", serialized); + assertEquals("{\"ANY\":[\"*\",{\"op\":\"EQUALS nop\"},{\"op\":\"EQUALS invokestatic\",\"args\":\"STARTS_WITH java/lang/Runtime\"}]}", serialized); InstructionMatchEntry entry = deserializeInsnEntry("{\n" + " \"ANY\": [\n" + " \"*\",\n" + - " { \"op\": \"EQUALS NOP\" },\n" + - " { \"op\": \"EQUALS INVOKESTATIC\", \"args\": \"STARTS_WITH java/lang/Runtime\" }\n" + + " { \"op\": \"EQUALS nop\" },\n" + + " { \"op\": \"EQUALS invokestatic\", \"args\": \"STARTS_WITH java/lang/Runtime\" }\n" + " ]\n" + "}"); assertEquals(multiInstruction, entry); @@ -104,12 +105,12 @@ void model() { // Process foo = Runtime.getRuntime().exec("foo"); DetectionArchetype archetype = new DetectionArchetype(SusLevel.MAXIMUM, "id", "description"); List entries = Arrays.asList( - new Instruction("INVOKESTATIC", "getRuntime()Ljava/lang/Runtime;", EQUALS, EQUALS), - new Instruction("LDC", null, EQUALS, null), - new Instruction("INVOKEVIRTUAL", "exec(Ljava/lang/String;)Ljava/lang/Process;", EQUALS, EQUALS) + new Instruction("invokestatic", "getRuntime()Ljava/lang/Runtime;", EQUALS, EQUALS), + new Instruction("ldc", null, EQUALS, null), + new Instruction("invokevirtual", "exec(Ljava/lang/String;)Ljava/lang/Process;", EQUALS, EQUALS) ); InstructionsMatchingModel insnModel = new InstructionsMatchingModel(Maps.of("key", entries)); - DynamicMatchingModel dynamicModel = new DynamicMatchingModel(Collections.emptyMap()); // TODO: Provide a value here + DynamicMatchingModel dynamicModel = new DynamicMatchingModel(Collections.emptyMap()); ScanModel model = new ScanModel(archetype, insnModel, dynamicModel); // Serialize, deserialize, and compare equality diff --git a/concoction-lib/src/test/java/info/mmpa/concoction/util/TestSerialization.java b/concoction-lib/src/test/java/info/mmpa/concoction/util/TestSerialization.java index 48ec34f..32b2a54 100644 --- a/concoction-lib/src/test/java/info/mmpa/concoction/util/TestSerialization.java +++ b/concoction-lib/src/test/java/info/mmpa/concoction/util/TestSerialization.java @@ -1,7 +1,10 @@ package info.mmpa.concoction.util; import info.mmpa.concoction.scan.model.ScanModel; -import info.mmpa.concoction.scan.model.insn.InstructionMatchEntry; +import info.mmpa.concoction.scan.model.dynamic.entry.Condition; +import info.mmpa.concoction.scan.model.dynamic.entry.DynamicMatchEntry; +import info.mmpa.concoction.scan.model.dynamic.entry.MethodLocation; +import info.mmpa.concoction.scan.model.insn.entry.InstructionMatchEntry; import org.junit.jupiter.api.Assertions; import javax.annotation.Nonnull; @@ -17,6 +20,21 @@ public static InstructionMatchEntry deserializeInsnEntry(@Nonnull String json) { return map(Serialization::deserializeInsnEntry, json); } + @Nonnull + public static DynamicMatchEntry deserializeDynamicEntry(@Nonnull String json) { + return map(Serialization::deserializeDynamicEntry, json); + } + + @Nonnull + public static MethodLocation deserializeMethodLocation(@Nonnull String json) { + return map(Serialization::deserializeMethodLocation, json); + } + + @Nonnull + public static Condition deserializeCondition(@Nonnull String json) { + return map(Serialization::deserializeCondition, json); + } + @Nonnull public static ScanModel deserializeModel(@Nonnull String json) { return map(Serialization::deserializeModel, json); From 63134fcbf55da4633d87e38e4bbe278760119713 Mon Sep 17 00:00:00 2001 From: Col-E Date: Thu, 10 Aug 2023 22:11:48 -0400 Subject: [PATCH 02/11] Dynamic condition implementations and serialization --- .../concoction/scan/dynamic/SsvmContext.java | 12 +- .../scan/dynamic/VirtualMachineExt.java | 34 ++ .../model/dynamic/entry/AnyCondition.java | 21 + .../scan/model/dynamic/entry/Condition.java | 13 +- .../dynamic/entry/ConditionDeserializer.java | 44 +- .../dynamic/entry/ConditionSerializer.java | 32 +- .../model/dynamic/entry/NoneCondition.java | 21 + .../dynamic/entry/NullParameterCondition.java | 65 +++ .../entry/NumericParameterCondition.java | 479 ++++++++++++++++++ .../entry/SingleConditionCheckingDynamic.java | 2 +- .../entry/StringParameterCondition.java | 181 +++++++ .../model/DynamicMatchSerializationTests.java | 100 +++- dependencies.gradle | 2 +- 13 files changed, 968 insertions(+), 38 deletions(-) create mode 100644 concoction-lib/src/main/java/info/mmpa/concoction/scan/dynamic/VirtualMachineExt.java create mode 100644 concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/AnyCondition.java create mode 100644 concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/NoneCondition.java create mode 100644 concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/NullParameterCondition.java create mode 100644 concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/NumericParameterCondition.java create mode 100644 concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/StringParameterCondition.java diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/dynamic/SsvmContext.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/dynamic/SsvmContext.java index e7af246..2b00e06 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/dynamic/SsvmContext.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/dynamic/SsvmContext.java @@ -4,7 +4,6 @@ import dev.xdark.ssvm.api.MethodInvoker; import dev.xdark.ssvm.api.VMInterface; import dev.xdark.ssvm.classloading.SupplyingClassLoaderInstaller; -import dev.xdark.ssvm.filesystem.FileManager; import dev.xdark.ssvm.invoke.InvocationUtil; import dev.xdark.ssvm.mirror.type.InstanceClass; import dev.xdark.ssvm.thread.OSThread; @@ -52,12 +51,7 @@ public SsvmContext(@Nonnull ApplicationModel model, @Nonnull SourcePathElement sourcePath, @Nonnull Collection scanModel) { // Create and initialize the VM. - VirtualMachine vm = new VirtualMachine() { - @Override - protected FileManager createFileManager() { - return new CustomFileManager(); - } - }; + VirtualMachine vm = new VirtualMachineExt() ; vm.getProperties().put("java.class.path", ""); // Hide class path of concoction from the VM vm.bootstrap(); @@ -93,7 +87,7 @@ protected FileManager createFileManager() { // SSVM manages its own memory, and this conflicts with it. Stubbing it out keeps everyone happy. InstanceClass bits = (InstanceClass) vm.findBootstrapClass("java/nio/Bits"); - vmi.setInvoker(bits.getMethod("reserveMemory", "(JJ)V"), MethodInvoker.noop()); + if (bits != null) vmi.setInvoker(bits.getMethod("reserveMemory", "(JJ)V"), MethodInvoker.noop()); } // Store VM instance. @@ -146,7 +140,7 @@ public InvocationUtil getInvoker() { deencapsulate.invoke(null, SsvmContext.class); } } catch (Exception ex) { - throw new IllegalStateException(ex); + throw new IllegalStateException("Failed to unlock reflection access", ex); } } } diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/dynamic/VirtualMachineExt.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/dynamic/VirtualMachineExt.java new file mode 100644 index 0000000..ae4e4a3 --- /dev/null +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/dynamic/VirtualMachineExt.java @@ -0,0 +1,34 @@ +package info.mmpa.concoction.scan.dynamic; + +import dev.xdark.ssvm.VirtualMachine; +import dev.xdark.ssvm.filesystem.FileManager; +import dev.xdark.ssvm.mirror.type.JavaClass; + +import javax.annotation.Nonnull; + +/** + * Basic extension of the VM class, tweaking some default manager implementations + * and providing access to additional data. + */ +public class VirtualMachineExt extends VirtualMachine { + private JavaClass charSequence; + + @Override + public void bootstrap() { + super.bootstrap(); + charSequence = findBootstrapClass("java/lang/CharSequence"); + } + + @Override + protected FileManager createFileManager() { + return new CustomFileManager(); + } + + /** + * @return {@link CharSequence} type. + */ + @Nonnull + public JavaClass getCharSequence() { + return charSequence; + } +} diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/AnyCondition.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/AnyCondition.java new file mode 100644 index 0000000..cb4c25f --- /dev/null +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/AnyCondition.java @@ -0,0 +1,21 @@ +package info.mmpa.concoction.scan.model.dynamic.entry; + +import info.mmpa.concoction.scan.dynamic.CallStackFrame; + +import javax.annotation.Nonnull; + +/** + * Condition that always matches. + */ +public class AnyCondition implements Condition { + public static final AnyCondition INSTANCE = new AnyCondition(); + + private AnyCondition() { + // deny construction + } + + @Override + public boolean match(@Nonnull CallStackFrame frame) { + return true; + } +} diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/Condition.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/Condition.java index b397ed3..29e84de 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/Condition.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/Condition.java @@ -1,21 +1,16 @@ package info.mmpa.concoction.scan.model.dynamic.entry; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import info.mmpa.concoction.scan.dynamic.CallStackFrame; import javax.annotation.Nonnull; -// TODO: Sub-types -// - Any (wildcard, useful where any execution of a 'MethodLocation' is flag-worthy) -// - Value matching for (Parameter N), (Field $NAME/$DESC) -// - Primitive math matching -// - Object null/not-null -// - String TextMatchMode matching -// - Also for char[] and other similar types (StringBuilder, StringBuffer, non-string CharSequence) -// - byte[] matching helpers - /** * Outline for condition matching in a given stack-frame. */ +@JsonSerialize(using = ConditionSerializer.class) +@JsonDeserialize(using = ConditionDeserializer.class) public interface Condition { /** * @param frame diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/ConditionDeserializer.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/ConditionDeserializer.java index 5d6d0e5..8236b45 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/ConditionDeserializer.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/ConditionDeserializer.java @@ -3,12 +3,16 @@ import com.fasterxml.jackson.core.JacksonException; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import info.mmpa.concoction.scan.model.TextMatchMode; import javax.annotation.Nonnull; import java.io.IOException; +import static info.mmpa.concoction.util.JsonUtil.breakByFirstSpace; + /** * Deserializes {@link Condition} shorthand JSON into instances. * @@ -30,7 +34,43 @@ public Condition deserialize(JsonParser jp, DeserializationContext ctx) throws I @Nonnull private Condition deserializeNode(JsonParser jp, JsonNode node) throws JacksonException { - // TODO: Implement when condition sub-types are fleshed out - throw new UnsupportedOperationException("implement condition deserialization"); + if (node.isTextual()) { + // Must be 'ANY' or 'NONE' + String nodeText = node.asText(); + if (nodeText.equalsIgnoreCase("ANY")) return AnyCondition.INSTANCE; + if (nodeText.equalsIgnoreCase("NONE")) return NoneCondition.INSTANCE; + throw new JsonMappingException(jp, "String representation of conditions can only be 'ANY' or 'NONE' but was: " + nodeText); + } else if (node.isObject()) { + // Most conditions will have an 'index' to compare against. + JsonNode indexNode = node.get("index"); + int index = indexNode == null ? -1 : indexNode.asInt(); + + // If we see a field 'null' we know it's a null-param check condition. + JsonNode nullNode = node.get("null"); + if (nullNode != null) + return new NullParameterCondition(index, nullNode.asBoolean()); + + // Get the other possible fields for other conditions, and see which makes the most sense here. + JsonNode extractionNode = node.get("extraction"); + JsonNode matchNode = node.get("match"); + if (matchNode == null) throw new JsonMappingException(jp, "Missing expected 'match' field"); + + // If the 'match' field value starts with a numeric op, it's a numeric param condition. + String matchRaw = matchNode.asText(); + if (NumericParameterCondition.startsWithOp(matchRaw)) { + return new NumericParameterCondition(index, NumericParameterCondition.fromString(matchRaw)); + } + + // The only remaining possibility is a string parameter condition. + String[] matchInputs = breakByFirstSpace(jp, matchNode.asText()); + TextMatchMode matchMode = TextMatchMode.valueOf(matchInputs[0]); + String match = matchInputs[1]; + StringParameterCondition.StringExtractionMode extractionMode = extractionNode == null ? + StringParameterCondition.StringExtractionMode.KNOWN_STRING_TYPES : + StringParameterCondition.StringExtractionMode.get(extractionNode.asText()); + return new StringParameterCondition(extractionMode, matchMode, match, index); + } + + throw new JsonMappingException(jp, "Comparison was not an object or textual model"); } } diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/ConditionSerializer.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/ConditionSerializer.java index bc59fe5..6bcc9de 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/ConditionSerializer.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/ConditionSerializer.java @@ -20,7 +20,35 @@ public ConditionSerializer() { } @Override - public void serialize(Condition entry, JsonGenerator jgen, SerializerProvider provider) throws IOException { - // TODO: Implement when condition sub-types are fleshed out + public void serialize(Condition condition, JsonGenerator jgen, SerializerProvider provider) throws IOException { + if (condition instanceof AnyCondition) { + jgen.writeString("ANY"); + } else if (condition instanceof NoneCondition) { + jgen.writeString("NONE"); + } else if (condition instanceof NullParameterCondition) { + NullParameterCondition nullParameterCondition = (NullParameterCondition) condition; + jgen.writeStartObject(); + jgen.writeNumberField("index", nullParameterCondition.getIndex()); + jgen.writeBooleanField("null", nullParameterCondition.isNull()); + jgen.writeEndObject(); + } else if (condition instanceof StringParameterCondition) { + StringParameterCondition stringParameterCondition = (StringParameterCondition) condition; + jgen.writeStartObject(); + if (stringParameterCondition.getIndex() >= 0) + jgen.writeNumberField("index", stringParameterCondition.getIndex()); + if (stringParameterCondition.getExtractionMode() != StringParameterCondition.StringExtractionMode.KNOWN_STRING_TYPES) + jgen.writeStringField("extraction", stringParameterCondition.getExtractionMode().getDisplay()); + jgen.writeStringField("match", stringParameterCondition.getMatchMode().name() + " " + stringParameterCondition.getMatch()); + jgen.writeEndObject(); + } else if (condition instanceof NumericParameterCondition) { + NumericParameterCondition numericParameterCondition = (NumericParameterCondition) condition; + jgen.writeStartObject(); + if (numericParameterCondition.getIndex() >= 0) + jgen.writeNumberField("index", numericParameterCondition.getIndex()); + jgen.writeStringField("match", numericParameterCondition.getComparisonOperation() + " " + numericParameterCondition.getComparisonValue()); + jgen.writeEndObject(); + } else { + throw new IllegalStateException("Unsupported condition class: " + condition.getClass().getName()); + } } } diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/NoneCondition.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/NoneCondition.java new file mode 100644 index 0000000..99d311a --- /dev/null +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/NoneCondition.java @@ -0,0 +1,21 @@ +package info.mmpa.concoction.scan.model.dynamic.entry; + +import info.mmpa.concoction.scan.dynamic.CallStackFrame; + +import javax.annotation.Nonnull; + +/** + * Condition that never matches. + */ +public class NoneCondition implements Condition { + public static final NoneCondition INSTANCE = new NoneCondition(); + + private NoneCondition() { + // deny construction + } + + @Override + public boolean match(@Nonnull CallStackFrame frame) { + return false; + } +} diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/NullParameterCondition.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/NullParameterCondition.java new file mode 100644 index 0000000..6c35c81 --- /dev/null +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/NullParameterCondition.java @@ -0,0 +1,65 @@ +package info.mmpa.concoction.scan.model.dynamic.entry; + +import dev.xdark.ssvm.execution.ExecutionContext; +import dev.xdark.ssvm.value.ObjectValue; +import info.mmpa.concoction.scan.dynamic.CallStackFrame; + +import javax.annotation.Nonnull; + +/** + * Condition implementation for object null/not-null parameter matching. + */ +public class NullParameterCondition implements Condition { + private final int index; + private final boolean isNull; + + /** + * @param index + * Index to check. + * @param isNull + * Flag to check for null, or not-null. + */ + public NullParameterCondition(int index, boolean isNull) { + this.index = index; + this.isNull = isNull; + } + + /** + * @return Index to check. + */ + public int getIndex() { + return index; + } + + /** + * @return Flag to check for null, or not-null. + */ + public boolean isNull() { + return isNull; + } + + @Override + public boolean match(@Nonnull CallStackFrame frame) { + ExecutionContext ctx = frame.getCtx(); + ObjectValue value = ctx.getLocals().loadReference(index); + return value.isNull() == isNull; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + NullParameterCondition that = (NullParameterCondition) o; + + if (index != that.index) return false; + return isNull == that.isNull; + } + + @Override + public int hashCode() { + int result = index; + result = 31 * result + (isNull ? 1 : 0); + return result; + } +} diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/NumericParameterCondition.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/NumericParameterCondition.java new file mode 100644 index 0000000..9d3efcf --- /dev/null +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/NumericParameterCondition.java @@ -0,0 +1,479 @@ +package info.mmpa.concoction.scan.model.dynamic.entry; + +import dev.xdark.ssvm.execution.ExecutionContext; +import dev.xdark.ssvm.execution.Locals; +import info.mmpa.concoction.scan.dynamic.CallStackFrame; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nonnull; +import java.text.NumberFormat; +import java.text.ParseException; + +/** + * Condition implementation for numeric parameter matching. + */ +public class NumericParameterCondition implements Condition { + private static final Logger logger = LoggerFactory.getLogger(NumericParameterCondition.class); + + private static final NumberFormat NUM_FMT = NumberFormat.getInstance(); + + /** + * Key for equality comparison. + */ + public static final String OP_EQUAL = "=="; + /** + * Key for non-equality comparison. + */ + public static final String OP_NOT_EQUAL = "!="; + /** + * Key for greater-than comparison. + */ + public static final String OP_GREATER = ">"; + /** + * Key for greater-than-or-equal comparison. + */ + public static final String OP_GREATER_EQUAL = ">="; + /** + * Key for less-than comparison. + */ + public static final String OP_LESS = "<"; + /** + * Key for less-than-or-equal comparison. + */ + public static final String OP_LESS_EQUAL = "<="; + /** + * Key for bitwise and comparison, where a match is any non-zero value. + */ + public static final String OP_HAS_MASK = "&"; + /** + * Key for modulo comparison, where a match is any zero-remainder value. + */ + public static final String OP_IS_DIVISOR = "%"; + + /** + * Array of all op names. + */ + private static final String[] OPS = { + OP_EQUAL, + OP_NOT_EQUAL, + OP_GREATER, + OP_GREATER_EQUAL, + OP_LESS, + OP_LESS_EQUAL, + OP_HAS_MASK, + OP_IS_DIVISOR + }; + + private final int index; + private final ComparisonWithOp match; + + /** + * @param index + * Index to match against. Will be used as the left side of the comparison. + * @param match + * Comparison to make against. + */ + public NumericParameterCondition(int index, @Nonnull ComparisonWithOp match) { + this.index = index; + this.match = match; + } + + /** + * @return Index to match against. Will be used as the left side of the comparison. + */ + public int getIndex() { + return index; + } + + /** + * @return Name of comparison operation. + */ + @Nonnull + public String getComparisonOperation() { + return match.opName; + } + + /** + * @return Value of comparison operation. Represents the right side of the comparison. + */ + @Nonnull + public String getComparisonValue() { + return match.opValue; + } + + @Override + public boolean match(@Nonnull CallStackFrame frame) { + ExecutionContext ctx = frame.getCtx(); + return match.compare(index, ctx.getLocals()); + } + + + /** + * @param text + * Text to check. + * + * @return {@code true} when it starts with a numeric op. + */ + public static boolean startsWithOp(@Nonnull String text) { + for (String op : OPS) + if (text.startsWith(op)) return true; + return false; + } + + /** + * @param text + * Text format of comparison. Format is {@code operation value}. + * + * @return Comparison impl of operation for the provided value. + */ + @Nonnull + public static ComparisonWithOp fromString(@Nonnull String text) { + // 2 args required: + String[] args = text.split("\\s+"); + if (args.length < 2) { + logger.error("Cannot convert '{}' to comparison impl, not in format ' '", text); + return new ComparisonWithOp(Comparison.DUMMY, "?", "?"); + } + + // Determine type of value, create operation matcher + String opStr = args[0]; + String valueStr = args[1]; + return new ComparisonWithOp(fromOpAndValue(opStr, valueStr), opStr, valueStr); + } + + /** + * @param op + * Argument portion extracted from text. + * @param value + * Value portion extracted from text. + * + * @return Comparison impl of operation for the provided value. + */ + @Nonnull + public static Comparison fromOpAndValue(@Nonnull String op, @Nonnull String value) { + try { + Number parsed = NUM_FMT.parse(value); + + if (parsed instanceof Integer) { + return intComparison(op, parsed.intValue()); + } else if (parsed instanceof Float) { + return floatComparison(op, parsed.floatValue()); + } else if (parsed instanceof Double) { + return doubleComparison(op, parsed.doubleValue()); + } else if (parsed instanceof Long) { + return longComparison(op, parsed.longValue()); + } + + logger.error("Cannot convert '{}' to comparison impl, unsupported numeric type: {}", value, parsed.getClass().getName()); + return Comparison.DUMMY; + } catch (ParseException ex) { + logger.error("Cannot convert '{}' to comparison impl, failed to parse value argument", value, ex); + return Comparison.DUMMY; + } + } + + /** + * @param op + * Operation to use. + * @param value + * Value of right side of the comparison. + * + * @return Comparison impl of operation for the given value. + */ + @Nonnull + public static FloatComparison floatComparison(@Nonnull String op, float value) { + switch (op) { + case OP_EQUAL: + return v -> v == value; + case OP_NOT_EQUAL: + return v -> v != value; + case OP_GREATER: + return v -> v > value; + case OP_GREATER_EQUAL: + return v -> v >= value; + case OP_LESS: + return v -> v < value; + case OP_LESS_EQUAL: + return v -> v <= value; + case OP_IS_DIVISOR: + return v -> (v % value) == 0; + } + logger.error("Unknown float comparison operation '{}'", op); + return FloatComparison.DUMMY; + } + + /** + * @param op + * Operation to use. + * @param value + * Value of right side of the comparison. + * + * @return Comparison impl of operation for the given value. + */ + @Nonnull + public static DoubleComparison doubleComparison(@Nonnull String op, double value) { + switch (op) { + case OP_EQUAL: + return v -> v == value; + case OP_NOT_EQUAL: + return v -> v != value; + case OP_GREATER: + return v -> v > value; + case OP_GREATER_EQUAL: + return v -> v >= value; + case OP_LESS: + return v -> v < value; + case OP_LESS_EQUAL: + return v -> v <= value; + case OP_IS_DIVISOR: + return v -> (v % value) == 0; + } + logger.error("Unknown double comparison operation '{}'", op); + return DoubleComparison.DUMMY; + } + + /** + * @param op + * Operation to use. + * @param value + * Value of right side of the comparison. + * + * @return Comparison impl of operation for the given value. + */ + @Nonnull + public static LongComparison longComparison(@Nonnull String op, long value) { + switch (op) { + case OP_EQUAL: + return v -> v == value; + case OP_NOT_EQUAL: + return v -> v != value; + case OP_GREATER: + return v -> v > value; + case OP_GREATER_EQUAL: + return v -> v >= value; + case OP_LESS: + return v -> v < value; + case OP_LESS_EQUAL: + return v -> v <= value; + case OP_HAS_MASK: + return v -> (v & value) != 0; + case OP_IS_DIVISOR: + return v -> (v % value) == 0; + } + logger.error("Unknown long comparison operation '{}'", op); + return LongComparison.DUMMY; + } + + /** + * @param op + * Operation to use. + * @param value + * Value of right side of the comparison. + * + * @return Comparison impl of operation for the given value. + */ + @Nonnull + public static IntComparison intComparison(@Nonnull String op, int value) { + switch (op) { + case OP_EQUAL: + return v -> v == value; + case OP_NOT_EQUAL: + return v -> v != value; + case OP_GREATER: + return v -> v > value; + case OP_GREATER_EQUAL: + return v -> v >= value; + case OP_LESS: + return v -> v < value; + case OP_LESS_EQUAL: + return v -> v <= value; + case OP_HAS_MASK: + return v -> (v & value) != 0; + case OP_IS_DIVISOR: + return v -> (v % value) == 0; + } + logger.error("Unknown int comparison operation '{}'", op); + return IntComparison.DUMMY; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + NumericParameterCondition that = (NumericParameterCondition) o; + + if (index != that.index) return false; + return match.equals(that.match); + } + + @Override + public int hashCode() { + int result = index; + result = 31 * result + match.hashCode(); + return result; + } + + /** + * Base float comparison outline. + */ + public interface FloatComparison extends Comparison { + /** + * Dummy that never matches. + */ + FloatComparison DUMMY = (v) -> false; + + @Override + default boolean compare(int index, @Nonnull Locals locals) { + float value = locals.loadFloat(index); + return compare(value); + } + + /** + * @param value + * Left side value of comparison. + * + * @return {@code true} on match success. + */ + boolean compare(float value); + } + + /** + * Base double comparison outline. + */ + public interface DoubleComparison extends Comparison { + /** + * Dummy that never matches. + */ + DoubleComparison DUMMY = (v) -> false; + + @Override + default boolean compare(int index, @Nonnull Locals locals) { + double value = locals.loadDouble(index); + return compare(value); + } + + /** + * @param value + * Left side value of comparison. + * + * @return {@code true} on match success. + */ + boolean compare(double value); + } + + /** + * Base long comparison outline. + */ + public interface LongComparison extends Comparison { + /** + * Dummy that never matches. + */ + LongComparison DUMMY = (v) -> false; + + @Override + default boolean compare(int index, @Nonnull Locals locals) { + long value = locals.loadLong(index); + return compare(value); + } + + /** + * @param value + * Left side value of comparison. + * + * @return {@code true} on match success. + */ + boolean compare(long value); + } + + /** + * Base int comparison outline. + */ + public interface IntComparison extends Comparison { + /** + * Dummy that never matches. + */ + IntComparison DUMMY = (v) -> false; + + @Override + default boolean compare(int index, @Nonnull Locals locals) { + int value = locals.loadInt(index); + return compare(value); + } + + /** + * @param value + * Left side value of comparison. + * + * @return {@code true} on match success. + */ + boolean compare(int value); + } + + /** + * Delegating implementation of {@link Comparison} with an associated name. + */ + public static class ComparisonWithOp implements Comparison { + private final Comparison comparison; + private final String opName; + private final String opValue; + + /** + * @param comparison + * Delegate comparison target. + * @param opName + * Name of comparison operation. + * @param opValue + * Value to compare against. + */ + public ComparisonWithOp(@Nonnull Comparison comparison, @Nonnull String opName, @Nonnull String opValue) { + this.comparison = comparison; + this.opName = opName; + this.opValue = opValue; + } + + @Override + public boolean compare(int index, @Nonnull Locals locals) { + return comparison.compare(index, locals); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ComparisonWithOp that = (ComparisonWithOp) o; + + if (!opName.equals(that.opName)) return false; + return opValue.equals(that.opValue); + } + + @Override + public int hashCode() { + int result = opName.hashCode(); + result = 31 * result + opValue.hashCode(); + return result; + } + } + + /** + * Base comparison outline. + */ + public interface Comparison { + /** + * Dummy that never matches. + */ + Comparison DUMMY = (index, locals) -> false; + + /** + * @param index + * Index of variable to load. + * @param locals + * Local variable table reference to load from. + * + * @return {@code true} on match success. + */ + boolean compare(int index, @Nonnull Locals locals); + } +} diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/SingleConditionCheckingDynamic.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/SingleConditionCheckingDynamic.java index fec84cf..d7c18d3 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/SingleConditionCheckingDynamic.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/SingleConditionCheckingDynamic.java @@ -85,7 +85,7 @@ public boolean equals(Object o) { public int hashCode() { int result = location.hashCode(); result = 31 * result + condition.hashCode(); - result = 31 * result + (when != null ? when.hashCode() : 0); + result = 31 * result + when.hashCode(); return result; } } diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/StringParameterCondition.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/StringParameterCondition.java new file mode 100644 index 0000000..7d4b2d8 --- /dev/null +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/StringParameterCondition.java @@ -0,0 +1,181 @@ +package info.mmpa.concoction.scan.model.dynamic.entry; + +import dev.xdark.ssvm.execution.ExecutionContext; +import dev.xdark.ssvm.mirror.type.JavaClass; +import dev.xdark.ssvm.value.ObjectValue; +import info.mmpa.concoction.scan.dynamic.CallStackFrame; +import info.mmpa.concoction.scan.dynamic.VirtualMachineExt; +import info.mmpa.concoction.scan.model.TextMatchMode; + +import javax.annotation.Nonnull; + +/** + * Condition implementation for text parameter matching. + */ +public class StringParameterCondition implements Condition { + private final StringExtractionMode extractionMode; + private final TextMatchMode matchMode; + private final String match; + private final int index; + + /** + * @param extractionMode + * Mode for pulling text from VM objects. + * @param matchMode + * Text match mode for {@link #getMatch()} against parameter values. + * @param match + * Text to match against parameter values. + * @param index + * Parameter index to compare against. Negative for any parameter. + */ + public StringParameterCondition(@Nonnull StringExtractionMode extractionMode, @Nonnull TextMatchMode matchMode, + @Nonnull String match, int index) { + this.extractionMode = extractionMode; + this.matchMode = matchMode; + this.match = match; + this.index = index; + } + + /** + * @return Mode for pulling text from VM objects. + */ + @Nonnull + public StringExtractionMode getExtractionMode() { + return extractionMode; + } + + /** + * @return Text match mode for {@link #getMatch()} against parameter values. + */ + @Nonnull + public TextMatchMode getMatchMode() { + return matchMode; + } + + /** + * @return Text to match against parameter values. + */ + @Nonnull + public String getMatch() { + return match; + } + + /** + * @return Parameter index to compare against. Negative for any parameter. + */ + public int getIndex() { + return index; + } + + @Override + public boolean match(@Nonnull CallStackFrame frame) { + // Get parameter value + ExecutionContext ctx = frame.getCtx(); + if (index < 0) { + // Match against any argument + int maxArgs = ctx.getMethod().getMaxArgs(); + for (int i = 0; i < maxArgs; i++) { + ObjectValue value = ctx.getLocals().loadReference(index); + if (matchValue(ctx, value)) + return true; + } + } else { + // Match against a single argument + ObjectValue value = ctx.getLocals().loadReference(index); + return matchValue(ctx, value); + } + + return false; + } + + private boolean matchValue(@Nonnull ExecutionContext ctx, @Nonnull ObjectValue value) { + if (value.isNull()) return false; + + // Extract string to compare to + VirtualMachineExt vm = (VirtualMachineExt) ctx.getVM(); + String matchTarget = null; + try { + switch (extractionMode) { + case ANY_TYPE_TOSTRING: + matchTarget = vm.getOperations().toString(value); + break; + case KNOWN_STRING_TYPES: + JavaClass valueType = vm.getMemoryManager().readClass(value); + if (valueType.isAssignableFrom(vm.getCharSequence())) + matchTarget = vm.getOperations().toString(value); + break; + } + } catch (Throwable ignored) { + // If a class defines a 'toString()' that throws an exception, we can't extract the value. + // For known string types, like String/StringBuilder/etc this is not an issue. + // It should only be very rare for somebody to implement a CharSequence themselves that does this. + } + + // Compare + return matchTarget != null && matchMode.matches(match, matchTarget); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + StringParameterCondition that = (StringParameterCondition) o; + + if (index != that.index) return false; + if (extractionMode != that.extractionMode) return false; + if (matchMode != that.matchMode) return false; + return match.equals(that.match); + } + + @Override + public int hashCode() { + int result = extractionMode.hashCode(); + result = 31 * result + matchMode.hashCode(); + result = 31 * result + match.hashCode(); + result = 31 * result + index; + return result; + } + + /** + * Extraction modes for pulling text from object values. + */ + public enum StringExtractionMode { + KNOWN_STRING_TYPES("known-types"), + ANY_TYPE_TOSTRING("any-type"); + + private final String display; + + StringExtractionMode(@Nonnull String display) { + this.display = display; + } + + /** + * @param text + * Input form. + * + * @return Mode matching input form name. + */ + @Nonnull + public static StringExtractionMode get(@Nonnull String text) { + if (text.equalsIgnoreCase(KNOWN_STRING_TYPES.name()) || + text.equalsIgnoreCase(KNOWN_STRING_TYPES.display)) { + return KNOWN_STRING_TYPES; + } + return ANY_TYPE_TOSTRING; + } + + /** + * @return Display name for mode. + */ + @Nonnull + public String getDisplay() { + return display; + } + + @Override + public String toString() { + return display; + } + } +} diff --git a/concoction-lib/src/test/java/info/mmpa/concoction/scan/model/DynamicMatchSerializationTests.java b/concoction-lib/src/test/java/info/mmpa/concoction/scan/model/DynamicMatchSerializationTests.java index a49e0fa..6f55d40 100644 --- a/concoction-lib/src/test/java/info/mmpa/concoction/scan/model/DynamicMatchSerializationTests.java +++ b/concoction-lib/src/test/java/info/mmpa/concoction/scan/model/DynamicMatchSerializationTests.java @@ -1,27 +1,99 @@ package info.mmpa.concoction.scan.model; -import info.mmpa.concoction.output.DetectionArchetype; -import info.mmpa.concoction.output.SusLevel; -import info.mmpa.concoction.scan.model.dynamic.DynamicMatchingModel; -import info.mmpa.concoction.scan.model.dynamic.entry.Condition; -import info.mmpa.concoction.scan.model.dynamic.entry.DynamicMatchEntry; -import info.mmpa.concoction.scan.model.insn.*; -import info.mmpa.concoction.scan.model.insn.entry.*; +import info.mmpa.concoction.scan.model.dynamic.entry.*; import org.junit.jupiter.api.Test; -import software.coley.collections.Maps; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import static info.mmpa.concoction.scan.model.TextMatchMode.EQUALS; +import static info.mmpa.concoction.scan.model.dynamic.entry.NumericParameterCondition.*; import static info.mmpa.concoction.util.TestSerialization.*; import static org.junit.jupiter.api.Assertions.*; /** - * Tests for {@link DynamicMatchingModel}, {@link DynamicMatchEntry} and {@link Condition} serialization. + * Tests for {@link DynamicMatchEntry} and {@link Condition} serialization. */ public class DynamicMatchSerializationTests { + @Test + void any() { + AnyCondition wildcard = AnyCondition.INSTANCE; + String serialized = serialize(wildcard); + assertEquals("\"ANY\"", serialized, "The any-condition should serialize to 'ANY'"); + + Condition deserializedCondition = deserializeCondition("\"ANY\""); + assertTrue(deserializedCondition instanceof AnyCondition, "'ANY' should be deserialized to any-condition"); + } + + @Test + void none() { + NoneCondition wildcard = NoneCondition.INSTANCE; + String serialized = serialize(wildcard); + assertEquals("\"NONE\"", serialized, "The none-condition should serialize to 'NONE'"); + + Condition deserializedCondition = deserializeCondition("\"NONE\""); + assertTrue(deserializedCondition instanceof NoneCondition, "'NONE' should be deserialized to none-condition"); + } + + @Test + void nullParam() { + NullParameterCondition condition = new NullParameterCondition(1, true); + String serialized = serialize(condition); + assertEquals("{\"index\":1,\"null\":true}", serialized); + + Condition deserializedCondition = deserializeCondition(serialized); + assertEquals(condition, deserializedCondition); + } + + @Test + void numericParam() { + int parameter = 1; + int cmpVal = 0; + IntComparison equalZero = intComparison(NumericParameterCondition.OP_EQUAL, cmpVal); + ComparisonWithOp equalZeroWithOp = fromString("== 0"); + NumericParameterCondition condition = new NumericParameterCondition(parameter, equalZeroWithOp); + assertTrue(equalZero.compare(0)); + assertFalse(equalZero.compare(1)); + assertFalse(equalZero.compare(-1)); + assertEquals("==", condition.getComparisonOperation()); + assertEquals("0", condition.getComparisonValue()); + + String serialized = serialize(condition); + assertEquals("{\"index\":" + parameter + ",\"match\":\"== " + cmpVal + "\"}", serialized); + + Condition deserializedCondition = deserializeCondition(serialized); + assertEquals(condition, deserializedCondition); + } + + @Test + void stringParam() { + int parameter = 1; + TextMatchMode matchMode = TextMatchMode.EQUALS; + String match = "hello world"; + StringParameterCondition.StringExtractionMode extractionMode = + StringParameterCondition.StringExtractionMode.KNOWN_STRING_TYPES; + StringParameterCondition condition = new StringParameterCondition(extractionMode, matchMode, match, parameter); + + String serialized = serialize(condition); + assertEquals("{\"index\":" + parameter + ",\"match\":\"" + matchMode.name() + " " + match + "\"}", serialized); + + Condition deserializedCondition = deserializeCondition(serialized); + assertEquals(condition, deserializedCondition); + } + + @Test + void entry() { + MethodLocation location = new MethodLocation("java/lang/", "foo", "(", + TextMatchMode.STARTS_WITH, TextMatchMode.STARTS_WITH, TextMatchMode.STARTS_WITH); + Condition condition = AnyCondition.INSTANCE; + When when = When.ENTRY; + DynamicMatchEntry entry = new SingleConditionCheckingDynamic(location, condition, when); + + String serialized = serialize(entry); + assertEquals("{\"location\":{" + + "\"class\":\"STARTS_WITH java/lang/\"," + + "\"mname\":\"STARTS_WITH foo\"," + + "\"mdesc\":\"STARTS_WITH (\"}," + + "\"condition\":\"ANY\"}", serialized); + DynamicMatchEntry deserializedEntry = deserializeDynamicEntry(serialized); + assertEquals(entry, deserializedEntry); + } } diff --git a/dependencies.gradle b/dependencies.gradle index 35e2f0d..37e0344 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -25,7 +25,7 @@ project.ext { slf4jLogging = 'org.slf4j:slf4j-jdk14:2.0.7' jsr305 = 'com.google.code.findbugs:jsr305:3.0.2' - lljzip = 'software.coley:lljzip:2.1.2' + lljzip = 'software.coley:lljzip:2.1.3' // Testing junit_api = "org.junit.jupiter:junit-jupiter-api:$junitVersion" From fce3c3bd3438c9c9585cbec2a598540f3b8c8243 Mon Sep 17 00:00:00 2001 From: Col-E Date: Thu, 10 Aug 2023 22:39:50 -0400 Subject: [PATCH 03/11] Allow method location serialization to drop fields if any value counts --- .../concoction/scan/model/TextMatchMode.java | 10 +++- .../entry/MethodLocationDeserializer.java | 46 +++++++++++++------ .../entry/MethodLocationSerializer.java | 13 ++++-- 3 files changed, 50 insertions(+), 19 deletions(-) diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/TextMatchMode.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/TextMatchMode.java index cccf46b..f6a50f3 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/TextMatchMode.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/TextMatchMode.java @@ -44,7 +44,15 @@ public enum TextMatchMode { Pattern pattern = RegexCache.pattern(src); if (pattern == null) return false; return pattern.matcher(input).find(); - }); + }), + /** + * Input does not matter, always match. + */ + ANYTHING((src, input) -> true), + /** + * Input does not matter, never match. + */ + NOTHING((src, input) -> false); // First arg is source text, second ard is input to check for match against the source. private final BiPredicate matcher; diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MethodLocationDeserializer.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MethodLocationDeserializer.java index 8f69d79..7a4962a 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MethodLocationDeserializer.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MethodLocationDeserializer.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.core.JacksonException; import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; @@ -9,6 +10,7 @@ import info.mmpa.concoction.scan.model.TextMatchMode; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.io.IOException; import static info.mmpa.concoction.util.JsonUtil.breakByFirstSpace; @@ -36,26 +38,40 @@ public MethodLocation deserialize(JsonParser jp, DeserializationContext ctx) thr private MethodLocation deserializeNode(JsonParser jp, JsonNode node) throws JacksonException { if (node.isObject()) { JsonNode classNode = node.get("class"); - if (classNode == null) - throw new JsonMappingException(jp, "Dynamic match entry location missing 'class' value"); JsonNode methodNameNode = node.get("mname"); - if (methodNameNode == null) - throw new JsonMappingException(jp, "Dynamic match entry location missing 'mname' value"); JsonNode methodDescNode = node.get("mdesc"); - if (methodDescNode == null) - throw new JsonMappingException(jp, "Dynamic match entry location missing 'mdesc' value"); - String[] classInputs = breakByFirstSpace(jp, classNode.asText()); - String[] methodNameInputs = breakByFirstSpace(jp, methodNameNode.asText()); - String[] methodDescInputs = breakByFirstSpace(jp, methodDescNode.asText()); + MatchPair classPair = getPair(jp, classNode); + MatchPair methodNamePair = getPair(jp, methodNameNode); + MatchPair methodDescPair = getPair(jp, methodDescNode); return new MethodLocation( - classInputs[1], - methodNameInputs[1], - methodDescInputs[1], - TextMatchMode.valueOf(classInputs[0]), - TextMatchMode.valueOf(methodNameInputs[0]), - TextMatchMode.valueOf(methodDescInputs[0]) + classPair.text, + methodNamePair.text, + methodDescPair.text, + classPair.mode, + methodNamePair.mode, + methodDescPair.mode ); } throw new JsonMappingException(jp, "Dynamic match entry expects a JSON object, or '*' literal for wildcards"); } + + @Nonnull + private MatchPair getPair(@Nonnull JsonParser jp, @Nullable JsonNode node) throws JsonProcessingException { + // Any pair with no node to pull from is filled in with 'match anything' + if (node == null) return MatchPair.ANY; + + String[] inputs = breakByFirstSpace(jp, node.asText()); + return new MatchPair(TextMatchMode.valueOf(inputs[0]), inputs[1]); + } + + private static class MatchPair { + private static final MatchPair ANY = new MatchPair(TextMatchMode.ANYTHING, ""); + private final TextMatchMode mode; + private final String text; + + public MatchPair(TextMatchMode mode, String text) { + this.mode = mode; + this.text = text; + } + } } diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MethodLocationSerializer.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MethodLocationSerializer.java index 3e15385..e8dddc2 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MethodLocationSerializer.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MethodLocationSerializer.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import info.mmpa.concoction.scan.model.TextMatchMode; import java.io.IOException; @@ -22,9 +23,15 @@ public MethodLocationSerializer() { @Override public void serialize(MethodLocation entry, JsonGenerator jgen, SerializerProvider provider) throws IOException { jgen.writeStartObject(); - jgen.writeStringField("class", entry.getClassMatchMode().name() + " " + entry.getClassName()); - jgen.writeStringField("mname", entry.getMethodNameMatchMode().name() + " " + entry.getMethodName()); - jgen.writeStringField("mdesc", entry.getMethodDescMatchMode().name() + " " + entry.getMethodDesc()); + TextMatchMode classMatchMode = entry.getClassMatchMode(); + TextMatchMode methodNameMatchMode = entry.getMethodNameMatchMode(); + TextMatchMode methodDescMatchMode = entry.getMethodDescMatchMode(); + if (classMatchMode != TextMatchMode.ANYTHING) + jgen.writeStringField("class", classMatchMode.name() + " " + entry.getClassName()); + if (methodNameMatchMode != TextMatchMode.ANYTHING) + jgen.writeStringField("mname", methodNameMatchMode.name() + " " + entry.getMethodName()); + if (methodDescMatchMode != TextMatchMode.ANYTHING) + jgen.writeStringField("mdesc", methodDescMatchMode.name() + " " + entry.getMethodDesc()); jgen.writeEndObject(); } } From ce6c9d2a9c4a398e4fda71b0211afe03f765382e Mon Sep 17 00:00:00 2001 From: Col-E Date: Thu, 10 Aug 2023 23:03:17 -0400 Subject: [PATCH 04/11] Add additional example models --- .../test/resources/models/NativeInterop.json | 21 ++++++++ .../src/test/resources/models/WinReg.json | 49 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 concoction-lib/src/test/resources/models/NativeInterop.json create mode 100644 concoction-lib/src/test/resources/models/WinReg.json diff --git a/concoction-lib/src/test/resources/models/NativeInterop.json b/concoction-lib/src/test/resources/models/NativeInterop.json new file mode 100644 index 0000000..c2dad11 --- /dev/null +++ b/concoction-lib/src/test/resources/models/NativeInterop.json @@ -0,0 +1,21 @@ +{ + "archetype": { + "level": "MAXIMUM", + "identifier": "Native interop", + "description": "Usage of apis that load native code into the VM, which is harder to analyze than code restricted to the JVM" + }, + "code-patterns": { + "0": [{ "op": "STARTS_WITH invoke", "args": "STARTS_WITH jdk/internal/loader" }], + "1": [{ "op": "STARTS_WITH invoke", "args": "STARTS_WITH jdk/internal/access/foreign" }], + "2": [{ "op": "STARTS_WITH invoke", "args": "STARTS_WITH java/lang/foreign" }], + "3": [{ "op": "STARTS_WITH invoke", "args": "STARTS_WITH com/sun/jna" }], + "4": [{ "op": "STARTS_WITH invoke", "args": "STARTS_WITH org/jnativehook" }] + }, + "code-behaviors": { + "0": { "location": { "class": "STARTS_WITH jdk/internal/loader" }, "condition": "ANY" }, + "1": { "location": { "class": "STARTS_WITH jdk/internal/access/foreign" }, "condition": "ANY" }, + "2": { "location": { "class": "STARTS_WITH java/lang/foreign" }, "condition": "ANY" }, + "3": { "location": { "class": "STARTS_WITH com/sun/jna" }, "condition": "ANY" }, + "4": { "location": { "class": "STARTS_WITH org/jnativehook" }, "condition": "ANY" } + } +} \ No newline at end of file diff --git a/concoction-lib/src/test/resources/models/WinReg.json b/concoction-lib/src/test/resources/models/WinReg.json new file mode 100644 index 0000000..37fe68e --- /dev/null +++ b/concoction-lib/src/test/resources/models/WinReg.json @@ -0,0 +1,49 @@ +{ + "archetype": { + "level": "MAXIMUM", + "identifier": "Windows Registry", + "description": "Indicators of working with Windows Registry" + }, + "code-patterns": { + "invokeRootUser": [{ "op": "STARTS_WITH invoke", "args": "STARTS_WITH java/util/prefs/Preferences.userRoot()" }], + "invokeRootSysm": [{ "op": "STARTS_WITH invoke", "args": "STARTS_WITH java/util/prefs/Preferences.systemRoot()" }], + }, + "code-behaviors": { + "lookupMethod": { + "ANY": [ + { + "location": { "class": "EQUALS java/lang/Class", "mname": "EQUALS getDeclaredMethod" }, + "condition": { "index": 1, "match": "EQUALS WindowsRegOpenKey" } + }, + { + "location": { "class": "EQUALS java/lang/Class", "mname": "EQUALS getDeclaredMethod" }, + "condition": { "index": 1, "match": "EQUALS WindowsRegCloseKey" } + }, + { + "location": { "class": "EQUALS java/lang/Class", "mname": "EQUALS getDeclaredMethod" }, + "condition": { "index": 1, "match": "EQUALS WindowsRegQueryValueEx" } + }, + { + "location": { "class": "EQUALS java/lang/Class", "mname": "EQUALS getDeclaredMethod" }, + "condition": { "index": 1, "match": "EQUALS WindowsRegEnumValue" } + }, + { + "location": { "class": "EQUALS java/lang/Class", "mname": "EQUALS getDeclaredMethod" }, + "condition": { "index": 1, "match": "EQUALS WindowsRegEnumKeyEx" } + }, + { + "location": { "class": "EQUALS java/lang/Class", "mname": "EQUALS getDeclaredMethod" }, + "condition": { "index": 1, "match": "EQUALS WindowsRegSetValueEx" } + }, + { + "location": { "class": "EQUALS java/lang/Class", "mname": "EQUALS getDeclaredMethod" }, + "condition": { "index": 1, "match": "EQUALS WindowsRegDeleteValue" } + }, + { + "location": { "class": "EQUALS java/lang/Class", "mname": "EQUALS getDeclaredMethod" }, + "condition": { "index": 1, "match": "EQUALS WindowsRegDeleteKey" } + } + ] + } + } +} \ No newline at end of file From d4daa8a1cd4751bb6f443d195d58bdcaf416443a Mon Sep 17 00:00:00 2001 From: Col-E Date: Thu, 10 Aug 2023 23:05:57 -0400 Subject: [PATCH 05/11] Remove extra comma --- concoction-lib/src/test/resources/models/WinReg.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/concoction-lib/src/test/resources/models/WinReg.json b/concoction-lib/src/test/resources/models/WinReg.json index 37fe68e..3a06acf 100644 --- a/concoction-lib/src/test/resources/models/WinReg.json +++ b/concoction-lib/src/test/resources/models/WinReg.json @@ -6,7 +6,7 @@ }, "code-patterns": { "invokeRootUser": [{ "op": "STARTS_WITH invoke", "args": "STARTS_WITH java/util/prefs/Preferences.userRoot()" }], - "invokeRootSysm": [{ "op": "STARTS_WITH invoke", "args": "STARTS_WITH java/util/prefs/Preferences.systemRoot()" }], + "invokeRootSysm": [{ "op": "STARTS_WITH invoke", "args": "STARTS_WITH java/util/prefs/Preferences.systemRoot()" }] }, "code-behaviors": { "lookupMethod": { From 966b7f82155bb2514c539feb429af2ee7d248fda Mon Sep 17 00:00:00 2001 From: Col-E Date: Fri, 11 Aug 2023 00:44:38 -0400 Subject: [PATCH 06/11] Add new ignore-case text match modes --- .../concoction/scan/model/TextMatchMode.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/TextMatchMode.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/TextMatchMode.java index f6a50f3..be1b5dd 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/TextMatchMode.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/TextMatchMode.java @@ -15,18 +15,34 @@ public enum TextMatchMode { * Exact match. */ EQUALS(String::equals), + /** + * Exact match, ignoring case. + */ + EQUALS_IC(String::equalsIgnoreCase), /** * Containment match. */ CONTAINS((src, input) -> input != null && input.contains(src)), + /** + * Containment match, ignoring case. + */ + CONTAINS_IC((src, input) -> input != null && input.toLowerCase().contains(src.toLowerCase())), /** * Input starts with an exact match. */ STARTS_WITH((src, input) -> input != null && input.startsWith(src)), + /** + * Input starts with an exact match. + */ + STARTS_WITH_IC((src, input) -> input != null && input.toLowerCase().startsWith(src.toLowerCase())), /** * Input ends with an exact match. */ ENDS_WITH((src, input) -> input != null && input.endsWith(src)), + /** + * Input ends with an exact match. + */ + ENDS_WITH_IC((src, input) -> input != null && input.toLowerCase().endsWith(src.toLowerCase())), /** * Input fully matches a pattern. */ From 7b33142daa453512d762d531d3ad719254287d02 Mon Sep 17 00:00:00 2001 From: Col-E Date: Fri, 11 Aug 2023 00:44:55 -0400 Subject: [PATCH 07/11] Document the JSON model format --- README.md | 4 + docs/ModelFormat.md | 263 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 267 insertions(+) create mode 100644 docs/ModelFormat.md diff --git a/README.md b/README.md index 0232753..a7fe51d 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ A shared Java malware scanner capable of static and dynamic analysis. Concoction can be used either as a Java library or as a command line application. +### Scan models + +The format of scan models is described [here](docs/ModelFormat.md). + ### As a library First add Concoction as a library to your project. It is available on maven central: diff --git a/docs/ModelFormat.md b/docs/ModelFormat.md new file mode 100644 index 0000000..90770fd --- /dev/null +++ b/docs/ModelFormat.md @@ -0,0 +1,263 @@ +# Model Format + +Concoction uses JSON to model what should be searched for during scanning. The model follows this outline: + +```json +{ + "archetype": { + "level": "STRONG", + "identifier": "Windows Registry", + "description": "Indicators of working with Windows Registry. Its very rare to see this with good intentions in Java." + }, + "code-patterns": { + "key": [ { ... }, { ... } ] + }, + "code-behaviors": { + "key": { ... } + } +} +``` + +## Archetype + +The archetype section of each model file outlines what the model is supposed to be targeting. +In this example, the model will be searching for things that indicate usage of the Windows Registry. +While some native applications on Windows use this to store preferences, it is exceptionally rare +to see Java applications doing this given the multi-platform focus of the language. + +This fact is mentioned in the description section. + +The threat/suspicion/risk levels are also declared. The possible levels are: + +- `MAXIMUM`: Almost guaranteed to be malicious. +- `STRONG`: Very likely to be malicious, but in some cases it may just be benign shoddy code. +- `MEDIUM`: Typically these are strictly context based. For instance, deleting a file is usually not a concern. Deleting core operating system files is a concern though. +- `WEAK`: Unlikely to be malicious in most circumstances. +- `NOTHING_BURGER`: Incredibly unlikely to be malicious in any circumstance. + +Given that the Windows Registry can be used to make a program launch at startup the risk factor +is marked as `STRONG`. + +## Code Patterns + +The `code-patterns` section of each model is a map where each entry is a `name --> pattern-list`. +The `pattern-list` describes a sequence of JVM instruction matchers. This means that if you define a list with +five entries, it will match five sequential instructions _(Unless you use wildcards, but more on that later)._ + +In Java class files, data is stored at the beginning of the file and instructions reference offsets that point +to this data. To make matching easier, we represent things as if all the data is inline and in a text format. +To illustrate this, lets outline what the potential instruction matchers are. + +### Instruction + +Matching an instruction has two parts: + +- Matching the instruction opcode by name +- Matching the instruction arguments by text representation + +The text-match modes supported are: + +- `EQUALS`: Content must be an exact match. +- `EQUALS_IC`: Content must be an exact match, but case of letters (`A` vs `a`) is ignored. +- `CONTAINS`: Content must contain some text. +- `CONTAINS_IC`: Content must contain some text, but case of letters (`A` vs `a`) is ignored. +- `STARTS_WITH`: Content must start with some text. +- `STARTS_WITH_IC`: Content must start with some text, but case of letters (`A` vs `a`) is ignored. +- `ENDS_WITH`: Content must end with some text. +- `ENDS_WITH_IC`: Content must end with some text, but case of letters (`A` vs `a`) is ignored. +- `REGEX_FULL_MATCH`: Content must be a complete match for a given regex pattern. +- `REGEX_PARTIAL_MATCH`: Content must be at least a partial match for a given regex pattern. +- `ANYTHING`: Content is matched regardless of what is declared. +- `NOTHING`: Content is never matched regardless of what is declared. + +The format is this matcher is as follows: +```json +{ + "op": " ", + "args": " " +} +``` +An example of matching a call to `Runtime.exec(String)` would look like: +```json +{ + "op": "EQUALS invokevirtual", + "args": "EQUALS java/lang/Runtime.exec(Ljava/lang/String;)Ljava/lang/Process;" +} +``` +The `exec` method is an instance method, so it will likely be invoked with `invokevirtual`, however `invokespecial` +would also be valid. Given that, you could alternatively use `STARTS_WITH invoke` to cover both bases. + +The argument for `invoke` instructions is the signature of the method. +We represent that as `.`. + +- The `owner-class` is the name of the class defining the method. +- The `name` is the name of the method. +- The `desc` is the type descriptor of the method. This follows the [descriptor format described in the JVMS spec](https://docs.oracle.com/javase/specs/jvms/se20/html/jvms-4.html#jvms-4.3). + +### Single Wildcard + +Simply a string containing `*` will indicate this value will match _any_ instruction once. + +### Multi-match Wildcard + +A string containing `**` will match _any_ instruction any amount of times. + +A string containing `*N` will match _any_ instruction `N` times. So `*3` will match up to three instructions in a row. + +These multi-match wildcards will continue to match sequential instructions up until +one matches the next matcher in the `pattern-list`. + +### Alternatives + +Say you have a `pattern-list` with three instructions you want to match, but the second instruction can be a number +of different things. You could use a wildcard here, but let's say you want to be more specific than that. +This is where alternatives come in. You can replace _one_ instruction matcher with multiple. + +Here's an example: +```json +{ + "code-patterns": { + "example": [ + { "op": "EQUALS foo", "args": "EQUALS bar" }, + { "ANY": [ + { "op": "EQUALS can-be", "args": "EQUALS this-one" }, + { "op": "EQUALS or", "args": "EQUALS this-one" } + ]}, + { "op": "EQUALS foo", "args": "EQUALS bar" } + ] + } +} +``` + +The options for alternatives are: +- `ANY`: Any of the provided matchers can match. +- `ALL`: All of the provided matchers must match. +- `NONE`: None of the provided matchers must match. + +The use case for `ANY` is what we covered in the example above. + +The use case for `ALL` is mostly for when you're using regex text match modes, and would like to ensure they all overlap. +For example, you could do a partial regex match for an ip-address, and another match for ending with a `.exe` file. +This would represent downloading a `.exe` file from a website accessed via direct ip-address. + +The use case for `NONE` is mostly for blacklisting. +For example, lets say there is some class `SussyApi` that defines ten methods. Eight of them malicious, but two are not. +Rather than define matches for all malicious cases, you can instead match against the two benign cases and invert the +match using `NONE`. + +## Code Behaviors + +The `code-behaviors` section of each model is a map where each entry is a `name --> matcher`. +The `matcher` will match against the current state of a simulated execution of the program being scanned. + +A `matcher` can either represent a single conditional check against the program state, or a list of checks similar +to the _"alternatives"_ described in the `code-patterns` section above. + +### Single condition + +The format for single condition matchers is: +```json + { + "location": { + "class": " ", + "mname": " ", + "mdesc": " " + }, + "condition": { ... } +} +``` +The `location` describes where in the program this condition applies to. +Each part of the location is optional. If you do not include it, any value for that part will match. +For example, if your location only includes `{ "class": "EQUALS java/lang/Runtime" }` then the `condition` will +apply to _any_ method within the `Runtime` class. + +The `condition` describes something in the program execution we want to match against. +The list of conditions are: + +#### Text value checking + +Using the given format you can check parameter/variables for different text comparisons: +```json +{ + "index": 1, + "extraction": "", + "match": " " +} +``` +The `extraction-mode` values are: +- `known-types`: Only types implementing `CharSequence` like `String`/`StringBuilder`/`StringBuffer` are checked. +- `any-type`: All object values are checked against their `toString()` value + +Additionally, the `extraction-mode` is optional. Not including it defaults to `known-types`. + +The `match` follows the same format of other similar components. The text match mode, then the text to match with after. + +#### Numeric value checking + +Using the given format you can check parameter/variables for different numeric comparisons: +```json +{ + "index": 1, + "match": " " +} +``` +The `value` is any number that can be parsed with [the default `NumberFormat` instance](https://docs.oracle.com/javase/8/docs/api/java/text/NumberFormat.html). + +The `operation` options are: + +- `==` Equals +- `!=` Not equals +- `>` Greater than +- `>=` Greater or equal than +- `<` Less than +- `<=` Less or equal than +- `&` Has bitwise mask of +- `%` Is divisible by + +#### Null checking + +Using the given format you can check parameter/variables for being `null` and not `null` +```json +{ + "index": 1, + "null": true +} +``` + +#### Any + +Using the string `ANY` will match under any circumstance. You can use this to register any case where the `location` +section matches the current location in the simulated program execution. + +#### None + +Using the string `NONE` will never match anything. As described with similar usages of inversions, this is primarily +used in blacklist cases. + +### Alternatives + +It's the same thing as before with `code-patterns`, but instead with the contents being `matcher` items. + +Using `ANY` you can say _"any one of these conditions can match to mark the current location in execution as a match"_. + +Using `ALL` allows you to say _"all of these conditions must match to mark the current location in execution as a match"_. + +Using `NONE` allows you to say _"none of these conditions can match if to mark the current location as a match"_. +To effectively use `NONE` you will want to combine it with `ALL`. +An example of which where we want to match all usages of `MaliciousApi` except the method `exceptThisOneOkMethod` would look like: +```json +{ + "ALL": [ + { + "location": { "class": "EQUALS com/example/MaliciousApi" }, + "condition": "ANY" + }, + { + "NONE": [{ + "location": { "class": "EQUALS com/example/MaliciousApi", "mname": "exceptThisOneOkMethod" }, + "condition": "ANY" + }] + } + ] +} +``` \ No newline at end of file From 95fae52c18506e5e138b893c872f432336d88631 Mon Sep 17 00:00:00 2001 From: Col-E Date: Fri, 11 Aug 2023 00:52:15 -0400 Subject: [PATCH 08/11] Optimize win-reg matching --- .../src/test/resources/models/WinReg.json | 45 +++++-------------- 1 file changed, 11 insertions(+), 34 deletions(-) diff --git a/concoction-lib/src/test/resources/models/WinReg.json b/concoction-lib/src/test/resources/models/WinReg.json index 3a06acf..04ec364 100644 --- a/concoction-lib/src/test/resources/models/WinReg.json +++ b/concoction-lib/src/test/resources/models/WinReg.json @@ -10,40 +10,17 @@ }, "code-behaviors": { "lookupMethod": { - "ANY": [ - { - "location": { "class": "EQUALS java/lang/Class", "mname": "EQUALS getDeclaredMethod" }, - "condition": { "index": 1, "match": "EQUALS WindowsRegOpenKey" } - }, - { - "location": { "class": "EQUALS java/lang/Class", "mname": "EQUALS getDeclaredMethod" }, - "condition": { "index": 1, "match": "EQUALS WindowsRegCloseKey" } - }, - { - "location": { "class": "EQUALS java/lang/Class", "mname": "EQUALS getDeclaredMethod" }, - "condition": { "index": 1, "match": "EQUALS WindowsRegQueryValueEx" } - }, - { - "location": { "class": "EQUALS java/lang/Class", "mname": "EQUALS getDeclaredMethod" }, - "condition": { "index": 1, "match": "EQUALS WindowsRegEnumValue" } - }, - { - "location": { "class": "EQUALS java/lang/Class", "mname": "EQUALS getDeclaredMethod" }, - "condition": { "index": 1, "match": "EQUALS WindowsRegEnumKeyEx" } - }, - { - "location": { "class": "EQUALS java/lang/Class", "mname": "EQUALS getDeclaredMethod" }, - "condition": { "index": 1, "match": "EQUALS WindowsRegSetValueEx" } - }, - { - "location": { "class": "EQUALS java/lang/Class", "mname": "EQUALS getDeclaredMethod" }, - "condition": { "index": 1, "match": "EQUALS WindowsRegDeleteValue" } - }, - { - "location": { "class": "EQUALS java/lang/Class", "mname": "EQUALS getDeclaredMethod" }, - "condition": { "index": 1, "match": "EQUALS WindowsRegDeleteKey" } - } - ] + "location": { "class": "EQUALS java/lang/Class", "mname": "EQUALS getDeclaredMethod" }, + "condition": { "ANY": [ + { "index": 1, "match": "EQUALS WindowsRegOpenKey" }, + { "index": 1, "match": "EQUALS WindowsRegCloseKey" }, + { "index": 1, "match": "EQUALS WindowsRegQueryValueEx" }, + { "index": 1, "match": "EQUALS WindowsRegEnumValue" }, + { "index": 1, "match": "EQUALS WindowsRegEnumKeyEx" }, + { "index": 1, "match": "EQUALS WindowsRegSetValueEx" }, + { "index": 1, "match": "EQUALS WindowsRegDeleteValue" }, + { "index": 1, "match": "EQUALS WindowsRegDeleteKey" } + ]} } } } \ No newline at end of file From bf78a310172b2ff7fa3a780c3aa1ee3d240074c3 Mon Sep 17 00:00:00 2001 From: Col-E Date: Fri, 11 Aug 2023 01:22:53 -0400 Subject: [PATCH 09/11] Add multi-matching on condition level --- .../dynamic/entry/AllMultiCondition.java | 26 ++++++++ .../dynamic/entry/AnyMultiCondition.java | 26 ++++++++ .../dynamic/entry/ConditionDeserializer.java | 40 +++++++++++- .../dynamic/entry/ConditionSerializer.java | 10 ++- .../model/dynamic/entry/MultiCondition.java | 61 +++++++++++++++++++ .../dynamic/entry/NoneMultiCondition.java | 28 +++++++++ 6 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/AllMultiCondition.java create mode 100644 concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/AnyMultiCondition.java create mode 100644 concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MultiCondition.java create mode 100644 concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/NoneMultiCondition.java diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/AllMultiCondition.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/AllMultiCondition.java new file mode 100644 index 0000000..c6de3e6 --- /dev/null +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/AllMultiCondition.java @@ -0,0 +1,26 @@ +package info.mmpa.concoction.scan.model.dynamic.entry; + +import info.mmpa.concoction.scan.dynamic.CallStackFrame; +import info.mmpa.concoction.scan.model.MultiMatchMode; + +import javax.annotation.Nonnull; +import java.util.List; + +/** + * A condition that defines multiple sub-conditions. + * All the sub-conditions must match the given input all at once. + */ +public class AllMultiCondition extends MultiCondition { + /** + * @param conditions + * Sub-conditions which must all match inputs in order to pass. + */ + public AllMultiCondition(@Nonnull List conditions) { + super(MultiMatchMode.ALL, conditions); + } + + @Override + public boolean match(@Nonnull CallStackFrame frame) { + return conditions.stream().allMatch(e -> e.match(frame)); + } +} diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/AnyMultiCondition.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/AnyMultiCondition.java new file mode 100644 index 0000000..65280ba --- /dev/null +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/AnyMultiCondition.java @@ -0,0 +1,26 @@ +package info.mmpa.concoction.scan.model.dynamic.entry; + +import info.mmpa.concoction.scan.dynamic.CallStackFrame; +import info.mmpa.concoction.scan.model.MultiMatchMode; + +import javax.annotation.Nonnull; +import java.util.List; + +/** + * A condition that defines multiple sub-condition. + * Any single one of the sub-condition must match the given input. + */ +public class AnyMultiCondition extends MultiCondition { + /** + * @param conditions + * Sub-conditions where any single input must match in order to pass. + */ + public AnyMultiCondition(@Nonnull List conditions) { + super(MultiMatchMode.ANY, conditions); + } + + @Override + public boolean match(@Nonnull CallStackFrame frame) { + return conditions.stream().anyMatch(e -> e.match(frame)); + } +} diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/ConditionDeserializer.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/ConditionDeserializer.java index 8236b45..98ee4da 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/ConditionDeserializer.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/ConditionDeserializer.java @@ -6,10 +6,13 @@ import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import info.mmpa.concoction.scan.model.MultiMatchMode; import info.mmpa.concoction.scan.model.TextMatchMode; import javax.annotation.Nonnull; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import static info.mmpa.concoction.util.JsonUtil.breakByFirstSpace; @@ -28,6 +31,10 @@ public ConditionDeserializer() { @Override public Condition deserialize(JsonParser jp, DeserializationContext ctx) throws IOException { + if (jp == null) + throw new IOException("Cannot deserialize null 'JavaParser' instance"); + else if (jp.getCodec() == null) + throw new IOException("Cannot deserialize 'JavaParser' instance with 'null' codec"); JsonNode node = jp.getCodec().readTree(jp); return deserializeNode(jp, node); } @@ -50,10 +57,41 @@ private Condition deserializeNode(JsonParser jp, JsonNode node) throws JacksonEx if (nullNode != null) return new NullParameterCondition(index, nullNode.asBoolean()); + // Check for multi-conditions + JsonNode anyNode = node.get("ANY"); + JsonNode allNode = node.get("ALL"); + JsonNode noneNode = node.get("NONE"); + MultiMatchMode multiMatchMode = null; + List conditions = new ArrayList<>(); + if (anyNode != null && anyNode.isArray()) { + multiMatchMode = MultiMatchMode.ANY; + for (JsonNode subNode : anyNode) + conditions.add(deserializeNode(jp, subNode)); + } else if (allNode != null && allNode.isArray()) { + multiMatchMode = MultiMatchMode.ALL; + for (JsonNode subNode : allNode) + conditions.add(deserializeNode(jp, subNode)); + } else if (noneNode != null && noneNode.isArray()) { + multiMatchMode = MultiMatchMode.NONE; + for (JsonNode subNode : noneNode) + conditions.add(deserializeNode(jp, subNode)); + } + if (multiMatchMode != null) { + switch (multiMatchMode) { + case ALL: + return new AllMultiCondition(conditions); + case ANY: + return new AnyMultiCondition(conditions); + case NONE: + return new NoneMultiCondition(conditions); + } + } + // Get the other possible fields for other conditions, and see which makes the most sense here. JsonNode extractionNode = node.get("extraction"); JsonNode matchNode = node.get("match"); - if (matchNode == null) throw new JsonMappingException(jp, "Missing expected 'match' field"); + if (matchNode == null) + throw new JsonMappingException(jp, "Missing expected 'match' field"); // If the 'match' field value starts with a numeric op, it's a numeric param condition. String matchRaw = matchNode.asText(); diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/ConditionSerializer.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/ConditionSerializer.java index 6bcc9de..d532025 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/ConditionSerializer.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/ConditionSerializer.java @@ -47,7 +47,15 @@ public void serialize(Condition condition, JsonGenerator jgen, SerializerProvide jgen.writeNumberField("index", numericParameterCondition.getIndex()); jgen.writeStringField("match", numericParameterCondition.getComparisonOperation() + " " + numericParameterCondition.getComparisonValue()); jgen.writeEndObject(); - } else { + } else if (condition instanceof MultiCondition) { + MultiCondition multiCondition = (MultiCondition) condition; + jgen.writeStartObject(); + jgen.writeFieldName(multiCondition.getMode().name()); + jgen.writeStartArray(); + for (Condition subCondition : multiCondition.getConditions()) + serialize(subCondition, jgen, provider); + jgen.writeEndArray(); + }else { throw new IllegalStateException("Unsupported condition class: " + condition.getClass().getName()); } } diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MultiCondition.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MultiCondition.java new file mode 100644 index 0000000..c6cc6b1 --- /dev/null +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MultiCondition.java @@ -0,0 +1,61 @@ +package info.mmpa.concoction.scan.model.dynamic.entry; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import info.mmpa.concoction.scan.model.MultiMatchMode; + +import javax.annotation.Nonnull; +import java.util.List; + +/** + * Base multi-condition outline. + * + * @see AllMultiCondition + * @see AnyMultiCondition + * @see NoneMultiCondition + */ +public abstract class MultiCondition implements Condition { + @JsonSerialize(contentUsing = ConditionSerializer.class) + @JsonDeserialize(contentUsing = ConditionDeserializer.class) + protected final List conditions; + protected final MultiMatchMode mode; + + public MultiCondition(@Nonnull MultiMatchMode mode, @Nonnull List conditions) { + this.mode = mode; + this.conditions = conditions; + } + + /** + * @return Conditions to wrap. Behavior changes based on the {@link #getMode() mode}. + */ + @Nonnull + public List getConditions() { + return conditions; + } + + /** + * @return Mode which determines the subtype. + */ + @Nonnull + public MultiMatchMode getMode() { + return mode; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + MultiCondition that = (MultiCondition) o; + + if (!conditions.equals(that.conditions)) return false; + return mode == that.mode; + } + + @Override + public int hashCode() { + int result = conditions.hashCode(); + result = 31 * result + mode.hashCode(); + return result; + } +} diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/NoneMultiCondition.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/NoneMultiCondition.java new file mode 100644 index 0000000..6f3b777 --- /dev/null +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/NoneMultiCondition.java @@ -0,0 +1,28 @@ +package info.mmpa.concoction.scan.model.dynamic.entry; + +import info.mmpa.concoction.scan.dynamic.CallStackFrame; +import info.mmpa.concoction.scan.model.MultiMatchMode; + +import javax.annotation.Nonnull; +import java.util.List; + +/** + * A condition that defines multiple sub-conditions. + * None the sub-conditions must match the given input. + *

+ * This is mostly useful as a blacklist modifier. + */ +public class NoneMultiCondition extends MultiCondition { + /** + * @param conditions + * Sub-conditions which must all not match inputs in order to pass. + */ + public NoneMultiCondition(@Nonnull List conditions) { + super(MultiMatchMode.NONE, conditions); + } + + @Override + public boolean match(@Nonnull CallStackFrame frame) { + return conditions.stream().noneMatch(e -> e.match(frame)); + } +} From 89912d3ade29fbd8f580dc70f482c15c7c2e34f4 Mon Sep 17 00:00:00 2001 From: Col-E Date: Fri, 11 Aug 2023 01:47:04 -0400 Subject: [PATCH 10/11] Extended case flexibility in deserialization of enum-names, --- .../dynamic/entry/ConditionDeserializer.java | 13 +++++--- .../entry/DynamicMatchEntryDeserializer.java | 3 +- .../entry/MethodLocationDeserializer.java | 3 +- .../InstructionMatchEntryDeserializer.java | 11 ++++--- .../mmpa/concoction/util/DeserializerExt.java | 31 +++++++++++++++++++ .../info/mmpa/concoction/util/EnumUtil.java | 24 ++++++++++++++ .../src/test/resources/models/WinReg.json | 2 +- 7 files changed, 74 insertions(+), 13 deletions(-) create mode 100644 concoction-lib/src/main/java/info/mmpa/concoction/util/DeserializerExt.java create mode 100644 concoction-lib/src/main/java/info/mmpa/concoction/util/EnumUtil.java diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/ConditionDeserializer.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/ConditionDeserializer.java index 98ee4da..89523bd 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/ConditionDeserializer.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/ConditionDeserializer.java @@ -5,9 +5,12 @@ import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import info.mmpa.concoction.scan.model.MultiMatchMode; import info.mmpa.concoction.scan.model.TextMatchMode; +import info.mmpa.concoction.util.DeserializerExt; +import info.mmpa.concoction.util.EnumUtil; import javax.annotation.Nonnull; import java.io.IOException; @@ -21,7 +24,7 @@ * * @see ConditionSerializer */ -public class ConditionDeserializer extends StdDeserializer { +public class ConditionDeserializer extends DeserializerExt { /** * New deserializer instance. */ @@ -58,9 +61,9 @@ private Condition deserializeNode(JsonParser jp, JsonNode node) throws JacksonEx return new NullParameterCondition(index, nullNode.asBoolean()); // Check for multi-conditions - JsonNode anyNode = node.get("ANY"); - JsonNode allNode = node.get("ALL"); - JsonNode noneNode = node.get("NONE"); + JsonNode anyNode = findCaseless(node, "ANY"); + JsonNode allNode = findCaseless(node, "ALL"); + JsonNode noneNode = findCaseless(node, "NONE"); MultiMatchMode multiMatchMode = null; List conditions = new ArrayList<>(); if (anyNode != null && anyNode.isArray()) { @@ -101,7 +104,7 @@ private Condition deserializeNode(JsonParser jp, JsonNode node) throws JacksonEx // The only remaining possibility is a string parameter condition. String[] matchInputs = breakByFirstSpace(jp, matchNode.asText()); - TextMatchMode matchMode = TextMatchMode.valueOf(matchInputs[0]); + TextMatchMode matchMode = EnumUtil.insensitiveValueOf(TextMatchMode.class, matchInputs[0]); String match = matchInputs[1]; StringParameterCondition.StringExtractionMode extractionMode = extractionNode == null ? StringParameterCondition.StringExtractionMode.KNOWN_STRING_TYPES : diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/DynamicMatchEntryDeserializer.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/DynamicMatchEntryDeserializer.java index 068ab51..07ef1dd 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/DynamicMatchEntryDeserializer.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/DynamicMatchEntryDeserializer.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import info.mmpa.concoction.scan.model.MultiMatchMode; import info.mmpa.concoction.scan.model.TextMatchMode; +import info.mmpa.concoction.util.EnumUtil; import javax.annotation.Nonnull; import java.io.IOException; @@ -52,7 +53,7 @@ private DynamicMatchEntry deserializeNode(JsonParser jp, JsonNode node) throws J // Construct the when JsonNode whenNode = node.get("when"); - When when = whenNode == null ? When.ENTRY : When.valueOf(whenNode.asText()); + When when = whenNode == null ? When.ENTRY : EnumUtil.insensitiveValueOf(When.class, whenNode.asText()); return new SingleConditionCheckingDynamic(location, condition, when); } else { diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MethodLocationDeserializer.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MethodLocationDeserializer.java index 7a4962a..7d97969 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MethodLocationDeserializer.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/dynamic/entry/MethodLocationDeserializer.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import info.mmpa.concoction.scan.model.TextMatchMode; +import info.mmpa.concoction.util.EnumUtil; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -61,7 +62,7 @@ private MatchPair getPair(@Nonnull JsonParser jp, @Nullable JsonNode node) throw if (node == null) return MatchPair.ANY; String[] inputs = breakByFirstSpace(jp, node.asText()); - return new MatchPair(TextMatchMode.valueOf(inputs[0]), inputs[1]); + return new MatchPair(EnumUtil.insensitiveValueOf(TextMatchMode.class, inputs[0]), inputs[1]); } private static class MatchPair { diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/InstructionMatchEntryDeserializer.java b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/InstructionMatchEntryDeserializer.java index 16e3958..fbf7c66 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/InstructionMatchEntryDeserializer.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/scan/model/insn/entry/InstructionMatchEntryDeserializer.java @@ -2,20 +2,21 @@ import com.fasterxml.jackson.core.JacksonException; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import info.mmpa.concoction.scan.model.MultiMatchMode; import info.mmpa.concoction.scan.model.TextMatchMode; -import static info.mmpa.concoction.util.JsonUtil.*; +import info.mmpa.concoction.util.EnumUtil; import javax.annotation.Nonnull; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import static info.mmpa.concoction.util.JsonUtil.breakByFirstSpace; + /** * Deserializes {@link InstructionMatchEntry} shorthand JSON into instances. * @@ -55,14 +56,14 @@ private InstructionMatchEntry deserializeNode(JsonParser jp, JsonNode node) thro if (argsNode == null) { // No arguments given return new Instruction(opInputs[1], null, - TextMatchMode.valueOf(opInputs[0]), null); + EnumUtil.insensitiveValueOf(TextMatchMode.class, opInputs[0]), null); } else { // Arguments given String[] argsInputs = breakByFirstSpace(jp, argsNode.asText()); return new Instruction(opInputs[1], argsInputs[1], - TextMatchMode.valueOf(opInputs[0]), TextMatchMode.valueOf(argsInputs[0])); + EnumUtil.insensitiveValueOf(TextMatchMode.class, opInputs[0]), + EnumUtil.insensitiveValueOf(TextMatchMode.class, argsInputs[0])); } - } else { // Should be a multi-instruction if no other case applies. // Determine which mode by its name. diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/util/DeserializerExt.java b/concoction-lib/src/main/java/info/mmpa/concoction/util/DeserializerExt.java new file mode 100644 index 0000000..cb8279a --- /dev/null +++ b/concoction-lib/src/main/java/info/mmpa/concoction/util/DeserializerExt.java @@ -0,0 +1,31 @@ +package info.mmpa.concoction.util; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Iterator; + +/** + * Extended deserializer with some common utilities. + * + * @param + * Target type to deserialize into. + */ +public abstract class DeserializerExt extends StdDeserializer { + protected DeserializerExt(@Nonnull Class vc) { + super(vc); + } + + @Nullable + protected static JsonNode findCaseless(@Nonnull JsonNode root, @Nonnull String targetFieldName) { + Iterator itr = root.fieldNames(); + while (itr.hasNext()) { + String fieldName = itr.next(); + if (targetFieldName.equalsIgnoreCase(fieldName)) + return root.get(fieldName); + } + return null; + } +} diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/util/EnumUtil.java b/concoction-lib/src/main/java/info/mmpa/concoction/util/EnumUtil.java new file mode 100644 index 0000000..16de4b0 --- /dev/null +++ b/concoction-lib/src/main/java/info/mmpa/concoction/util/EnumUtil.java @@ -0,0 +1,24 @@ +package info.mmpa.concoction.util; + +/** + * Basic enum utils. + */ +public class EnumUtil { + /** + * @param cls + * Class of enum type. + * @param name + * Enum constant name to resolve to value. + * @param + * Enum type. + * + * @return Enum constant by name (case insensitive). + */ + public static > T insensitiveValueOf(Class cls, String name) { + for (T constant : cls.getEnumConstants()) { + if (constant.name().equalsIgnoreCase(name)) + return constant; + } + throw new IllegalStateException("No match for '" + name + "' for enum type '" + cls.getSimpleName() + "'"); + } +} diff --git a/concoction-lib/src/test/resources/models/WinReg.json b/concoction-lib/src/test/resources/models/WinReg.json index 04ec364..dd37e48 100644 --- a/concoction-lib/src/test/resources/models/WinReg.json +++ b/concoction-lib/src/test/resources/models/WinReg.json @@ -11,7 +11,7 @@ "code-behaviors": { "lookupMethod": { "location": { "class": "EQUALS java/lang/Class", "mname": "EQUALS getDeclaredMethod" }, - "condition": { "ANY": [ + "condition": { "any": [ { "index": 1, "match": "EQUALS WindowsRegOpenKey" }, { "index": 1, "match": "EQUALS WindowsRegCloseKey" }, { "index": 1, "match": "EQUALS WindowsRegQueryValueEx" }, From 4cc6196f303f89610bbb95102e75d8b89ad0d7de Mon Sep 17 00:00:00 2001 From: Col-E Date: Fri, 11 Aug 2023 02:05:27 -0400 Subject: [PATCH 11/11] Disable dynamic scanning by default in concoction builder --- .../java/info/mmpa/concoction/Concoction.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/concoction-lib/src/main/java/info/mmpa/concoction/Concoction.java b/concoction-lib/src/main/java/info/mmpa/concoction/Concoction.java index d30fa83..a4bad17 100644 --- a/concoction-lib/src/main/java/info/mmpa/concoction/Concoction.java +++ b/concoction-lib/src/main/java/info/mmpa/concoction/Concoction.java @@ -30,6 +30,7 @@ public class Concoction { private final Map scanModels = new HashMap<>(); private ArchiveLoadContext supportingPathLoadContext = ArchiveLoadContext.RANDOM_ACCESS_JAR; private int inputDepth = 3; + private boolean dynamicScanning; private Concoction() { } @@ -60,6 +61,18 @@ public Concoction withMaxInputDirectoryDepth(int inputDepth) { return this; } + /** + * Activates dynamic scanning capabilities. + * By default, they are disabled, + * + * @return Self. + */ + @Nonnull + public Concoction withDynamicScanning() { + this.dynamicScanning = true; + return this; + } + /** * Sets supporting path/input archive loading context. * @@ -313,9 +326,9 @@ public NavigableMap scan() throws DynamicScanException { } // Then dynamic scanning - List dynamicModels = scanModels.values().stream() + List dynamicModels = dynamicScanning ? scanModels.values().stream() .filter(ScanModel::hasDynamicModel) - .collect(Collectors.toList()); + .collect(Collectors.toList()) : Collections.emptyList(); if (!insnModels.isEmpty()) { // TODO: Point these interfaces to proper implementations // - This will currently scan nothing