GP-5878 New parsing and byte transformation for the Byte Pattern Search

capability.
This commit is contained in:
ghidravision
2025-08-11 12:33:19 +00:00
parent b7b8d798be
commit 097392e1eb
16 changed files with 3687 additions and 1 deletions

View File

@@ -1096,3 +1096,6 @@ src/test.slow/resources/ghidra/app/script/GhidraScriptAsk.properties||GHIDRA||||
src/test/resources/defaultTools/TestCodeBrowser.tool||GHIDRA||||END|
src/test/resources/ghidra/app/util/opinion/decompile_debug_test.xml||GHIDRA||||END|
src/test/resources/ghidra/app/util/opinion/test.ord||GHIDRA||||END|
src/test/resources/ghidra/util/bytesearch/bytepatternsearch/basic_search_collection.xml||GHIDRA||||END|
src/test/resources/ghidra/util/bytesearch/bytepatternsearch/dword_qword_for32bit_pattern.xml||GHIDRA||||END|
src/test/resources/ghidra/util/bytesearch/bytepatternsearch/parse_errors.xml||GHIDRA||||END|

View File

@@ -39,7 +39,7 @@
<LI><A href="Search_for_DirectReferences.htm">Search For Direct References</A></LI>
<LI><A href="Search_Instruction_Patterns.htm">Search For Instruction Patterns</A></LI>
<LI><A href="Search_Instruction_Patterns.htm">Search For Instruction Patterns</A></LI>
</UL>
<P>&nbsp;</P>

View File

@@ -0,0 +1,99 @@
/* ###
* 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.util.bytesearch.bytepatternsearch;
/**
* This class handles search configurations for Ghidra's Byte Pattern Searcher which dictate the
* types of data manipulations performed by {@link BpsTransformationEngine} required prior to search
* submission.
* <P>
* The design of the Byte pattern Searcher and its supporting sub-capabilities and classes can be
* found in the "Search For Byte Patterns" page in the help viewer.
* <P>
* Supported data manipulations are found in {@link BpsTransformationEngine}.
* <P>
* Search configurations determine byte preparation manipulations prior to search submission.
*/
class BpsConfig {
private boolean isSearchBE;
private boolean isSearchLE;
private boolean extendPattern; // Zero-extend patterns to fill larger word sizes
private int programWordSize;
/**
* Default constructor initializing standard search settings; primarily used for search patterns
* which are 8 bits. Patterns of this size will not need splitting nor will they need to be
* manipulated to be LE as endianness of a pattern this size does not change the pattern.
*
* @param isByteExtension choice to zero-extend patterns to fit the word size of the program
* @param programWordSize word size of the program
*/
BpsConfig(int programWordSize, boolean isByteExtension) {
this.programWordSize = programWordSize;
this.extendPattern = isByteExtension;
}
/**
* Constructor for handling search settings for patterns greater than 8 bits in size.
*
* @param programWordSize word size of the program
* @param isByteExtension choice to zero-extend patterns to fit the word size of the program
* @param isSearchBE choice to perform Big Endian search
* @param isSearchLE choice to perform Little Endian search
*/
public BpsConfig(int programWordSize, boolean isByteExtension,
boolean isSearchBE, boolean isSearchLE) {
this(programWordSize, isByteExtension);
this.isSearchBE = isSearchBE;
this.isSearchLE = isSearchLE;
}
/**
* {@return big endian choice}
*/
boolean isSearchBE() {
return this.isSearchBE;
}
/**
* {@return little endian choice}
*/
boolean isSearchLE() {
return this.isSearchLE;
}
/**
* Search for patterns which have been zero-padded to fill the word size of the program.
* <P>
* <B>Note:</B> Quantization of word sizes is performed - <I>Example</I>: if the byte pattern's
* word size is 16 (Word) but the program's word size is 64 (QWord), choosing to extend the data
* type will produce 2 additional byte arrays, zero-padded to 32 (DWord) and 64 (QWord).
*
* @return extend pattern choice
*/
boolean isExtendPattern() {
return this.extendPattern;
}
/**
* {@return word size of the program}
*/
int getProgramWordSize() {
return this.programWordSize;
}
}

View File

@@ -0,0 +1,100 @@
/* ###
* 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.util.bytesearch.bytepatternsearch;
/**
* A helper class to build up the potentially complicated {@link BpsConfig}.
* <P>
* Manage the generation of a search configuration for use by Ghidra's Byte Pattern Search
* capability.
* <P>
* The design of the Byte pattern Searcher and its supporting sub-capabilities and classes can be
* found in the "Search For Byte Patterns" page in the help viewer.
*/
public class BpsConfigBuilder {
private int programWordSize;
private boolean isByteExtension;
private boolean littleEndianSearch;
private boolean bigEndianSearch = true;
/**
* Constructor.
*
* @param programWordSize the word size of the program; must be greater than 0
*/
public BpsConfigBuilder(int programWordSize) {
if (programWordSize <= 0) {
throw new IllegalArgumentException("Invalid program word size: " + programWordSize);
}
this.programWordSize = programWordSize;
}
/**
* Specify whether to perform a zero-extension of byte patterns to fit the program word size if
* the pattern's word size is smaller than the program's.
* <p>
* A true setting will perform quantization of sizes to build out the pattern's word size
* iteratively until it fills the program's word size. See {@link BpsConfig} for details.
*
* @param isByteExtension true signals to zero-extend byte patterns to fill program word size
*
* @return this builder instance
*/
public BpsConfigBuilder setPerformExtension(boolean isByteExtension) {
this.isByteExtension = isByteExtension;
return this;
}
/**
* Specify whether to search for patterns as Little Endian.
*
* @param isLeSearch true signals the transformation of patterns to be LE prior to
* search
*
* @return this builder instance
*/
public BpsConfigBuilder setLittleEndianSearch(boolean isLeSearch) {
this.littleEndianSearch = isLeSearch;
return this;
}
/**
* Specify whether to search for patterns as Big Endian. Default is true.
*
* @param isBeSearch true signals patterns to be searched as BE (their assumed default
* endianness)
*
* @return this builder instance
*/
public BpsConfigBuilder setBigEndianSearch(boolean isBeSearch) {
this.bigEndianSearch = isBeSearch;
return this;
}
/**
* Builds the final {@link BpsConfig}.
* @return the new search configuration
*/
public BpsConfig build() {
// Default search configuration does not perform splitting
return new BpsConfig(this.programWordSize, this.isByteExtension,
this.bigEndianSearch, this.littleEndianSearch);
}
}

View File

@@ -0,0 +1,140 @@
/* ###
* 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.util.bytesearch.bytepatternsearch;
import java.nio.ByteBuffer;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* This class represents a byte pattern in support of the {@link BpsXmlParser} of the Byte Pattern
* Search capability within Ghidra.
* <P>
* The design of the XML Parser and its supporting classes can be found in {@link BpsXmlParser}.
* <P>
* The design of the Byte pattern Searcher and its supporting sub-capabilities and classes can be
* found in the "Search For Byte Patterns" page in the help viewer.
* <P>
* This class represents a byte pattern object which is populated by {@link BpsXmlParser}. This
* object wraps a search byte array (inside a {@link ByteBuffer}), its parent {@link BpsSearchItem},
* and its parsed XML tag name for use downstream.
*
* <h4>Object Variables</h4>
* <ul>
* <li><strong>name</strong>
* <P>
* Indicates how to handle a pattern's matching and result analysis. Supported Names:
* <ul>
* <li>{@code <Bytes>} &ndash; The default tag. Indicates a standard constant, function, or table
* search.</li>
* <li>{@code <Blacklist>} &ndash; Unique to function-based searches. Enforces filtering rules.</li>
* </ul>
* <p>
* The pattern's search type is indicated in the parent {@link BpsSearchItem}:
* <em>OrderedFunction</em>-type and <em>Table</em>-type searches will enforce the order of the
* buffer as presented in the pattern. See {@link BpsSearchType} for a complete discussion on search
* types.
* <p>
*
* <li><strong>buffer</strong>
* <p>
* The byte array wrapped in a {@link ByteBuffer}. NOTE: all byte patterns must be in big Endian
* form.</li>
*
* <li><strong>parentSearchItem</strong>
* <p>
* Links to the owning {@code <SearchItem>} tag. This link is needed for result context
* preservation: when a match is found, the engine extracts metadata from the parent
* {@link BpsSearchItem} to populate the displayed results. Without this parental link, the results
* table will have empty gaps and lack critical context.</li>
* <li><strong>transformation</strong>
* <p>
* The transformation details of how this pattern was formed; see {@link BpsTransformation} for
* details.</li>
* </ul>
*/
public final class BpsPattern {
private final String name;
private ByteBuffer buffer;
private BpsSearchItem searchItem;
private final BpsTransformation transformation;
/**
* Constructor.
*
* @param name parsed XML tag containing this byte pattern array
* @param rawBytes parsed byte array wrapped in a ByteBuffer
* @param parentSearchItem parent search item containing this pattern for searching
* @param transformationRecord byte manipulation order
*/
public BpsPattern(String name, ByteBuffer rawBytes, BpsSearchItem parentSearchItem,
BpsTransformation transformationRecord) {
this.name = name;
this.buffer = rawBytes.duplicate().rewind(); // Safeguard original data integrity
this.searchItem = parentSearchItem;
this.transformation = transformationRecord;
}
/**
* {@return the name as parsed from the XML}
*/
public String getName() {
return name;
}
/**
* {@return the parent search item containing this byte array}
*/
public BpsSearchItem getSearchItem() {
return searchItem;
}
/**
* {@return the buffer containing the byte pattern. Downstream consumers are prevented from
* corrupting the pattern}
*/
public ByteBuffer getBuffer() {
return buffer.asReadOnlyBuffer().rewind();
}
/**
* {@return the transformation record of byte manipulations performed on the pattern}
*/
public BpsTransformation getTransformation() {
return transformation;
}
@Override
public String toString() {
return getDescription();
}
/**
* {@return the description of the pattern}
*/
public String getDescription() {
ByteBuffer view = getBuffer();
byte[] bytes = new byte[view.remaining()];
view.get(bytes);
String hexBytes = IntStream.range(0, bytes.length)
.mapToObj(i -> String.format("%02X", bytes[i]))
.collect(Collectors.joining(" "));
return String.format("[Pattern Name: %s | Word Size: %s] -> Pattern: %s | Form: [%s]",
searchItem.getPatternName(),
searchItem.getWordSize(), hexBytes, transformation.toString());
}
}

