diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/ByteMatcher.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/ByteMatcher.java index 63336c8dff..5bf837a2e2 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/ByteMatcher.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/ByteMatcher.java @@ -27,14 +27,23 @@ import ghidra.features.base.memsearch.gui.SearchSettings; */ public abstract class ByteMatcher { + private final String name; private final String input; private final SearchSettings settings; - protected ByteMatcher(String input, SearchSettings settings) { + protected ByteMatcher(String name, String input, SearchSettings settings) { + this.name = name; this.input = input; this.settings = settings; } + /** + * {@return the name of this byte matcher.} + */ + public String getName() { + return name; + } + /** * Returns the original input text that generated this ByteMatacher. * @return the original input text that generated this BytesMatcher @@ -120,7 +129,10 @@ public abstract class ByteMatcher { /** * Record class to contain a match specification. + * @param start the offset in the buffer where the match starts + * @param length the length of the match + * @param matcher the matcher the found the match */ - public record ByteMatch(int start, int length) {} + public record ByteMatch(int start, int length, ByteMatcher matcher) {} } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/CombinedByteMatcher.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/CombinedByteMatcher.java new file mode 100644 index 0000000000..5a78a596db --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/CombinedByteMatcher.java @@ -0,0 +1,98 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.features.base.memsearch.matcher; + +import java.util.Iterator; +import java.util.List; + +import ghidra.features.base.memsearch.bytesequence.ExtendedByteSequence; +import ghidra.features.base.memsearch.gui.SearchSettings; +import ghidra.features.base.memsearch.searcher.MemorySearcher; + +/** + * A ByteMatcher that searches an input sequence for matches from multiple patterns. This is + * useful for using with the {@link MemorySearcher} so that multiple patterns can be searched with + * only one pass through memory, thus paying the memory I/O costs only once. The resulting matches + * will contain the sub ByteMatcher that matched so that it is easy to know which of the multiple + * patterns matched. + */ +public class CombinedByteMatcher extends ByteMatcher { + + private List matchers; + + public CombinedByteMatcher(List matchers, SearchSettings settings) { + super("Multi-Pattern Matcher", null, settings); + this.matchers = matchers; + } + + @Override + public Iterable match(ExtendedByteSequence bytes) { + return new MultiMatcherIterator(bytes); + } + + @Override + public String getDescription() { + return getName(); + } + + @Override + public String getToolTip() { + return null; + } + + private class MultiMatcherIterator implements Iterable, Iterator { + + private Iterator matcherIterator; + private Iterator currentMatchIterator; + private ExtendedByteSequence bytes; + + MultiMatcherIterator(ExtendedByteSequence bytes) { + this.bytes = bytes; + matcherIterator = matchers.iterator(); + currentMatchIterator = getNextMatchIterator(); + } + + @Override + public boolean hasNext() { + while (currentMatchIterator != null && !currentMatchIterator.hasNext()) { + currentMatchIterator = getNextMatchIterator(); + } + return currentMatchIterator != null; + } + + private Iterator getNextMatchIterator() { + if (matcherIterator.hasNext()) { + ByteMatcher matcher = matcherIterator.next(); + return matcher.match(bytes).iterator(); + } + return null; + } + + @Override + public ByteMatch next() { + if (hasNext()) { + return currentMatchIterator.next(); + } + return null; + } + + @Override + public Iterator iterator() { + return this; + } + + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/InvalidByteMatcher.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/InvalidByteMatcher.java index ecea33ae32..ae0d741f24 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/InvalidByteMatcher.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/InvalidByteMatcher.java @@ -50,7 +50,7 @@ public class InvalidByteMatcher extends ByteMatcher { * a negative number. */ public InvalidByteMatcher(String errorMessage, boolean isValidInput) { - super(null, null); + super("Invalid", null, null); this.errorMessage = errorMessage; this.isValidInput = isValidInput; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/MaskedByteSequenceByteMatcher.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/MaskedByteSequenceByteMatcher.java index 0d587af996..b63451556e 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/MaskedByteSequenceByteMatcher.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/MaskedByteSequenceByteMatcher.java @@ -52,7 +52,7 @@ public class MaskedByteSequenceByteMatcher extends ByteMatcher { */ public MaskedByteSequenceByteMatcher(String input, byte[] bytes, byte[] masks, SearchSettings settings) { - super(input, settings); + super("Masked Byte Sequence Matcher", input, settings); if (masks == null) { masks = new byte[bytes.length]; @@ -145,7 +145,8 @@ public class MaskedByteSequenceByteMatcher extends ByteMatcher { while (nextPossibleStart >= 0) { startIndex = nextPossibleStart + 1; if (isValidMatch(nextPossibleStart)) { - return new ByteMatch(nextPossibleStart, searchBytes.length); + return new ByteMatch(nextPossibleStart, searchBytes.length, + MaskedByteSequenceByteMatcher.this); } nextPossibleStart = findNextPossibleStart(startIndex); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/RegExByteMatcher.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/RegExByteMatcher.java index 99f850e03f..21a7449682 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/RegExByteMatcher.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/matcher/RegExByteMatcher.java @@ -32,7 +32,11 @@ public class RegExByteMatcher extends ByteMatcher { private final Pattern pattern; public RegExByteMatcher(String input, SearchSettings settings) { - super(input, settings); + this("Regex Matcher", input, settings); + } + + public RegExByteMatcher(String name, String input, SearchSettings settings) { + super(name, input, settings); // without DOTALL mode, bytes that match line terminator characters will cause // the regular expression pattern to not match. this.pattern = Pattern.compile(input, Pattern.DOTALL); @@ -133,7 +137,7 @@ public class RegExByteMatcher extends ByteMatcher { if (start >= byteSequence.getLength()) { return null; } - return new ByteMatch(start, end - start); + return new ByteMatch(start, end - start, RegExByteMatcher.this); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/searcher/MemorySearcher.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/searcher/MemorySearcher.java index fc3a64e8b2..30a46bd018 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/searcher/MemorySearcher.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/searcher/MemorySearcher.java @@ -246,7 +246,7 @@ public class MemorySearcher { for (ByteMatch byteMatch : matcher.match(searchSequence)) { Address address = searchBytes.getAddress(byteMatch.start()); byte[] bytes = searchSequence.getBytes(byteMatch.start(), byteMatch.length()); - MemoryMatch match = new MemoryMatch(address, bytes, matcher); + MemoryMatch match = new MemoryMatch(address, bytes, byteMatch.matcher()); if (filter.test(match)) { return match; } @@ -269,7 +269,7 @@ public class MemorySearcher { for (ByteMatch byteMatch : matcher.match(searchSequence)) { Address address = searchBytes.getAddress(byteMatch.start()); byte[] bytes = searchSequence.getBytes(byteMatch.start(), byteMatch.length()); - MemoryMatch match = new MemoryMatch(address, bytes, matcher); + MemoryMatch match = new MemoryMatch(address, bytes, byteMatch.matcher()); if (filter.test(match)) { last = match; } @@ -316,7 +316,7 @@ public class MemorySearcher { for (ByteMatch byteMatch : matcher.match(searchSequence)) { Address address = searchBytes.getAddress(byteMatch.start()); byte[] bytes = searchSequence.getBytes(byteMatch.start(), byteMatch.length()); - MemoryMatch match = new MemoryMatch(address, bytes, matcher); + MemoryMatch match = new MemoryMatch(address, bytes, byteMatch.matcher()); if (filter.test(match)) { if (accumulator.size() >= searchLimit) { return false; diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/bytesequence/CombinedByteMatcherTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/bytesequence/CombinedByteMatcherTest.java new file mode 100644 index 0000000000..10662bf8e7 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/bytesequence/CombinedByteMatcherTest.java @@ -0,0 +1,92 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.features.base.memsearch.bytesequence; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.stream.StreamSupport; + +import org.junit.Before; +import org.junit.Test; + +import ghidra.features.base.memsearch.matcher.*; +import ghidra.features.base.memsearch.matcher.ByteMatcher.ByteMatch; + +public class CombinedByteMatcherTest { + private ByteMatcher xxxByteMatcher; + private ByteMatcher yyyByteMatcher; + private ByteMatcher zzzByteMatcher; + private CombinedByteMatcher multiMatcher; + + @Before + public void setUp() { + + xxxByteMatcher = new RegExByteMatcher("xxx", null); + yyyByteMatcher = new RegExByteMatcher("yyy", null); + zzzByteMatcher = new RegExByteMatcher("zzz", null); + multiMatcher = + new CombinedByteMatcher(List.of(xxxByteMatcher, yyyByteMatcher, zzzByteMatcher), null); + } + + @Test + public void textFindsOneEachPatterns() { + List results = match("fooxxxbar, fooyyybar, foozzzbar"); + assertEquals(3, results.size()); + assertEquals(new ByteMatch(3, 3, xxxByteMatcher), results.get(0)); + assertEquals(new ByteMatch(15, 3, yyyByteMatcher), results.get(1)); + assertEquals(new ByteMatch(26, 3, zzzByteMatcher), results.get(2)); + } + + @Test + public void textFindsMutliplePatterns() { + List results = match("xxxyyyzzzxxxyyyzzz"); + assertEquals(6, results.size()); + assertEquals(new ByteMatch(0, 3, xxxByteMatcher), results.get(0)); + assertEquals(new ByteMatch(9, 3, xxxByteMatcher), results.get(1)); + assertEquals(new ByteMatch(3, 3, yyyByteMatcher), results.get(2)); + assertEquals(new ByteMatch(12, 3, yyyByteMatcher), results.get(3)); + assertEquals(new ByteMatch(6, 3, zzzByteMatcher), results.get(4)); + assertEquals(new ByteMatch(15, 3, zzzByteMatcher), results.get(5)); + } + + @Test + public void testNoMatches() { + List results = match("There are no matches here!"); + assertEquals(0, results.size()); + } + + private List match(String s) { + ExtendedByteSequence sequence = createByteSequence(s); + Iterable match = multiMatcher.match(sequence); + return StreamSupport.stream(match.spliterator(), false).toList(); + } + + private ExtendedByteSequence createByteSequence(String s) { + ByteSequence main = new ByteArrayByteSequence(makeBytes(s)); + ByteSequence extra = new ByteArrayByteSequence(makeBytes("")); + return new ExtendedByteSequence(main, extra, 0); + } + + private byte[] makeBytes(String string) { + byte[] bytes = new byte[string.length()]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte) string.charAt(i); + } + return bytes; + } + +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/bytesequence/MaskedBytesSequenceByteMatcherTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/bytesequence/MaskedBytesSequenceByteMatcherTest.java index aaec032521..2999857918 100644 --- a/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/bytesequence/MaskedBytesSequenceByteMatcherTest.java +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/bytesequence/MaskedBytesSequenceByteMatcherTest.java @@ -49,10 +49,10 @@ public class MaskedBytesSequenceByteMatcherTest { Iterator it = byteMatcher.match(byteSequence).iterator(); assertTrue(it.hasNext()); - assertEquals(new ByteMatch(2, 3), it.next()); + assertEquals(new ByteMatch(2, 3, byteMatcher), it.next()); assertTrue(it.hasNext()); - assertEquals(new ByteMatch(9, 3), it.next()); + assertEquals(new ByteMatch(9, 3, byteMatcher), it.next()); assertFalse(it.hasNext()); @@ -66,8 +66,8 @@ public class MaskedBytesSequenceByteMatcherTest { Iterator it = byteMatcher.match(byteSequence).iterator(); - assertEquals(new ByteMatch(2, 3), it.next()); - assertEquals(new ByteMatch(9, 3), it.next()); + assertEquals(new ByteMatch(2, 3, byteMatcher), it.next()); + assertEquals(new ByteMatch(9, 3, byteMatcher), it.next()); assertNull(it.next()); } @@ -81,9 +81,9 @@ public class MaskedBytesSequenceByteMatcherTest { Iterator it = byteMatcher.match(byteSequence).iterator(); - assertEquals(new ByteMatch(1, 3), it.next()); - assertEquals(new ByteMatch(6, 3), it.next()); - assertEquals(new ByteMatch(8, 3), it.next()); + assertEquals(new ByteMatch(1, 3, byteMatcher), it.next()); + assertEquals(new ByteMatch(6, 3, byteMatcher), it.next()); + assertEquals(new ByteMatch(8, 3, byteMatcher), it.next()); assertNull(it.next()); } diff --git a/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/bytesequence/RegExByteMatcherTest.java b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/bytesequence/RegExByteMatcherTest.java index ff027deffc..4c949db118 100644 --- a/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/bytesequence/RegExByteMatcherTest.java +++ b/Ghidra/Features/Base/src/test/java/ghidra/features/base/memsearch/bytesequence/RegExByteMatcherTest.java @@ -45,10 +45,10 @@ public class RegExByteMatcherTest { Iterator it = byteMatcher.match(byteSequence).iterator(); assertTrue(it.hasNext()); - assertEquals(new ByteMatch(4, 3), it.next()); + assertEquals(new ByteMatch(4, 3, byteMatcher), it.next()); assertTrue(it.hasNext()); - assertEquals(new ByteMatch(14, 3), it.next()); + assertEquals(new ByteMatch(14, 3, byteMatcher), it.next()); assertFalse(it.hasNext()); @@ -61,8 +61,8 @@ public class RegExByteMatcherTest { Iterator it = byteMatcher.match(byteSequence).iterator(); - assertEquals(new ByteMatch(4, 3), it.next()); - assertEquals(new ByteMatch(14, 3), it.next()); + assertEquals(new ByteMatch(4, 3, byteMatcher), it.next()); + assertEquals(new ByteMatch(14, 3, byteMatcher), it.next()); assertNull(it.next()); }