Skip to content

Commit

Permalink
Allow instruction matching models to define multiple variant signatures
Browse files Browse the repository at this point in the history
  • Loading branch information
Col-E committed Jul 10, 2023
1 parent 777981e commit 2225a97
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,12 @@ public int hashCode() {
result = 31 * result + path.hashCode();
return result;
}

@Override
public String toString() {
return "Detection{" +
"archetype=" + archetype +
", path=" + path +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,19 @@

import javax.annotation.Nonnull;

/**
* Outline of instruction level matching.
*/
@JsonSerialize(using = InstructionMatchEntrySerializer.class)
@JsonDeserialize(using = InstructionMatchEntryDeserializer.class)
public interface InstructionMatchEntry {
/**
* @param method
* Method containing the instruction.
* @param insn
* Instruction to match.
*
* @return {@code true} on match.
*/
boolean match(@Nonnull MethodNode method, @Nonnull AbstractInsnNode insn);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,34 @@
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;
import software.coley.collections.delegate.DelegatingList;

import javax.annotation.Nonnull;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
* Model representing a signature comprised of one or more instruction matchers.
* Model representing pattern matching for a {@link DetectionArchetype detection archetype}.
* The model may have one or more variants describing different signature techniques.
*/
public class InstructionsMatchingModel {
private final DetectionArchetype archetype;
@JsonDeserialize(contentUsing = InstructionMatchEntryDeserializer.class)
@JsonSerialize(contentUsing = InstructionMatchEntrySerializer.class)
private final List<InstructionMatchEntry> entries;
private final Map<String, InstructionMatchingList> variants;

/**
* @param archetype
* Information about what the signature is matching.
* @param entries
* List of instruction matchers forming a single signature.
* @param variants
* Map of variants to detect the pattern.
* Map values are lists of instruction matchers forming a single signature.
*/
public InstructionsMatchingModel(@JsonProperty("archetype") @Nonnull DetectionArchetype archetype,
@JsonProperty("entries") @Nonnull List<InstructionMatchEntry> entries) {
@JsonProperty("variants") @Nonnull Map<String, List<InstructionMatchEntry>> variants) {
this.archetype = archetype;
this.entries = Collections.unmodifiableList(entries);
this.variants = variants.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey,
e -> new InstructionMatchingList(e.getValue())));
}

/**
Expand All @@ -51,6 +55,15 @@ public void match(@Nonnull ResultsSink sink, @Nonnull MethodPathElement path,
// Skip methods without code
if (methodNode.instructions == null) return;

// Scan with each variant
for (List<InstructionMatchEntry> entries : variants.values())
matchVariant(sink, path, methodNode, entries);
}

private void matchVariant(@Nonnull ResultsSink sink,
@Nonnull MethodPathElement path,
@Nonnull MethodNode methodNode,
@Nonnull List<InstructionMatchEntry> entries) {
// Iterate over instructions and match against the matcher entries.
// A match will be reported if all entries successfully match in a row for some range of instructions.
//
Expand Down Expand Up @@ -129,11 +142,12 @@ public DetectionArchetype getArchetype() {
}

/**
* @return List of instruction matchers forming a single signature.
* @return Map of variants to detect the pattern.
* Map values are lists of instruction matchers forming a single signature.
*/
@Nonnull
public List<InstructionMatchEntry> getEntries() {
return entries;
public Map<String, ? extends List<InstructionMatchEntry>> getVariants() {
return variants;
}

@Override
Expand All @@ -143,18 +157,31 @@ public boolean equals(Object o) {

InstructionsMatchingModel that = (InstructionsMatchingModel) o;

return entries.equals(that.entries);
return variants.equals(that.variants);
}

@Override
public int hashCode() {
return entries.hashCode();
return variants.hashCode();
}

@Override
public String toString() {
return "InstructionsMatchingModel{" +
"archetype=" + archetype +
", entries[" + entries.size() + "]}";
", variants[" + variants.size() + "]}";
}

/**
* Hack to get JSON deserialization enough type information to deserialize otherwise nebulous
* typing for {@link #variants}. Since the public getter only exposes {@link List} its not
* a hindrance to the public API.
*/
@JsonDeserialize(contentUsing = InstructionMatchEntryDeserializer.class)
@JsonSerialize(contentUsing = InstructionMatchEntrySerializer.class)
private static class InstructionMatchingList extends DelegatingList<InstructionMatchEntry> {
public InstructionMatchingList(@Nonnull List<InstructionMatchEntry> delegate) {
super(delegate);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.Test;
import software.coley.collections.Maps;
import software.coley.collections.Sets;

import javax.annotation.Nonnull;
import java.io.IOException;
Expand All @@ -21,11 +22,11 @@
import java.nio.file.Paths;
import java.util.Collections;
import java.util.NavigableSet;
import java.util.Set;

import static info.mmpa.concoction.util.Casting.cast;
import static info.mmpa.concoction.util.Serialization.deserializeModel;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assertions.*;

public class InstructionMatchingTests {
// TODO: Create more test cases
Expand All @@ -37,14 +38,18 @@ void test() {
Results results = results("Runtime_exec.json", RuntimeExec.class);
NavigableSet<Detection> detections = results.detections();

// Should be one result
assertEquals(1, detections.size());
// There are four sample methods which exhibit match-worthy behavior.
assertEquals(4, detections.size());

// Should be in the example class's calc method
Detection detection = detections.iterator().next();
MethodPathElement path = cast(detection.path());
assertEquals("example/RuntimeExec", path.getClassName());
assertEquals("calc()V", path.localDisplay());
// We should have one match in each of these methods.
Set<String> remainingMatches = Sets.ofVar("calc1", "calc2", "calc3", "calc4");
for (Detection detection : detections) {
MethodPathElement path = cast(detection.path());
String detectionMethodName = path.getMethodName();
if (!remainingMatches.remove(detectionMethodName))
fail("Match in unexpected method: " + detectionMethodName);
}
assertTrue(remainingMatches.isEmpty());
} catch (IOException ex) {
fail(ex);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import info.mmpa.concoction.output.SusLevel;
import info.mmpa.concoction.scan.model.method.*;
import org.junit.jupiter.api.Test;
import software.coley.collections.Maps;

import java.util.Arrays;
import java.util.List;
Expand Down Expand Up @@ -105,7 +106,7 @@ void model() {
new Instruction("LDC", null, EQUALS, null),
new Instruction("INVOKEVIRTUAL", "exec(Ljava/lang/String;)Ljava/lang/Process;", EQUALS, EQUALS)
);
InstructionsMatchingModel model = new InstructionsMatchingModel(archetype, entries);
InstructionsMatchingModel model = new InstructionsMatchingModel(archetype, Maps.of("key", entries));

// Serialize, deserialize, and compare equality
String serialized = serialize(model);
Expand Down
33 changes: 28 additions & 5 deletions concoction-lib/src/test/resources/models/Runtime_exec.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,32 @@
"identifier": "runtime-exec",
"description": "Runtime.getRuntime().exec(...) can be used to run applications on the system"
},
"entries": [
{ "op": "EQUALS INVOKESTATIC", "args": "EQUALS getRuntime()Ljava/lang/Runtime;" },
{ "op": "EQUALS LDC" },
{ "op": "EQUALS INVOKEVIRTUAL", "args": "EQUALS exec(Ljava/lang/String;)Ljava/lang/Process;" }
]
"variants": {
"1": [
{ "op": "EQUALS INVOKESTATIC", "args": "EQUALS getRuntime()Ljava/lang/Runtime;" },
{ "op": "EQUALS LDC" },
{ "op": "EQUALS INVOKEVIRTUAL", "args": "EQUALS exec(Ljava/lang/String;)Ljava/lang/Process;" }
],
"2": [
{ "op": "EQUALS INVOKESTATIC", "args": "EQUALS getRuntime()Ljava/lang/Runtime;" },
"**",
{ "op": "EQUALS LDC" },
{ "op": "EQUALS INVOKEVIRTUAL", "args": "EQUALS exec(Ljava/lang/String;)Ljava/lang/Process;" }
],
"3": [
{
"ANY": [
{ "op": "STARTS_WITH GET", "args": "ENDS_WITH Ljava/lang/Runtime;" },
{ "op": "STARTS_WITH INVOKE", "args": "ENDS_WITH )Ljava/lang/Runtime;" }
]
},
{ "op": "EQUALS LDC" },
{ "op": "EQUALS INVOKEVIRTUAL", "args": "EQUALS exec(Ljava/lang/String;)Ljava/lang/Process;" }
],
"4": [
{ "op": "EQUALS CHECKCAST", "args": "EQUALS java/lang/Runtime" },
{ "op": "EQUALS LDC" },
{ "op": "EQUALS INVOKEVIRTUAL", "args": "EQUALS exec(Ljava/lang/String;)Ljava/lang/Process;" }
]
}
}
18 changes: 17 additions & 1 deletion concoction-lib/src/testFixtures/java/example/RuntimeExec.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,23 @@
import java.io.IOException;

public class RuntimeExec {
public static void calc() throws IOException {
private static final Runtime runtime = Runtime.getRuntime();
private static final Object runtimeObject = runtime;

public static void calc1() throws IOException {
Runtime.getRuntime().exec("calc");
}

public static void calc2() throws IOException {
Runtime runtime = Runtime.getRuntime();
runtime.exec("calc");
}

public static void calc3() throws IOException {
runtime.exec("calc");
}

public static void calc4() throws IOException {
((Runtime) runtimeObject).exec("calc");
}
}

0 comments on commit 2225a97

Please sign in to comment.