View File

@@ -0,0 +1,150 @@
/* ###
* 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.util.bytesearch.bytepatternsearch;
import java.util.*;
/**
* This class represents a collection of {@link BpsSearchItem}'s and their corresponding attributes
* in support of Ghidra's Byte Pattern Searcher capability.
* <P>
* The design of the XML Parser and its supporting classes, as well as an explanation of the
* required XML format can be found in {@link BpsXmlParser}.
* <P>
* The design of the Byte Pattern Searcher and its supporting sub-capabilities and classes can be
* found in the "Search For Byte Patterns" page in the help viewer.
* <P>
* This class manages a Search, or list, of {@link BpsSearchItem} objects parsed from the
* {@code <SearchItem>} tag. A search is populated by {@link BpsXmlParser} and referenced for
* processing by downstream Byte Pattern Searcher components.
*
* <h4>Byte Pattern Search Configurations</h4>
* <P>
* <strong>Required Variables:</strong>
* </p>
* <ul>
* <li>{@code searchName} &ndash; Name of the search collection.</li>
* </ul>
*
* <P>
* <strong>Optional Variables:</strong>
* <ul>
* <li>{@code description} &ndash; Text description of the pattern collection.</li>
* <li>{@code submitter} &ndash; Name of the pattern contributor.</li>
* </ul>
* <strong>Note:</strong> additional attributes may be included on the {@code<Search>} tag, they
* will be stored in a map for filtering and sorting downstream.
*/
public class BpsSearch {
private List<BpsSearchItem> searchItems;
private String searchName;
private String id;
private String description;
private String submitter;
private Map<String, String> additionalAttributes;
/**
* Constructor.
*
* @param searchName name identifying a Search
* @param generatedKey unique value across all BpsSearch and BpsSearchItem objects; used for
* skipping patterns at the {@code <Search>} and {@code <SearchItem>} levels.
* @param searchItems belonging to this Search
*/
public BpsSearch(String searchName, String generatedKey, List<BpsSearchItem> searchItems) {
this.searchName = searchName;
this.id = generatedKey;
this.searchItems = searchItems;
this.additionalAttributes = new HashMap<>();
}
/**
* {@return the name of the Search}
*/
public String getSearchName() {
return this.searchName;
}
/**
* {@return the search ID}
*/
public String getId() {
return this.id;
}
/**
* {@return the list of search items belonging to the Search}
*/
public List<BpsSearchItem> getSearchItems() {
return Collections.unmodifiableList(searchItems);
}
/**
* Set the description for the search.
*
* @param description of the search
*/
void setDescription(String description) {
this.description = description;
}
/**
* {@return the description for the search}
*/
public String getDescription() {
return this.description;
}
/**
* Set the contributor's identification.
*
* @param submitter who contributed the search
*/
void setSubmitter(String submitter) {
this.submitter = submitter;
}
/**
* {@return the submitter's identification}
*/
public String getSubmitter() {
return this.submitter;
}
/**
* Add attribute from the {@code<Search>} tag to the map for use downstream.
*
* @param key attribute key
* @param value attribute value
*/
void setAttribute(String key, String value) {
this.additionalAttributes.put(key, value);
}
/**
* {@return the additional attributes parsed from the {@code<Search>} tag}
*/
public Map<String, String> getAdditionalAttributes() {
return Collections.unmodifiableMap(additionalAttributes);
}
@Override
public String toString() {
return this.searchName;
}
}

View File

@@ -0,0 +1,172 @@
/* ###
* 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.util.bytesearch.bytepatternsearch;
import java.util.*;
/**
* This class represents a parsed search item as used by the Byte Pattern Searcher capability within
* Ghidra.
* <P>
* The design of the XML Parser and its supporting classes can be found in {@link BpsXmlParser}.
* <P>
* The design of the Byte Pattern Searcher and its supporting sub-capabilities and classes can be
* found in the "Search For Byte Patterns" page in the help viewer.
* <P>
* This class manages the search item characteristics parsed from the {@code <SearchItem>} XML tag.
*
* <h4>Byte Pattern Search Item Configurations</h4>
* <P>
* <strong>Required Variables:</strong>
* <ul>
* <li>patternName &ndash; Name of the pattern for referencing.</li>
* <li>wordSize &ndash; The pattern word size, resolved from the WordSize attribute from the
* {@code<SearchItem>} tag.</li>
* <li>searchType &ndash; The {@link BpsSearchType} strategy (table, constant, function, or ordered
* function search, see {@link BpsSearchType}) which dictates how patterns are searched.</li>
* <li>patterns &ndash; The {@code <BpsPattern>} collection of search byte sequences.</li>
* </ul>
*
* <P>
* <strong>Optional Variables:</strong>
* <ul>
* <li>description &ndash; Text explanation detailing the purpose of the search pattern.</li>
* <li>submitter &ndash; The contributor who added this pattern to the {@link BpsSearch}.</li>
* </ul>
* <strong>Note:</strong> additional attributes may be included on the {@code<Search>} tag, they
* will be stored in a map for filtering and sorting downstream.
*/
public class BpsSearchItem {
private String patternName;
private String id;
private int wordSize;
private BpsSearchType searchType;
private List<BpsPattern> patterns;
private String description;
private String submitter;
private Map<String, String> additionalAttributes;
/**
* Constructor.
*
* @param patternName name of pattern
* @param key unique key to facilitate skipping patterns at the <SearchItem> level
* @param wordSize word size for pattern bytes (e.g. byte-8, word-16, dword-32, qword-64)
* @param searchType how the bytes should be searched
*/
public BpsSearchItem(String patternName, String key, int wordSize, BpsSearchType searchType) {
this.patternName = patternName;
this.id = key;
this.wordSize = wordSize;
this.searchType = searchType;
this.additionalAttributes = new HashMap<>();
}
/**
* {@return the pattern name}
*/
public String getPatternName() {
return this.patternName;
}
/**
* {@return the search item's ID}
*/
public String getId() {
return this.id;
}
/**
* {@return the pattern's word size}
*/
public int getWordSize() {
return this.wordSize;
}
/**
* {@return the pattern's search type}
*/
public BpsSearchType getSearchType() {
return this.searchType;
}
/**
* {@return the pattern's byte collection}
*/
public List<BpsPattern> getPatterns() {
return Collections.unmodifiableList(this.patterns);
}
/**
* Set collection of byte arrays for the pattern.
*
* @param patterns the byte arrays representing the pattern
*/
void setBytePatterns(List<BpsPattern> patterns) {
this.patterns = new ArrayList<>(patterns);
}
/**
* Set the search item's description.
*
* @param description of the search item
*/
void setDescription(String description) {
this.description = description;
}
/**
* {@return the search item's description}
*/
public String getDescription() {
return this.description;
}
/**
* Set pattern submitter identification.
*
* @param submitter of the search item
*/
void setSubmitter(String submitter) {
this.submitter = submitter;
}
/**
* {@return the submitter of the pattern}
*/
public String getSubmitter() {
return this.submitter;
}
/**
* Add attribute to the list for use downstream.
*
* @param key attribute key
* @param value attribute value
*/
void addAttribute(String key, String value) {
this.additionalAttributes.put(key, value);
}
/**
* {@return the additional attributes map parsed from the {@code<SearchItem>} tag}
*/
public Map<String, String> getAdditionalAttributes() {
return Collections.unmodifiableMap(this.additionalAttributes);
}
}

View File

@@ -0,0 +1,54 @@
/* ###
* 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.util.bytesearch.bytepatternsearch;
/**
* This enum represents the type of search to be conducted as used by the Byte Pattern Searcher
* capability within Ghidra.
* <P>
* The design of the Byte Pattern Searcher and its supporting sub-capabilities and classes can be
* found in the "Search For Byte Patterns" page in the help viewer.
* <P>
* This enum manages the possible supported search types.
* <P>
* All {@link BpsSearchItem} objects will have a searchType which indicate how the
* {@link BpsPattern} items should be searched for. A complete discussion of all search types can be
* found in the "Search For Byte Patterns" page in the help viewer.
*/
public enum BpsSearchType {
/**
* Table Search: byte patterns represent a table and must be found in the same order
* as in the pattern file
*/
TABLE_SEARCH,
/**
* Constant Search: byte patterns to be found anywhere and in any order in the program.
*/
CONSTANT_SEARCH,
/**
* Function Search: byte patterns must all be found within the same function.
*/
FUNCTION_SEARCH,
/**
* Ordered Function Search: byte patterns must all be found within the same function
* and also be found in the same order as in the pattern file.
*/
ORDERED_FUNCTION_SEARCH
}

View File

