rizin/librz/search/bytes_search.c

354 lines
12 KiB
C

// SPDX-FileCopyrightText: 2024 RizinOrg <info@rizin.re>
// SPDX-FileCopyrightText: 2024 deroad <wargio@libero.it>
// SPDX-License-Identifier: LGPL-3.0-only
#include <rz_search.h>
#include <rz_util.h>
#include <rz_util/rz_assert.h>
#include <rz_util/rz_str.h>
#include <rz_util/rz_buf.h>
#include <rz_util/rz_regex.h>
#include <rz_util/rz_mem.h>
#include "search_internal.h"
/**
* \brief Initialize a new search bytes pattern and return it.
*
* \param bytes The bytes to search for.
* \param mask The mask to apply to the pattern and the data before comparison. (optional)
* \param length Length of \p bytes and \p mask (if not NULL).
* \param pattern_desc An optional description string of the pattern.
* \param compile_regex If true it compiles \p bytes as regex.
* This will make the search use the regex, instead of comparing the bytes.
* The \p mask is ignored in this case.
*
* \return The initalized pattern or NULL in case of failure.
*/
RZ_API RZ_OWN RzSearchBytesPattern *rz_search_bytes_pattern_new(RZ_OWN ut8 *bytes, RZ_NULLABLE RZ_OWN ut8 *mask, size_t length, RZ_NULLABLE const char *pattern_desc, bool compile_regex) {
rz_return_val_if_fail(bytes && length > 0, NULL);
RzSearchBytesPattern *pat = RZ_NEW0(RzSearchBytesPattern);
if (!pat) {
RZ_LOG_ERROR("Failed to allocate pattern struct.\n");
free(bytes);
free(mask);
return NULL;
}
pat->bytes = bytes;
if (compile_regex) {
pat->regex = rz_regex_new_bytes(bytes, length, RZ_REGEX_EXTENDED, RZ_REGEX_DEFAULT, NULL);
}
pat->mask = mask;
pat->length = length;
pat->pattern_desc = pattern_desc;
return pat;
}
RZ_API void rz_search_bytes_pattern_free(RZ_NULLABLE RZ_OWN RzSearchBytesPattern *hp) {
if (!hp) {
return;
}
free(hp->bytes);
rz_regex_free(hp->regex);
free(hp->mask);
free(hp);
}
/**
* \brief Return the pattern description if any.
*
* \return The pattern description string or NULL if there is none.
*/
RZ_API const char *rz_search_bytes_pattern_desc(RZ_NONNULL const RzSearchBytesPattern *bp) {
rz_return_val_if_fail(bp, 0);
return bp->pattern_desc;
}
/**
* \brief Return the pattern length in number of bytes.
*
* \return Return the pattern length in number of bytes or 0 in case of failure.
*/
RZ_API size_t rz_search_bytes_pattern_len(RZ_NONNULL const RzSearchBytesPattern *bp) {
rz_return_val_if_fail(bp, 0);
return bp->length;
}
RZ_API RZ_OWN RzSearchBytesPattern *rz_search_bytes_pattern_copy(RZ_NONNULL RZ_BORROW RzSearchBytesPattern *hp) {
rz_return_val_if_fail(hp, NULL);
return rz_search_bytes_pattern_new(rz_new_copy(hp->length, hp->bytes), rz_new_copy(hp->length, hp->mask), hp->length, hp->pattern_desc, hp->regex != NULL);
}
static bool parse_custom_mask(const char *bytes_pattern, const RzRegexMatch *mask_match, const RzRegexMatch *bytes_match, ut8 *mask) {
if (mask_match->len != bytes_match->len) {
RZ_LOG_ERROR("Mask and bytes must have the same number of nibbles. "
"But they mismatch: %" PFMTSZu " != %" PFMTSZu "\n",
mask_match->len, bytes_match->len);
return false;
}
if (strchr(bytes_pattern + bytes_match->start, '.')) {
RZ_LOG_ERROR("With a custom mask no wildcards are allowed.\n");
return false;
}
rz_hex_str2bin_mask(bytes_pattern + mask_match->start, mask, NULL, false);
return true;
}
/**
* \brief Parses a byte pattern description string to a RzSearchBytesPattern object.
*
* The given byte pattern string should be of the form: `<bytes>:<mask>`.
*
* Formatting rules:
* - The `mask` is optional.
* - Mask and bytes should be given in hexadeciaml numbers.
* - If the mask is given, it must be the same length as the bytes.
* - Each nibble can be replaced with a '.' to indicate a wildcard nibble.
* - Odd number of nibbles are extended on the left hand side (MSB side).
* This means the nibble at the right hand side is aligned to a byte.
*
* Examples:
* - `a987`: Exact pattern for the bytes '\xa9\x87'.
* - `a9.7`: Pattern for the bytes '\xa9\x.7', but the '.' part can be any nibble.
* - `.a9...c.99`: Pattern with multiple wildcards.
* - `a907:ff0f`: Pattern with a mask of `ff0f` and bytes of '\xa9\x07'.
* - `0xa907:0xff0f`: Mask and bytes with "0x" prefixes.
*/
RZ_API RZ_OWN RzSearchBytesPattern *rz_search_parse_byte_pattern(const char *byte_pattern, RZ_NULLABLE const char *pattern_desc) {
rz_return_val_if_fail(byte_pattern, NULL);
size_t size = 0;
ut8 *bytes = RZ_NEWS0(ut8, strlen(byte_pattern) + 1);
ut8 *mask = RZ_NEWS0(ut8, strlen(byte_pattern) + 1);
RzPVector *matches = NULL;
if (!bytes || !mask) {
RZ_LOG_ERROR("Allocation falied.\n");
goto error;
}
// Some sanity checks
size_t ddot_count = rz_str_char_count(byte_pattern, ':');
if (ddot_count > 1) {
RZ_LOG_ERROR("More than one ':' is invalid.\n");
goto error;
}
bool has_wildcard = strchr(byte_pattern, '.') != NULL;
bool custom_mask = ddot_count == 1 ? true : false;
if (rz_regex_contains("[^a-fA-F0-9.:x]", byte_pattern, RZ_REGEX_ZERO_TERMINATED, 0, RZ_REGEX_DEFAULT)) {
RZ_LOG_ERROR("Pattern contains forbitten characters. Allowed is only '0x', '0-9', 'a-f', 'A-F', '.' and ':'.\n");
goto error;
}
RzRegex *regex = rz_regex_new("^(0x)?([a-fA-F.0-9]+)(:(0x)?([a-fA-F0-9.]+))?", RZ_REGEX_DEFAULT, RZ_REGEX_DEFAULT, NULL);
matches = rz_regex_match_all_not_grouped(regex, byte_pattern, RZ_REGEX_ZERO_TERMINATED, 0, RZ_REGEX_DEFAULT);
rz_regex_free(regex);
if (!matches) {
RZ_LOG_ERROR("Regex matching failed.\n");
goto error;
}
RzRegexMatch *bytes_match = rz_pvector_at(matches, 2);
RzRegexMatch *mask_match = rz_pvector_at(matches, 5);
if (!bytes_match || (custom_mask && !mask_match)) {
RZ_LOG_ERROR("Regex matching failed. Wrong group count.\n");
goto error;
}
if (custom_mask && strchr(byte_pattern + mask_match->len, '.')) {
RZ_LOG_ERROR("The mask cannot contain wildcards.\n");
goto error;
}
bool odd_nibbles = bytes_match->len % 2 == 1;
bool use_mask = custom_mask || has_wildcard || odd_nibbles;
if (custom_mask) {
use_mask = true;
if (!parse_custom_mask(byte_pattern, mask_match, bytes_match, mask)) {
goto error;
}
}
char *byte_str = rz_str_newlen(byte_pattern + bytes_match->start, bytes_match->len);
size = rz_hex_str2bin_mask(byte_str, bytes, custom_mask ? NULL : mask, false);
free(byte_str);
rz_pvector_free(matches);
RzSearchBytesPattern *pat = rz_search_bytes_pattern_new(bytes, use_mask ? mask : NULL, size, pattern_desc, false);
if (!use_mask) {
free(mask);
}
return pat;
error:
free(mask);
free(bytes);
rz_pvector_free(matches);
return NULL;
}
static inline bool bytes_pattern_compare_no_mask(RZ_BORROW RZ_NONNULL const ut8 *buffer, RzSearchBytesPattern *hp) {
// Without mask only a memcmp().
return memcmp(buffer, hp->bytes, hp->length) == 0;
}
static inline bool bytes_pattern_compare_masked(RZ_BORROW RZ_NONNULL const ut8 *buffer, RzSearchBytesPattern *hp) {
// We can't compare by casting the buffer address
// to an ut64, ut32 etc. because the buffer address can be unaligned
// for this integer. So we would get undefined behavior.
// This is pretty much the simplest we can do
// (except writing assembly I guess).
for (size_t i = 0; i < hp->length; i++) {
ut8 mbyte = hp->mask[i];
if ((hp->bytes[i] & mbyte) != (buffer[i] & mbyte)) {
return false;
}
}
return true;
}
static bool bytes_find(RzSearchFindOpt *fopts, void *user, ut64 address, const RzBuffer *buffer,
RZ_OUT RzThreadQueue *hits, RZ_OUT size_t *n_hits) {
if (!fopts) {
RZ_LOG_ERROR("bytes_find requires valid find options.\n");
return false;
}
ut64 size = 0;
// Remove const classifier. Because the buffer API is not constified, unfortunately.
const ut8 *raw_buf = rz_buf_get_whole_hot_paths((RzBuffer *)buffer, &size);
void **it = NULL;
RzPVector /*<BytesPattern *>*/ *patterns = (RzPVector *)user;
*n_hits = 0;
rz_pvector_foreach (patterns, it) {
RzSearchBytesPattern *hp = (RzSearchBytesPattern *)*it;
if (hp->regex) {
RzRegexMulti *re = rz_regex_multi_clone(hp->regex, true);
RzPVector *matches = fopts->match_overlap ? rz_regex_match_all_overlap(hp->regex, (const char *)raw_buf, size, 0, RZ_REGEX_DEFAULT) : rz_regex_match_all(hp->regex, (const char *)raw_buf, size, 0, RZ_REGEX_DEFAULT);
rz_regex_free_multi_clone(re);
void **it;
RzPVector *match;
rz_pvector_foreach (matches, it) {
match = *it;
RzRegexMatch *group0 = rz_pvector_at(match, 0);
if (fopts->alignment > 1 && rz_mem_align_padding(address + group0->start, fopts->alignment) != 0) {
// Match has not the correct alignment in memory.
continue;
}
RzSearchHit *hit = rz_search_hit_new(hp->pattern_desc, address + group0->start, group0->len, NULL);
if (!hit || !rz_th_queue_push(hits, hit, true)) {
rz_search_hit_free(hit);
rz_pvector_free(matches);
return false;
}
(*n_hits)++;
}
rz_pvector_free(matches);
continue;
}
for (size_t offset = 0; offset < size;) {
RAW_BUF_ITER_ALIGN(fopts, address, offset);
size_t leftovers = size - offset;
if (hp->length > leftovers) {
break;
}
if (!hp->mask) {
if (!bytes_pattern_compare_no_mask(raw_buf + offset, hp)) {
offset++;
continue;
}
} else {
if (!bytes_pattern_compare_masked(raw_buf + offset, hp)) {
offset++;
continue;
}
}
RzSearchHit *hit = rz_search_hit_new(hp->pattern_desc, address + offset, hp->length, NULL);
if (!hit || !rz_th_queue_push(hits, hit, true)) {
rz_search_hit_free(hit);
return false;
}
(*n_hits)++;
offset += fopts->match_overlap ? 1 : hp->length;
}
}
return true;
}
static bool bytes_is_empty(void *user) {
return rz_pvector_empty((RzPVector *)user);
}
/**
* \brief Allocates and initialize a RzSearchCollection for hexadecimal byte searching.
*
* \return On success returns a valid pointer, otherwise NULL
*/
RZ_API RZ_OWN RzSearchCollection *rz_search_collection_bytes() {
RzPVector /*<BytesPattern *>*/ *patterns = rz_pvector_new((RzPVectorFree)rz_search_bytes_pattern_free);
if (!patterns) {
RZ_LOG_ERROR("search: failed to initialize bytes collection\n");
return NULL;
}
return rz_search_collection_new_bytes_space(bytes_find, bytes_is_empty, (RzSearchFreeCallback)rz_pvector_free, patterns);
}
/**
* \brief Parses and adds a hex pattern into a bytes RzSearchCollection
*
* \param[in] col The RzSearchCollection to use
* \param[in] bytes_pattern The hexadecimal pattern to add
*
* \return On success returns true, otherwise false
*/
RZ_API bool rz_search_collection_bytes_add_pattern(RZ_NONNULL RzSearchCollection *col, RZ_NONNULL RZ_OWN RzSearchBytesPattern *bytes_pattern) {
rz_return_val_if_fail(col && bytes_pattern, false);
if (!rz_search_collection_has_find_callback(col, bytes_find)) {
RZ_LOG_ERROR("search: cannot add hex to non-bytes collection\n");
rz_search_bytes_pattern_free(bytes_pattern);
return false;
}
if (!rz_pvector_push((RzPVector *)col->user, bytes_pattern)) {
RZ_LOG_ERROR("search: cannot add byte pattern to search.\n");
rz_search_bytes_pattern_free(bytes_pattern);
return false;
}
return true;
}
/**
* \brief Adds a custom bytes & mask pattern into a bytes RzSearchCollection
*
* \param col The RzSearchCollection to use
* \param[in] pattern_desc The pattern description
* \param[in] bytes The bytes to use
* \param[in] mask The mask to apply (can be NULL)
* \param[in] length The length of bytes & mask
*
* \return On success returns true, otherwise false
*/
RZ_API bool rz_search_collection_bytes_add(RZ_NONNULL RzSearchCollection *col, RZ_NULLABLE const char *pattern_desc, RZ_NONNULL const ut8 *bytes, RZ_NULLABLE const ut8 *mask, size_t length) {
rz_return_val_if_fail(col && pattern_desc && bytes, false);
if (!rz_search_collection_has_find_callback(col, bytes_find)) {
RZ_LOG_ERROR("search: cannot add bytes to non-bytes collection\n");
return false;
} else if (length < 1) {
RZ_LOG_ERROR("search: cannot add an empty byte sequence to a bytes collection\n");
return false;
} else if (RZ_STR_ISEMPTY(pattern_desc)) {
RZ_LOG_ERROR("search: metadata is empty for the bytes collection\n");
return false;
}
RzSearchBytesPattern *hp = rz_search_bytes_pattern_new(rz_new_copy(length, bytes), rz_new_copy(length, mask), length, pattern_desc, false);
if (!hp) {
return false;
} else if (!rz_pvector_push((RzPVector *)col->user, hp)) {
RZ_LOG_ERROR("search: cannot add bytes pattern.\n");
rz_search_bytes_pattern_free(hp);
return false;
}
return true;
}