diff --git a/plugins/inmemoryscanner/Readme.md b/plugins/inmemoryscanner/Readme.md index 4d181a7f..15b0416f 100644 --- a/plugins/inmemoryscanner/Readme.md +++ b/plugins/inmemoryscanner/Readme.md @@ -71,8 +71,6 @@ For example the following diagram shows the memory padding of one VAD region con Shared memory regions that are not the base image of the process are skipped by default in order to reduce scanning time. This behavior can be controlled via the `scan_all_regions` config option. -To further optimize scan duration, memory regions >50MB will be ignored as well. -If desired, it is possible to increase or reduce the threshold via the `maximum_scan_size` config option. ### In Depth Example @@ -89,7 +87,7 @@ Consider the following VAD entry from the vad tree of a process `winlogon.exe` w | `EndAddress` | 1f43b599000 | This region has a size of `0x1f43b599000` - `0x1f43b593000` = `0x6000`. -However the pages from `0x1f43b596000` to `0x1f43b598000` (size `0x2000`) are not mapped into memory. +However, the pages from `0x1f43b596000` to `0x1f43b598000` (size `0x2000`) are not mapped into memory. Therefore, the resulting files will have the size of `0x5000` (mapped size + one zero-page). Note that the start and end address however are the original ones. ```console @@ -130,7 +128,6 @@ For this, add the following parts to the _VMICore_ config and tweak them to your | `directory` | Path to the folder where the compiled _VMICore_ plugins are located. | | `dump_memory` | Boolean. If set to `true` will result in scanned memory being dumped to files. Regions will be dumped to an `inmemorydumps` subfolder in the output directory. | | `ignored_processes` | List with processes that will not be scanned (or dumped) during the final scan. | -| `maximum_scan_size` | Number of bytes for the size of the largest contiguous memory region that will still be scanned. Defaults to `52428800` (50MB). | | `output_path` | Optional output path. If this is a relative path it is interpreted relatively to the _VMICore_ results directory. | | `plugins` | Add your plugin here by the exact name of your shared library (e.g. `libinmemoryscanner.so`). All plugin specific config keys should be added as sub-keys under this name. | | `scan_all_regions` | Optional boolean (defaults to `false`). Indicates whether to eagerly scan all memory regions as opposed to ignoring shared memory. | @@ -147,7 +144,6 @@ plugin_system: signature_file: /usr/local/share/inmemsigs/sigs.sig dump_memory: false scan_all_regions: false - maximum_scan_size: 52428800 output_path: "" ignored_processes: - SearchUI.exe diff --git a/plugins/inmemoryscanner/src/lib/CMakeLists.txt b/plugins/inmemoryscanner/src/lib/CMakeLists.txt index 6767c9e3..390d21c1 100644 --- a/plugins/inmemoryscanner/src/lib/CMakeLists.txt +++ b/plugins/inmemoryscanner/src/lib/CMakeLists.txt @@ -7,7 +7,7 @@ add_library(inmemoryscanner-obj OBJECT InMemory.cpp OutputXML.cpp Scanner.cpp - Yara.cpp) + YaraInterface.cpp) target_compile_features(inmemoryscanner-obj PUBLIC cxx_std_20) set_target_properties(inmemoryscanner-obj PROPERTIES POSITION_INDEPENDENT_CODE TRUE) target_include_directories(inmemoryscanner-obj INTERFACE $) @@ -16,6 +16,11 @@ include(FindPkgConfig) pkg_check_modules(YARA REQUIRED yara>=4) target_link_libraries(inmemoryscanner-obj PUBLIC ${YARA_LINK_LIBRARIES}) + +if (${YARA_VERSION} VERSION_GREATER_EQUAL 4.1) + target_compile_definitions(inmemoryscanner-obj PRIVATE LIBYARA_4_1) +endif () + pkg_check_modules(TCLAP REQUIRED tclap>=1.2) include(FetchContent) diff --git a/plugins/inmemoryscanner/src/lib/Common.h b/plugins/inmemoryscanner/src/lib/Common.h index 76f58c31..240f87a4 100644 --- a/plugins/inmemoryscanner/src/lib/Common.h +++ b/plugins/inmemoryscanner/src/lib/Common.h @@ -5,6 +5,7 @@ #include #include #include +#include #define INMEMORY_LOGGER_NAME std::string("InMemory_").append(FILENAME_STEM) @@ -14,6 +15,8 @@ namespace InMemoryScanner { std::string matchName; int64_t position; + + bool operator==(const Match& rhs) const = default; }; struct Rule @@ -21,5 +24,12 @@ namespace InMemoryScanner std::string ruleName; std::string ruleNamespace; std::vector matches; + + bool operator==(const Rule& rhs) const = default; }; + + inline std::size_t bytesToNumberOfPages(std::size_t size) + { + return (size + VmiCore::PagingDefinitions::pageSizeInBytes - 1) / VmiCore::PagingDefinitions::pageSizeInBytes; + } } diff --git a/plugins/inmemoryscanner/src/lib/Config.cpp b/plugins/inmemoryscanner/src/lib/Config.cpp index 860bcafd..a236566b 100644 --- a/plugins/inmemoryscanner/src/lib/Config.cpp +++ b/plugins/inmemoryscanner/src/lib/Config.cpp @@ -8,8 +8,6 @@ using VmiCore::Plugin::PluginInterface; namespace InMemoryScanner { - constexpr uint64_t defaultMaxScanSize = 52428800; // 50MB - Config::Config(const PluginInterface* pluginInterface) : logger(pluginInterface->newNamedLogger(INMEMORY_LOGGER_NAME)) { @@ -23,7 +21,6 @@ namespace InMemoryScanner outputPath = rootNode["output_path"].as(); dumpMemory = rootNode["dump_memory"].as(false); scanAllRegions = rootNode["scan_all_regions"].as(false); - maximumScanSize = rootNode["maximum_scan_size"].as(defaultMaxScanSize); auto ignoredProcessesVec = rootNode["ignored_processes"].as>(std::vector()); @@ -61,11 +58,6 @@ namespace InMemoryScanner return dumpMemory; } - uint64_t Config::getMaximumScanSize() const - { - return maximumScanSize; - } - void Config::overrideDumpMemoryFlag(bool value) { dumpMemory = value; diff --git a/plugins/inmemoryscanner/src/lib/Config.h b/plugins/inmemoryscanner/src/lib/Config.h index 6cfc63f7..2c4c79c1 100644 --- a/plugins/inmemoryscanner/src/lib/Config.h +++ b/plugins/inmemoryscanner/src/lib/Config.h @@ -32,8 +32,6 @@ namespace InMemoryScanner [[nodiscard]] virtual bool isDumpingMemoryActivated() const = 0; - [[nodiscard]] virtual uint64_t getMaximumScanSize() const = 0; - virtual void overrideDumpMemoryFlag(bool value) = 0; protected: @@ -59,8 +57,6 @@ namespace InMemoryScanner [[nodiscard]] bool isDumpingMemoryActivated() const override; - [[nodiscard]] uint64_t getMaximumScanSize() const override; - void overrideDumpMemoryFlag(bool value) override; private: @@ -70,6 +66,5 @@ namespace InMemoryScanner std::set ignoredProcesses; bool dumpMemory{}; bool scanAllRegions{}; - uint64_t maximumScanSize{}; }; } diff --git a/plugins/inmemoryscanner/src/lib/IYaraInterface.h b/plugins/inmemoryscanner/src/lib/IYaraInterface.h new file mode 100644 index 00000000..c3815e37 --- /dev/null +++ b/plugins/inmemoryscanner/src/lib/IYaraInterface.h @@ -0,0 +1,30 @@ +#ifndef INMEMORYSCANNER_IYARAINTERFACE_H +#define INMEMORYSCANNER_IYARAINTERFACE_H + +#include "Common.h" +#include +#include +#include +#include + +namespace InMemoryScanner +{ + class YaraException : public std::runtime_error + { + using std::runtime_error::runtime_error; + }; + + class IYaraInterface + { + public: + virtual ~IYaraInterface() = default; + + virtual std::vector scanMemory(VmiCore::addr_t regionBase, + std::span mappedRegions) = 0; + + protected: + IYaraInterface() = default; + }; +} + +#endif // INMEMORYSCANNER_IYARAINTERFACE_H diff --git a/plugins/inmemoryscanner/src/lib/InMemory.cpp b/plugins/inmemoryscanner/src/lib/InMemory.cpp index 0ccb9088..2587904f 100644 --- a/plugins/inmemoryscanner/src/lib/InMemory.cpp +++ b/plugins/inmemoryscanner/src/lib/InMemory.cpp @@ -11,7 +11,7 @@ #include "Config.h" #include "Dumping.h" #include "Filenames.h" -#include "Yara.h" +#include "YaraInterface.h" #include #include #include @@ -44,7 +44,7 @@ namespace InMemoryScanner { configuration->overrideDumpMemoryFlag(dumpMemoryArgument.getValue()); } - auto yara = std::make_unique(configuration->getSignatureFile()); + auto yara = std::make_unique(configuration->getSignatureFile()); auto dumping = std::make_unique(pluginInterface, configuration); scanner = std::make_unique(pluginInterface, configuration, std::move(yara), std::move(dumping)); } diff --git a/plugins/inmemoryscanner/src/lib/Scanner.cpp b/plugins/inmemoryscanner/src/lib/Scanner.cpp index 5984069e..9c11a7a1 100644 --- a/plugins/inmemoryscanner/src/lib/Scanner.cpp +++ b/plugins/inmemoryscanner/src/lib/Scanner.cpp @@ -1,26 +1,29 @@ #include "Scanner.h" #include "Common.h" #include "Filenames.h" +#include #include #include -#include #include +#include using VmiCore::ActiveProcessInformation; using VmiCore::addr_t; +using VmiCore::MappedRegion; using VmiCore::MemoryRegion; using VmiCore::pid_t; +using VmiCore::PagingDefinitions::pageSizeInBytes; using VmiCore::Plugin::PluginInterface; namespace InMemoryScanner { Scanner::Scanner(PluginInterface* pluginInterface, std::shared_ptr configuration, - std::unique_ptr yaraEngine, + std::unique_ptr yaraInterface, std::unique_ptr dumping) : pluginInterface(pluginInterface), configuration(std::move(configuration)), - yaraEngine(std::move(yaraEngine)), + yaraInterface(std::move(yaraInterface)), dumping(std::move(dumping)), logger(pluginInterface->newNamedLogger(INMEMORY_LOGGER_NAME)), inMemResultsLogger(pluginInterface->newNamedLogger(INMEMORY_LOGGER_NAME)) @@ -45,76 +48,104 @@ namespace InMemoryScanner bool Scanner::shouldRegionBeScanned(const MemoryRegion& memoryRegionDescriptor) { - bool verdict = true; if (configuration->isScanAllRegionsActivated()) { return true; } if (memoryRegionDescriptor.isSharedMemory && !memoryRegionDescriptor.isProcessBaseImage) { - verdict = false; logger->info("Skipping: Is shared memory and not the process base image."); + return false; } - return verdict; + return true; } - void - Scanner::scanMemoryRegion(pid_t pid, const std::string& processName, const MemoryRegion& memoryRegionDescriptor) + std::vector Scanner::constructPaddedMemoryRegion(std::span regions) + { + std::vector result; + + if (regions.empty()) + { + return result; + } + + std::size_t regionSize = 0; + for (const auto& region : regions) + { + regionSize += region.asSpan().size(); + regionSize += pageSizeInBytes; + } + // last region should not have succeeding padding page + regionSize -= pageSizeInBytes; + + result.reserve(regionSize); + // copy first region + auto frontRegionSpan = regions.front().asSpan(); + std::ranges::copy(frontRegionSpan.begin(), frontRegionSpan.end(), std::back_inserter(result)); + + // copy the rest of the regions with a padding page in between each chunk + for (std::size_t i = 1; i < regions.size(); i++) + { + result.insert(result.end(), pageSizeInBytes, 0); + auto regionSpan = regions[i].asSpan(); + std::ranges::copy(regionSpan.begin(), regionSpan.end(), std::back_inserter(result)); + } + + return result; + } + + void Scanner::scanMemoryRegion(pid_t pid, + addr_t dtb, + const std::string& processName, + const MemoryRegion& memoryRegionDescriptor) { logger->info("Scanning Memory region", {{"VA", fmt::format("{:x}", memoryRegionDescriptor.base)}, {"Size", memoryRegionDescriptor.size}, {"Module", memoryRegionDescriptor.moduleName}}); - if (shouldRegionBeScanned(memoryRegionDescriptor)) + if (!shouldRegionBeScanned(memoryRegionDescriptor)) { - auto scanSize = memoryRegionDescriptor.size; - auto maximumScanSize = configuration->getMaximumScanSize(); - if (scanSize > maximumScanSize) - { - logger->info("Memory region is too big, reducing to maximum scan size", - {{"Size", scanSize}, {"MaximumScanSize", maximumScanSize}}); - scanSize = maximumScanSize; - } + return; + } - logger->debug("Start getProcessMemoryRegion", {{"Size", scanSize}}); + auto memoryMapping = pluginInterface->mapProcessMemoryRegion( + memoryRegionDescriptor.base, dtb, bytesToNumberOfPages(memoryRegionDescriptor.size)); + auto mappedRegions = memoryMapping->getMappedRegions(); - auto memoryRegion = pluginInterface->readProcessMemoryRegion(pid, memoryRegionDescriptor.base, scanSize); + if (mappedRegions.empty()) + { + logger->debug("Extracted memory region has size 0, skipping"); + return; + } - logger->debug("End getProcessMemoryRegion", {{"Size", scanSize}}); - if (memoryRegion->empty()) - { - logger->debug("Extracted memory region has size 0, skipping"); - } - else - { - if (configuration->isDumpingMemoryActivated()) - { - logger->debug("Start dumpVadRegionToFile", {{"Size", memoryRegion->size()}}); - dumping->dumpMemoryRegion(processName, pid, memoryRegionDescriptor, *memoryRegion); - logger->debug("End dumpVadRegionToFile"); - } + if (configuration->isDumpingMemoryActivated()) + { + logger->debug("Start dumpVadRegionToFile", {{"Size", memoryRegionDescriptor.size}}); - logger->debug("Start scanMemory", {{"Size", memoryRegion->size()}}); + auto paddedRegion = constructPaddedMemoryRegion(mappedRegions); - // The semaphore protects the yara rules from being accessed more than YR_MAX_THREADS (32 atm.) times in - // parallel. - semaphore.wait(); - auto results = yaraEngine->scanMemory(*memoryRegion); - semaphore.notify(); + dumping->dumpMemoryRegion(processName, pid, memoryRegionDescriptor, paddedRegion); + } - logger->debug("End scanMemory"); + logger->debug("Start scanMemory", {{"Size", memoryRegionDescriptor.size}}); - if (!results->empty()) - { - for (const auto& result : *results) - { - pluginInterface->sendInMemDetectionEvent(result.ruleName); - } - outputXml.addResult(processName, pid, memoryRegionDescriptor.base, *results); - logInMemoryResultToTextFile(processName, pid, memoryRegionDescriptor.base, *results); - } + // The semaphore protects the yara rules from being accessed more than YR_MAX_THREADS (32 atm.) times in + // parallel. + semaphore.wait(); + auto results = yaraInterface->scanMemory(memoryRegionDescriptor.base, mappedRegions); + semaphore.notify(); + + logger->debug("End scanMemory"); + + if (!results.empty()) + { + for (const auto& result : results) + { + pluginInterface->sendInMemDetectionEvent(result.ruleName); } + outputXml.addResult(processName, pid, memoryRegionDescriptor.base, results); + logInMemoryResultToTextFile(processName, pid, memoryRegionDescriptor.base, results); } } @@ -141,8 +172,10 @@ namespace InMemoryScanner { try { - scanMemoryRegion( - processInformation->pid, *processInformation->fullName, memoryRegionDescriptor); + scanMemoryRegion(processInformation->pid, + processInformation->processUserDtb, + *processInformation->fullName, + memoryRegionDescriptor); } catch (const std::exception& exc) { diff --git a/plugins/inmemoryscanner/src/lib/Scanner.h b/plugins/inmemoryscanner/src/lib/Scanner.h index 37db1ac8..57a6af70 100644 --- a/plugins/inmemoryscanner/src/lib/Scanner.h +++ b/plugins/inmemoryscanner/src/lib/Scanner.h @@ -2,11 +2,12 @@ #include "Config.h" #include "Dumping.h" +#include "IYaraInterface.h" #include "OutputXML.h" #include "Semaphore.h" -#include "YaraInterface.h" #include #include +#include #include #include // NOLINT(modernize-deprecated-headers) @@ -17,7 +18,7 @@ namespace InMemoryScanner public: Scanner(VmiCore::Plugin::PluginInterface* pluginInterface, std::shared_ptr configuration, - std::unique_ptr yaraEngine, + std::unique_ptr yaraInterface, std::unique_ptr dumping); [[nodiscard]] static std::unique_ptr getFilenameFromPath(const std::string& path); @@ -31,7 +32,7 @@ namespace InMemoryScanner private: VmiCore::Plugin::PluginInterface* pluginInterface; std::shared_ptr configuration; - std::unique_ptr yaraEngine; + std::unique_ptr yaraInterface; OutputXML outputXml{}; std::unique_ptr dumping; std::unique_ptr logger; @@ -41,7 +42,10 @@ namespace InMemoryScanner [[nodiscard]] bool shouldRegionBeScanned(const VmiCore::MemoryRegion& memoryRegionDescriptor); + static std::vector constructPaddedMemoryRegion(std::span regions); + void scanMemoryRegion(pid_t pid, + uint64_t dtb, const std::string& processName, const VmiCore::MemoryRegion& memoryRegionDescriptor); diff --git a/plugins/inmemoryscanner/src/lib/Yara.cpp b/plugins/inmemoryscanner/src/lib/Yara.cpp deleted file mode 100644 index 86c49307..00000000 --- a/plugins/inmemoryscanner/src/lib/Yara.cpp +++ /dev/null @@ -1,89 +0,0 @@ -#include "Yara.h" - -namespace InMemoryScanner -{ - Yara::Yara(const std::string& rulesFile) - { - int err = 0; - - err = yr_initialize(); - if (err != ERROR_SUCCESS) - { - throw YaraException("Cannot initialize Yara. Error code: " + std::to_string(err)); - } - - err = yr_rules_load(rulesFile.c_str(), &rules); - if (err != ERROR_SUCCESS) - { - throw YaraException("Cannot load rules. Error code: " + std::to_string(err)); - } - } - - Yara::~Yara() - { - yr_rules_destroy(rules); - yr_finalize(); - } - - std::unique_ptr> Yara::scanMemory(std::vector& buffer) - { - auto results = std::make_unique>(); - int err = 0; - - err = yr_rules_scan_mem(rules, buffer.data(), buffer.size(), 0, yaraCallback, results.get(), 0); - if (err != ERROR_SUCCESS) - { - throw YaraException("Error scanning memory. Error code: " + std::to_string(err)); - } - - return results; - } - - int Yara::yaraCallback(YR_SCAN_CONTEXT* context, int message, void* message_data, void* user_data) - { - int ret = 0; - switch (message) - { - case CALLBACK_MSG_RULE_MATCHING: - ret = handleRuleMatch( - context, static_cast(message_data), static_cast*>(user_data)); - break; - case CALLBACK_MSG_RULE_NOT_MATCHING: - [[fallthrough]]; - case CALLBACK_MSG_SCAN_FINISHED: - ret = CALLBACK_CONTINUE; - break; - default: - ret = CALLBACK_ERROR; - break; - } - - return ret; - } - - int Yara::handleRuleMatch(YR_SCAN_CONTEXT* context, YR_RULE* rule, std::vector* results) - { - YR_STRING* string = nullptr; - YR_MATCH* match = nullptr; - - Rule tmpRule; - tmpRule.ruleName = rule->identifier; - tmpRule.ruleNamespace = rule->ns->name; - - yr_rule_strings_foreach(rule, string) - { - yr_string_matches_foreach(context, string, match) // NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) - { - Match tmpMatch; - tmpMatch.matchName = string->identifier; - tmpMatch.position = match->offset; - - tmpRule.matches.push_back(tmpMatch); - } - } - - results->push_back(tmpRule); - - return CALLBACK_CONTINUE; - } -} diff --git a/plugins/inmemoryscanner/src/lib/Yara.h b/plugins/inmemoryscanner/src/lib/Yara.h deleted file mode 100644 index 2df566ce..00000000 --- a/plugins/inmemoryscanner/src/lib/Yara.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include "YaraInterface.h" -#include - -namespace InMemoryScanner -{ - class Yara : public YaraInterface - { - public: - explicit Yara(const std::string& rulesFile); - - ~Yara() override; - - std::unique_ptr> scanMemory(std::vector& buffer) override; - - private: - YR_RULES* rules = nullptr; - - [[nodiscard]] static int - yaraCallback(YR_SCAN_CONTEXT* context, int message, void* message_data, void* user_data); - - [[nodiscard]] static int handleRuleMatch(YR_SCAN_CONTEXT* context, YR_RULE* rule, std::vector* results); - }; -} diff --git a/plugins/inmemoryscanner/src/lib/YaraInterface.cpp b/plugins/inmemoryscanner/src/lib/YaraInterface.cpp new file mode 100644 index 00000000..1c849b37 --- /dev/null +++ b/plugins/inmemoryscanner/src/lib/YaraInterface.cpp @@ -0,0 +1,146 @@ +#include "YaraInterface.h" +#include + +using VmiCore::addr_t; +using VmiCore::MappedRegion; +using VmiCore::PagingDefinitions::pageSizeInBytes; + +namespace +{ + struct YaraIteratorContext + { + std::vector blocks; + std::size_t index; + }; + + YR_MEMORY_BLOCK* get_next_block(YR_MEMORY_BLOCK_ITERATOR* iterator) + { + if (auto* iteratorContext = static_cast(iterator->context); + ++iteratorContext->index < iteratorContext->blocks.size()) + { + return &iteratorContext->blocks[iteratorContext->index]; + } + + return nullptr; + } + + YR_MEMORY_BLOCK* get_first_block(YR_MEMORY_BLOCK_ITERATOR* iterator) + { + auto* iteratorContext = static_cast(iterator->context); + iteratorContext->index = 0; + + return &iteratorContext->blocks[iteratorContext->index]; + } + + const uint8_t* fetch_block_data(YR_MEMORY_BLOCK* block) + { + return static_cast(block->context); + } +} + +namespace InMemoryScanner +{ + YaraInterface::YaraInterface(const std::string& rulesFile) + { + auto err = yr_initialize(); + if (err != ERROR_SUCCESS) + { + throw YaraException(fmt::format("Cannot initialize Yara. Error code: {}", err)); + } + + err = yr_rules_load(rulesFile.c_str(), &rules); + if (err != ERROR_SUCCESS) + { + throw YaraException(fmt::format("Cannot load rules. Error code: {}", err)); + } + } + + YaraInterface::~YaraInterface() + { + if (rules) + { + yr_rules_destroy(rules); + yr_finalize(); + } + } + + std::vector YaraInterface::scanMemory(addr_t regionBase, std::span mappedRegions) + { + std::vector results; + + YaraIteratorContext iteratorContext{}; + iteratorContext.blocks.reserve(mappedRegions.size()); + for (const auto& mappedRegion : mappedRegions) + { + iteratorContext.blocks.emplace_back(mappedRegion.num_pages * pageSizeInBytes, + mappedRegion.guestBaseVA - regionBase, + mappedRegion.mappingBase, + &fetch_block_data); + } +#ifdef LIBYARA_4_1 + YR_MEMORY_BLOCK_ITERATOR iterator{.context = &iteratorContext, + .first = &get_first_block, + .next = &get_next_block, + .file_size = nullptr, + .last_error = ERROR_SUCCESS}; +#else + YR_MEMORY_BLOCK_ITERATOR iterator{ + .context = &iteratorContext, .first = &get_first_block, .next = &get_next_block}; +#endif + + if (auto err = yr_rules_scan_mem_blocks(rules, &iterator, 0, yaraCallback, &results, 0); err != ERROR_SUCCESS) + { + throw YaraException(fmt::format("Error scanning memory. Error code: {}", err)); + } + + return results; + } + + int YaraInterface::yaraCallback(YR_SCAN_CONTEXT* context, int message, void* message_data, void* user_data) + { + int ret = 0; + switch (message) + { + case CALLBACK_MSG_RULE_MATCHING: + ret = handleRuleMatch( + context, static_cast(message_data), static_cast*>(user_data)); + break; + case CALLBACK_MSG_RULE_NOT_MATCHING: + [[fallthrough]]; + case CALLBACK_MSG_SCAN_FINISHED: + ret = CALLBACK_CONTINUE; + break; + default: + ret = CALLBACK_ERROR; + break; + } + + return ret; + } + + int YaraInterface::handleRuleMatch(YR_SCAN_CONTEXT* context, YR_RULE* rule, std::vector* results) + { + YR_STRING* string = nullptr; + YR_MATCH* match = nullptr; + + Rule tmpRule; + tmpRule.ruleName = rule->identifier; + tmpRule.ruleNamespace = rule->ns->name; + + yr_rule_strings_foreach(rule, string) + { + yr_string_matches_foreach(context, string, match) // NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) + { + Match tmpMatch; + tmpMatch.matchName = string->identifier; + tmpMatch.position = match->base + match->offset; + + tmpRule.matches.push_back(tmpMatch); + } + } + + results->push_back(tmpRule); + + return CALLBACK_CONTINUE; + } +} diff --git a/plugins/inmemoryscanner/src/lib/YaraInterface.h b/plugins/inmemoryscanner/src/lib/YaraInterface.h index ad56c641..923d7fb3 100644 --- a/plugins/inmemoryscanner/src/lib/YaraInterface.h +++ b/plugins/inmemoryscanner/src/lib/YaraInterface.h @@ -1,24 +1,49 @@ #pragma once #include "Common.h" -#include +#include "IYaraInterface.h" +#include +#include namespace InMemoryScanner { - class YaraException : public std::runtime_error + class YaraInterface : public IYaraInterface { public: - explicit YaraException(const std::string& Message) : std::runtime_error(Message.c_str()){}; - }; + explicit YaraInterface(const std::string& rulesFile); - class YaraInterface - { - public: - virtual ~YaraInterface() = default; + YaraInterface(const YaraInterface& other) = delete; + + YaraInterface(YaraInterface&& other) noexcept : rules(other.rules) + { + other.rules = nullptr; + } + + YaraInterface& operator=(const YaraInterface& other) = delete; + + YaraInterface& operator=(YaraInterface&& other) noexcept + { + if (this == &other) + { + return *this; + } + + rules = other.rules; + other.rules = nullptr; + + return *this; + } + + ~YaraInterface() override; + + std::vector scanMemory(VmiCore::addr_t regionBase, + std::span mappedRegions) override; + + private: + YR_RULES* rules = nullptr; - [[nodiscard]] virtual std::unique_ptr> scanMemory(std::vector& buffer) = 0; + static int yaraCallback(YR_SCAN_CONTEXT* context, int message, void* message_data, void* user_data); - protected: - YaraInterface() = default; + static int handleRuleMatch(YR_SCAN_CONTEXT* context, YR_RULE* rule, std::vector* results); }; } diff --git a/plugins/inmemoryscanner/test/CMakeLists.txt b/plugins/inmemoryscanner/test/CMakeLists.txt index 86e67b5f..9ab81ba0 100644 --- a/plugins/inmemoryscanner/test/CMakeLists.txt +++ b/plugins/inmemoryscanner/test/CMakeLists.txt @@ -1,6 +1,7 @@ add_executable(inmemoryscanner-test - FakeYara.cpp - Scanner_unittest.cpp) + FakeYaraInterface.cpp + Scanner_unittest.cpp + YaraInterface_unittest.cpp) target_link_libraries(inmemoryscanner-test inmemoryscanner-obj pthread) # Setup bundled google test framework diff --git a/plugins/inmemoryscanner/test/FakeYara.h b/plugins/inmemoryscanner/test/FakeYara.h deleted file mode 100644 index 7996caa6..00000000 --- a/plugins/inmemoryscanner/test/FakeYara.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include - -namespace InMemoryScanner -{ - class FakeYara : public YaraInterface - { - public: - std::unique_ptr> scanMemory(std::vector& buffer) override; - - bool max_threads_exceeded = false; - - private: - int concurrentThreads = 0; - }; -} diff --git a/plugins/inmemoryscanner/test/FakeYara.cpp b/plugins/inmemoryscanner/test/FakeYaraInterface.cpp similarity index 58% rename from plugins/inmemoryscanner/test/FakeYara.cpp rename to plugins/inmemoryscanner/test/FakeYaraInterface.cpp index badee8b1..1b0606e3 100644 --- a/plugins/inmemoryscanner/test/FakeYara.cpp +++ b/plugins/inmemoryscanner/test/FakeYaraInterface.cpp @@ -1,10 +1,12 @@ -#include "FakeYara.h" +#include "FakeYaraInterface.h" #include #include // NOLINT(modernize-deprecated-headers) namespace InMemoryScanner { - std::unique_ptr> FakeYara::scanMemory([[maybe_unused]] std::vector& buffer) + std::vector + FakeYaraInterface::scanMemory([[maybe_unused]] VmiCore::addr_t regionBase, + [[maybe_unused]] std::span mappedRegions) { concurrentThreads++; if (concurrentThreads > YR_MAX_THREADS) @@ -13,6 +15,6 @@ namespace InMemoryScanner } std::this_thread::sleep_for(std::chrono::seconds(1)); concurrentThreads--; - return std::make_unique>(); + return {}; } } diff --git a/plugins/inmemoryscanner/test/FakeYaraInterface.h b/plugins/inmemoryscanner/test/FakeYaraInterface.h new file mode 100644 index 00000000..62d69bc9 --- /dev/null +++ b/plugins/inmemoryscanner/test/FakeYaraInterface.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +namespace InMemoryScanner +{ + class FakeYaraInterface : public IYaraInterface + { + public: + std::vector scanMemory(VmiCore::addr_t regionBase, + std::span mappedRegions) override; + + bool max_threads_exceeded = false; + + private: + int concurrentThreads = 0; + }; +} diff --git a/plugins/inmemoryscanner/test/Scanner_unittest.cpp b/plugins/inmemoryscanner/test/Scanner_unittest.cpp index 5d7e78c5..4c63d56b 100644 --- a/plugins/inmemoryscanner/test/Scanner_unittest.cpp +++ b/plugins/inmemoryscanner/test/Scanner_unittest.cpp @@ -1,15 +1,17 @@ -#include "FakeYara.h" +#include "FakeYaraInterface.h" #include "mock_Config.h" #include "mock_Dumping.h" -#include "mock_Yara.h" +#include "mock_YaraInterface.h" #include #include #include #include +#include #include #include #include #include +#include using testing::_; using testing::An; @@ -20,10 +22,14 @@ using testing::NiceMock; using testing::Return; using testing::Unused; using VmiCore::ActiveProcessInformation; +using VmiCore::addr_t; +using VmiCore::MappedRegion; using VmiCore::MemoryRegion; using VmiCore::MockLogger; using VmiCore::MockMemoryRegionExtractor; using VmiCore::MockPageProtection; +using VmiCore::pid_t; +using VmiCore::PagingDefinitions::pageSizeInBytes; using VmiCore::Plugin::MockPluginInterface; namespace InMemoryScanner @@ -31,9 +37,10 @@ namespace InMemoryScanner class ScannerTestBaseFixture : public testing::Test { protected: - const size_t maxScanSize = 0x3200000; const pid_t testPid = 4; + const addr_t testDtb = 0x9876; const pid_t processIdWithSharedBaseImageRegion = 5; + const addr_t dtbWithSharedBaseImageRegion = 0x8765; std::unique_ptr pluginInterface = std::make_unique(); std::shared_ptr configuration = std::make_shared(); @@ -46,8 +53,10 @@ namespace InMemoryScanner std::filesystem::path inMemoryDumpsPath = "inMemDumps"; std::filesystem::path dumpedRegionsPath = inMemoryDumpsPath / "dumpedRegions"; - VmiCore::addr_t startAddress = 0x1234000; - size_t size = 0x666; + addr_t startAddress = 0x1234000; + size_t size = pageSizeInBytes; + std::vector testPageContent = std::vector(size, 1); + std::vector regionMappings = std::vector(1, MappedRegion(startAddress, testPageContent)); std::unique_ptr>> runningProcesses; @@ -55,23 +64,16 @@ namespace InMemoryScanner { ON_CALL(*pluginInterface, newNamedLogger(_)) .WillByDefault([]() { return std::make_unique>(); }); - ON_CALL(*pluginInterface, getResultsDir()).WillByDefault([]() { return std::make_unique(); }); - ON_CALL(*configuration, getMaximumScanSize()).WillByDefault(Return(maxScanSize)); - // make sure that we return a non-empty memory region or else we might skip important parts - ON_CALL(*pluginInterface, readProcessMemoryRegion(_, _, _)) - .WillByDefault([]() { return std::make_unique>(6, 9); }); - ON_CALL(*configuration, getOutputPath()) .WillByDefault([inMemoryDumpsPath = inMemoryDumpsPath]() { return inMemoryDumpsPath; }); - ON_CALL(*configuration, getMaximumScanSize()).WillByDefault(Return(maxScanSize)); runningProcesses = std::make_unique>>(); auto m1 = std::make_unique(); systemMemoryRegionExtractorRaw = m1.get(); runningProcesses->push_back(std::make_shared( ActiveProcessInformation{0, - 0, - 0, + testDtb, + testDtb, testPid, 0, "System.exe", @@ -83,8 +85,8 @@ namespace InMemoryScanner sharedBaseImageMemoryRegionExtractorRaw = m2.get(); runningProcesses->push_back(std::make_shared( ActiveProcessInformation{0, - 0, - 0, + dtbWithSharedBaseImageRegion, + dtbWithSharedBaseImageRegion, processIdWithSharedBaseImageRegion, 0, "SomeProcess.exe", @@ -92,7 +94,10 @@ namespace InMemoryScanner std::make_unique(""), std::move(m2), false})); - }; + + createMemoryMapping(testDtb, startAddress, bytesToNumberOfPages(size), regionMappings); + createMemoryMapping(dtbWithSharedBaseImageRegion, startAddress, bytesToNumberOfPages(size), regionMappings); + } std::shared_ptr getProcessInfoFromRunningProcesses(pid_t pid) { @@ -100,7 +105,20 @@ namespace InMemoryScanner runningProcesses->cend(), [pid = pid](const std::shared_ptr& a) { return a->pid == pid; }); - }; + } + + void + createMemoryMapping(addr_t dtb, addr_t baseVA, std::size_t numberOfPages, std::span mappedRegions) + { + ON_CALL(*pluginInterface, mapProcessMemoryRegion(baseVA, dtb, numberOfPages)) + .WillByDefault( + [mappedRegions = mappedRegions]() + { + auto mapping = std::make_unique(); + ON_CALL(*mapping, getMappedRegions()).WillByDefault(Return(mappedRegions)); + return mapping; + }); + } }; class ScannerTestFixtureDumpingDisabled : public ScannerTestBaseFixture @@ -115,7 +133,9 @@ namespace InMemoryScanner ON_CALL(*configuration, isDumpingMemoryActivated()).WillByDefault(Return(false)); auto dumping = std::make_unique>(); dumpingRawPointer = dumping.get(); - scanner.emplace(pluginInterface.get(), configuration, std::unique_ptr{}, std::move(dumping)); + auto yara = std::make_unique>(); + ON_CALL(*yara, scanMemory(_, _)).WillByDefault(Return(std::vector{})); + scanner.emplace(pluginInterface.get(), configuration, std::move(yara), std::move(dumping)); }; }; @@ -152,8 +172,8 @@ namespace InMemoryScanner return memoryRegions; }); auto dumping = std::make_unique(pluginInterface.get(), configuration); - auto yara = std::make_unique>(); - ON_CALL(*yara, scanMemory(_)).WillByDefault([]() { return std::make_unique>(); }); + auto yara = std::make_unique>(); + ON_CALL(*yara, scanMemory(_, _)).WillByDefault(Return(std::vector{})); scanner.emplace(pluginInterface.get(), configuration, std::move(yara), std::move(dumping)); }; @@ -169,21 +189,17 @@ namespace InMemoryScanner } }; - TEST_F(ScannerTestFixtureDumpingDisabled, scanProcess_largeMemoryRegion_trimToMaxScanSize) + std::vector constructPaddedRegion(const std::initializer_list>& list) { - ON_CALL(*systemMemoryRegionExtractorRaw, extractAllMemoryRegions()) - .WillByDefault( - [startAddress = startAddress, maxScanSize = maxScanSize]() - { - auto memoryRegions = std::make_unique>(); - memoryRegions->emplace_back( - startAddress, maxScanSize + 1, "", std::make_unique(), false, false, false); - return memoryRegions; - }); + std::vector paddedRegion{}; + paddedRegion.reserve(list.size() * pageSizeInBytes); - EXPECT_CALL(*pluginInterface, readProcessMemoryRegion(testPid, startAddress, maxScanSize)) - .WillOnce(Return(ByMove(std::make_unique>()))); - EXPECT_NO_THROW(scanner->scanProcess(getProcessInfoFromRunningProcesses(testPid))); + for (const auto& el : list) + { + std::copy(el.begin(), el.end(), std::back_inserter(paddedRegion)); + } + + return paddedRegion; } TEST_F(ScannerTestFixtureDumpingDisabled, scanProcess_smallMemoryRegion_originalReadMemoryRegionSize) @@ -197,10 +213,18 @@ namespace InMemoryScanner startAddress, size, "", std::make_unique(), false, false, false); return memoryRegions; }); + auto process = getProcessInfoFromRunningProcesses(testPid); - EXPECT_CALL(*pluginInterface, readProcessMemoryRegion(testPid, startAddress, size)) - .WillOnce(Return(ByMove(std::make_unique>()))); - EXPECT_NO_THROW(scanner->scanProcess(getProcessInfoFromRunningProcesses(testPid))); + EXPECT_CALL(*pluginInterface, + mapProcessMemoryRegion(startAddress, process->processUserDtb, bytesToNumberOfPages(size))) + .WillOnce( + [regionMappings = regionMappings]() + { + auto mapping = std::make_unique(); + EXPECT_CALL(*mapping, getMappedRegions()).WillOnce(Return(regionMappings)); + return mapping; + }); + EXPECT_NO_THROW(scanner->scanProcess(process)); } TEST_F(ScannerTestFixtureDumpingDisabled, scanProcess_disabledDumping_dumpingNotCalled) @@ -214,10 +238,8 @@ namespace InMemoryScanner startAddress, size, "", std::make_unique(), false, false, false); return memoryRegions; }); - EXPECT_CALL(*pluginInterface, readProcessMemoryRegion(testPid, startAddress, size)) - .WillOnce(Return(ByMove(std::make_unique>()))); - EXPECT_CALL(*dumpingRawPointer, dumpMemoryRegion(_, _, _, _)).Times(0); + EXPECT_CALL(*dumpingRawPointer, dumpMemoryRegion(_, _, _, _)).Times(0); EXPECT_NO_THROW(scanner->scanProcess(getProcessInfoFromRunningProcesses(testPid))); } @@ -225,13 +247,14 @@ namespace InMemoryScanner { std::string fullProcessName = "abcdefghijklmnopqrstuvwxyz!1!"; std::string trimmedProcessName = "abcdefghijklmn"; - auto pid = 123; + pid_t pid = 123; + addr_t dtb = 0x4444; auto memoryRegionExtractor = std::make_unique(); auto* memoryRegionExtractorRaw = memoryRegionExtractor.get(); auto processWithLongName = std::make_shared( ActiveProcessInformation{0, - 0, - 0, + dtb, + dtb, pid, 0, trimmedProcessName, @@ -256,6 +279,7 @@ namespace InMemoryScanner startAddress + size, uidRegEx); auto expectedFileNameWithPathRegEx = "^" + (dumpedRegionsPath / expectedFileNameRegEx).string() + "$"; + createMemoryMapping(dtb, startAddress, bytesToNumberOfPages(size), regionMappings); EXPECT_CALL(*pluginInterface, writeToFile(ContainsRegex(expectedFileNameWithPathRegEx), An&>())); @@ -283,7 +307,7 @@ namespace InMemoryScanner TEST_F(ScannerTestFixtureDumpingDisabled, scanAllProcesses_MoreScanningThreadThanAllowedByYara_ThreadLimitNotExceeded) { - auto yaraFake = std::make_unique(); + auto yaraFake = std::make_unique(); auto* yaraFakeRaw = yaraFake.get(); scanner.emplace( pluginInterface.get(), configuration, std::move(yaraFake), std::make_unique>()); @@ -291,8 +315,8 @@ namespace InMemoryScanner auto* memoryRegionExtractorRaw = memoryRegionExtractor.get(); auto processInfo = std::make_shared( ActiveProcessInformation{0, - 0, - 0, + testDtb, + testDtb, testPid, 0, "System.exe", @@ -327,13 +351,14 @@ namespace InMemoryScanner { std::string fullProcessName = "abcdefghijklmnop"; std::string trimmedProcessName = "abcdefghijklmn"; - auto pid = 123; + pid_t pid = 312; + addr_t dtb = 0x5555; auto memoryRegionExtractor = std::make_unique(); auto* memoryRegionExtractorRaw = memoryRegionExtractor.get(); auto processInfo = std::make_shared( ActiveProcessInformation{0, - 0, - 0, + dtb, + dtb, pid, 0, "", @@ -369,6 +394,7 @@ namespace InMemoryScanner return memoryRegions; }); + createMemoryMapping(dtb, startAddress, bytesToNumberOfPages(size), regionMappings); EXPECT_CALL(*pluginInterface, writeToFile(_, An())).Times(AnyNumber()); EXPECT_CALL(*pluginInterface, writeToFile(expectedFileName.string(), expectedFileContent + "\n")).Times(1); @@ -376,4 +402,45 @@ namespace InMemoryScanner ASSERT_NO_THROW(scanner->scanAllProcesses()); ASSERT_NO_THROW(scanner->saveOutput()); } + + TEST_F(ScannerTestFixtureDumpingEnabled, scanProcess_complexMemoryRegion_regionWithCorrectPaddingDumped) + { + pid_t pid = 333; + addr_t dtb = 0x4554; + auto memoryRegionExtractor = std::make_unique(); + auto* memoryRegionExtractorRaw = memoryRegionExtractor.get(); + auto processInfo = + std::make_shared(ActiveProcessInformation{0, + dtb, + dtb, + pid, + 0, + "", + std::make_unique(""), + std::make_unique(""), + std::move(memoryRegionExtractor), + false}); + // Layout of complex region: 1 page, followed by 2 unmapped pages, followed by 2 pages + std::size_t complexRegionSize = 5 * pageSizeInBytes; + auto complexRegionDescriptor = MemoryRegion( + startAddress, complexRegionSize, "", std::make_unique(), false, false, false); + ON_CALL(*memoryRegionExtractorRaw, extractAllMemoryRegions()) + .WillByDefault( + [&memoryRegionDescriptor = complexRegionDescriptor]() + { + auto memoryRegions = std::make_unique>(); + memoryRegions->push_back(std::move(memoryRegionDescriptor)); + + return memoryRegions; + }); + auto twoPageRegionContent = std::vector(2 * pageSizeInBytes, 0xCA); + std::vector complexMappings{{startAddress, testPageContent}, + {startAddress + 3 * pageSizeInBytes, twoPageRegionContent}}; + createMemoryMapping(dtb, startAddress, bytesToNumberOfPages(complexRegionSize), complexMappings); + auto paddingPage = std::vector(pageSizeInBytes, 0); + auto expectedPaddedRegion = constructPaddedRegion({testPageContent, paddingPage, twoPageRegionContent}); + + EXPECT_CALL(*pluginInterface, writeToFile(_, expectedPaddedRegion)).Times(1); + ASSERT_NO_THROW(scanner->scanProcess(processInfo)); + } } diff --git a/plugins/inmemoryscanner/test/YaraInterface_unittest.cpp b/plugins/inmemoryscanner/test/YaraInterface_unittest.cpp new file mode 100644 index 00000000..f9539eb1 --- /dev/null +++ b/plugins/inmemoryscanner/test/YaraInterface_unittest.cpp @@ -0,0 +1,186 @@ +#include +#include +#include +#include +#include + +using testing::UnorderedElementsAre; +using VmiCore::PagingDefinitions::pageSizeInBytes; + +namespace InMemoryScanner +{ + std::vector constructPageWithContent(const std::string& string, bool insertAtBack = false) + { + std::vector result(pageSizeInBytes, 0); + auto insertPosition = insertAtBack ? result.end() - string.size() : result.begin(); + std::copy(string.begin(), string.end(), insertPosition); + return result; + } + + std::string compileYaraRules(std::string_view rules) + { + auto fileName = fmt::format("{}.sigs", testing::UnitTest::GetInstance()->current_test_info()->name()); + + if (yr_initialize() != ERROR_SUCCESS) + { + throw std::runtime_error("Unable to initialize libyara"); + } + YR_COMPILER* compiler = nullptr; + if (yr_compiler_create(&compiler) != ERROR_SUCCESS) + { + throw std::runtime_error("Unable to create yara compiler"); + } + if (auto syntax_errors = yr_compiler_add_string(compiler, rules.data(), nullptr) > 0) + { + throw std::runtime_error( + fmt::format("Compiled rules are faulty. Number of syntax errors: {}", syntax_errors)); + } + YR_RULES* compiled_rules = nullptr; + if (yr_compiler_get_rules(compiler, &compiled_rules)) + { + throw std::runtime_error("Unable to obtain rules from compiler"); + } + if (yr_rules_save(compiled_rules, fileName.c_str()) != ERROR_SUCCESS) + { + throw std::runtime_error("Unable to save rules to file"); + } + yr_compiler_destroy(compiler); + yr_finalize(); + + return fileName; + } + + TEST(YaraTest, scanMemory_MissingMemoryRegion_NoMatch) + { + auto* rules = R"( + rule testRule + { + strings: + $test = "ABCD" + $test2 = "DCBA" + + condition: + all of them + } + )"; + auto yaraInterface = YaraInterface(compileYaraRules(rules)); + auto subRegion1 = constructPageWithContent("ABCD"); + std::vector memoryRegions{{0x0, subRegion1}}; + + auto matches = yaraInterface.scanMemory(memoryRegions.front().guestBaseVA, memoryRegions); + + EXPECT_EQ(matches.size(), 0); + } + + TEST(YaraTest, scanMemory_StringSplitInHalfThroughSubRegionBoundary_NoMatch) + { + auto* rules = R"( + rule testRule + { + strings: + $test = "CDDC" + + condition: + all of them + } + )"; + auto yaraInterface = YaraInterface(compileYaraRules(rules)); + auto subRegion1 = constructPageWithContent("ABCD", true); + auto subRegion2 = constructPageWithContent("DCBA", false); + std::vector memoryRegions{{0x0, subRegion1}, {pageSizeInBytes, subRegion2}}; + + auto matches = yaraInterface.scanMemory(memoryRegions.front().guestBaseVA, memoryRegions); + + EXPECT_EQ(matches.size(), 0); + } + + TEST(YaraTest, scanMemory_AllOfConditionStringsInDifferentRegions_NoMatch) + { + auto* rules = R"( + rule testRule + { + strings: + $test = "ABCD" + $test2 = "DCBA" + + condition: + all of them + } + )"; + auto yaraInterface = YaraInterface(compileYaraRules(rules)); + auto subRegion1 = constructPageWithContent("ABCD"); + auto subRegion2 = constructPageWithContent("DCBA"); + std::vector memoryRegion1{{0x0, subRegion1}}; + std::vector memoryRegion2{{4 * pageSizeInBytes, subRegion2}}; + + auto matches1 = yaraInterface.scanMemory(memoryRegion1.front().guestBaseVA, memoryRegion1); + auto matches2 = yaraInterface.scanMemory(memoryRegion2.front().guestBaseVA, memoryRegion2); + + EXPECT_EQ(matches1.size(), 0); + EXPECT_EQ(matches2.size(), 0); + } + + TEST(YaraTest, scanMemory_AllOfConditionStringsInDifferentSubRegions_Matches) + { + auto* rules = R"( + rule testRule + { + strings: + $test = "ABCD" + $test2 = "DCBA" + + condition: + all of them + } + )"; + auto yaraInterface = YaraInterface(compileYaraRules(rules)); + auto subRegion1 = constructPageWithContent("ABCD"); + auto subRegion2 = constructPageWithContent("DCBA"); + std::vector memoryRegions{{0x0, subRegion1}, {4 * pageSizeInBytes, subRegion2}}; + Rule expectedMatch{"testRule", "default", {{"$test", 0x0}, {"$test2", 4 * pageSizeInBytes}}}; + + auto matches = yaraInterface.scanMemory(memoryRegions.front().guestBaseVA, memoryRegions); + + ASSERT_EQ(matches.size(), 1); + EXPECT_THAT(matches, UnorderedElementsAre(expectedMatch)); + } + + TEST(YaraTest, scanMemory_DifferentRulesInRegions_BothMatch) + { + auto* rules = R"( + rule testRule + { + strings: + $test = "ABCD" + $test2 = "DCBA" + + condition: + all of them + } + + rule testRule2 + { + strings: + $test = "E" + $test2 = "F" + + condition: + all of them + } + )"; + auto yaraInterface = YaraInterface(compileYaraRules(rules)); + auto subRegion1 = constructPageWithContent("ABCD"); + auto subRegion2 = constructPageWithContent("DCBA"); + auto subRegion3 = constructPageWithContent("EFGH"); + std::vector memoryRegions{ + {0x0, subRegion1}, {4 * pageSizeInBytes, subRegion2}, {8 * pageSizeInBytes, subRegion3}}; + Rule expectedMatch1{"testRule", "default", {{"$test", 0x0}, {"$test2", 4 * pageSizeInBytes}}}; + Rule expectedMatch2{ + "testRule2", "default", {{"$test", 8 * pageSizeInBytes}, {"$test2", 8 * pageSizeInBytes + 1}}}; + + auto matches = yaraInterface.scanMemory(memoryRegions.front().guestBaseVA, memoryRegions); + + ASSERT_EQ(matches.size(), 2); + EXPECT_THAT(matches, UnorderedElementsAre(expectedMatch1, expectedMatch2)); + } +} diff --git a/plugins/inmemoryscanner/test/mock_Config.h b/plugins/inmemoryscanner/test/mock_Config.h index d643dae7..108089fe 100644 --- a/plugins/inmemoryscanner/test/mock_Config.h +++ b/plugins/inmemoryscanner/test/mock_Config.h @@ -14,7 +14,6 @@ namespace InMemoryScanner MOCK_METHOD(bool, isProcessIgnored, (const std::string& processName), (const, override)); MOCK_METHOD(bool, isScanAllRegionsActivated, (), (const, override)); MOCK_METHOD(bool, isDumpingMemoryActivated, (), (const, override)); - MOCK_METHOD(uint64_t, getMaximumScanSize, (), (const, override)); MOCK_METHOD(void, overrideDumpMemoryFlag, (bool value), (override)); }; } diff --git a/plugins/inmemoryscanner/test/mock_Yara.h b/plugins/inmemoryscanner/test/mock_Yara.h deleted file mode 100644 index 1dea4dc1..00000000 --- a/plugins/inmemoryscanner/test/mock_Yara.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include -#include - -namespace InMemoryScanner -{ - class MockYara : public YaraInterface - { - public: - MOCK_METHOD(std::unique_ptr>, scanMemory, (std::vector & buffer), (override)); - }; -} diff --git a/plugins/inmemoryscanner/test/mock_YaraInterface.h b/plugins/inmemoryscanner/test/mock_YaraInterface.h new file mode 100644 index 00000000..d9efa3c4 --- /dev/null +++ b/plugins/inmemoryscanner/test/mock_YaraInterface.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +namespace InMemoryScanner +{ + class MockYaraInterface : public IYaraInterface + { + public: + MOCK_METHOD(std::vector, + scanMemory, + (VmiCore::addr_t, std::span), + (override)); + }; +} diff --git a/vmicore/src/include/CMakeLists.txt b/vmicore/src/include/CMakeLists.txt index 7dd44541..47f773a1 100644 --- a/vmicore/src/include/CMakeLists.txt +++ b/vmicore/src/include/CMakeLists.txt @@ -17,6 +17,8 @@ target_sources(vmicore-public-headers INTERFACE vmicore/callback.h vmicore/vmi/IBreakpoint.h vmicore/vmi/IIntrospectionAPI.h + vmicore/vmi/IMemoryMapping.h + vmicore/vmi/MappedRegion.h vmicore/vmi/events/IInterruptEvent.h vmicore/vmi/events/IRegisterReadable.h vmicore/filename.h diff --git a/vmicore/src/include/vmicore/plugins/PluginInterface.h b/vmicore/src/include/vmicore/plugins/PluginInterface.h index 94b548a9..3b7bc148 100644 --- a/vmicore/src/include/vmicore/plugins/PluginInterface.h +++ b/vmicore/src/include/vmicore/plugins/PluginInterface.h @@ -7,6 +7,7 @@ #include "../vmi/BpResponse.h" #include "../vmi/IBreakpoint.h" #include "../vmi/IIntrospectionAPI.h" +#include "../vmi/IMemoryMapping.h" #include "../vmi/events/IInterruptEvent.h" #include #include @@ -22,19 +23,21 @@ namespace VmiCore::Plugin class PluginInterface { public: - constexpr static uint8_t API_VERSION = 15; + constexpr static uint8_t API_VERSION = 16; virtual ~PluginInterface() = default; /** - * Reads a region of contiguous virtual memory from a process. The starting offset as well as the size must be - * 4kb page aligned. + * Map a guest memory region into the address space of the introspection application. See IMemoryMapping.h for + * more details. * - * @return A unique pointer to a byte vector containing the memory content. Subregions that could not be - * extracted (e.g. because they are paged out) will be replaced by a single all zero padding page. + * @param baseVA Start of the virtual address range inside the guest. + * @param dtb Process dtb + * @param numberOfPages Size of the region in 4kb pages. + * @return An instance of IMemoryMapping. See IMemoryMapping.h for details. */ - [[nodiscard]] virtual std::unique_ptr> - readProcessMemoryRegion(pid_t pid, addr_t address, size_t numberOfBytes) const = 0; + [[nodiscard]] virtual std::unique_ptr + mapProcessMemoryRegion(addr_t baseVA, addr_t dtb, std::size_t numberOfPages) const = 0; /** * Obtain a vector containing an OS-agnostic representation of all currently running processes. diff --git a/vmicore/src/include/vmicore/vmi/IMemoryMapping.h b/vmicore/src/include/vmicore/vmi/IMemoryMapping.h new file mode 100644 index 00000000..a2e7cd6e --- /dev/null +++ b/vmicore/src/include/vmicore/vmi/IMemoryMapping.h @@ -0,0 +1,41 @@ +#ifndef VMICORE_IMEMORYMAPPING_H +#define VMICORE_IMEMORYMAPPING_H + +#include "MappedRegion.h" +#include + +namespace VmiCore +{ + class MemoryMappingError : public std::runtime_error + { + using std::runtime_error::runtime_error; + }; + + /** + * Class representing chunks of memory that were able to be mapped within in a given virtual address range. + */ + class IMemoryMapping + { + public: + virtual ~IMemoryMapping() = default; + + /** + * Retrieves a set of memory mapping descriptors. See MappedRegion.h for details. Elements are ordered from + * lowest to highest guest VA. + * + * @throws MemoryMappingError Will occur if unmap has already been called. + */ + [[nodiscard]] virtual std::span getMappedRegions() const = 0; + + /** + * Will unmap all mappings. This function will also be called as soon as an instance of this class goes out of + * scope. + */ + virtual void unmap() = 0; + + protected: + IMemoryMapping() = default; + }; +} + +#endif // VMICORE_IMEMORYMAPPING_H diff --git a/vmicore/src/include/vmicore/vmi/MappedRegion.h b/vmicore/src/include/vmicore/vmi/MappedRegion.h new file mode 100644 index 00000000..92103828 --- /dev/null +++ b/vmicore/src/include/vmicore/vmi/MappedRegion.h @@ -0,0 +1,54 @@ +#ifndef VMICORE_MAPPEDREGION_H +#define VMICORE_MAPPEDREGION_H + +#include "../os/PagingDefinitions.h" +#include "../types.h" +#include +#include +#include + +namespace VmiCore +{ + /** + * Represents a chunk of contiguous memory that has been mapped into the address space of the introspection + * application. + */ + struct MappedRegion + { + /// A virtual address representing the start of the memory region inside the guest. + addr_t guestBaseVA; + /// Size of the memory region in pages. Will always be the finest granularity, even if the guest uses large + /// pages. + std::size_t num_pages; + /// Base address of the mapped memory region inside the introspection application's address space. + void* mappingBase; + + MappedRegion(addr_t guestBaseVA, std::size_t num_pages, void* mappingBase) + : guestBaseVA(guestBaseVA), num_pages(num_pages), mappingBase(mappingBase) + { + } + + MappedRegion(addr_t guestBaseVA, std::span mapping) + : guestBaseVA(guestBaseVA), + num_pages(mapping.size() / PagingDefinitions::pageSizeInBytes), + mappingBase(static_cast(mapping.data())) + { + if (mapping.size() % PagingDefinitions::pageSizeInBytes != 0) + { + throw std::invalid_argument("Mapping has to be page aligned"); + } + } + + /** + * Convenience method for safe access to the mapped memory. + */ + [[nodiscard]] std::span asSpan() const + { + return {static_cast(mappingBase), num_pages * PagingDefinitions::pageSizeInBytes}; + } + + bool operator==(const MappedRegion&) const = default; + }; +} + +#endif // VMICORE_MAPPEDREGION_H diff --git a/vmicore/src/lib/CMakeLists.txt b/vmicore/src/lib/CMakeLists.txt index 9c2e81e3..d5ee3dc1 100644 --- a/vmicore/src/lib/CMakeLists.txt +++ b/vmicore/src/lib/CMakeLists.txt @@ -28,6 +28,7 @@ add_library(vmicore-lib OBJECT vmi/InterruptEventSupervisor.cpp vmi/InterruptGuard.cpp vmi/LibvmiInterface.cpp + vmi/MemoryMapping.cpp vmi/SingleStepSupervisor.cpp vmi/VmiInitData.cpp vmi/VmiInitError.cpp) diff --git a/vmicore/src/lib/os/windows/ActiveProcessesSupervisor.cpp b/vmicore/src/lib/os/windows/ActiveProcessesSupervisor.cpp index e637ca1b..c4fb1661 100644 --- a/vmicore/src/lib/os/windows/ActiveProcessesSupervisor.cpp +++ b/vmicore/src/lib/os/windows/ActiveProcessesSupervisor.cpp @@ -3,6 +3,7 @@ #include #include #include +#include namespace VmiCore::Windows { diff --git a/vmicore/src/lib/plugins/PluginSystem.cpp b/vmicore/src/lib/plugins/PluginSystem.cpp index 251def93..cba1bed3 100644 --- a/vmicore/src/lib/plugins/PluginSystem.cpp +++ b/vmicore/src/lib/plugins/PluginSystem.cpp @@ -1,17 +1,18 @@ #include "PluginSystem.h" +#include "../vmi/MemoryMapping.h" +#include "PluginException.h" #include +#include #include #include #include #include -#include namespace VmiCore { namespace { bool isInstanciated = false; - constexpr char const* paddingLogFile = "memoryExtractionPaddingLog.txt"; } PluginSystem::PluginSystem(std::shared_ptr configInterface, @@ -42,62 +43,11 @@ namespace VmiCore isInstanciated = false; } - std::unique_ptr> - PluginSystem::readPagesWithUnmappedRegionPadding(uint64_t pageAlignedVA, uint64_t cr3, uint64_t numberOfPages) const + std::unique_ptr + PluginSystem::mapProcessMemoryRegion(addr_t baseVA, addr_t dtb, std::size_t numberOfPages) const { - if (pageAlignedVA % PagingDefinitions::pageSizeInBytes != 0) - { - throw std::invalid_argument( - fmt::format("{}: Starting address {:#x} is not aligned to page boundary", __func__, pageAlignedVA)); - } - auto vadIdentifier(fmt::format("CR3 {:#x} VAD @ {:#x}-{:#x}", - cr3, - pageAlignedVA, - (pageAlignedVA + numberOfPages * PagingDefinitions::pageSizeInBytes))); - auto memoryRegion = std::make_unique>(); - auto needsPadding = true; - for (uint64_t currentPageIndex = 0; currentPageIndex < numberOfPages; currentPageIndex++) - { - auto memoryPage = std::vector(PagingDefinitions::pageSizeInBytes); - if (vmiInterface->readXVA(pageAlignedVA, cr3, memoryPage, memoryPage.size())) - { - if (!needsPadding) - { - needsPadding = true; - logger->info("First successful page extraction after padding", - {{WRITE_TO_FILE_TAG, paddingLogFile}, - {"vadIdentifier", vadIdentifier}, - {"pageAlignedVA", fmt::format("{:#x}", pageAlignedVA)}}); - } - memoryRegion->insert(memoryRegion->cend(), memoryPage.cbegin(), memoryPage.cend()); - } - else - { - if (needsPadding) - { - memoryRegion->insert(memoryRegion->cend(), PagingDefinitions::pageSizeInBytes, 0x0); - needsPadding = false; - logger->info("Start of padding", - {{WRITE_TO_FILE_TAG, paddingLogFile}, - {"vadIdentifier", vadIdentifier}, - {"pageAlignedVA", fmt::format("{:#x}", pageAlignedVA)}}); - } - } - pageAlignedVA += PagingDefinitions::pageSizeInBytes; - } - return memoryRegion; - } - - std::unique_ptr> - PluginSystem::readProcessMemoryRegion(pid_t pid, addr_t address, size_t count) const - { - if (count % PagingDefinitions::pageSizeInBytes != 0) - { - throw std::invalid_argument("Size of memory region must be page size aligned."); - } - auto numberOfPages = count >> PagingDefinitions::numberOfPageIndexBits; - auto process = activeProcessesSupervisor->getProcessInformationByPid(pid); - return readPagesWithUnmappedRegionPadding(address, process->processDtb, numberOfPages); + return std::make_unique( + loggingLib, vmiInterface, vmiInterface->mmapGuest(baseVA, dtb, numberOfPages)); } void PluginSystem::registerProcessStartEvent( diff --git a/vmicore/src/lib/plugins/PluginSystem.h b/vmicore/src/lib/plugins/PluginSystem.h index b516ffe1..c8be60c0 100644 --- a/vmicore/src/lib/plugins/PluginSystem.h +++ b/vmicore/src/lib/plugins/PluginSystem.h @@ -8,7 +8,6 @@ #include "../os/IActiveProcessesSupervisor.h" #include "../vmi/InterruptEventSupervisor.h" #include "../vmi/LibvmiInterface.h" -#include "PluginException.h" #include #include #include @@ -78,11 +77,8 @@ namespace VmiCore [[nodiscard]] std::unique_ptr getResultsDir() const override; - [[nodiscard]] std::unique_ptr> - readPagesWithUnmappedRegionPadding(uint64_t pageAlignedVA, uint64_t cr3, uint64_t numberOfPages) const; - - [[nodiscard]] std::unique_ptr> - readProcessMemoryRegion(pid_t pid, addr_t address, size_t numberOfBytes) const override; + [[nodiscard]] std::unique_ptr + mapProcessMemoryRegion(addr_t baseVA, addr_t dtb, std::size_t numberOfPages) const override; [[nodiscard]] std::unique_ptr>> getRunningProcesses() const override; diff --git a/vmicore/src/lib/vmi/LibvmiInterface.cpp b/vmicore/src/lib/vmi/LibvmiInterface.cpp index f3144bf2..d272f0e2 100644 --- a/vmicore/src/lib/vmi/LibvmiInterface.cpp +++ b/vmicore/src/lib/vmi/LibvmiInterface.cpp @@ -3,6 +3,7 @@ #include "VmiException.h" #include "VmiInitData.h" #include "VmiInitError.h" +#include #include #include @@ -182,6 +183,26 @@ namespace VmiCore return true; } + mapped_regions_t LibvmiInterface::mmapGuest(addr_t baseVA, addr_t dtb, std::size_t numberOfPages) + { + mapped_regions_t regions{}; + auto accessContext = createVirtualAddressAccessContext(baseVA, dtb); + std::lock_guard lock(libvmiLock); + if (vmi_mmap_guest_2(vmiInstance, &accessContext, numberOfPages, PROT_READ, ®ions) != VMI_SUCCESS) + { + throw VmiException(fmt::format("{}: Unable to create memory mapping for VA {:#x} with number of pages {}", + std::source_location::current().function_name(), + baseVA, + numberOfPages)); + } + return regions; + } + + void LibvmiInterface::freeMappedRegions(const mapped_regions_t& mappedRegions) + { + vmi_free_mapped_regions(vmiInstance, &mappedRegions); + } + void LibvmiInterface::write8PA(addr_t physicalAddress, uint8_t value) { auto accessContext = createPhysicalAddressAccessContext(physicalAddress); diff --git a/vmicore/src/lib/vmi/LibvmiInterface.h b/vmicore/src/lib/vmi/LibvmiInterface.h index 91c08727..820ab8d9 100644 --- a/vmicore/src/lib/vmi/LibvmiInterface.h +++ b/vmicore/src/lib/vmi/LibvmiInterface.h @@ -33,6 +33,10 @@ namespace VmiCore virtual void clearEvent(vmi_event_t& event, bool deallocate) = 0; + virtual mapped_regions_t mmapGuest(addr_t baseVA, addr_t dtb, std::size_t numberOfPages) = 0; + + virtual void freeMappedRegions(const mapped_regions_t& mappedRegions) = 0; + virtual void write8PA(addr_t physicalAddress, uint8_t value) = 0; virtual void eventsListen(uint32_t timeout) = 0; @@ -79,6 +83,10 @@ namespace VmiCore [[nodiscard]] bool readXVA(addr_t virtualAddress, addr_t cr3, std::vector& content, std::size_t size) override; + mapped_regions_t mmapGuest(addr_t baseVA, addr_t dtb, std::size_t numberOfPages) override; + + void freeMappedRegions(const mapped_regions_t& mappedRegions) override; + void write8PA(addr_t physicalAddress, uint8_t value) override; void eventsListen(uint32_t timeout) override; diff --git a/vmicore/src/lib/vmi/MemoryMapping.cpp b/vmicore/src/lib/vmi/MemoryMapping.cpp new file mode 100644 index 00000000..992ad3ea --- /dev/null +++ b/vmicore/src/lib/vmi/MemoryMapping.cpp @@ -0,0 +1,50 @@ +#include "MemoryMapping.h" +#include + +// Currently implemented in GNU libstdc++ but missing in LLVM libc++. +#ifdef __cpp_lib_is_layout_compatible +#include + +static_assert(std::is_layout_compatible_v, + "Layout of libvmi mapped_region not compatible with VmiCore MappedRegion"); +#endif + +namespace VmiCore +{ + MemoryMapping::MemoryMapping(const std::shared_ptr& logging, + std::shared_ptr vmiInterface, + mapped_regions_t mappedRegions) + : logger(logging->newNamedLogger(FILENAME_STEM)), + vmiInterface(std::move(vmiInterface)), + libvmiMappings(mappedRegions) + { + } + + MemoryMapping::~MemoryMapping() + { + if (isMapped) + { + unmap(); + } + } + + std::span MemoryMapping::getMappedRegions() const + { + if (!isMapped) + { + throw MemoryMappingError("Cannot retrieve mappings for regions that have already been unmapped"); + } + + return {std::bit_cast(libvmiMappings.regions), libvmiMappings.size}; + } + + void MemoryMapping::unmap() + { + if (isMapped) + { + vmiInterface->freeMappedRegions(libvmiMappings); + + isMapped = false; + } + } +} diff --git a/vmicore/src/lib/vmi/MemoryMapping.h b/vmicore/src/lib/vmi/MemoryMapping.h new file mode 100644 index 00000000..4714ce80 --- /dev/null +++ b/vmicore/src/lib/vmi/MemoryMapping.h @@ -0,0 +1,39 @@ +#ifndef VMICORE_MEMORYMAPPING_H +#define VMICORE_MEMORYMAPPING_H + +#include "../io/ILogging.h" +#include "LibvmiInterface.h" +#include + +namespace VmiCore +{ + class MemoryMapping final : public IMemoryMapping + { + public: + MemoryMapping(const std::shared_ptr& logging, + std::shared_ptr vmiInterface, + mapped_regions_t mappedRegions); + + ~MemoryMapping() override; + + MemoryMapping(const MemoryMapping&) = delete; + + MemoryMapping(const MemoryMapping&&) = delete; + + MemoryMapping& operator=(const MemoryMapping&) = delete; + + MemoryMapping& operator=(const MemoryMapping&&) = delete; + + [[nodiscard]] std::span getMappedRegions() const override; + + void unmap() override; + + private: + std::unique_ptr logger; + std::shared_ptr vmiInterface; + mapped_regions_t libvmiMappings; + bool isMapped = true; + }; +} + +#endif // VMICORE_MEMORYMAPPING_H diff --git a/vmicore/test/CMakeLists.txt b/vmicore/test/CMakeLists.txt index 3740f820..5ab81977 100644 --- a/vmicore/test/CMakeLists.txt +++ b/vmicore/test/CMakeLists.txt @@ -6,6 +6,8 @@ add_executable(vmicore-test lib/vmi/ContextSwitchHandler_UnitTest.cpp lib/vmi/InterruptEventSupervisor_UnitTest.cpp lib/vmi/LibvmiInterface_UnitTest.cpp + lib/vmi/MappedRegion_UnitTest.cpp + lib/vmi/MemoryMapping_UnitTest.cpp lib/vmi/SingleStepSupervisor_UnitTest.cpp) target_compile_options(vmicore-test PRIVATE -Wno-missing-field-initializers) target_link_libraries(vmicore-test vmicore-lib pthread) diff --git a/vmicore/test/include/CMakeLists.txt b/vmicore/test/include/CMakeLists.txt index cda0fd87..cab41c75 100644 --- a/vmicore/test/include/CMakeLists.txt +++ b/vmicore/test/include/CMakeLists.txt @@ -18,7 +18,8 @@ target_sources(vmicore-public-test-headers INTERFACE vmicore_test/plugins/mock_PluginInterface.h vmicore_test/vmi/mock_Breakpoint.h vmicore_test/vmi/mock_InterruptEvent.h - vmicore_test/vmi/mock_IntrospectionAPI.h) + vmicore_test/vmi/mock_IntrospectionAPI.h + vmicore_test/vmi/mock_MemoryMapping.h) target_include_directories(vmicore-public-test-headers INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}") target_compile_features(vmicore-public-test-headers INTERFACE cxx_std_20) target_link_libraries(vmicore-public-test-headers INTERFACE vmicore-public-headers gmock) diff --git a/vmicore/test/include/vmicore_test/plugins/mock_PluginInterface.h b/vmicore/test/include/vmicore_test/plugins/mock_PluginInterface.h index 15e32a8e..4f272122 100644 --- a/vmicore/test/include/vmicore_test/plugins/mock_PluginInterface.h +++ b/vmicore/test/include/vmicore_test/plugins/mock_PluginInterface.h @@ -9,9 +9,9 @@ namespace VmiCore::Plugin class MockPluginInterface : public PluginInterface { public: - MOCK_METHOD(std::unique_ptr>, - readProcessMemoryRegion, - (pid_t, addr_t, size_t), + MOCK_METHOD(std::unique_ptr, + mapProcessMemoryRegion, + (addr_t, addr_t, std::size_t), (const, override)); MOCK_METHOD(std::unique_ptr>>, diff --git a/vmicore/test/include/vmicore_test/vmi/mock_MemoryMapping.h b/vmicore/test/include/vmicore_test/vmi/mock_MemoryMapping.h new file mode 100644 index 00000000..12c49403 --- /dev/null +++ b/vmicore/test/include/vmicore_test/vmi/mock_MemoryMapping.h @@ -0,0 +1,18 @@ +#ifndef VMICORE_MOCK_MEMORYMAPPING_H +#define VMICORE_MOCK_MEMORYMAPPING_H + +#include +#include + +namespace VmiCore +{ + class MockMemoryMapping : public IMemoryMapping + { + public: + MOCK_METHOD(std::span, getMappedRegions, (), (const override)); + + MOCK_METHOD(void, unmap, (), (override)); + }; +} + +#endif // VMICORE_MOCK_MEMORYMAPPING_H diff --git a/vmicore/test/lib/plugins/PluginSystem_UnitTest.cpp b/vmicore/test/lib/plugins/PluginSystem_UnitTest.cpp index 6f7bcdd4..41c42bf4 100644 --- a/vmicore/test/lib/plugins/PluginSystem_UnitTest.cpp +++ b/vmicore/test/lib/plugins/PluginSystem_UnitTest.cpp @@ -214,197 +214,4 @@ namespace VmiCore std::advance(regionIterator, 2); EXPECT_EQ(regionIterator->size, vadRootNodeLeftChildMemoryRegionSize); } - - struct memoryRegionTestInformation - { - uint64_t virtualAddress; - uint64_t cr3; - size_t contentSize; - std::vector memoryPageContent; - }; - - class ReadProcessMemoryRegionFixture : public PluginSystemFixture - { - protected: - uint64_t unalignedVA = 1234; - uint64_t singlePageRegionBaseVA = 1234 * PagingDefinitions::pageSizeInBytes; - uint64_t threePagesRegionBaseVA = 2345 * PagingDefinitions::pageSizeInBytes; - uint64_t sevenPagesRegionBaseVA = 6666 * PagingDefinitions::pageSizeInBytes; - - memoryRegionTestInformation singlePageMemoryRegion{ - singlePageRegionBaseVA, - systemCR3, - PagingDefinitions::pageSizeInBytes, - std::vector(PagingDefinitions::pageSizeInBytes, 0xCD)}; - std::unique_ptr> threePagesMemoryRegion; - std::unique_ptr> sevenPagesMemoryRegionInfo; - - std::unique_ptr> - createMultipageRegionInformation(uint64_t baseVA, uint64_t cr3, size_t numberOfBytes) - { - auto resultVector = std::make_unique>(); - if (numberOfBytes > 0) - { - uint64_t numberOfSubsequentPages = - ((baseVA + numberOfBytes - 1) >> PagingDefinitions::numberOfPageIndexBits) - - (baseVA >> PagingDefinitions::numberOfPageIndexBits); - for (uint64_t i = 0; i <= numberOfSubsequentPages; ++i) - { - uint64_t currentPageContentSize = - i == numberOfSubsequentPages - ? numberOfBytes - (numberOfSubsequentPages * PagingDefinitions::pageSizeInBytes) - : PagingDefinitions::pageSizeInBytes; - resultVector->push_back({baseVA + (i * PagingDefinitions::pageSizeInBytes), - cr3, - currentPageContentSize, - std::vector(currentPageContentSize, static_cast(i))}); - } - } - return resultVector; - } - - void setupThreePagesRegionReturns() - { - threePagesMemoryRegion = createMultipageRegionInformation( - threePagesRegionBaseVA, systemCR3, 3 * PagingDefinitions::pageSizeInBytes); - threePagesMemoryRegion->at(1).memoryPageContent.clear(); // simulate non mapped page - for (const auto& element : *threePagesMemoryRegion) - { - setupMemoryRegionReturns(element); - } - } - - void setupMemoryRegionReturns(const memoryRegionTestInformation& memoryRegionInfo) - { - if (!memoryRegionInfo.memoryPageContent.empty()) - { - ON_CALL(*mockVmiInterface, readXVA(memoryRegionInfo.virtualAddress, memoryRegionInfo.cr3, _, _)) - .WillByDefault( - [memoryPageContent = - memoryRegionInfo.memoryPageContent]([[maybe_unused]] uint64_t virtualAddress, - [[maybe_unused]] uint64_t cr3, - std::vector& buffer, - [[maybe_unused]] std::size_t size) - { - buffer = memoryPageContent; - return true; - }); - } - else - { - ON_CALL(*mockVmiInterface, readXVA(memoryRegionInfo.virtualAddress, memoryRegionInfo.cr3, _, _)) - .WillByDefault(Return(false)); - } - } - - void setupSevenPagesRegionReturns() - { - sevenPagesMemoryRegionInfo = createMultipageRegionInformation(6666 * PagingDefinitions::pageSizeInBytes, - process4.directoryTableBase, - 7 * PagingDefinitions::pageSizeInBytes); - sevenPagesMemoryRegionInfo->at(0).memoryPageContent.clear(); - sevenPagesMemoryRegionInfo->at(1).memoryPageContent.clear(); - sevenPagesMemoryRegionInfo->at(3).memoryPageContent.clear(); - sevenPagesMemoryRegionInfo->at(4).memoryPageContent.clear(); - sevenPagesMemoryRegionInfo->at(6).memoryPageContent.clear(); - for (const auto& element : *sevenPagesMemoryRegionInfo) - { - setupMemoryRegionReturns(element); - } - } - - void SetUp() override - { - PluginSystemFixture::SetUp(); - - setupMemoryRegionReturns(singlePageMemoryRegion); - setupThreePagesRegionReturns(); - setupSevenPagesRegionReturns(); - } - }; - - TEST_F(ReadProcessMemoryRegionFixture, - readProcessMemoryRegion_virtualAddressNotPageAligned_invalidArgumentException) - { - size_t numberOfBytes = 4; - std::unique_ptr> data; - - EXPECT_THROW(data = pluginInterface->readProcessMemoryRegion(process4.processId, unalignedVA, numberOfBytes), - std::invalid_argument); - } - - TEST_F(ReadProcessMemoryRegionFixture, readProcessMemoryRegion_NumberOfBytesIsZero_emptyVector) - { - size_t numberOfBytes = 0; - std::unique_ptr> data; - - ASSERT_NO_THROW( - data = pluginInterface->readProcessMemoryRegion(process4.processId, singlePageRegionBaseVA, numberOfBytes)); - - EXPECT_TRUE(data->empty()); - } - - TEST_F(ReadProcessMemoryRegionFixture, readProcessMemoryRegion_unknownPid_invalidArgumentException) - { - size_t numberOfBytes = singlePageMemoryRegion.contentSize; - - EXPECT_THROW(auto _unused = - pluginInterface->readProcessMemoryRegion(unusedPid, singlePageRegionBaseVA, numberOfBytes), - std::invalid_argument); - } - - TEST_F(ReadProcessMemoryRegionFixture, readProcessMemoryRegion_smallMemoryRegion_validMemoryRegion) - { - size_t numberOfBytes = singlePageMemoryRegion.contentSize; - std::unique_ptr> data; - - ASSERT_NO_THROW( - data = pluginInterface->readProcessMemoryRegion(process4.processId, singlePageRegionBaseVA, numberOfBytes)); - - EXPECT_EQ(singlePageMemoryRegion.memoryPageContent, *data); - } - - TEST_F(ReadProcessMemoryRegionFixture, readProcessMemoryRegion_memoryRegionWithNonMappedPage_validMemoryRegion) - { - size_t numberOfBytes = 3 * PagingDefinitions::pageSizeInBytes; - std::unique_ptr> data; - std::vector resultMemoryRegion; - - // First page - resultMemoryRegion.insert(resultMemoryRegion.end(), - threePagesMemoryRegion->at(0).memoryPageContent.cbegin(), - threePagesMemoryRegion->at(0).memoryPageContent.cend()); - // Padding - resultMemoryRegion.insert(resultMemoryRegion.end(), PagingDefinitions::pageSizeInBytes, 0x0); - // Third page - resultMemoryRegion.insert(resultMemoryRegion.end(), - threePagesMemoryRegion->at(2).memoryPageContent.cbegin(), - threePagesMemoryRegion->at(2).memoryPageContent.cend()); - - ASSERT_NO_THROW( - data = pluginInterface->readProcessMemoryRegion(process4.processId, threePagesRegionBaseVA, numberOfBytes)); - - EXPECT_EQ(resultMemoryRegion, *data); - } - - TEST_F(ReadProcessMemoryRegionFixture, readProcessMemoryRegion_memoryRegionWithManyUnmappedPages_validMemoryRegion) - { - size_t sevenPagesSizeInBytes = 7 * PagingDefinitions::pageSizeInBytes; - std::vector expectedMemoryRegion; - expectedMemoryRegion.insert(expectedMemoryRegion.end(), PagingDefinitions::pageSizeInBytes, 0x0); - expectedMemoryRegion.insert(expectedMemoryRegion.end(), - sevenPagesMemoryRegionInfo->at(2).memoryPageContent.cbegin(), - sevenPagesMemoryRegionInfo->at(2).memoryPageContent.cend()); - expectedMemoryRegion.insert(expectedMemoryRegion.end(), PagingDefinitions::pageSizeInBytes, 0x0); - expectedMemoryRegion.insert(expectedMemoryRegion.end(), - sevenPagesMemoryRegionInfo->at(5).memoryPageContent.cbegin(), - sevenPagesMemoryRegionInfo->at(5).memoryPageContent.cend()); - expectedMemoryRegion.insert(expectedMemoryRegion.end(), PagingDefinitions::pageSizeInBytes, 0x0); - std::unique_ptr> data; - - ASSERT_NO_THROW(data = pluginInterface->readProcessMemoryRegion( - process4.processId, sevenPagesRegionBaseVA, sevenPagesSizeInBytes)); - - EXPECT_EQ(expectedMemoryRegion, *data); - } } diff --git a/vmicore/test/lib/plugins/mock_PluginSystem.h b/vmicore/test/lib/plugins/mock_PluginSystem.h index 11f89697..0fcfeaeb 100644 --- a/vmicore/test/lib/plugins/mock_PluginSystem.h +++ b/vmicore/test/lib/plugins/mock_PluginSystem.h @@ -6,13 +6,11 @@ namespace VmiCore class MockPluginSystem : public IPluginSystem { public: - MOCK_METHOD(std::unique_ptr>, - readProcessMemoryRegion, - (pid_t, addr_t, size_t), + MOCK_METHOD(std::unique_ptr, + mapProcessMemoryRegion, + (addr_t, addr_t, std::size_t), (const override)); - MOCK_METHOD(std::unique_ptr>, getProcessMemoryRegions, (pid_t), (const override)); - MOCK_METHOD(std::unique_ptr>>, getRunningProcesses, (), diff --git a/vmicore/test/lib/vmi/MappedRegion_UnitTest.cpp b/vmicore/test/lib/vmi/MappedRegion_UnitTest.cpp new file mode 100644 index 00000000..d5dde27b --- /dev/null +++ b/vmicore/test/lib/vmi/MappedRegion_UnitTest.cpp @@ -0,0 +1,14 @@ +#include +#include +#include +#include + +using VmiCore::MappedRegion; + +TEST(MappedRegionTests, structFieldOrdering) +{ + mapped_region_t libvmiRegion{.start_va = 1, .num_pages = 2, .access_ptr = reinterpret_cast(3)}; + MappedRegion vmicoreRegion{1, 2, reinterpret_cast(3)}; + + EXPECT_EQ(std::bit_cast(libvmiRegion), vmicoreRegion); +} diff --git a/vmicore/test/lib/vmi/MemoryMapping_UnitTest.cpp b/vmicore/test/lib/vmi/MemoryMapping_UnitTest.cpp new file mode 100644 index 00000000..63f01acb --- /dev/null +++ b/vmicore/test/lib/vmi/MemoryMapping_UnitTest.cpp @@ -0,0 +1,29 @@ +#include "../io/mock_Logging.h" +#include "mock_LibvmiInterface.h" +#include +#include + +using testing::NiceMock; + +namespace VmiCore +{ + TEST(MemoryMappingTest, getMappedRegions_validState_mappings) + { + auto memoryMapping = MemoryMapping(std::make_shared>(), + std::make_shared>(), + mapped_regions_t{}); + + EXPECT_NO_THROW(auto _mappings = memoryMapping.getMappedRegions()); + } + + TEST(MemoryMappingTest, getMappedRegions_alreadyUnmapped_throws) + { + auto memoryMapping = MemoryMapping(std::make_shared>(), + std::make_shared>(), + mapped_regions_t{}); + + memoryMapping.unmap(); + + EXPECT_ANY_THROW(auto _mappings = memoryMapping.getMappedRegions()); + } +} diff --git a/vmicore/test/lib/vmi/mock_LibvmiInterface.h b/vmicore/test/lib/vmi/mock_LibvmiInterface.h index 68da8e06..319316db 100644 --- a/vmicore/test/lib/vmi/mock_LibvmiInterface.h +++ b/vmicore/test/lib/vmi/mock_LibvmiInterface.h @@ -27,6 +27,10 @@ namespace VmiCore MOCK_METHOD(bool, readXVA, (uint64_t, uint64_t, std::vector&, std::size_t), (override)); + MOCK_METHOD(mapped_regions_t, mmapGuest, (addr_t, addr_t, std::size_t), (override)); + + MOCK_METHOD(void, freeMappedRegions, (const mapped_regions_t&), (override)); + MOCK_METHOD(void, write8PA, (uint64_t, uint8_t), (override)); MOCK_METHOD(void, eventsListen, (uint32_t), (override));