@@ -0,0 +1,94 @@
/* ###
* 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.util.bytesearch.bytepatternsearch;
import java.util.ArrayList;
import java.util.List;
import ghidra.program.model.lang.Endian;
/**
* This class records a byte pattern's transformation in support of Ghidra's Byte Pattern Search
* capability.
* <P>
* The design of the Byte pattern Searcher and its supporting sub-capabilities and classes can be
* found in the "Search For Byte Patterns" page in the help viewer.
* <P>
* Indicates the structural state mutations which have been applied to a {@link BpsPattern}.
* <P>
* <B>NOTE</B>: Once patterns are generated, they do not change (instead, when transforming a
* pattern, a new pattern is generated and has its own transformation record) so pattern
* transformation records are not modifiable.
*
* @param endianness of the pattern
* @param extensionSize word size the pattern was padded to (can be null)
* @param splitToSize word size the pattern was split to (can be null)
* @param splitPartIndex index of the split that this pattern represents (eg. index=1 indicates the
* first part of a split) (can be null)
* @param totalSplitParts total count of all parts of a split pattern (can be null)
*/
public record BpsTransformation(
Endian endianness,
Integer extensionSize,
Integer splitToSize,
Integer splitPartIndex,
Integer totalSplitParts) {
/**
* Factory method for an unmodified baseline pattern.
* <P>
* <B>NOTE</B>: All patterns prior to any transformations are safely assumed to be big endian.
*
* @return the transformation
*/
public static BpsTransformation empty() {
return new BpsTransformation(Endian.BIG, null, null, null, null);
}
/**
* {@return Creates a copy of this record that uses the Little Endian setting}
*/
public BpsTransformation asLittleEndian() {
Integer padSize = extensionSize;
Integer splitSize = splitToSize;
Integer splitIndex = splitPartIndex;
Integer splitParts = totalSplitParts;
return new BpsTransformation(Endian.LITTLE, padSize, splitSize, splitIndex,
splitParts);
}
@Override
public String toString() {
return getDescription();
}
/**
* {@return the description of the pattern's transformation history}
*/
public String getDescription() {
List<String> steps = new ArrayList<>();
steps.add("Endian: " + endianness.getDisplayName());
if (extensionSize != null) {
steps.add("Padded to " + extensionSize);
}
if (splitPartIndex != null) {
steps.add(String.format("Split to %d Word Size | Chunk %d of %d", splitToSize,
splitPartIndex, totalSplitParts));
}
return String.join(" -> ", steps);
}
}

View File

@@ -0,0 +1,412 @@
/* ###
* 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.util.bytesearch.bytepatternsearch;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.*;
import ghidra.program.model.lang.Endian;
/**
* This class performs byte pattern transformations in support of Ghidra's Byte Pattern Search
* capability.
* <P>
* The design of the Byte pattern Searcher and its supporting sub-capabilities and classes can be
* found in the "Search For Byte Patterns" page in the help viewer.
* <P>
* This transformer is used to prepare byte patterns parsed by the {@link BpsXmlParser} and
* populated in {@link BpsSearchItem} objects, prior to submitting them for search.
* <P>
* Supported transformations are as follows:
* <ul>
* <li><b>Little Endian</b> - Reverse a big Endian byte array. This is determined either
* programmatically based on the program's endianness or is user-controlled.</li>
* <li><b>Byte Splitting</b> - If the pattern's word size is larger than the program's word
* boundary, split the pattern to fit the boundary. This is determined programmatically and is not
* user-controlled.</li>
* <li><b>Byte Zero-Padding</b> - If the pattern's word size is smaller than the program's word
* boundary, zero-pad the byte pattern to fit the program's word boundary. The decision to extend
* bytes is user-controlled.</li>
* </ul>
* <P>
* A skip list is used to allow users to skip patterns at the {@link BpsSearch} and
* {@link BpsSearchItem} level.
*/
public class BpsTransformationEngine {
/**
* Process an entire search library parsed by {@link BpsXmlParser} and populated in
* {@link BpsSearch} and {@link BpsSearchItem} objects in preparation for search submission.
*
* @param searchLibrary collection of search objects
* @param config search preferences from user input and program details
*
* @return All patterns prepared for search submission
*/
public List<BpsPattern> createPatterns(List<BpsSearch> searchLibrary, BpsConfig config) {
return createPatterns(searchLibrary, null, config);
}
/**
* Process an entire search library parsed by {@link BpsXmlParser} and populated in
* {@link BpsSearch} and {@link BpsSearchItem} objects in preparation for search submission.
* <P>
* Skip over any patterns whose id's are included in a skip list.
*
* @param searchLibrary collection of search objects
* @param skipIds list of {@link BpsSearch} and/or {@link BpsSearchItem} id's to skip over
* @param config search preferences from user input and program details
*
* @return All patterns prepared for search submission
*/
public List<BpsPattern> createPatterns(List<BpsSearch> searchLibrary,
Set<String> skipIds, BpsConfig config) {
List<BpsPattern> results = new ArrayList<>();
if (skipIds == null) {
skipIds = Collections.emptySet();
}
for (BpsSearch search : searchLibrary) {
if (skipIds.contains(search.getId())) {
continue;
}
for (BpsSearchItem searchItem : search.getSearchItems()) {
if (skipIds.contains(searchItem.getId())) {
continue;
}
int patternWordSize = searchItem.getWordSize();
List<BpsPattern> patterns = searchItem.getPatterns();
List<BpsPattern> transformed = transformPatterns(patterns, config, patternWordSize);
results.addAll(transformed);
}
}
return results;
}
/**
* Transform patterns to accommodate search configurations as identified in {@link BpsConfig}.
* <P>
* Transformations are completed in the following order:
* <ol>
* <li><b>Little Endian</b> - generate the LE arrangement from the BE pattern.</li>
* <li><b>Byte Splitting</b> - If the pattern's word size is larger than the program's word
* boundary, split the pattern to fit the boundary.</li>
* <li><b>Byte Zero-Padding</b> - If the pattern's word size is smaller than the program's word
* boundary, zero-pad the byte pattern to fit the program's word boundary.</li>
* </ol>
* Generally, these transformations are determined by the program's details (endianness and word
* boundary), but user input may also dictate how patterns are prepared for search.
* <P>
*
* @param originals collection of byte patterns for transformation
* @param config search configuration
* @param searchItemWordSize the search item word size
*
* @return collection of transformed byte patterns
*/
private List<BpsPattern> transformPatterns(List<BpsPattern> originals,
BpsConfig config, int searchItemWordSize) {
// 1. Process endianness if needed
// Endianness does not matter and splitting is not needed if word size is a single byte;
// keep constructor simple
boolean ignoreEndianess = searchItemWordSize <= 8;
List<BpsPattern> endianOutput = new ArrayList<>();
if (!ignoreEndianess && config.isSearchLE()) {
for (BpsPattern pattern : originals) {
if (config.isSearchBE()) {
endianOutput.add(pattern); // maintain BE arrangement
}
endianOutput.add(createLEPattern(pattern));
}
}
else {
endianOutput = originals;
}
// 2. Process pattern splitting if needed
boolean splitPattern = config.getProgramWordSize() < searchItemWordSize;
List<BpsPattern> splitOutput = new ArrayList<>();
if (splitPattern) {
for (BpsPattern pattern : endianOutput) {
splitOutput.addAll(createSplitPattern(pattern, config));
}
}
else {
splitOutput = endianOutput;
}
// 3. Process pattern expansion if needed
List<BpsPattern> finalOutput = new ArrayList<>();
if (config.isExtendPattern()) {
for (BpsPattern pattern : splitOutput) {
finalOutput.addAll(createExtendedPattern(pattern, config));
}
}
else {
finalOutput = splitOutput;
}
return finalOutput;
}
/**
* Rearrange a big endian pattern to be little endian. This is generally completed prior to
* performing any zero-extending or splitting of byte patterns.
*
* @param pattern big endian byte pattern
*
* @return little endian pattern arrangement
*/
private BpsPattern createLEPattern(BpsPattern pattern) {
BpsTransformation transformation = pattern.getTransformation();
BpsTransformation leTransformation = transformation.asLittleEndian();
ByteBuffer leBuff = createLitteEndianBuffer(pattern);
String name = pattern.getName();
BpsSearchItem searchItem = pattern.getSearchItem();
return new BpsPattern(name, leBuff, searchItem, leTransformation);
}
/**
* Generate LE arrangement of pattern.
*
* @param pattern to make LE
*
* @return pattern LE byte buffer
*/
private ByteBuffer createLitteEndianBuffer(BpsPattern pattern) {
ByteBuffer originalBuffer = pattern.getBuffer();
int capacity = originalBuffer.capacity();
ByteBuffer leBuff = ByteBuffer.allocate(capacity);
leBuff.order(ByteOrder.LITTLE_ENDIAN);
BpsSearchItem searchItem = pattern.getSearchItem();
int wordSize = searchItem.getWordSize();
switch (wordSize) {
case 16:
short asShort = originalBuffer.getShort();
leBuff.putShort(asShort);
break;
case 32:
int asInt = originalBuffer.getInt();
leBuff.putInt(asInt);
break;
case 64:
long asLong = originalBuffer.getLong();
leBuff.putLong(asLong);
break;
}
return leBuff;
}
/**
* Split a byte pattern to fit within the program's word boundary as indicated in the search
* configuration {@link BpsConfig}. More than a single split is possible depending on the word
* size of the pattern and the word size boundary of the program.
* <P>
* All split byte segments are placed in a list and stored as separate, split, patterns as
* tracked by each pattern's {@link BpsTransformation}. The searcher will submit searches and
* perform follow-on processing to ensure that split patterns are found appropriately adjacent
* to each other.
* <P>
* This transformation is generally performed after little endian rearrangement, if it is
* required, and prior to pattern zero-extention, if it is required.
*
* @param pattern to be split
* @param config search configuration
*
* @return split pattern
*/
private List<BpsPattern> createSplitPattern(BpsPattern pattern, BpsConfig config) {
// Pattern's byte buffer has read-only protections
ByteBuffer originalByteBuffer = pattern.getBuffer();
byte[] bytes = new byte[originalByteBuffer.remaining()];
BpsSearchItem searchItem = pattern.getSearchItem();
int programWordSize = config.getProgramWordSize();
int numOfSplits = searchItem.getWordSize() / programWordSize;
int targetByteSize = bytes.length / numOfSplits;
byte[][] splitBytes = new byte[numOfSplits][targetByteSize];
List<BpsPattern> splitPatterns = new ArrayList<BpsPattern>();
for (int i = 0; i < numOfSplits; i++) {
originalByteBuffer.get(splitBytes[i], 0, targetByteSize);
BpsTransformation transformation = pattern.getTransformation();
Endian endianness = transformation.endianness();
int splitIndex = i + 1;
// If a pattern needs split, we can confidently assume that it has not been previously
// zero-extended, so we can safely null out this value in the new record.
BpsTransformation newTransformation = new BpsTransformation(endianness,
null, programWordSize, splitIndex, numOfSplits);
String patternName = pattern.getName();
ByteBuffer buffer = ByteBuffer.wrap(splitBytes[i]);
BpsPattern splitPattern =
new BpsPattern(patternName, buffer, searchItem, newTransformation);
splitPatterns.add(splitPattern);
}
return splitPatterns;
}
/**
* Zero-extend a byte pattern to fill the program's word size boundary. We perform padding
* quantization to accommodate program word sizes which are more than twice the size of the
* pattern's word size.
* <P>
* <i> Quantization Extension Examples: </i>
* <table border="1">
* <tr>
* <th>Pattern Word Size</th>
* <th>Program Word Size</th>
* <th>Generated Extended Patterns</th>
* </tr>
* <tr>
* <td align=center>16</td>
* <td align=center>64</td>
* <td align=center>32 and 64</td>
* </tr>
* <tr>
* <td align=center>32</td>
* <td align=center>64</td>
* <td align=center>64</td>
* </tr>
* </table>
* <P>
* This transformation is generally performed after little endian rearrangement, if it is
* required, and after pattern splitting, if it is required.
*
* @param pattern byte pattern for extension
* @param config search configuration
*
* @return extended pattern
*/
private List<BpsPattern> createExtendedPattern(BpsPattern pattern, BpsConfig config) {
List<BpsPattern> paddedBytesList = new ArrayList<BpsPattern>();
BpsSearchItem searchItem = pattern.getSearchItem();
int searchItemWordSize = searchItem.getWordSize();
int programWordSize = config.getProgramWordSize();
List<Integer> padSizes = getPadSizes(searchItemWordSize, programWordSize);
for (int size : padSizes) {
BpsPattern extendedPattern = padByteArray(pattern, size);
paddedBytesList.add(extendedPattern);
}
return paddedBytesList;
}
/**
* If the user requests to extend the data types to fit in the program's word size (and the
* search bytes' word size is smaller than the program's), we zero-pad the byte array.
* <P>
* <B> The endianness of the pattern dictates where the zeros are placed: </B>
* <P>
* <I> Big endian</I>: zeros are placed on the right of each byte - at the highest address
* <P>
* <I> Little endian</I>: zeros are placed on the left of each byte - at the lowest address
* <p>
*
* @param pattern to be extended
* @param targetSize word size to extend to
*
* @return extended pattern
*/
private BpsPattern padByteArray(BpsPattern pattern, int targetSize) {
// Calculate total bytes needed for the target size
int byteWordSize = 8;
int totalBytes = targetSize / byteWordSize;
ByteBuffer paddedBuffer = ByteBuffer.allocate(totalBytes);
ByteBuffer storedBuffer = pattern.getBuffer().duplicate();
byte[] bytes = new byte[storedBuffer.remaining()];
storedBuffer.get(bytes);
// Insert zeros and data based on true Endian alignment
BpsTransformation transformations = pattern.getTransformation();
Endian endianness = transformations.endianness();
if (endianness == Endian.LITTLE) {
// Little Endian: Data goes at the lowest addresses (start of buffer)
paddedBuffer.put(bytes);
while (paddedBuffer.hasRemaining()) {
paddedBuffer.put((byte) 0x00);
}
}
else {
// Big Endian: Data goes at the highest addresses (end of buffer)
int numZeros = totalBytes - bytes.length;
for (int p = 0; p < numZeros; p++) {
paddedBuffer.put((byte) 0x00);
}
paddedBuffer.put(bytes);
}
// Expanded patterns will not previously have been split, so we can confidently null those
// details related to splitting out
BpsTransformation newTransformation = new BpsTransformation(endianness, targetSize,
null, null, null);
String patternName = pattern.getName();
BpsSearchItem parentSearchItem = pattern.getSearchItem();
return new BpsPattern(patternName, paddedBuffer, parentSearchItem, newTransformation);
}
/**
* Helper method for determining the collection of word sizes to zero-pad a byte array to.
* <P>
* This could be 1 or more size values depending on the comparison between the pattern's word
* size and the program's word size.
* <P>
* <B>Quantization Example:</B> if the pattern word size is 16bit and the program's word size is
* 64, we should zero-pad to 32bit and 64bit - returning both of these numbers as pad sizes to
* cycle through later.
* <p>
* NOTE: At this point, we know that zero-padding needs to take place based on previous checks.
* We are safely assuming that the word size of the search byte array is at least 16bit because
* of previous filtering.
* <p>
*
* @param patternWordSize bit length of the pattern to extend
* @param progWordSize bit length of the program's word size boundary
*
* @return list of word sizes to extend to
*/
private List<Integer> getPadSizes(int patternWordSize, int progWordSize) {
List<Integer> padSizes = new ArrayList<Integer>();
switch (progWordSize) {
case 32:
padSizes.add(32);
break;
case 64:
// Quantization: if the byte array's word size is 16, we pad to 32 as well as 64.
if (patternWordSize == 16) {
padSizes.add(32);
}
padSizes.add(64);
}
return padSizes;
}
}

