diff --git a/docs/hooks/address-fetching.md b/docs/hooks/address-fetching.md index ac2f793..7801d73 100644 --- a/docs/hooks/address-fetching.md +++ b/docs/hooks/address-fetching.md @@ -4,29 +4,39 @@ ## Pattern Scan -To do a pattern scan, call: +To do a pattern scan using KMP, given string pattern: ```cpp -auto* addr = dku::Hook::Assembly::search_pattern< - "40 57 " // each pattern is separated by whitespace " " - "48 83 EC 30 " - "48 8B 0D ?? ?? ?? ?? " // wildcard is ?? - "48 8B FA " - "48 81 C1 D0 00 00 00 " - "E8 ?? ?? ?? ?? " - "48 8B C8 " - "E8 ?? ?? ?? ??">(); -INFO("found address at {:X}", AsAddress(addr)); - -// delayed match -auto TestAlByte = dku::Hook::Assembly::make_pattern<"84 C0">(); -auto addr = 0x7FF712345678; -if (TestAlByte.match(addr)) {} +std::string pattern{ "40 57 48 83 EC 30 48 8B 0D ?? ?? ?? ??" }; ``` -::: warning Planned Change -This API has been planned for breaking changes. -::: +### Syntax + +```cpp +void* search_pattern( + std::string_view pattern, + std::uintptr_t base = 0, + std::size_t size = 0 +); +``` + +### Parameter + ++ `pattern` : string pattern to search, spacing is optional, each byte must be two characters. ++ `base` : address of memory block to search, defaults to module.textx. ++ `size` : size of memory block to search, defaults to module.textx.size. + +Returns `nullptr` if not found. Otherwise return the first match. + +### Linear Search + +To use pattern at compile time, use specialized template version: + +```cpp +void* search_pattern<"40 57 48 83 EC 30 48 8B 0D ?? ?? ?? ??">(base = 0, size = 0); +``` + +This template version performs a linear search instead of default KMP. ## Rip Addressing diff --git a/include/DKUtil/Impl/Hook/Assembly.hpp b/include/DKUtil/Impl/Hook/Assembly.hpp index 0fbe73a..682d149 100644 --- a/include/DKUtil/Impl/Hook/Assembly.hpp +++ b/include/DKUtil/Impl/Hook/Assembly.hpp @@ -83,7 +83,7 @@ namespace DKUtil::Hook::Assembly namespace rules { - [[nodiscard]] inline consteval std::byte hexachar_to_hexadec(char a_hi, char a_lo) noexcept + [[nodiscard]] inline constexpr std::byte hexachar_to_hexadec(char a_hi, char a_lo) noexcept { constexpr auto lut = []() noexcept { std::array::max() + 1> a{}; @@ -202,19 +202,168 @@ namespace DKUtil::Hook::Assembly static_assert((std::integral && ...), "all bytes must be an integral type"); return { static_cast(a_bytes)... }; } + + inline constexpr std::byte WILDCARD{ 0x00 }; + + [[nodiscard]] inline std::string sanitize(std::string_view a_pattern) + { + std::string pattern = string::trim_copy(string::replace_all(a_pattern, "0x")); + + auto report_sanitizer_problem = [&](std::size_t i, std::string_view err = {}) { + std::string_view prev{ pattern.data(), i }; + std::string_view next{ pattern.data() + i + 1 }; + FATAL("Can't sanitize pattern:\n{}\n{}{{{}}}{}", err, prev, pattern[i], next); + }; + + // 1) pattern can't begin with wildcards + if (characters::wildcard(pattern[0])) { + report_sanitizer_problem(0, "pattern can't begin with wildcards"); + } + + std::size_t digits{ 0 }; + for (std::size_t i = 0; i < pattern.size(); ++i) { + char c = pattern[i]; + + if (!characters::hexadecimal(c) && + !characters::whitespace(c) && + !characters::wildcard(c)) { + // 2) xdigit only + report_sanitizer_problem(i, "invalid hex character"); + } + + digits = characters::whitespace(c) ? 0 : digits + 1; + } + + // 3) must be 2 char per byte + if (digits % 2) { + report_sanitizer_problem(pattern.size() - 1, "each byte must be 2 characters"); + } + + return pattern; + } + + struct ByteMatch + { + std::byte hex; + bool wildcard; + }; + + [[nodiscard]] inline std::vector make_byte_matches(std::string_view a_pattern) + { + std::vector bytes; + + for (std::size_t i = 0; i < a_pattern.size(); ++i) { + switch (a_pattern[i]) { + case ' ': + break; + case '?': + bytes.emplace_back(Pattern::WILDCARD, true); + ++i; + break; + default: + bytes.emplace_back(dku::string::lexical_cast({ a_pattern.data() + i, 2 }, true), false); + ++i; + break; + } + } + + return bytes; + } } // namespace detail - template - [[nodiscard]] inline constexpr auto make_pattern() noexcept + /** \brief Search a byte pattern in memory, kmp. + * \param a_pattern : hex string pattern of bytes. Spacing is optional. e.g. "FF 15 ????????". + * \param a_base : base address of memory block to search, default to module textx section. + * \param a_size : size of memory block to search, default to module textx size. + * \return void* : pointer of first match, nullptr if none found. + */ + [[nodiscard]] inline std::byte* search_pattern( + std::string_view a_pattern, + model::concepts::dku_memory auto a_base = 0, + std::size_t a_size = 0) { - return Pattern::do_make_pattern(); + std::uintptr_t base{ AsAddress(a_base) }; + + auto [textx, size] = Module::get().section(Module::Section::textx); + + if (!base) { + base = textx; + } + + if (!a_size) { + a_size = size; + } + + auto* begin = static_cast(AsPointer(base)); + auto* end = adjust_pointer(begin, a_size); + auto bytes = Pattern::make_byte_matches(Pattern::sanitize(a_pattern)); + + constexpr std::size_t NPOS = static_cast(-1); + std::vector prefix(bytes.size() + 1); + std::size_t pos{ 1 }; + std::size_t cnd{ 0 }; + + prefix[0] = NPOS; + + while (pos < bytes.size()) { + if (bytes[pos].wildcard || + bytes[cnd].wildcard || + bytes[pos].hex == bytes[cnd].hex) { + prefix[pos] = prefix[cnd]; + } else { + prefix[pos] = cnd; + cnd = prefix[cnd]; + while (cnd != NPOS && + !bytes[pos].wildcard && + !bytes[cnd].wildcard && + bytes[pos].hex != bytes[cnd].hex) { + cnd = prefix[cnd]; + } + } + ++pos; + ++cnd; + } + + prefix[pos] = cnd; + + std::size_t j{ 0 }; + std::size_t k{ 0 }; + std::ptrdiff_t firstMatch{ -1 }; + + while (j < a_size) { + if (bytes[k].wildcard || bytes[k].hex == *adjust_pointer(begin, j)) { + ++j; + ++k; + if (k == bytes.size()) { + firstMatch = j - k; + break; + } + } else { + k = prefix[k]; + if (k == NPOS) { + ++j; + ++k; + } + } + } + + if (firstMatch == -1) { + return nullptr; + } else { + return adjust_pointer(begin, firstMatch); + } } + /** \brief Search a byte pattern in memory, linear. + * \brief To use pattern as argument, use search_pattern(pattern) instead. + * \param a_base : base address of memory block to search, default to module textx section. + * \param a_size : size of memory block to search, default to module textx size. + * \return void* : pointer of first match, nullptr if none found. + */ template [[nodiscard]] inline void* search_pattern(std::uintptr_t a_base = 0, std::size_t a_size = 0) noexcept { - auto& base = Module::get(); - auto [textx, size] = base.section(dku::Hook::Module::Section::textx); + auto [textx, size] = Module::get().section(Hook::Module::Section::textx); if (!a_base) { a_base = textx; @@ -226,7 +375,7 @@ namespace DKUtil::Hook::Assembly const auto* begin = static_cast(AsPointer(a_base)); const auto* end = adjust_pointer(begin, a_size); - + for (auto* mem = begin; mem != end; ++mem) { if (P.match(AsAddress(mem))) { return AsPointer(mem); @@ -236,9 +385,15 @@ namespace DKUtil::Hook::Assembly return nullptr; } + /** \brief Search a byte pattern in memory, linear. + * \brief To use pattern as argument, use search_pattern(pattern) instead. + * \param a_base : base address of memory block to search, default to module textx section. + * \param a_size : size of memory block to search, default to module textx size. + * \return void* : pointer of first match, nullptr if none found. + */ template [[nodiscard]] inline void* search_pattern(std::uintptr_t a_base = 0, std::size_t a_size = 0) noexcept { - return search_pattern()>(a_base, a_size); + return search_pattern()>(); } } // namespace DKUtil::Hook::Assembly