From ccea4a0f0d994f03870935649468cc33583f80c0 Mon Sep 17 00:00:00 2001 From: orange Date: Sun, 15 Mar 2026 16:54:47 +0300 Subject: [PATCH 1/3] added stuff --- include/omath/utility/elf_pattern_scan.hpp | 6 + include/omath/utility/macho_pattern_scan.hpp | 6 + include/omath/utility/pe_pattern_scan.hpp | 6 + source/utility/elf_pattern_scan.cpp | 105 +++++++++ source/utility/macho_pattern_scan.cpp | 114 ++++++++++ source/utility/pe_pattern_scan.cpp | 96 ++++++++ tests/general/unit_test_elf_scanner.cpp | 212 +++++++++++++++++- .../unit_test_macho_memory_file_scan.cpp | 145 ++++++++++++ .../general/unit_test_pe_memory_file_scan.cpp | 128 +++++++++++ 9 files changed, 810 insertions(+), 8 deletions(-) create mode 100644 tests/general/unit_test_macho_memory_file_scan.cpp create mode 100644 tests/general/unit_test_pe_memory_file_scan.cpp diff --git a/include/omath/utility/elf_pattern_scan.hpp b/include/omath/utility/elf_pattern_scan.hpp index 97375dec..2198aab8 100644 --- a/include/omath/utility/elf_pattern_scan.hpp +++ b/include/omath/utility/elf_pattern_scan.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include "section_scan_result.hpp" namespace omath @@ -21,5 +22,10 @@ namespace omath static std::optional scan_for_pattern_in_file(const std::filesystem::path& path_to_file, const std::string_view& pattern, const std::string_view& target_section_name = ".text"); + + [[nodiscard]] + static std::optional + scan_for_pattern_in_memory_file(std::span file_data, const std::string_view& pattern, + const std::string_view& target_section_name = ".text"); }; } // namespace omath \ No newline at end of file diff --git a/include/omath/utility/macho_pattern_scan.hpp b/include/omath/utility/macho_pattern_scan.hpp index a4bc9a46..0db3d903 100644 --- a/include/omath/utility/macho_pattern_scan.hpp +++ b/include/omath/utility/macho_pattern_scan.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include "section_scan_result.hpp" namespace omath @@ -21,5 +22,10 @@ namespace omath static std::optional scan_for_pattern_in_file(const std::filesystem::path& path_to_file, const std::string_view& pattern, const std::string_view& target_section_name = "__text"); + + [[nodiscard]] + static std::optional + scan_for_pattern_in_memory_file(std::span file_data, const std::string_view& pattern, + const std::string_view& target_section_name = "__text"); }; } // namespace omath diff --git a/include/omath/utility/pe_pattern_scan.hpp b/include/omath/utility/pe_pattern_scan.hpp index 12d67927..9e0b2d7a 100644 --- a/include/omath/utility/pe_pattern_scan.hpp +++ b/include/omath/utility/pe_pattern_scan.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "section_scan_result.hpp" namespace omath @@ -23,5 +24,10 @@ namespace omath static std::optional scan_for_pattern_in_file(const std::filesystem::path& path_to_file, const std::string_view& pattern, const std::string_view& target_section_name = ".text"); + + [[nodiscard]] + static std::optional + scan_for_pattern_in_memory_file(std::span file_data, const std::string_view& pattern, + const std::string_view& target_section_name = ".text"); }; } // namespace omath \ No newline at end of file diff --git a/source/utility/elf_pattern_scan.cpp b/source/utility/elf_pattern_scan.cpp index be5d3a03..e2fcffd1 100644 --- a/source/utility/elf_pattern_scan.cpp +++ b/source/utility/elf_pattern_scan.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -140,6 +141,87 @@ namespace std::uintptr_t raw_base_addr{}; std::vector data; }; + template + std::optional get_elf_section_from_memory_impl(const std::span data, + const std::string_view& section_name) + { + using FH = typename ElfHeaders::FileHeader; + using SH = typename ElfHeaders::SectionHeader; + + if (data.size() < sizeof(FH)) + return std::nullopt; + + const auto* file_header = reinterpret_cast(data.data()); + + const auto shoff = static_cast(file_header->e_shoff); + const auto shnum = static_cast(file_header->e_shnum); + const auto shstrndx = static_cast(file_header->e_shstrndx); + + const auto shstrtab_hdr_off = shoff + shstrndx * sizeof(SH); + if (shstrtab_hdr_off + sizeof(SH) > data.size()) + return std::nullopt; + + const auto* shstrtab_hdr = reinterpret_cast(data.data() + shstrtab_hdr_off); + const auto shstrtab_off = static_cast(shstrtab_hdr->sh_offset); + const auto shstrtab_size = static_cast(shstrtab_hdr->sh_size); + + if (shstrtab_off + shstrtab_size > data.size()) + return std::nullopt; + + const auto* shstrtab = reinterpret_cast(data.data() + shstrtab_off); + + for (std::size_t i = 0; i < shnum; ++i) + { + const auto sect_hdr_off = shoff + i * sizeof(SH); + if (sect_hdr_off + sizeof(SH) > data.size()) + continue; + + const auto* section = reinterpret_cast(data.data() + sect_hdr_off); + + if (std::cmp_greater_equal(section->sh_name, shstrtab_size)) + continue; + + if (std::string_view{shstrtab + section->sh_name} != section_name) + continue; + + const auto raw_off = static_cast(section->sh_offset); + const auto sec_size = static_cast(section->sh_size); + + if (raw_off + sec_size > data.size()) + return std::nullopt; + + ExtractedSection out; + out.virtual_base_addr = static_cast(section->sh_addr); + out.raw_base_addr = raw_off; + out.data.assign(data.data() + raw_off, data.data() + raw_off + sec_size); + return out; + } + return std::nullopt; + } + + std::optional get_elf_section_by_name_from_memory(const std::span data, + const std::string_view& section_name) + { + constexpr std::string_view valid_elf_signature = "\x7F" + "ELF"; + if (data.size() < ei_nident) + return std::nullopt; + + if (std::string_view{reinterpret_cast(data.data()), valid_elf_signature.size()} + != valid_elf_signature) + return std::nullopt; + + const auto class_byte = static_cast(data[ei_class]); + + if (class_byte == elfclass64) + return get_elf_section_from_memory_impl(data, section_name); + + if (class_byte == elfclass32) + return get_elf_section_from_memory_impl(data, section_name); + + return std::nullopt; + } + [[maybe_unused]] std::optional get_elf_section_by_name(const std::filesystem::path& path, const std::string_view& section_name) @@ -322,4 +404,27 @@ namespace omath .raw_base_addr = pe_section->raw_base_addr, .target_offset = offset}; } + + std::optional + ElfPatternScanner::scan_for_pattern_in_memory_file(const std::span file_data, + const std::string_view& pattern, + const std::string_view& target_section_name) + { + const auto section = get_elf_section_by_name_from_memory(file_data, target_section_name); + + if (!section.has_value()) [[unlikely]] + return std::nullopt; + + const auto scan_result = + PatternScanner::scan_for_pattern(section->data.cbegin(), section->data.cend(), pattern); + + if (scan_result == section->data.cend()) + return std::nullopt; + + const auto offset = std::distance(section->data.begin(), scan_result); + + return SectionScanResult{.virtual_base_addr = section->virtual_base_addr, + .raw_base_addr = section->raw_base_addr, + .target_offset = offset}; + } } // namespace omath \ No newline at end of file diff --git a/source/utility/macho_pattern_scan.cpp b/source/utility/macho_pattern_scan.cpp index 84aa14c0..2e518b4d 100644 --- a/source/utility/macho_pattern_scan.cpp +++ b/source/utility/macho_pattern_scan.cpp @@ -5,6 +5,7 @@ #include "omath/utility/pattern_scan.hpp" #include #include +#include #include #include @@ -231,6 +232,96 @@ namespace return std::nullopt; } + template + std::optional extract_section_from_memory_impl(const std::span data, + const std::string_view& section_name) + { + if (data.size() < sizeof(HeaderType)) + return std::nullopt; + + const auto* header = reinterpret_cast(data.data()); + + std::size_t cmd_offset = sizeof(HeaderType); + + for (std::uint32_t i = 0; i < header->ncmds; ++i) + { + if (cmd_offset + sizeof(LoadCommand) > data.size()) + return std::nullopt; + + const auto* lc = reinterpret_cast(data.data() + cmd_offset); + + if (lc->cmd != segment_cmd) + { + cmd_offset += lc->cmdsize; + continue; + } + + if (cmd_offset + sizeof(SegmentType) > data.size()) + return std::nullopt; + + const auto* segment = reinterpret_cast(data.data() + cmd_offset); + + if (!segment->nsects) + { + cmd_offset += lc->cmdsize; + continue; + } + + std::size_t sect_offset = cmd_offset + sizeof(SegmentType); + + for (std::uint32_t j = 0; j < segment->nsects; ++j) + { + if (sect_offset + sizeof(SectionType) > data.size()) + return std::nullopt; + + const auto* section = reinterpret_cast(data.data() + sect_offset); + + if (get_section_name(section->sectname) != section_name) + { + sect_offset += sizeof(SectionType); + continue; + } + + const auto raw_off = static_cast(section->offset); + const auto sec_size = static_cast(section->size); + + if (raw_off + sec_size > data.size()) + return std::nullopt; + + ExtractedSection out; + out.virtual_base_addr = static_cast(section->addr); + out.raw_base_addr = raw_off; + out.data.assign(data.data() + raw_off, data.data() + raw_off + sec_size); + return out; + } + + cmd_offset += lc->cmdsize; + } + + return std::nullopt; + } + + [[nodiscard]] + std::optional get_macho_section_by_name_from_memory(const std::span data, + const std::string_view& section_name) + { + if (data.size() < sizeof(std::uint32_t)) + return std::nullopt; + + std::uint32_t magic{}; + std::memcpy(&magic, data.data(), sizeof(magic)); + + if (magic == mh_magic_64 || magic == mh_cigam_64) + return extract_section_from_memory_impl( + data, section_name); + + if (magic == mh_magic_32 || magic == mh_cigam_32) + return extract_section_from_memory_impl(data, + section_name); + + return std::nullopt; + } + [[nodiscard]] std::optional get_macho_section_by_name(const std::filesystem::path& path, const std::string_view& section_name) @@ -346,4 +437,27 @@ namespace omath .raw_base_addr = macho_section->raw_base_addr, .target_offset = offset}; } + + std::optional + MachOPatternScanner::scan_for_pattern_in_memory_file(const std::span file_data, + const std::string_view& pattern, + const std::string_view& target_section_name) + { + const auto section = get_macho_section_by_name_from_memory(file_data, target_section_name); + + if (!section.has_value()) [[unlikely]] + return std::nullopt; + + const auto scan_result = + PatternScanner::scan_for_pattern(section->data.cbegin(), section->data.cend(), pattern); + + if (scan_result == section->data.cend()) + return std::nullopt; + + const auto offset = std::distance(section->data.begin(), scan_result); + + return SectionScanResult{.virtual_base_addr = section->virtual_base_addr, + .raw_base_addr = section->raw_base_addr, + .target_offset = offset}; + } } // namespace omath diff --git a/source/utility/pe_pattern_scan.cpp b/source/utility/pe_pattern_scan.cpp index 58ec3793..ab40c123 100644 --- a/source/utility/pe_pattern_scan.cpp +++ b/source/utility/pe_pattern_scan.cpp @@ -7,6 +7,7 @@ #include #include #include +#include // Internal PE shit defines // Big thx for linuxpe sources as ref @@ -244,6 +245,78 @@ namespace std::vector data; }; + [[nodiscard]] + std::optional extract_section_from_pe_memory(const std::span data, + const std::string_view& section_name) + { + if (data.size() < sizeof(DosHeader)) + return std::nullopt; + + const auto* dos_header = reinterpret_cast(data.data()); + + if (invalid_dos_header_file(*dos_header)) + return std::nullopt; + + const auto nt_off = static_cast(dos_header->e_lfanew); + + if (nt_off + sizeof(ImageNtHeaders) > data.size()) + return std::nullopt; + + const auto* x86_hdrs = + reinterpret_cast*>(data.data() + nt_off); + + NtHeaderVariant nt_headers; + if (x86_hdrs->optional_header.magic == opt_hdr32_magic) + nt_headers = *x86_hdrs; + else if (x86_hdrs->optional_header.magic == opt_hdr64_magic) + { + if (nt_off + sizeof(ImageNtHeaders) > data.size()) + return std::nullopt; + nt_headers = *reinterpret_cast*>(data.data() + nt_off); + } + else + return std::nullopt; + + if (invalid_nt_header_file(nt_headers)) + return std::nullopt; + + return std::visit( + [&data, §ion_name, nt_off](const auto& concrete_headers) -> std::optional + { + constexpr std::size_t sig_size = sizeof(concrete_headers.signature); + const auto section_table_off = nt_off + sig_size + sizeof(FileHeader) + + concrete_headers.file_header.size_optional_header; + + for (std::size_t i = 0; i < concrete_headers.file_header.num_sections; ++i) + { + const auto sh_off = section_table_off + i * sizeof(SectionHeader); + if (sh_off + sizeof(SectionHeader) > data.size()) + return std::nullopt; + + const auto* section = reinterpret_cast(data.data() + sh_off); + + if (std::string_view(section->name) != section_name) + continue; + + const auto raw_off = static_cast(section->ptr_raw_data); + const auto raw_size = static_cast(section->size_raw_data); + + if (raw_off + raw_size > data.size()) + return std::nullopt; + + std::vector section_data(data.data() + raw_off, data.data() + raw_off + raw_size); + + return ExtractedSection{ + .virtual_base_addr = static_cast( + section->virtual_address + concrete_headers.optional_header.image_base), + .raw_base_addr = raw_off, + .data = std::move(section_data)}; + } + return std::nullopt; + }, + nt_headers); + } + [[nodiscard]] std::optional extract_section_from_pe_file(const std::filesystem::path& path_to_file, const std::string_view& section_name) @@ -383,4 +456,27 @@ namespace omath .raw_base_addr = pe_section->raw_base_addr, .target_offset = offset}; } + + std::optional + PePatternScanner::scan_for_pattern_in_memory_file(const std::span file_data, + const std::string_view& pattern, + const std::string_view& target_section_name) + { + const auto pe_section = extract_section_from_pe_memory(file_data, target_section_name); + + if (!pe_section.has_value()) [[unlikely]] + return std::nullopt; + + const auto scan_result = + PatternScanner::scan_for_pattern(pe_section->data.cbegin(), pe_section->data.cend(), pattern); + + if (scan_result == pe_section->data.cend()) + return std::nullopt; + + const auto offset = std::distance(pe_section->data.begin(), scan_result); + + return SectionScanResult{.virtual_base_addr = pe_section->virtual_base_addr, + .raw_base_addr = pe_section->raw_base_addr, + .target_offset = offset}; + } } // namespace omath \ No newline at end of file diff --git a/tests/general/unit_test_elf_scanner.cpp b/tests/general/unit_test_elf_scanner.cpp index c2a9a283..9113453d 100644 --- a/tests/general/unit_test_elf_scanner.cpp +++ b/tests/general/unit_test_elf_scanner.cpp @@ -1,17 +1,213 @@ // // Created by Vladislav on 30.12.2025. // -// /Users/vladislav/Downloads/valencia +#include +#include +#include #include #include -#include -TEST(unit_test_elf_pattern_scan_file, ScanMissingPattern) +#include + +using namespace omath; + +// ---- helpers --------------------------------------------------------------- + +// Minimal ELF64 file with a single .text section containing known bytes. +// Layout: +// 0x000 : ELF64 file header (64 bytes) +// 0x040 : section data (padded to 0x20 bytes) +// 0x060 : section name table ".text\0" + "\0" (empty name for SHN_UNDEF) +// 0x080 : section header table (3 entries × 64 bytes = 0xC0) +static std::vector make_elf64_with_text_section(const std::vector& code_bytes) { - //FIXME: Implement normal tests :) - //constexpr std::string_view path = "/Users/vladislav/Downloads/crackme"; + // Fixed layout constants + constexpr std::size_t text_off = 0x40; + constexpr std::size_t text_size = 0x20; // always 32 bytes (code padded with zeros) + constexpr std::size_t shstrtab_off = text_off + text_size; + // ".text\0" = 6 chars, prepend \0 for SHN_UNDEF → "\0.text\0" + constexpr std::size_t shstrtab_size = 8; // "\0.text\0\0" + constexpr std::size_t shdr_table_off = shstrtab_off + shstrtab_size; + constexpr std::size_t shdr_size = 64; // sizeof(Elf64_Shdr) + constexpr std::size_t num_sections = 3; // null + .text + .shstrtab + constexpr std::size_t total_size = shdr_table_off + num_sections * shdr_size; + + std::vector buf(total_size, std::byte{0}); + + auto w8 = [&](std::size_t off, std::uint8_t v) { buf[off] = std::byte{v}; }; + auto w16 = [&](std::size_t off, std::uint16_t v) + { std::memcpy(buf.data() + off, &v, 2); }; + auto w32 = [&](std::size_t off, std::uint32_t v) + { std::memcpy(buf.data() + off, &v, 4); }; + auto w64 = [&](std::size_t off, std::uint64_t v) + { std::memcpy(buf.data() + off, &v, 8); }; + + // --- ELF64 file header --- + // e_ident + buf[0] = std::byte{0x7F}; + buf[1] = std::byte{'E'}; + buf[2] = std::byte{'L'}; + buf[3] = std::byte{'F'}; + w8(4, 2); // ELFCLASS64 + w8(5, 1); // ELFDATA2LSB + w8(6, 1); // EV_CURRENT + // rest of e_ident is 0 + w16(16, 2); // e_type = ET_EXEC + w16(18, 62); // e_machine = EM_X86_64 + w32(20, 1); // e_version + w64(24, 0); // e_entry + w64(32, 0); // e_phoff + w64(40, static_cast(shdr_table_off)); // e_shoff + w32(48, 0); // e_flags + w16(52, 64); // e_ehsize + w16(54, 56); // e_phentsize + w16(56, 0); // e_phnum + w16(58, static_cast(shdr_size)); // e_shentsize + w16(60, static_cast(num_sections)); // e_shnum + w16(62, 2); // e_shstrndx = 2 (.shstrtab is section index 2) + + // --- section data (.text) --- + const std::size_t copy_len = std::min(code_bytes.size(), text_size); + for (std::size_t i = 0; i < copy_len; ++i) + buf[text_off + i] = std::byte{code_bytes[i]}; + + // --- .shstrtab data: "\0.text\0\0" --- + // index 0 → "" (SHN_UNDEF name) + // index 1 → ".text" + // index 7 → ".shstrtab" (we cheat and use index 1 for .shstrtab too, fine for test) + buf[shstrtab_off + 0] = std::byte{0}; + buf[shstrtab_off + 1] = std::byte{'.'}; + buf[shstrtab_off + 2] = std::byte{'t'}; + buf[shstrtab_off + 3] = std::byte{'e'}; + buf[shstrtab_off + 4] = std::byte{'x'}; + buf[shstrtab_off + 5] = std::byte{'t'}; + buf[shstrtab_off + 6] = std::byte{0}; + buf[shstrtab_off + 7] = std::byte{0}; + + // --- section headers --- + // Elf64_Shdr fields (all offsets relative to start of a section header): + // 0 sh_name (4) + // 4 sh_type (4) + // 8 sh_flags (8) + // 16 sh_addr (8) + // 24 sh_offset (8) + // 32 sh_size (8) + // 40 sh_link (4) + // 44 sh_info (4) + // 48 sh_addralign(8) + // 56 sh_entsize (8) + + // Section 0: null + // (all zeros – already zeroed) + + // Section 1: .text + { + const std::size_t base = shdr_table_off + 1 * shdr_size; + w32(base + 0, 1); // sh_name → index 1 in shstrtab → ".text" + w32(base + 4, 1); // sh_type = SHT_PROGBITS + w64(base + 8, 6); // sh_flags = SHF_ALLOC|SHF_EXECINSTR + w64(base + 16, static_cast(text_off)); // sh_addr (same as offset in test) + w64(base + 24, static_cast(text_off)); // sh_offset + w64(base + 32, static_cast(text_size)); // sh_size + w64(base + 48, 16); // sh_addralign + } + + // Section 2: .shstrtab + { + const std::size_t base = shdr_table_off + 2 * shdr_size; + w32(base + 0, 0); // sh_name → index 0 → "" (good enough for scanner) + w32(base + 4, 3); // sh_type = SHT_STRTAB + w64(base + 24, static_cast(shstrtab_off)); // sh_offset + w64(base + 32, static_cast(shstrtab_size)); // sh_size + } + + return buf; +} + +// ---- tests ----------------------------------------------------------------- + +TEST(unit_test_elf_pattern_scan_memory, finds_pattern) +{ + const std::vector code = {0x55, 0x48, 0x89, 0xE5, 0xC3}; + const auto buf = make_elf64_with_text_section(code); + const auto span = std::span{buf}; + + const auto result = ElfPatternScanner::scan_for_pattern_in_memory_file(span, "55 48 89 E5", ".text"); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->target_offset, 0); +} + +TEST(unit_test_elf_pattern_scan_memory, finds_pattern_with_wildcard) +{ + const std::vector code = {0xDE, 0xAD, 0xBE, 0xEF, 0x00}; + const auto buf = make_elf64_with_text_section(code); + + const auto result = + ElfPatternScanner::scan_for_pattern_in_memory_file(std::span{buf}, "DE ?? BE EF", ".text"); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->target_offset, 0); +} + +TEST(unit_test_elf_pattern_scan_memory, pattern_not_found_returns_nullopt) +{ + const std::vector code = {0x01, 0x02, 0x03, 0x04}; + const auto buf = make_elf64_with_text_section(code); + + const auto result = + ElfPatternScanner::scan_for_pattern_in_memory_file(std::span{buf}, "AA BB CC", ".text"); + + EXPECT_FALSE(result.has_value()); +} + +TEST(unit_test_elf_pattern_scan_memory, invalid_data_returns_nullopt) +{ + const std::vector garbage(64, std::byte{0xFF}); + const auto result = + ElfPatternScanner::scan_for_pattern_in_memory_file(std::span{garbage}, "FF FF", ".text"); + + EXPECT_FALSE(result.has_value()); +} + +TEST(unit_test_elf_pattern_scan_memory, empty_data_returns_nullopt) +{ + const auto result = ElfPatternScanner::scan_for_pattern_in_memory_file({}, "FF", ".text"); + EXPECT_FALSE(result.has_value()); +} + +TEST(unit_test_elf_pattern_scan_memory, missing_section_returns_nullopt) +{ + const std::vector code = {0x90, 0x90}; + const auto buf = make_elf64_with_text_section(code); + + const auto result = ElfPatternScanner::scan_for_pattern_in_memory_file(std::span{buf}, + "90 90", ".nonexistent"); + + EXPECT_FALSE(result.has_value()); +} + +TEST(unit_test_elf_pattern_scan_memory, matches_file_scan) +{ + // Read test binary itself and compare memory scan vs file scan + std::ifstream file("/proc/self/exe", std::ios::binary); + if (!file.is_open()) + GTEST_SKIP() << "Cannot open /proc/self/exe"; + + const std::string raw{std::istreambuf_iterator(file), {}}; + std::vector data(raw.size()); + std::transform(raw.begin(), raw.end(), data.begin(), + [](char c) { return std::byte{static_cast(c)}; }); - //const auto res = omath::ElfPatternScanner::scan_for_pattern_in_file(path, "F3 0F 1E FA 55 48 89 E5 B8 00 00 00 00", ".text"); - //EXPECT_TRUE(res.has_value()); + constexpr std::string_view pattern = "7F 45 4C 46"; // ELF magic at start of .text unlikely; use any known bytes + const auto file_result = ElfPatternScanner::scan_for_pattern_in_file("/proc/self/exe", pattern, ".text"); + const auto mem_result = + ElfPatternScanner::scan_for_pattern_in_memory_file(std::span{data}, pattern, ".text"); - //std::println("In virtual mem: 0x{:x}", res->virtual_base_addr+res->target_offset); + EXPECT_EQ(file_result.has_value(), mem_result.has_value()); + if (file_result && mem_result) + { + EXPECT_EQ(file_result->virtual_base_addr, mem_result->virtual_base_addr); + EXPECT_EQ(file_result->raw_base_addr, mem_result->raw_base_addr); + EXPECT_EQ(file_result->target_offset, mem_result->target_offset); + } } diff --git a/tests/general/unit_test_macho_memory_file_scan.cpp b/tests/general/unit_test_macho_memory_file_scan.cpp new file mode 100644 index 00000000..e9d02125 --- /dev/null +++ b/tests/general/unit_test_macho_memory_file_scan.cpp @@ -0,0 +1,145 @@ +// Tests for MachOPatternScanner::scan_for_pattern_in_memory_file +#include +#include +#include +#include +#include + +using namespace omath; + +// Build a minimal Mach-O 64-bit file in memory with a single __text section. +// Layout: +// 0x000 : MachHeader64 (32 bytes) +// 0x020 : SegmentCommand64 (72 bytes) +// 0x068 : Section64 (80 bytes) ← follows segment command inline +// 0x0B8 : section raw data (padded to 0x20 bytes) +static std::vector make_macho64_with_text_section(const std::vector& code_bytes) +{ + constexpr std::uint32_t mh_magic_64 = 0xFEEDFACF; + constexpr std::uint32_t lc_segment_64 = 0x19; + + // MachHeader64 layout (32 bytes): + // 0 magic, 4 cputype, 8 cpusubtype, 12 filetype, 16 ncmds, 20 sizeofcmds, 24 flags, 28 reserved + constexpr std::size_t hdr_size = 32; + + // SegmentCommand64 layout (72 bytes): + // 0 cmd, 4 cmdsize, 8 segname[16], 24 vmaddr, 32 vmsize, 40 fileoff, 48 filesize, + // 56 maxprot, 60 initprot, 64 nsects, 68 flags + constexpr std::size_t seg_size = 72; + + // Section64 layout (80 bytes): + // 0 sectname[16], 16 segname[16], 32 addr, 40 size, 48 offset, 52 align, + // 56 reloff, 60 nreloc, 64 flags, 68 reserved1, 72 reserved2, 76 reserved3 + constexpr std::size_t sect_hdr_size = 80; + + constexpr std::size_t text_raw_off = hdr_size + seg_size + sect_hdr_size; // 0xB8 + constexpr std::size_t text_raw_size = 0x20; + constexpr std::size_t total_size = text_raw_off + text_raw_size; + constexpr std::uint64_t text_vmaddr = 0x100001000ULL; + + constexpr std::uint32_t cmd_size = + static_cast(seg_size + sect_hdr_size); // segment + 1 section + + std::vector buf(total_size, std::byte{0}); + + auto w32 = [&](std::size_t off, std::uint32_t v) { std::memcpy(buf.data() + off, &v, 4); }; + auto w64 = [&](std::size_t off, std::uint64_t v) { std::memcpy(buf.data() + off, &v, 8); }; + + // MachHeader64 + w32(0, mh_magic_64); + w32(4, 0x0100000C); // cputype = CPU_TYPE_ARM64 (doesn't matter for scan) + w32(12, 2); // filetype = MH_EXECUTE + w32(16, 1); // ncmds = 1 + w32(20, cmd_size); // sizeofcmds + + // SegmentCommand64 at 0x20 + constexpr std::size_t seg_off = hdr_size; + w32(seg_off + 0, lc_segment_64); + w32(seg_off + 4, cmd_size); + std::memcpy(buf.data() + seg_off + 8, "__TEXT", 6); // segname + w64(seg_off + 24, text_vmaddr); // vmaddr + w64(seg_off + 32, text_raw_size); // vmsize + w64(seg_off + 40, text_raw_off); // fileoff + w64(seg_off + 48, text_raw_size); // filesize + w32(seg_off + 64, 1); // nsects + + // Section64 at 0x68 + constexpr std::size_t sect_off = seg_off + seg_size; + std::memcpy(buf.data() + sect_off + 0, "__text", 6); // sectname + std::memcpy(buf.data() + sect_off + 16, "__TEXT", 6); // segname + w64(sect_off + 32, text_vmaddr); // addr + w64(sect_off + 40, text_raw_size); // size + w32(sect_off + 48, static_cast(text_raw_off)); // offset (file offset) + + // Section data + const std::size_t copy_len = std::min(code_bytes.size(), text_raw_size); + for (std::size_t i = 0; i < copy_len; ++i) + buf[text_raw_off + i] = std::byte{code_bytes[i]}; + + return buf; +} + +// ---- tests ----------------------------------------------------------------- + +TEST(unit_test_macho_memory_file_scan, finds_pattern) +{ + const std::vector code = {0x55, 0x48, 0x89, 0xE5, 0xC3}; + const auto buf = make_macho64_with_text_section(code); + + const auto result = + MachOPatternScanner::scan_for_pattern_in_memory_file(std::span{buf}, "55 48 89 E5"); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->target_offset, 0); +} + +TEST(unit_test_macho_memory_file_scan, finds_pattern_with_wildcard) +{ + const std::vector code = {0xDE, 0xAD, 0xBE, 0xEF}; + const auto buf = make_macho64_with_text_section(code); + + const auto result = + MachOPatternScanner::scan_for_pattern_in_memory_file(std::span{buf}, "DE ?? BE EF"); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->target_offset, 0); +} + +TEST(unit_test_macho_memory_file_scan, pattern_not_found_returns_nullopt) +{ + const std::vector code = {0x01, 0x02, 0x03}; + const auto buf = make_macho64_with_text_section(code); + + const auto result = + MachOPatternScanner::scan_for_pattern_in_memory_file(std::span{buf}, "AA BB CC"); + + EXPECT_FALSE(result.has_value()); +} + +TEST(unit_test_macho_memory_file_scan, invalid_data_returns_nullopt) +{ + const std::vector garbage(64, std::byte{0xFF}); + const auto result = + MachOPatternScanner::scan_for_pattern_in_memory_file(std::span{garbage}, "FF FF"); + EXPECT_FALSE(result.has_value()); +} + +TEST(unit_test_macho_memory_file_scan, empty_data_returns_nullopt) +{ + const auto result = MachOPatternScanner::scan_for_pattern_in_memory_file({}, "FF"); + EXPECT_FALSE(result.has_value()); +} + +TEST(unit_test_macho_memory_file_scan, raw_addr_and_virtual_addr_correct) +{ + const std::vector code = {0xCA, 0xFE, 0xBA, 0xBE}; + const auto buf = make_macho64_with_text_section(code); + + constexpr std::size_t expected_raw_off = 32 + 72 + 80; // hdr + seg + sect_hdr + const auto result = + MachOPatternScanner::scan_for_pattern_in_memory_file(std::span{buf}, "CA FE BA BE"); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->raw_base_addr, expected_raw_off); + EXPECT_EQ(result->virtual_base_addr, 0x100001000ULL); +} diff --git a/tests/general/unit_test_pe_memory_file_scan.cpp b/tests/general/unit_test_pe_memory_file_scan.cpp new file mode 100644 index 00000000..ff2f557e --- /dev/null +++ b/tests/general/unit_test_pe_memory_file_scan.cpp @@ -0,0 +1,128 @@ +// Tests for PePatternScanner::scan_for_pattern_in_memory_file +#include +#include +#include +#include +#include + +using namespace omath; + +// Reuse the fake-module builder from unit_test_pe_pattern_scan_loaded.cpp but +// lay out the buffer as a raw PE *file* (ptr_raw_data != virtual_address). +static std::vector make_fake_pe_file(std::uint32_t virtual_address, std::uint32_t ptr_raw_data, + std::uint32_t section_size, + const std::vector& code_bytes) +{ + constexpr std::uint32_t e_lfanew = 0x80; + constexpr std::uint32_t nt_sig = 0x4550; + constexpr std::uint16_t opt_magic = 0x020B; // PE32+ + constexpr std::uint16_t num_sections = 1; + constexpr std::uint16_t opt_hdr_size = 0xF0; + constexpr std::uint32_t section_table_off = e_lfanew + 4 + 20 + opt_hdr_size; + constexpr std::uint32_t section_header_size = 40; + + const std::uint32_t total_size = ptr_raw_data + section_size + 0x100; + std::vector buf(total_size, std::byte{0}); + + auto w16 = [&](std::size_t off, std::uint16_t v) { std::memcpy(buf.data() + off, &v, 2); }; + auto w32 = [&](std::size_t off, std::uint32_t v) { std::memcpy(buf.data() + off, &v, 4); }; + auto w64 = [&](std::size_t off, std::uint64_t v) { std::memcpy(buf.data() + off, &v, 8); }; + + // DOS header + w16(0x00, 0x5A4D); + w32(0x3C, e_lfanew); + + // NT signature + w32(e_lfanew, nt_sig); + + // FileHeader + const std::size_t fh_off = e_lfanew + 4; + w16(fh_off + 2, num_sections); + w16(fh_off + 16, opt_hdr_size); + + // OptionalHeader PE32+ + const std::size_t opt_off = fh_off + 20; + w16(opt_off + 0, opt_magic); + w64(opt_off + 24, 0x140000000ULL); // ImageBase + + // Section header (.text) + const std::size_t sh_off = section_table_off; + std::memcpy(buf.data() + sh_off, ".text", 5); + w32(sh_off + 8, section_size); // VirtualSize + w32(sh_off + 12, virtual_address); // VirtualAddress + w32(sh_off + 16, section_size); // SizeOfRawData + w32(sh_off + 20, ptr_raw_data); // PointerToRawData + + // Place code at raw file offset + const std::size_t copy_len = std::min(code_bytes.size(), static_cast(section_size)); + for (std::size_t i = 0; i < copy_len; ++i) + buf[ptr_raw_data + i] = std::byte{code_bytes[i]}; + + return buf; +} + +// ---- tests ----------------------------------------------------------------- + +TEST(unit_test_pe_memory_file_scan, finds_pattern) +{ + const std::vector code = {0x90, 0x01, 0x02, 0x03, 0x04}; + const auto buf = make_fake_pe_file(0x1000, 0x400, static_cast(code.size()), code); + + const auto result = PePatternScanner::scan_for_pattern_in_memory_file(std::span{buf}, "90 01 02"); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->target_offset, 0); + EXPECT_EQ(result->raw_base_addr, 0x400u); +} + +TEST(unit_test_pe_memory_file_scan, finds_pattern_with_wildcard) +{ + const std::vector code = {0xDE, 0xAD, 0xBE, 0xEF}; + const auto buf = make_fake_pe_file(0x2000, 0x600, static_cast(code.size()), code); + + const auto result = + PePatternScanner::scan_for_pattern_in_memory_file(std::span{buf}, "DE ?? BE EF"); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->target_offset, 0); +} + +TEST(unit_test_pe_memory_file_scan, pattern_not_found_returns_nullopt) +{ + const std::vector code = {0x01, 0x02, 0x03}; + const auto buf = make_fake_pe_file(0x1000, 0x400, static_cast(code.size()), code); + + const auto result = + PePatternScanner::scan_for_pattern_in_memory_file(std::span{buf}, "AA BB CC"); + + EXPECT_FALSE(result.has_value()); +} + +TEST(unit_test_pe_memory_file_scan, invalid_data_returns_nullopt) +{ + const std::vector garbage(128, std::byte{0xFF}); + const auto result = PePatternScanner::scan_for_pattern_in_memory_file(std::span{garbage}, "FF FF"); + EXPECT_FALSE(result.has_value()); +} + +TEST(unit_test_pe_memory_file_scan, empty_data_returns_nullopt) +{ + const auto result = PePatternScanner::scan_for_pattern_in_memory_file({}, "FF"); + EXPECT_FALSE(result.has_value()); +} + +TEST(unit_test_pe_memory_file_scan, raw_addr_differs_from_virtual_address) +{ + // ptr_raw_data = 0x600, virtual_address = 0x3000 — different intentionally + const std::vector code = {0xCA, 0xFE, 0xBA, 0xBE}; + const auto buf = make_fake_pe_file(0x3000, 0x600, static_cast(code.size()), code); + + const auto result = + PePatternScanner::scan_for_pattern_in_memory_file(std::span{buf}, "CA FE BA BE"); + + ASSERT_TRUE(result.has_value()); + // raw_base_addr should be ptr_raw_data, not virtual_address + EXPECT_EQ(result->raw_base_addr, 0x600u); + // virtual_base_addr = virtual_address + image_base + EXPECT_EQ(result->virtual_base_addr, 0x3000u + 0x140000000ULL); +} From 4f1c42d6f6dfc6187c916021b985ad584db95d5b Mon Sep 17 00:00:00 2001 From: orange Date: Sun, 15 Mar 2026 17:04:21 +0300 Subject: [PATCH 2/3] tests fix --- tests/general/unit_test_macho_memory_file_scan.cpp | 4 ++-- tests/general/unit_test_pe_memory_file_scan.cpp | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/general/unit_test_macho_memory_file_scan.cpp b/tests/general/unit_test_macho_memory_file_scan.cpp index e9d02125..a00bdfe1 100644 --- a/tests/general/unit_test_macho_memory_file_scan.cpp +++ b/tests/general/unit_test_macho_memory_file_scan.cpp @@ -35,7 +35,7 @@ static std::vector make_macho64_with_text_section(const std::vector(seg_size + sect_hdr_size); // segment + 1 section @@ -141,5 +141,5 @@ TEST(unit_test_macho_memory_file_scan, raw_addr_and_virtual_addr_correct) ASSERT_TRUE(result.has_value()); EXPECT_EQ(result->raw_base_addr, expected_raw_off); - EXPECT_EQ(result->virtual_base_addr, 0x100001000ULL); + EXPECT_EQ(result->virtual_base_addr, 0x1000u); } diff --git a/tests/general/unit_test_pe_memory_file_scan.cpp b/tests/general/unit_test_pe_memory_file_scan.cpp index ff2f557e..9143395f 100644 --- a/tests/general/unit_test_pe_memory_file_scan.cpp +++ b/tests/general/unit_test_pe_memory_file_scan.cpp @@ -43,7 +43,7 @@ static std::vector make_fake_pe_file(std::uint32_t virtual_address, s // OptionalHeader PE32+ const std::size_t opt_off = fh_off + 20; w16(opt_off + 0, opt_magic); - w64(opt_off + 24, 0x140000000ULL); // ImageBase + w64(opt_off + 24, 0ULL); // ImageBase = 0 to keep virtual_base_addr in 32-bit range // Section header (.text) const std::size_t sh_off = section_table_off; @@ -123,6 +123,6 @@ TEST(unit_test_pe_memory_file_scan, raw_addr_differs_from_virtual_address) ASSERT_TRUE(result.has_value()); // raw_base_addr should be ptr_raw_data, not virtual_address EXPECT_EQ(result->raw_base_addr, 0x600u); - // virtual_base_addr = virtual_address + image_base - EXPECT_EQ(result->virtual_base_addr, 0x3000u + 0x140000000ULL); + // virtual_base_addr = virtual_address + image_base (image_base = 0) + EXPECT_EQ(result->virtual_base_addr, 0x3000u); } From 130277c1ae347d7040eb04ef1b80aaa5112bd768 Mon Sep 17 00:00:00 2001 From: orange Date: Sun, 15 Mar 2026 17:20:28 +0300 Subject: [PATCH 3/3] refactored test --- tests/general/unit_test_elf_scanner.cpp | 39 +++++++++++++------------ 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/tests/general/unit_test_elf_scanner.cpp b/tests/general/unit_test_elf_scanner.cpp index 9113453d..bfe3f712 100644 --- a/tests/general/unit_test_elf_scanner.cpp +++ b/tests/general/unit_test_elf_scanner.cpp @@ -3,9 +3,11 @@ // #include #include +#include #include #include #include +#include #include using namespace omath; @@ -188,26 +190,25 @@ TEST(unit_test_elf_pattern_scan_memory, missing_section_returns_nullopt) TEST(unit_test_elf_pattern_scan_memory, matches_file_scan) { - // Read test binary itself and compare memory scan vs file scan - std::ifstream file("/proc/self/exe", std::ios::binary); - if (!file.is_open()) - GTEST_SKIP() << "Cannot open /proc/self/exe"; - - const std::string raw{std::istreambuf_iterator(file), {}}; - std::vector data(raw.size()); - std::transform(raw.begin(), raw.end(), data.begin(), - [](char c) { return std::byte{static_cast(c)}; }); - - constexpr std::string_view pattern = "7F 45 4C 46"; // ELF magic at start of .text unlikely; use any known bytes - const auto file_result = ElfPatternScanner::scan_for_pattern_in_file("/proc/self/exe", pattern, ".text"); - const auto mem_result = - ElfPatternScanner::scan_for_pattern_in_memory_file(std::span{data}, pattern, ".text"); + // Write our synthetic ELF to a temp file and verify memory scan == file scan + const std::vector code = {0x48, 0x89, 0xE5, 0xDE, 0xAD, 0xBE, 0xEF, 0x00}; + const auto buf = make_elf64_with_text_section(code); - EXPECT_EQ(file_result.has_value(), mem_result.has_value()); - if (file_result && mem_result) + const auto tmp_path = std::filesystem::temp_directory_path() / "omath_elf_test.elf"; { - EXPECT_EQ(file_result->virtual_base_addr, mem_result->virtual_base_addr); - EXPECT_EQ(file_result->raw_base_addr, mem_result->raw_base_addr); - EXPECT_EQ(file_result->target_offset, mem_result->target_offset); + std::ofstream out(tmp_path, std::ios::binary); + out.write(reinterpret_cast(buf.data()), static_cast(buf.size())); } + + const auto file_result = ElfPatternScanner::scan_for_pattern_in_file(tmp_path, "48 89 E5 DE AD", ".text"); + const auto mem_result = + ElfPatternScanner::scan_for_pattern_in_memory_file(std::span{buf}, "48 89 E5 DE AD", ".text"); + + std::filesystem::remove(tmp_path); + + ASSERT_TRUE(file_result.has_value()); + ASSERT_TRUE(mem_result.has_value()); + EXPECT_EQ(file_result->virtual_base_addr, mem_result->virtual_base_addr); + EXPECT_EQ(file_result->raw_base_addr, mem_result->raw_base_addr); + EXPECT_EQ(file_result->target_offset, mem_result->target_offset); }