View File

@@ -0,0 +1,639 @@
/* ###
* 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.util.bytesearch.bytepatternsearch;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import org.apache.commons.lang3.Strings;
import org.xml.sax.*;
import ghidra.app.util.importer.MessageLog;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
import ghidra.xml.*;
/**
* This is the main class which parses pattern search XML files as used by the Byte Pattern Search
* capability within Ghidra.
* <P>
* The design of the Byte pattern Searcher and its supporting sub-capabilities and classes can be
* found in the "Search For Byte Patterns" page in the help viewer.
* <P>
* This class parses the provided byte pattern search XML files, populating corresponding Byte
* Pattern Searcher objects: {@link BpsSearch},{@link BpsSearchItem}, and {@link BpsPattern} for
* processing and searching downstream.
* <P>
* A search pattern XML file contains a set of byte patterns, organized into a simple hierarchy,
* which contain specific meta-data details that will shape the search criteria. A full discussion
* on the required XML schema can be found in the "Search For Byte Patterns" page in the help
* viewer.
* <P>
* <strong>Note:</strong> All XML search documents must adhere to the structure outlined in the Byte
* Pattern Search help file. Parse errors due to malformed XML or an XML structure that does not
* follow the form, will result in accumulated parse errors which will be displayed once parsing has
* completed.
*/
public class BpsXmlParser {
private static final String BPS_LIBRARY = "library";
private static final String NAME = "Name";
private static final String SEARCH_TYPE = "SearchType";
private static final String DATA_TYPE = "DataType";
private static final String WORD_SIZE = "WordSize";
private static final String SEARCH_ITEM = "SearchItem";
private static int keyCounter; // Used for unique key generation within a pattern file
/***
* Parse the provided XML file, generating a list of {@link BpsSearch} objects. A map is used to
* support the skipping of patterns at the {@code<Search>} and {@code<SearchItem>} levels.
* <P>
* All pattern files must have surrounding {@code<BytePatternSearchLibrary>} tags and adhere to
* the structure outlined in the Byte pattern Search help file.
* <P>
*
* @param searchFile XML file to be parsed
* @param monitor the task monitor
*
* @return Byte pattern searches
*
* @throws SAXException for malformed XML parse errors
*/
public static List<BpsSearch> parseSearchFile(File searchFile,
TaskMonitor monitor) throws SAXException {
try {
return doParseSearchFile(searchFile, monitor);
}
catch (CancelledException e) {
return List.of();
}
}
private static List<BpsSearch> doParseSearchFile(File searchFile,
TaskMonitor monitor) throws SAXException, CancelledException {
XmlMessageLog log = new XmlMessageLog();
AccumulatingErrorHandler errorHandler = new AccumulatingErrorHandler(log);
XmlPullParser parser;
keyCounter = 0; // Reset the key for each pattern file
List<BpsSearch> searches = new ArrayList<>();
try {
parser = XmlPullParserFactory.create(searchFile, errorHandler, false);
String parseName = parser.peek().getName().toLowerCase();
if (!parseName.contains(BPS_LIBRARY)) {
trackParseError(
"Line: " + parser.getLineNumber() + ", Col: " + parser.getColumnNumber() +
": Pattern file must have a wrapper <BytePatternSearchLibrary> tag " +
"to start.",
log, errorHandler);
}
XmlElement startSearchElement = parser.start();
// Each pattern file contains a collection of searches
while (parser.peek().isStart()) {
monitor.checkCancelled();
String tag = parser.peek().getName();
// This new version of Strings will ignore case by default
if (Strings.CS.containsAny(tag, "Search", "Algorithm", "Family")) {
List<BpsSearch> search = parseSearch(parser, log, errorHandler, monitor);
searches.addAll(search);
}
else {
trackParseError(
"Line: " + parser.getLineNumber() + ", Col: " + parser.getColumnNumber() +
": <Search> tag expected, current tag not supported: " + tag + ".",
log, errorHandler);
parser.discardSubTree(tag);
}
}
parser.end(startSearchElement);
}
catch (SAXException | IOException e) {
log.appendException(e);
// Some severe fatal errors will break the parsing early, but standard violation
// errors will be collected for review after parsing completes.
}
// Check if any errors were collected during the parsing process
if (errorHandler.foundErrors()) {
String combinedMessage = errorHandler.getExceptions()
.stream()
.map(exception -> exception.getMessage())
.collect(Collectors.joining("\n"));
// SAXParser exceptions do not allow for the inspection of each error encountered.
// Since we are collecting errors as we go, allowing the parser to continue until
// finished, we need more than a string message of the exception encountered - we need
// access to the underlying exceptions for precise testing and error resolution.
throw new BpsXmlValidationException(
"XML parsing failed with " + errorHandler.getExceptions().size() + " errors:\n" +
combinedMessage,
errorHandler.getExceptions());
}
log.appendMsg("XML parsed successfully with zero errors");
return searches;
}
/**
* Parse a {@code<Search>} tag and subsequent {@code <SearchItem>} tags. Searches are stored as
* {@link BpsSearch} objects and have 1 required tag attribute: "Name".
* <P>
*
* @param parser the parser
* @param log message log
* @param errorHandler for standard violation XML errors
* @param monitor the task monitor
*
* @return the search items
*
* @throws SAXException for malformed XML parse errors
*/
private static List<BpsSearch> parseSearch(XmlPullParser parser, XmlMessageLog log,
AccumulatingErrorHandler errorHandler, TaskMonitor monitor)
throws SAXException, CancelledException {
XmlElement searchStart = parser.start("Family", "Search");
List<BpsSearch> searches = new ArrayList<>();
// Search tags must have a "Name" attribute
if (searchStart.hasAttribute(NAME)) {
String name = searchStart.getAttribute(NAME);
// Only 1 attribute to skip, gatherTagAttributes() requires this to be a list
List<String> skipAttribute = List.of(NAME);
Map<String, String> additionalAttributes =
gatherTagAttributes(searchStart, skipAttribute);
List<BpsSearchItem> searchItems = new ArrayList<>();
while (parser.peek().isStart()) {
monitor.checkCancelled();
String tag = parser.peek().getName();
if (!SEARCH_ITEM.equals(tag)) {
trackParseError("Line: " + parser.getLineNumber() + ", Col: " +
parser.getColumnNumber() + ": Tag not supported: " + tag +
", <SearchItem> tag expected. Skipping subtree.", log, errorHandler);
parser.discardSubTree(parser.next()); // Un-handled element
continue;
}
BpsSearchItem item = parseSearchItem(parser, log, errorHandler);
if (item != null) {
searchItems.add(item);
}
}
parser.end(searchStart);
if (!searchItems.isEmpty()) {
String parserName = parser.getName();
String key = generateKey(parserName, name);
BpsSearch search = new BpsSearch(name, key, searchItems);
populateSearchAttributes(additionalAttributes, search, log);
searches.add(search);
}
else {
log.appendMsg("No valid <SearchItems> found for family: " + name + ", skipping.");
}
}
else {
trackParseError("Line: " + parser.getLineNumber() + ", Col: " +
parser.getColumnNumber() +
": All <Search> tags must contain a 'Name' attribute. Skipping this Search.",
log, errorHandler);
parser.discardSubTree(searchStart);
}
return searches;
}
/**
* The key helps differentiate between searches to support skipping patterns at the
* {@code<Search>} and {@code<SearchItem>} levels.
* <P>
* Example key: filename.xml_SearchName_1
*
* @param fileName pattern file name
* @param name parsed {@code<Search>} or {@code<SearchItem>} name
*
* @return generated key
*/
private static String generateKey(String fileName, String name) {
return (String.format("%s_%s_%d", fileName, name, keyCounter++));
}
/**
* Unroll the mapping of key/value attribute pairs parsed from the XML {@code <Search>} tag and
* populate attribute values in {@link BpsSearch}.
* <P>
* NOTE: this is kept separate from buildSearchItem because there are some attributes between
* the two tags ({@code<Search>} and {@code<SearchItem>}) which overlap in name, but not in
* value. Specifically, both tags may contain "Description" and "Submitter" keys but their
* values will likely be different and need to be maintained separately. Keeping these separate
* requires two different managers to populate their corresponding object values.
* <P>
*
* @param additionalTagAttributes extra parsed attributes on the {@code <Search>} tag
* @param search object that the tag attributes are assigned to
* @param log the XML log
*/
private static void populateSearchAttributes(
Map<String, String> additionalTagAttributes, BpsSearch search,
XmlMessageLog log) {
for (String key : additionalTagAttributes.keySet()) {
String value = additionalTagAttributes.get(key);
switch (key) {
case "Description":
search.setDescription(value);
break;
case "Submitter":
search.setSubmitter(value);
break;
default:
log.appendMsg(
"Unknown attribute on <Search> tag encountered: " + key + ", adding to " +
"map for downstream use.");
search.setAttribute(key, value);
break;
}
}
}
/**
* Parse a {@code<SearchItem>} tag and subsequent {@code<Byte>} tags. SearchItems are stored as
* {@link BpsSearchItem} objects and have 3 required tag attributes: "Name", "WordSize", and
* "SearchType".
*
* @param parser the parser
* @param log the XML log
* @param errorHandler for handling standard violation XML errors
*
* @throws SAXException for malformed XML parse errors
*/
private static BpsSearchItem parseSearchItem(XmlPullParser parser,
XmlMessageLog log, AccumulatingErrorHandler errorHandler) throws SAXException {
XmlElement searchTag = parser.start("SearchItem");
// Gather required attributes. "DataType" is a compatible substitute for WordSize
if (!searchTag.hasAttribute(NAME) || !searchTag.hasAttribute(SEARCH_TYPE) ||
(!searchTag.hasAttribute(WORD_SIZE) && !searchTag.hasAttribute(DATA_TYPE))) {
trackParseError("Line: " + parser.getLineNumber() + ", Col: " +
parser.getColumnNumber() +
": All <SearchItem> tags must contain: 'Name', " +
"'WordSize', and 'SearchType' attributes. Skipping this SearchItem.", log,
errorHandler);
parser.discardSubTree(searchTag);
return null;
}
String name = searchTag.getAttribute(NAME);
// "DataType" is a compatible substitute for WordSize
String targetValue =
(searchTag.getAttribute(WORD_SIZE) != null) ? searchTag.getAttribute(WORD_SIZE)
: searchTag.getAttribute(DATA_TYPE);
int wordSize = lookUpWordSize(targetValue, log, errorHandler);
String searchType = searchTag.getAttribute(SEARCH_TYPE);
List<String> attributeSkipList = List.of(NAME, WORD_SIZE, SEARCH_TYPE);
Map<String, String> additionalAttributes =
gatherTagAttributes(searchTag, attributeSkipList);
String fileName = parser.getName();
BpsSearchItem searchItem = buildSearchItem(name, fileName, wordSize, searchType,
additionalAttributes, log, errorHandler);
List<BpsPattern> byteCollection =
gatherSearchItemBytes(parser, wordSize, searchItem, log, errorHandler);
if (byteCollection != null) {
searchItem.setBytePatterns(byteCollection);
}
else {
log.appendMsg(
"No byte pattern found for SearchItem: " + name + " line: " +
parser.getLineNumber() + ", skipping.");
}
parser.end(searchTag);
return searchItem;
}
/**
* Generate {@link BpsSearchItem} object from parsed {@code<SearchItem>} tag.
* <P>
*
* @param name SearchItem name
* @param wordSize byte pattern word size
* @param parsedSearchType the search type (e.g. function, constant, table, or orderedFunction)
* default is constant search
* @param additionalAttributes parsed extra attributes on the {@code<SearchItem>} tag
* @param log the XML log
* @param errorHandler for handling invalid "searchType" attribute value
*
* @return the search item
*
* @throws SAXException for malformed XML parse errors
*/
private static BpsSearchItem buildSearchItem(String name, String filename, int wordSize,
String parsedSearchType,
Map<String, String> additionalAttributes, XmlMessageLog log,
AccumulatingErrorHandler errorHandler) throws SAXException {
BpsSearchType searchType;
switch (parsedSearchType.toLowerCase()) {
case "function":
searchType = BpsSearchType.FUNCTION_SEARCH;
break;
case "constant":
searchType = BpsSearchType.CONSTANT_SEARCH;
break;
case "table", "wholetable":
searchType = BpsSearchType.TABLE_SEARCH;
break;
case "orderedfunction":
searchType = BpsSearchType.ORDERED_FUNCTION_SEARCH;
break;
default:
trackParseError("SearchType: '" + parsedSearchType + "' is unknown. " +
"Using default constant search.", log, errorHandler);
searchType = BpsSearchType.CONSTANT_SEARCH;
break;
}
String generatedKey = generateKey(filename, name);
BpsSearchItem searchItem =
new BpsSearchItem(name, generatedKey, wordSize, searchType);
for (String key : additionalAttributes.keySet()) {
String value = additionalAttributes.get(key);
switch (key) {
case "Description":
searchItem.setDescription(value);
break;
case "Submitter":
searchItem.setSubmitter(value);
break;
default:
log.appendMsg(
"Unknown attribute on <SearchItem> tag encountered: " + key +
", stored for use " +
"downstream.");
searchItem.addAttribute(key, value);
break;
}
}
return searchItem;
}
/**
* Parse each {@code <Byte>} sub tag. Bytes are currently assumed to be Hex or ASCII values and
* are parsed & stored accordingly. ASCII values are indicated in the XML as having a word size
* = -1. Each {@code<SearchItem>} has at least 1 {@code<Byte>} tag but could have hundreds of
* separate ones.
* <P>
* This method parses all {@code<byte>} tags and returns the list of {@link BpsPattern} objects
* which contain, not just the parsed byte string, but also metadata about the bytes.
* <P>
* <B>NOTE:</B> All byte patterns must be in big endian arrangement upon parsing.
*
* @param parser the parser
* @param wordSize the byte pattern word size
* @param log XML log
* @param errorHandler for standard XML violation errors
*
* @return list of byte patterns in the {@code<SearchItem>}
*
* @throws SAXException for malformed hex errors
*/
private static List<BpsPattern> gatherSearchItemBytes(XmlPullParser parser, int wordSize,
BpsSearchItem parentSearchItem, XmlMessageLog log,
AccumulatingErrorHandler errorHandler) throws SAXException {
List<BpsPattern> byteCollection = new ArrayList<>();
while (parser.peek().isStart()) {
XmlElement byteTag = parser.start();
String tagName = byteTag.getName();
// Ghidra's XML parser ties text content to XML elements (unlike other standard parsers)
// so we must process the closing tag first in order to capture the text as that is
// the tag that the text is attached to.
byteTag = parser.end(byteTag);
String byteString = byteTag.getText().trim();
byteString = byteString.replaceAll("\\s", "");
byte[] rawBytes;
ByteBuffer buffer = null;
if (wordSize == -1) { // indicates an ASCII value, handled differently
rawBytes = byteString.getBytes();
buffer = ByteBuffer.wrap(rawBytes);
}
else if (byteString.length() % 2 == 0) { // well-formed hex
rawBytes = HexFormat.of().parseHex(byteString);
buffer = ByteBuffer.wrap(rawBytes);
}
else {
trackParseError("Line: " + parser.getLineNumber() + ", Col: " +
parser.getColumnNumber() + ": Byte string is not an even size and " +
"cannot be parsed as Hex. Either adjust WordSize attribute to " +
"ASCII or zero-pad hex to make it well-formed. Skipping entire " +
"search item.", log, errorHandler);
// If even 1 <Byte> pattern is invalid, skip all <Byte> tags in the <SearchItem>
// So that we don't perform a search on an incomplete <SearchItem>
while (parser.peek().getName().equals("Bytes")) {
parser.next();
}
return null; // force the skipping of this <SearchItem>
}
byteCollection.add(
new BpsPattern(tagName, buffer, parentSearchItem, BpsTransformation.empty()));
}
return byteCollection;
}
/**
* Parse attribute values in the form of "Name"="Value". Any number of attributes may be
* included in the tag. Any attributes not outlined as required or optional will still be kept
* in a map for filtering downstream.
* <P>
* <B>NOTE:</B> All required attributes for a particular tag must be handled separately and are
* included in the skipList parameter so that they are not processed twice.
* <P>
*
* @param currTag current XML element
* @param skipList required attributes to skip and not be parsed/stored twice
*
* @return gathered tag attributes
*/
private static Map<String, String> gatherTagAttributes(XmlElement currTag,
List<String> skipList) {
Iterator<Entry<String, String>> attributes = currTag.getAttributeIterator();
Map<String, String> additionalTags = new TreeMap<>();
while (attributes.hasNext()) {
Entry<String, String> attribute = attributes.next();
String key = attribute.getKey();
if (!skipList.contains(key)) {
additionalTags.put(key, attribute.getValue());
}
}
return additionalTags;
}
/**
* Word sizes in the XML are indicated as String word sizes. Translate these to int values for
* use later. ASCII is encoded with a value of -1 and an unknown data type is encoded as the
* default size of 16 (word) with an error message appended to the handler.
* <P>
*
* @param wordSize parsed from "WordSize" attribute on {@code<SearchItem>} tag
* @param errorHandler track any errors encountered with parsing the word size
* @param log for logging progress
*
* @return word size, default 16 indicates an invalid word size
*
* @throws SAXException for invalid word size
*/
private static int lookUpWordSize(String wordSize, XmlMessageLog log,
AccumulatingErrorHandler errorHandler) throws SAXException {
switch (wordSize.toLowerCase()) {
case "byte":
return 8;
case "word":
return 16;
case "dword":
return 32;
case "qword":
return 64;
case "ascii":
return -1;
default:
trackParseError("Invalid word size encountered: " + wordSize + ". Default size " +
"of 16 is being used.", log, errorHandler);
return 16;
}
}
/**
* Helper for generating error alerts during parsing.
* <P>
*
* @param specificMessage error message specific to the problem encountered.
* @param log for tracking progress
* @param errorHandler for handling standard XML violation errors
*
* @throws SAXException for handling malformed XML parse errors
*/
private static void trackParseError(String specificMessage,
XmlMessageLog log, AccumulatingErrorHandler errorHandler) throws SAXException {
log.appendMsg(specificMessage);
errorHandler.error(new SAXParseException(specificMessage, null));
}
}
/**
* Custom exception class adds a field for holding collected errors for ease of access and testing
* downstream.
*/
class BpsXmlValidationException extends SAXException {
private final List<SAXParseException> errors = new ArrayList<>();
BpsXmlValidationException(String message, List<SAXParseException> errors) {
super(message);
this.errors.addAll(errors);
}
// This allows us to test the number of errors caught directly
int getErrorCount() {
return errors.size();
}
// Let users inspect the raw errors if needed
List<SAXParseException> getErrors() {
return errors;
}
}
/**
* Custom ErrorHandler for XML parsing which keeps track of errors allowing parsing to complete
* instead of breaking on minor infractions in the XML.
*/
class AccumulatingErrorHandler implements ErrorHandler {
private MessageLog log;
private final List<SAXParseException> exceptions = new ArrayList<>();
AccumulatingErrorHandler(MessageLog log) {
this.log = log;
}
@Override
public void error(SAXParseException exception) throws SAXException {
log.appendMsg(exception.getMessage());
exceptions.add(exception);
}
@Override
public void fatalError(SAXParseException exception) throws SAXException {
log.appendMsg(exception.getMessage());
exceptions.add(exception);
}
@Override
public void warning(SAXParseException exception) throws SAXException {
log.appendMsg(exception.getMessage());
exceptions.add(exception);
}
/**
* {@return the list of exceptions found during parsing}
*/
public List<SAXParseException> getExceptions() {
return exceptions;
}
/**
* {@return the error status of the parser}
*/
public boolean foundErrors() {
return !exceptions.isEmpty();
}
}

