diff --git a/source/lua/lua_pattern_scan.cpp b/source/lua/lua_pattern_scan.cpp index 56345a4f..8754b3ae 100644 --- a/source/lua/lua_pattern_scan.cpp +++ b/source/lua/lua_pattern_scan.cpp @@ -10,7 +10,6 @@ #include #include #include -#endif namespace omath::lua { @@ -101,4 +100,5 @@ namespace omath::lua return *result; }; } -} // namespace omath::lua \ No newline at end of file +} // namespace omath::lua +#endif \ No newline at end of file diff --git a/tests/lua/pe_scanner_tests.lua b/tests/lua/pe_scanner_tests.lua new file mode 100644 index 00000000..1125aaed --- /dev/null +++ b/tests/lua/pe_scanner_tests.lua @@ -0,0 +1,66 @@ +-- PatternScanner tests: generic scan over a Lua string buffer + +function PatternScanner_FindsExactPattern() + local buf = "\x90\x01\x02\x03\x04" + local offset = omath.PatternScanner.scan(buf, "90 01 02") + assert(offset ~= nil, "expected pattern to be found") + assert(offset == 0, "expected offset 0, got " .. tostring(offset)) +end + +function PatternScanner_FindsPatternAtNonZeroOffset() + local buf = "\x00\x00\xAB\xCD\xEF" + local offset = omath.PatternScanner.scan(buf, "AB CD EF") + assert(offset ~= nil, "expected pattern to be found") + assert(offset == 2, "expected offset 2, got " .. tostring(offset)) +end + +function PatternScanner_WildcardMatches() + local buf = "\xDE\xAD\xBE\xEF" + local offset = omath.PatternScanner.scan(buf, "DE ?? BE") + assert(offset ~= nil, "expected wildcard match") + assert(offset == 0) +end + +function PatternScanner_ReturnsNilWhenNotFound() + local buf = "\x01\x02\x03" + local offset = omath.PatternScanner.scan(buf, "AA BB CC") + assert(offset == nil, "expected nil for not-found pattern") +end + +function PatternScanner_ReturnsNilForEmptyBuffer() + local offset = omath.PatternScanner.scan("", "90 01") + assert(offset == nil) +end + +-- PePatternScanner tests: scan_in_module uses FAKE_MODULE_BASE injected from C++ +-- The fake module contains {0x90, 0x01, 0x02, 0x03, 0x04} placed at raw offset 0x200 + +function PeScanner_FindsExactPattern() + local addr = omath.PePatternScanner.scan_in_module(FAKE_MODULE_BASE, "90 01 02") + assert(addr ~= nil, "expected pattern to be found in module") + local offset = addr - FAKE_MODULE_BASE + assert(offset == 0x200, string.format("expected offset 0x200, got 0x%X", offset)) +end + +function PeScanner_WildcardMatches() + local addr = omath.PePatternScanner.scan_in_module(FAKE_MODULE_BASE, "90 ?? 02") + assert(addr ~= nil, "expected wildcard match in module") + local offset = addr - FAKE_MODULE_BASE + assert(offset == 0x200, string.format("expected offset 0x200, got 0x%X", offset)) +end + +function PeScanner_ReturnsNilWhenNotFound() + local addr = omath.PePatternScanner.scan_in_module(FAKE_MODULE_BASE, "AA BB CC DD") + assert(addr == nil, "expected nil for not-found pattern") +end + +function PeScanner_CustomSectionFallsBackToNil() + -- Request a section that doesn't exist in our fake module + local addr = omath.PePatternScanner.scan_in_module(FAKE_MODULE_BASE, "90 01 02", ".rdata") + assert(addr == nil, "expected nil for wrong section name") +end + +-- SectionScanResult: verify the type is registered and tostring works on a C++-returned value +function SectionScanResult_TypeIsRegistered() + assert(omath.SectionScanResult ~= nil, "SectionScanResult type should be registered") +end diff --git a/tests/lua/unit_test_lua_pe_scanner.cpp b/tests/lua/unit_test_lua_pe_scanner.cpp new file mode 100644 index 00000000..ac7737dc --- /dev/null +++ b/tests/lua/unit_test_lua_pe_scanner.cpp @@ -0,0 +1,113 @@ +// +// Created by orange on 10.03.2026. +// +#include +#include +#include +#include +#include +#include + +namespace +{ + std::vector make_fake_pe_module(std::uint32_t base_of_code, std::uint32_t size_code, + 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_hdr_size = 40; + constexpr std::uint32_t text_chars = 0x60000020; + + const std::uint32_t headers_end = section_table_off + section_hdr_size; + const std::uint32_t code_end = base_of_code + size_code; + const std::uint32_t total_size = std::max(headers_end, code_end) + 0x100; + std::vector buf(total_size, 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); }; + + w16(0x00, 0x5A4D); + w32(0x3C, e_lfanew); + w32(e_lfanew, nt_sig); + + const std::size_t fh = e_lfanew + 4; + w16(fh + 2, num_sections); + w16(fh + 16, opt_hdr_size); + + const std::size_t opt = fh + 20; + w16(opt + 0, opt_magic); + w32(opt + 4, size_code); + w32(opt + 20, base_of_code); + w64(opt + 24, 0); + w32(opt + 32, 0x1000); + w32(opt + 36, 0x200); + w32(opt + 56, code_end); + w32(opt + 60, headers_end); + w32(opt + 108, 0); + + const std::size_t sh = section_table_off; + std::memcpy(buf.data() + sh, ".text", 5); + w32(sh + 8, size_code); + w32(sh + 12, base_of_code); + w32(sh + 16, size_code); + w32(sh + 20, base_of_code); + w32(sh + 36, text_chars); + + if (base_of_code + code_bytes.size() <= buf.size()) + std::memcpy(buf.data() + base_of_code, code_bytes.data(), code_bytes.size()); + + return buf; + } +} // namespace + +class LuaPeScanner : public ::testing::Test +{ +protected: + lua_State* L = nullptr; + std::vector m_fake_module; + + void SetUp() override + { + const std::vector code = {0x90, 0x01, 0x02, 0x03, 0x04}; + m_fake_module = make_fake_pe_module(0x200, static_cast(code.size()), code); + + L = luaL_newstate(); + luaL_openlibs(L); + omath::lua::LuaInterpreter::register_lib(L); + + lua_pushinteger(L, static_cast( + reinterpret_cast(m_fake_module.data()))); + lua_setglobal(L, "FAKE_MODULE_BASE"); + + if (luaL_dofile(L, LUA_SCRIPTS_DIR "/pe_scanner_tests.lua") != LUA_OK) + FAIL() << lua_tostring(L, -1); + } + + void TearDown() override { lua_close(L); } + + void check(const char* func_name) + { + lua_getglobal(L, func_name); + if (lua_pcall(L, 0, 0, 0) != LUA_OK) + { + FAIL() << lua_tostring(L, -1); + lua_pop(L, 1); + } + } +}; + +TEST_F(LuaPeScanner, PatternScanner_FindsExactPattern) { check("PatternScanner_FindsExactPattern"); } +TEST_F(LuaPeScanner, PatternScanner_FindsPatternAtOffset) { check("PatternScanner_FindsPatternAtNonZeroOffset"); } +TEST_F(LuaPeScanner, PatternScanner_WildcardMatches) { check("PatternScanner_WildcardMatches"); } +TEST_F(LuaPeScanner, PatternScanner_ReturnsNilWhenNotFound) { check("PatternScanner_ReturnsNilWhenNotFound"); } +TEST_F(LuaPeScanner, PatternScanner_ReturnsNilForEmptyBuffer){ check("PatternScanner_ReturnsNilForEmptyBuffer"); } +TEST_F(LuaPeScanner, PeScanner_FindsExactPattern) { check("PeScanner_FindsExactPattern"); } +TEST_F(LuaPeScanner, PeScanner_WildcardMatches) { check("PeScanner_WildcardMatches"); } +TEST_F(LuaPeScanner, PeScanner_ReturnsNilWhenNotFound) { check("PeScanner_ReturnsNilWhenNotFound"); } +TEST_F(LuaPeScanner, PeScanner_CustomSectionFallsBackToNil) { check("PeScanner_CustomSectionFallsBackToNil"); } +TEST_F(LuaPeScanner, SectionScanResult_TypeIsRegistered) { check("SectionScanResult_TypeIsRegistered"); }