View File

@@ -0,0 +1,260 @@
/* ###
* 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.util.bytesearch.bytepatternsearch;
import static org.junit.Assert.*;
import java.io.File;
import java.nio.ByteBuffer;
import java.util.HexFormat;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import generic.test.AbstractGenericTest;
import ghidra.util.task.TaskMonitor;
import resources.ResourceManager;
public class BpsParserTest extends AbstractGenericTest {
private String xmlFile = "ghidra/util/bytesearch/bytepatternsearch/basic_search_collection.xml";
private File testXmlFile;
private List<BpsSearch> searchCollection;
@Before
public void setUp() throws Exception {
testXmlFile = ResourceManager.getResourceFile(xmlFile);
searchCollection = BpsXmlParser.parseSearchFile(testXmlFile, TaskMonitor.DUMMY);
}
/**
* Simple test for successful loading and parsing of example XML.
*/
@Test
public void testParserLoadXml() {
BpsSearch search1 = searchCollection.get(0);
assertEquals("SearchID wasn't generated correctly", "basic_search_collection.xml_Search1_1",
search1.getId());
assertEquals("1", search1.getDescription());
assertEquals("Search1", search1.getSearchName());
assertEquals("test", search1.getSubmitter());
assertEquals("Search1", search1.toString());
assertEquals("Incorrect number of search items parsed for collection", 1,
search1.getSearchItems().size());
BpsSearchItem searchItem = search1.getSearchItems().get(0);
assertEquals("SearchIteam Id wasn't generated correctly",
"basic_search_collection.xml_Constant 1_0", searchItem.getId());
assertEquals("Human", searchItem.getSubmitter());
assertEquals(searchItem.getSearchType(), BpsSearchType.CONSTANT_SEARCH);
assertEquals("Simple constant search", searchItem.getDescription());
assertEquals("Word size wasn't parsed correctly", 64, searchItem.getWordSize()); // QWord becomes 64
}
/**
* Ensure that multiple collections with multiple searchItems are being parsed correctly
*/
@Test
public void testMultiCollectionParse() {
assertEquals("Incorrect number of search collections parsed", 9,
searchCollection.size());
/*
Search Collection 3
<Search Name="Search3" Description="" Category="A" Type="Strict">
...
</SearchItem>
// 8 Search items
*/
BpsSearch search3 = searchCollection.get(2);
assertEquals("Collection 3 should have 8 search items", 8, search3.getSearchItems().size());
/*
Search Collection 5
<Search Name="Search5" Description="1">
<SearchItem Name="Constant 1" WordSize="byte" SearchType="Table" Submitter="Human" Description="Simple constant search" Reference="Dictionary">
<Bytes> 67 </Bytes>
...
...
// 34 Bytes elements
*/
BpsSearch search5 = searchCollection.get(4);
List<BpsSearchItem> searchItems = search5.getSearchItems();
BpsSearchItem searchItem = searchItems.get(0);
List<BpsPattern> bytes = searchItem.getPatterns();
assertEquals("Collection 5 should have 1 search item with 34 byte tags", 34, bytes.size());
}
/*
* The current legacy object, and XML, follows a construct with multiple tag types. We need to
* respect these tag types and store them as an attribute under "TagType" so that they can be
* easily retrieved to handle the bytes appropriately. Some tags are expected to occur
* in tandem with each other in the same SearchItem.
* <P>
* NOTE: future design is to eliminate multiple tag types and assign attributes to byte tags as
* the previous test assumes for.
*/
@Test
public void testByteTagsConvertedToAttributesToFitLegacyObject() {
int fluxCapacitor = 7;
BpsSearch search = searchCollection.get(fluxCapacitor);
List<BpsSearchItem> searchItems = search.getSearchItems();
BpsSearchItem searchItem = searchItems.get(0);
BpsPattern bytes = searchItem.getPatterns().get(0);
String tagName = bytes.getName();
assertEquals("Parser handles tags around byte strings as attributes",
"Bytes", tagName);
}
/**
* When each byte string is parsed, it should be stored according to the indicated DataType
* attribute on the {@code<SearchItem>} tag.
*/
@Test
public void testVerifyBytesParsedAccordingToXmlDataType() {
String originalHexString = "67e6096a85ae67bb";
BpsSearchItem searchItem = searchCollection.get(0).getSearchItems().get(0);
BpsPattern bytes = searchItem.getPatterns().get(0);
// QWord
ByteBuffer byteBuffer = bytes.getBuffer();
byte[] byteArray = new byte[byteBuffer.remaining()];
byteBuffer.get(byteArray);
assertEquals("Parsed bytes do not match original set", originalHexString,
HexFormat.of().formatHex(byteArray));
// Word
int fluxCapacitor = 5;
searchItem = searchCollection.get(fluxCapacitor).getSearchItems().get(0);
bytes = searchItem.getPatterns().get(0);
byteBuffer = bytes.getBuffer();
byteArray = new byte[byteBuffer.remaining()];
byteBuffer.get(byteArray);
assertEquals("Parsed bytes do not match original set", "67e6",
HexFormat.of().formatHex(byteArray));
}
/**
* For ease of tracking search item details later, keep a link between the byte pattern and its
* parent search item.
*/
@Test
public void testPatternLinkBacktoParentSearchItem() {
BpsSearchItem parentItem =
searchCollection.get(0).getSearchItems().get(0);
BpsPattern childPattern = parentItem.getPatterns().get(0);
assertEquals("Patterns should link to their parent search item.", parentItem,
childPattern.getSearchItem());
assertEquals("Parent item knows the search type",
childPattern.getSearchItem().getSearchType(), BpsSearchType.CONSTANT_SEARCH);
}
/**
* Generate search type objects for tracking search-specific information and use later as
* part of search post-processing.
*/
@Test
public void testSearchTypeObjectGeneration() {
BpsSearchItem searchItem = searchCollection.get(0).getSearchItems().get(0);
assertEquals("Wrong (or no) search type object created", searchItem.getSearchType(),
BpsSearchType.CONSTANT_SEARCH);
searchItem = searchCollection.get(1).getSearchItems().get(0);
assertEquals("Wrong (or no) search type object created", searchItem.getSearchType(),
BpsSearchType.TABLE_SEARCH);
}
@Test
public void testAdditionalAttributesOnSearchandSearchItems() {
int searchIndex = 2;
BpsSearch search = searchCollection.get(searchIndex);
assertEquals("Search3 tag has 2 additional attributes", 2,
search.getAdditionalAttributes().size());
BpsSearchItem searchItem = search.getSearchItems().get(0);
assertEquals("SearchItem has 1 additional attribute (Reference)", 1,
searchItem.getAdditionalAttributes().size());
}
@Test
public void testParsingErrorHandler() throws SAXException {
String currXmlFile = "ghidra/util/bytesearch/bytepatternsearch/parse_errors.xml";
File currTestXmlFile = ResourceManager.getResourceFile(currXmlFile);
String message = "";
int errorCount = 0;
List<SAXParseException> errors = null;
try {
searchCollection = BpsXmlParser.parseSearchFile(currTestXmlFile, TaskMonitor.DUMMY);
fail("Should have thrown a SAXException but did not");
}
catch (BpsXmlValidationException e) {
message = e.getMessage();
errorCount = e.getErrorCount();
errors = e.getErrors();
}
assertEquals(12, errorCount);
// error with wrapper tag
assertTrue(message.contains("XML parsing failed"));
assertTrue(
message.contains(
"Pattern file must have a wrapper <BytePatternSearchLibrary> tag to start."));
// error with level tag
assertTrue(message.contains("<Search> tag expected"));
// ensure correct errors were collected
assertEquals(
"org.xml.sax.SAXParseException; Line: 54, Col: 11: Byte string is not an even size and cannot be parsed as Hex. Either adjust WordSize attribute to ASCII or zero-pad hex to make it well-formed. Skipping entire search item.",
errors.get(8).toString());
}
/**
* Verify successful loading of provided sample file with slightly different schema.
*
* @throws SAXException for parsing exception
*/
@Test
public void testProvidedSamplePatternFile() throws SAXException {
String currXmlFile =
"ghidra/util/bytesearch/bytepatternsearch/dword_qword_for32bit_pattern.xml";
File currTestXmlFile = ResourceManager.getResourceFile(currXmlFile);
try {
searchCollection = BpsXmlParser.parseSearchFile(currTestXmlFile, TaskMonitor.DUMMY);
}
catch (BpsXmlValidationException e) {
fail("This file should parse without errors to prove compatibility");
}
BpsSearchItem item = searchCollection.get(0).getSearchItems().get(0);
assertEquals("SearchItem was not parsed correctly.", "DIP32",
item.getPatternName());
assertEquals("Search tag name type isn't correct", "FunctionConstant",
item.getPatterns().get(0).getName());
}
}

View File

@@ -0,0 +1,153 @@
<BytePatternSearchLibrary>
<Search Name="Search1" Description="1" Submitter="test">
<SearchItem Name="Constant 1" WordSize="QWord" SearchType="Constant" Submitter="Human" Description="Simple constant search" Reference="Dictionary">
<Bytes> 67e6096a85ae67bb </Bytes>
</SearchItem>
</Search>
<Search Name="Search2" Description="">
<SearchItem Name="Constant 1" WordSize="DWord" SearchType="Table" Submitter="Human" Description="Simple constant search" Reference="Dictionary">
<Bytes> 67e6096a </Bytes>
<Bytes> 85ae67bb </Bytes>
<Bytes> 67e6096a </Bytes>
<Bytes> 85ae67bb </Bytes>
</SearchItem>
<SearchItem Name="Constant" WordSize="DWord" SearchType="Table" Submitter="Human" Description="Simple constant search" Reference="Dictionary">
<Bytes> 67e6096a85ae67bb </Bytes>
<Bytes> 67e6096a </Bytes>
<Bytes> 85ae67bb </Bytes>
<Bytes> 67e6096a85ae67bb </Bytes>
</SearchItem>
</Search>
<Search Name="Search3" Description="" Category="A" Type="Strict">
<SearchItem Name="Constant 1" WordSize="QWord" SearchType="Function" Submitter="Human" Description="Simple constant search" Reference="Dictionary">
<Bytes> 67e6096a85ae67bb </Bytes>
<Bytes> 67e6096a85ae67bb </Bytes>
</SearchItem>
<SearchItem Name="Constant 2" WordSize="QWord" SearchType="Function" Submitter="Human" Description="Simple constant search" Reference="Dictionary">
<Bytes> 67e6096a85ae67bb </Bytes>
<Bytes> 67e6096a85ae67bb </Bytes>
</SearchItem>
<SearchItem Name="Constant 3" WordSize="QWord" SearchType="Function" Submitter="Human" Description="Simple constant search" Reference="Dictionary">
<Bytes> 67e6096a85ae67bb </Bytes>
<Bytes> 67e6096a85ae67bb </Bytes>
</SearchItem>
<SearchItem Name="Constant 4" WordSize="QWord" SearchType="Function" Submitter="Human" Description="Simple constant search" Reference="Dictionary">
<Bytes> 67e6096a85ae67bb </Bytes>
<Bytes> 67e6096a85ae67bb </Bytes>
</SearchItem>
<SearchItem Name="Constant 5" WordSize="QWord" SearchType="Function" Submitter="Human" Description="Simple constant search" Reference="Dictionary">
<Bytes> 67e6096a85ae67bb </Bytes>
<Bytes> 67e6096a85ae67bb </Bytes>
</SearchItem>
<SearchItem Name="Constant 6" WordSize="QWord" SearchType="Function" Submitter="Human" Description="Simple constant search" Reference="Dictionary">
<Bytes> 67e6096a85ae67bb </Bytes>
<Bytes> 67e6096a85ae67bb </Bytes>
</SearchItem>
<SearchItem Name="Constant 7" WordSize="QWord" SearchType="Function" Submitter="Human" Description="Simple constant search" Reference="Dictionary">
<Bytes> 67e6096a85ae67bb </Bytes>
<Bytes> 67e6096a85ae67bb </Bytes>
</SearchItem>
<SearchItem Name="Constant 8" WordSize="QWord" SearchType="Function" Submitter="Human" Description="Simple constant search" Reference="Dictionary">
<Bytes> 67e6096a85ae67bb </Bytes>
<Bytes> 67e6096a85ae67bb </Bytes>
</SearchItem>
</Search>
<Search Name="Search4" Description="">
<SearchItem Name="Constant 1" WordSize="QWord" SearchType="Function" Submitter="Human" Description="Simple constant search" Reference="Dictionary">
<Bytes> 67e6096a85ae67bb </Bytes>
<Bytes> 67e6096a85ae67bb </Bytes>
</SearchItem>
<SearchItem Name="Constant 2" WordSize="QWord" SearchType="Function" Submitter="Human" Description="Simple constant search" Reference="Dictionary">
<Bytes> 67e6096a85ae67bb </Bytes>
<Bytes> 67e6096a85ae67bb </Bytes>
</SearchItem>
<SearchItem Name="Constant 3" WordSize="QWord" SearchType="Function" Submitter="Human" Description="Simple constant search" Reference="Dictionary">
<Bytes> 67e6096a85ae67bb </Bytes>
<Bytes> 67e6096a85ae67bb </Bytes>
</SearchItem>
<SearchItem Name="Constant 4" WordSize="QWord" SearchType="Function" Submitter="Human" Description="Simple constant search" Reference="Dictionary">
<Bytes> 67e6096a85ae67bb </Bytes>
<Bytes> 67e6096a85ae67bb </Bytes>
</SearchItem>
<SearchItem Name="Constant 5" WordSize="QWord" SearchType="Function" Submitter="Human" Description="Simple constant search" Reference="Dictionary">
<Bytes> 67e6096a85ae67bb </Bytes>
<Bytes> 67e6096a85ae67bb </Bytes>
</SearchItem>
<SearchItem Name="Constant 6" WordSize="QWord" SearchType="Function" Submitter="Human" Description="Simple constant search" Reference="Dictionary">
<Bytes> 67e6096a85ae67bb </Bytes>
<Bytes> 67e6096a85ae67bb </Bytes>
</SearchItem>
<SearchItem Name="Constant 7" WordSize="QWord" SearchType="Function" Submitter="Human" Description="Simple constant search" Reference="Dictionary">
<Bytes> 67e6096a85ae67bb </Bytes>
<Bytes> 67e6096a85ae67bb </Bytes>
</SearchItem>
<SearchItem Name="Constant 8" WordSize="QWord" SearchType="Function" Submitter="Human" Description="Simple constant search" Reference="Dictionary">
<Bytes> 67e6096a85ae67bb </Bytes>
<Bytes> 67e6096a85ae67bb </Bytes>
</SearchItem>
</Search>
<Search Name="Search5" Description="1">
<SearchItem Name="Constant 1" WordSize="byte" SearchType="Table" Submitter="Human" Description="Simple constant search" Reference="Dictionary">
<Bytes> 67 </Bytes>
<Bytes> e6 </Bytes>
<Bytes> 09 </Bytes>
<Bytes> 6a </Bytes>
<Bytes> 85 </Bytes>
<Bytes> ae </Bytes>
<Bytes> 67 </Bytes>
<Bytes> bb </Bytes>
<Bytes> 67 </Bytes>
<Bytes> e6 </Bytes>
<Bytes> 09 </Bytes>
<Bytes> 6a </Bytes>
<Bytes> 85 </Bytes>
<Bytes> ae </Bytes>
<Bytes> 67 </Bytes>
<Bytes> bb </Bytes>
<Bytes> 67 </Bytes>
<Bytes> e6 </Bytes>
<Bytes> 09 </Bytes>
<Bytes> 6a </Bytes>
<Bytes> 85 </Bytes>
<Bytes> ae </Bytes>
<Bytes> 67 </Bytes>
<Bytes> bb </Bytes>
<Bytes> 67 </Bytes>
<Bytes> e6 </Bytes>
<Bytes> 09 </Bytes>
<Bytes> 6a </Bytes>
<Bytes> 85 </Bytes>
<Bytes> ae </Bytes>
<Bytes> 67 </Bytes>
<Bytes> bb </Bytes>
<Bytes> 67 </Bytes>
<Bytes> e6 </Bytes>
</SearchItem>
</Search>
<Search Name="Search6" Description="1">
<SearchItem Name="Constant" WordSize="word" SearchType="Function" Submitter="Human" Description="Simple constant search" Reference="Dictionary">
<Bytes> 67e6 </Bytes>
<Bytes> 096a </Bytes>
<Bytes> 85ae </Bytes>
<Bytes> 67bb </Bytes>
</SearchItem>
</Search>
<Search Name="Search7" Description="1">
<SearchItem Name="Constant" WordSize="QWord" SearchType="Constant" Submitter="Human" Description="Simple constant search" Reference="Dictionary">
<Bytes> 67e6096a85ae67bb </Bytes>
</SearchItem>
</Search>
<Search Name="Search8">
<SearchItem Name="TagTest" WordSize="Word" SearchType="orderedfunction">
<Bytes>01 23</Bytes>
<Bytes>45 67</Bytes>
<InvalidatorBytes>89 ab</InvalidatorBytes>
</SearchItem>
</Search>
<Search Name="Search9">
<SearchItem Name="ascii test" WordSize="Ascii" SearchType="Constant">
<Bytes>hello</Bytes>
<Bytes>world</Bytes>
</SearchItem>
</Search>
</BytePatternSearchLibrary>

View File

@@ -0,0 +1,35 @@
<BytePatternSearchLibrary>
<Family Name="TestD" Filter="TypeA">
<SearchItem Name="DIP32" DataType="Dword" SearchType="Function" NotInSearchLength="0" SearchLength="5" Classifier="Test" Submitter="JJ" Description="DIP 32 bit function contants (45188e60)">
<FunctionConstant>f0f0f0f0</FunctionConstant>
<FunctionConstant>fff0000f</FunctionConstant>
<FunctionConstant>33333333</FunctionConstant>
<FunctionConstant>03fc03fc</FunctionConstant>
<FunctionConstant>aaaaaaaa</FunctionConstant>
</SearchItem>
<SearchItem Name="DIP64" DataType="Qword" SearchType="Function" NotInSearchLength="0" SearchLength="3" Classifier="Test" Submitter="JJ" Description="DIP 64 bit function constants - made up for testing purposes (45188e60)">
<FunctionConstant>f0f0f0f0fff0000f</FunctionConstant>
<FunctionConstant>3333333303fc03fc</FunctionConstant>
<FunctionConstant>aaaaaaaafcfcfcfc</FunctionConstant>
</SearchItem>
<SearchItem Name="DER" DataType="Byte" SearchType="WholeTable" TableLength="16" Classifier="Test" Submitter="JJ" Description="DER Byte Table (45182880)">
<AllBytes>00 00 01 01 01 01 01 01 00 01 01 01 01 01 01 00</AllBytes>
</SearchItem>
<SearchItem Name="DTr0" DataType="Dword" SearchType="WholeTable" TableLength="64" Classifier="Test" Submitter="JJ" Description="DTr0 Dword Table (45182890)">
<AllBytes>02080800 00080000 02000002 02080802 02000000 00080802 00080002 02000002 00080802 02080800 02080000 00000802 02000802 02000000 00000000 00080002 00080000 00000002 02000800 00080800 02080802 02080000 00000802 02000800 00000002 00000800 00080800 02080002 00000800 02000802 02080002 00000000 00000000 02080802 02000800 00080002 02080800 00080000 00000802 02000800 02080002 00000800 00080800 02000002 00080802 00000002 02000002 02080000 02080802 00080800 02080000 02000802 02000000 00000802 00080002 00000000 00080000 02000000 02000802 02080800 00000002 02080002 00000800 00080802</AllBytes>
</SearchItem>
</Family>
<Family Name="TestQ64" Filter="TypeB">
<SearchItem Name="QFunction1" DataType="Qword" SearchType="Function" NotInSearchLength="0" SearchLength="3" Classifier="Test" Submitter="JJ" Description="Unsure what function contants in (180008258)">
<FunctionConstant>00002b992ddfa232</FunctionConstant>
<FunctionConstant>ffffffffffffffff</FunctionConstant>
<FunctionConstant>00002b992ddfa233</FunctionConstant>
</SearchItem>
<SearchItem Name="QFunction2" DataType="Qword" SearchType="Function" NotInSearchLength="0" SearchLength="2" Classifier="Test" Submitter="JJ" Description="Unsure what function contants refed by (180008258)">
<FunctionConstant>FFFFD466D2205DCD</FunctionConstant>
<FunctionConstant>00002B992DDFA232</FunctionConstant>
</SearchItem>
</Family>
</BytePatternSearchLibrary>

View File

@@ -0,0 +1,101 @@
<Wrapper>
<BadTag>
<SearchItem>
<Bytes>00 00</Bytes>
</SearchItem>
</BadTag>
<Search noAttribute="name">
<SearchItem>
<Bytes>00 00</Bytes>
</SearchItem>
</Search>
<Search Name="good">
<SearchPiece Name="good" WordSize="QWord">
<Bytes>00 00</Bytes>
</SearchPiece>
</Search>
<Search Name="good">
<SearchItem Name="Bad" WordSize="DWord" SearchPlan="bad">
<Bytes>00 00</Bytes>
</SearchItem>
</Search>
<Search Name="good">
<SearchItem Name="notBad" DataType="DWord" SearchType="function">
<Bytes>00 00 00 00 00 00</Bytes>
</SearchItem>
</Search>
<Search Name="2SearchItems">
<SearchItem Name="notBad" DataType="DWord" SearchType="function">
<Bytes>00 00 00 00 00 00</Bytes>
</SearchItem>
<SearchItem Name="bad" DataType="DWord" SearchType="bad">
<Bytes>00 00 00 00 00 00</Bytes>
</SearchItem>
</Search>
<Search missing="BadFamily">
<SearchItem Name="notBad" DataType="DWord" SearchType="function">
<Bytes>00 00 00 00 00 00</Bytes>
</SearchItem>
<SearchItem Name="notBad2" DataType="DWord" SearchType="function">
<Bytes>00 00 00 00 00 00</Bytes>
</SearchItem>
</Search>
<Search Name="1SearchItemBad">
<SearchItem Name="shouldBeSkipped" dt="DWord" SearchType="function">
<Bytes>00 00 00 00 00 00</Bytes>
</SearchItem>
<SearchItem Name="shouldNotBeSkiped" DataType="DWord" SearchType="function">
<Bytes>00 00 00 00 00 00</Bytes>
</SearchItem>
</Search>
<Search Name="Search9">
<SearchItem Name="UnevenHexTest" DataType="DWord" SearchLength="2" SearchType="Constant">
<Bytes>67e6096</Bytes>
<Bytes>67e6096a</Bytes>
</SearchItem>
</Search>
<Search Name="SameName">
<SearchItem Name="UniqueName" DataType="DWord" SearchLength="2" SearchType="Constant">
<Bytes>67e60960</Bytes>
<Bytes>67e6096a</Bytes>
</SearchItem>
</Search>
<Search Name="SameName">
<SearchItem Name="SameName" DataType="DWord" SearchLength="2" SearchType="Constant">
<Bytes>67e6096a</Bytes>
<Bytes>67e6096a</Bytes>
</SearchItem>
<SearchItem Name="Samename" DataType="DWord" SearchLength="2" SearchType="Constant">
<Bytes>67e60a96</Bytes>
<Bytes>67e6096a</Bytes>
</SearchItem>
</Search>
<Search Name="DifferentName">
<SearchItem Name="SameName" DataType="DWord" SearchLength="2" SearchType="Constant">
<Bytes>67e6096a</Bytes>
<Bytes>67e6096a</Bytes>
</SearchItem>
<SearchItem Name="Samename" DataType="DWord" SearchLength="2" SearchType="Constant">
<Bytes>67e60a96</Bytes>
<Bytes>67e6096a</Bytes>
</SearchItem>
</Search>
<Search Name="Invalid WordSize">
<SearchItem Name="bad wordsize" DataType="bad" SearchLength="2" SearchType="Constant">
<Bytes>67e6096a</Bytes>
<Bytes>67e6096a</Bytes>
</SearchItem>
</Search>
<Search>
<SearchItem>
<Bytes>67e6096a</Bytes>
<Bytes>67e6096a</Bytes>
</SearchItem>
</Search>
<Search Name="no attrs on searchItem">
<SearchItem>
<Bytes>67e6096a</Bytes>
<Bytes>67e6096a</Bytes>
</SearchItem>
</Search>
</Wrapper>