Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
name: Windows tests
name: Cross-platform tests

on:
push:
pull_request:

jobs:
build-and-test:
windows:
runs-on: windows-latest
strategy:
fail-fast: false
Expand All @@ -25,3 +25,35 @@ jobs:
ctest --test-dir build
-C ${{ matrix.configuration }}
--output-on-failure

fedora:
runs-on: ubuntu-latest
container: fedora:44
env:
STRATA_DMA_RUNTIME_DIR: /tmp/memprocfs-runtime
MEMPROCFS_LINUX_URL: >-
https://github.com/ufrisk/MemProcFS/releases/download/v5.16/MemProcFS_files_and_binaries_v5.16.14-linux_x64-20260211.tar.gz
MEMPROCFS_LINUX_SHA256: bc7ea472af63894b15ff6ebc93d62cdf35d3e802e9db157598fef79a4737af0b
steps:
- name: Install Fedora build dependencies
run: dnf install -y cmake curl gcc-c++ git gzip libusb1 ninja-build tar
- uses: actions/checkout@v5
- name: Install matching MemProcFS build runtime
shell: bash
run: |
mkdir -p "$STRATA_DMA_RUNTIME_DIR"
curl --fail --location --silent --show-error \
"$MEMPROCFS_LINUX_URL" --output /tmp/memprocfs-runtime.tar.gz
echo "$MEMPROCFS_LINUX_SHA256 /tmp/memprocfs-runtime.tar.gz" | sha256sum --check
tar -xzf /tmp/memprocfs-runtime.tar.gz -C "$STRATA_DMA_RUNTIME_DIR"
test -f "$STRATA_DMA_RUNTIME_DIR/vmm.so"
test -f "$STRATA_DMA_RUNTIME_DIR/leechcore.so"
- name: Configure
run: >-
cmake -S . -B build -G Ninja
-DSTRATA_DMA_BUILD_EXAMPLE=OFF
-DSTRATA_DMA_BUILD_TESTS=ON
- name: Build
run: cmake --build build --parallel
- name: Test
run: ctest --test-dir build --output-on-failure
91 changes: 74 additions & 17 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
cmake_minimum_required(VERSION 3.20)
project(StrataDMA VERSION 2.0.0 LANGUAGES CXX)

if(NOT WIN32)
message(FATAL_ERROR "StrataDMA currently supports Windows only.")
if(NOT WIN32 AND NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
message(FATAL_ERROR "StrataDMA supports Windows and Linux only.")
endif()
if(NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
message(FATAL_ERROR "StrataDMA requires a 64-bit toolchain.")
endif()

option(STRATA_DMA_BUILD_EXAMPLE "Build the existing example executable" ON)
option(STRATA_DMA_BUILD_TESTS "Build hardware-independent mock tests" ON)
option(STRATA_DMA_LINK_RUNTIME
"Link Linux consumers directly to the MemProcFS shared libraries" ON)
set(STRATA_DMA_RUNTIME_DIR "" CACHE PATH
"Directory containing the MemProcFS runtime libraries")
if(NOT STRATA_DMA_RUNTIME_DIR AND DEFINED ENV{STRATA_DMA_RUNTIME_DIR})
file(TO_CMAKE_PATH "$ENV{STRATA_DMA_RUNTIME_DIR}" STRATA_DMA_RUNTIME_DIR)
endif()

set(STRATA_DMA_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/StrataDMA/StrataDMA")
set(STRATA_DMA_SOURCE_DIR "${STRATA_DMA_ROOT}/StrataDMA")
Expand All @@ -28,22 +38,56 @@ target_compile_features(strata_dma PUBLIC cxx_std_17)
target_include_directories(strata_dma PUBLIC
"$<BUILD_INTERFACE:${STRATA_DMA_ROOT}>"
"$<INSTALL_INTERFACE:include>")
target_compile_definitions(strata_dma PUBLIC NOMINMAX)
target_link_libraries(strata_dma PUBLIC
"${STRATA_DMA_SOURCE_DIR}/libs/vmm.lib"
"${STRATA_DMA_SOURCE_DIR}/libs/leechcore.lib")
target_compile_definitions(strata_dma PUBLIC
NOMINMAX
$<$<PLATFORM_ID:Linux>:LINUX>)
set_target_properties(strata_dma PROPERTIES POSITION_INDEPENDENT_CODE ON)

if(WIN32)
target_link_libraries(strata_dma PUBLIC
"${STRATA_DMA_SOURCE_DIR}/libs/vmm.lib"
"${STRATA_DMA_SOURCE_DIR}/libs/leechcore.lib")
elseif(STRATA_DMA_LINK_RUNTIME)
if(NOT STRATA_DMA_RUNTIME_DIR)
message(FATAL_ERROR
"A Linux build requires STRATA_DMA_RUNTIME_DIR to point to a "
"MemProcFS directory containing vmm.so and leechcore.so.")
endif()
get_filename_component(STRATA_DMA_RUNTIME_DIR
"${STRATA_DMA_RUNTIME_DIR}" ABSOLUTE)
find_file(STRATA_DMA_VMM_LIBRARY NAMES vmm.so
PATHS "${STRATA_DMA_RUNTIME_DIR}" NO_DEFAULT_PATH REQUIRED)
find_file(STRATA_DMA_LEECHCORE_LIBRARY NAMES leechcore.so
PATHS "${STRATA_DMA_RUNTIME_DIR}" NO_DEFAULT_PATH REQUIRED)
target_link_directories(strata_dma PUBLIC "${STRATA_DMA_RUNTIME_DIR}")
target_link_libraries(strata_dma PUBLIC "-l:vmm.so" "-l:leechcore.so")
set_target_properties(strata_dma PROPERTIES
BUILD_RPATH "${STRATA_DMA_RUNTIME_DIR}")
endif()

if(CMAKE_SYSTEM_NAME STREQUAL "Linux"
AND STRATA_DMA_BUILD_EXAMPLE
AND NOT STRATA_DMA_LINK_RUNTIME)
message(FATAL_ERROR
"STRATA_DMA_BUILD_EXAMPLE requires STRATA_DMA_LINK_RUNTIME=ON.")
endif()

if(MSVC)
target_compile_options(strata_dma PRIVATE /W4 /permissive-)
elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(strata_dma PRIVATE
-Wall -Wextra -Wno-format -Wno-unused-variable)
endif()

if(STRATA_DMA_BUILD_EXAMPLE)
add_executable(strata_dma_example "${STRATA_DMA_ROOT}/main.cpp")
target_link_libraries(strata_dma_example PRIVATE StrataDMA::StrataDMA)
add_custom_command(TARGET strata_dma_example POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"${STRATA_DMA_SOURCE_DIR}/deps/info.db"
"$<TARGET_FILE_DIR:strata_dma_example>/info.db")
if(WIN32)
add_custom_command(TARGET strata_dma_example POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"${STRATA_DMA_SOURCE_DIR}/deps/info.db"
"$<TARGET_FILE_DIR:strata_dma_example>/info.db")
endif()
endif()

if(STRATA_DMA_BUILD_TESTS)
Expand All @@ -61,11 +105,18 @@ if(STRATA_DMA_BUILD_TESTS)
"${STRATA_DMA_SOURCE_DIR}/DMA.Types.cpp")

add_library(strata_dma_test_core STATIC ${STRATA_DMA_TEST_CORE_SOURCES})
set_target_properties(strata_dma_test_core PROPERTIES
POSITION_INDEPENDENT_CODE ON)
target_compile_features(strata_dma_test_core PUBLIC cxx_std_17)
target_include_directories(strata_dma_test_core PUBLIC "${STRATA_DMA_ROOT}")
target_compile_definitions(strata_dma_test_core PUBLIC NOMINMAX)
target_compile_definitions(strata_dma_test_core PUBLIC
NOMINMAX
$<$<PLATFORM_ID:Linux>:LINUX>)
if(MSVC)
target_compile_options(strata_dma_test_core PRIVATE /W4 /WX /permissive-)
elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(strata_dma_test_core PRIVATE
-Wall -Wextra -Wno-format -Wno-unused-variable)
endif()

function(strata_add_mock_suite target suite_source suite_name suite_label)
Expand All @@ -78,6 +129,9 @@ if(STRATA_DMA_BUILD_TESTS)
STRATA_TEST_SUITE="${suite_name}")
if(MSVC)
target_compile_options(${target} PRIVATE /W4 /WX /permissive-)
elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(${target} PRIVATE
-Wall -Wextra -Wno-format -Wno-unused-variable)
endif()
add_test(NAME ${target} COMMAND ${target})
set_tests_properties(${target} PROPERTIES
Expand Down Expand Up @@ -105,18 +159,21 @@ install(FILES
"${STRATA_DMA_SOURCE_DIR}/DMA.hpp"
"${STRATA_DMA_SOURCE_DIR}/DMA.Backend.hpp"
"${STRATA_DMA_SOURCE_DIR}/DMA.Context.hpp"
"${STRATA_DMA_SOURCE_DIR}/DMA.Platform.hpp"
"${STRATA_DMA_SOURCE_DIR}/DMA.Types.hpp"
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/StrataDMA")
install(FILES
"${STRATA_DMA_SOURCE_DIR}/deps/vmmdll.h"
"${STRATA_DMA_SOURCE_DIR}/deps/leechcore.h"
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/StrataDMA/deps")
install(FILES
"${STRATA_DMA_SOURCE_DIR}/libs/vmm.lib"
"${STRATA_DMA_SOURCE_DIR}/libs/leechcore.lib"
DESTINATION "${CMAKE_INSTALL_LIBDIR}")
install(FILES "${STRATA_DMA_SOURCE_DIR}/deps/info.db"
DESTINATION "${CMAKE_INSTALL_DATADIR}/StrataDMA")
if(WIN32)
install(FILES
"${STRATA_DMA_SOURCE_DIR}/libs/vmm.lib"
"${STRATA_DMA_SOURCE_DIR}/libs/leechcore.lib"
DESTINATION "${CMAKE_INSTALL_LIBDIR}")
install(FILES "${STRATA_DMA_SOURCE_DIR}/deps/info.db"
DESTINATION "${CMAKE_INSTALL_DATADIR}/StrataDMA")
endif()
install(FILES
"${CMAKE_CURRENT_SOURCE_DIR}/LICENSE"
"${CMAKE_CURRENT_SOURCE_DIR}/THIRD_PARTY_NOTICES.md"
Expand Down
54 changes: 46 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# StrataDMA

[![Windows tests](https://github.com/chase-irql/StrataDMA/actions/workflows/tests.yml/badge.svg)](https://github.com/chase-irql/StrataDMA/actions/workflows/tests.yml)
[![Windows and Linux tests](https://github.com/chase-irql/StrataDMA/actions/workflows/tests.yml/badge.svg)](https://github.com/chase-irql/StrataDMA/actions/workflows/tests.yml)
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](LICENSE)

A C++17 wrapper around MemProcFS/VMMDLL for authorized DMA-backed Windows
memory inspection. The repository vendors the MemProcFS 5.16.5 and LeechCore
headers and x64 import libraries.
A cross-platform C++17 wrapper around MemProcFS/VMMDLL for authorized
DMA-backed Windows memory inspection. Windows and 64-bit Linux hosts are
supported; the inspected target remains Windows. The repository vendors the
MemProcFS 5.16.5 and LeechCore headers plus Windows x64 import libraries.

Use it only on systems and processes you are authorized to inspect.

Expand Down Expand Up @@ -54,12 +55,34 @@ ctest --test-dir build -C Release --output-on-failure
cmake --install build --config Release --prefix package
```

Options are `STRATA_DMA_BUILD_EXAMPLE` and `STRATA_DMA_BUILD_TESTS`.
Options are `STRATA_DMA_BUILD_EXAMPLE`, `STRATA_DMA_BUILD_TESTS`, and
`STRATA_DMA_LINK_RUNTIME`. The last option is Linux-only in effect and defaults
to `ON`. Dynamic hosts that preload `leechcore.so` and `vmm.so` themselves may
turn it off; the final module must then allow and resolve the VMMDLL symbols.

At runtime, place `vmm.dll` and `leechcore.dll` beside the consuming executable.
Place `info.db` there as well when InfoDB/symbol functionality is used. The
repository contains import libraries, not the two runtime DLLs.

On Fedora, install the compiler and MemProcFS's USB dependency, unpack the
matching official MemProcFS Linux release, and point CMake at that directory:

```bash
sudo dnf install cmake gcc-c++ libusb1 ninja-build
export STRATA_DMA_RUNTIME_DIR=/opt/memprocfs
cmake -S . -B build -G Ninja \
-DSTRATA_DMA_BUILD_EXAMPLE=ON \
-DSTRATA_DMA_BUILD_TESTS=ON
cmake --build build --parallel
ctest --test-dir build --output-on-failure
```

The Linux runtime directory must contain `vmm.so` and `leechcore.so`; keep
`info.db` and any device or symbol plugins from the same MemProcFS release in
that directory. CMake links against the two shared libraries without copying
them into the install tree. At runtime, put the directory in the loader search
path or launch with `LD_LIBRARY_PATH="$STRATA_DMA_RUNTIME_DIR"`.

## Initialization and attachment

```cpp
Expand Down Expand Up @@ -349,7 +372,8 @@ PE-dump reconstruction, CR3 recovery and rollback, native/WoW64 PEB parsing,
physical-map export, RWX cave scanning, registry, VFS, and unsupported backend
operations. Failure injection exercises prepare/execute, read/write, plugin,
timeout, malformed-data, and I/O paths. MSVC warnings are errors for the mock
test core, and GitHub Actions runs Debug and Release builds.
test core. GitHub Actions runs Windows Debug/Release builds and a Fedora GCC
build.

```powershell
cmake -S . -B build -A x64 `
Expand All @@ -362,6 +386,12 @@ ctest --test-dir build -C Debug --output-on-failure
ctest --test-dir build -C Debug -L scatter --output-on-failure
```

The equivalent Linux label command omits the multi-config argument:

```bash
ctest --test-dir build -L scatter --output-on-failure
```

All native VMMDLL function calls are contained in `DMA.Backend.cpp`.
Implement `IVmmBackend` and pass a `shared_ptr` to `DMA` to add deterministic
fixtures without hardware. Optional methods return `DMAStatus::Unsupported`
Expand All @@ -370,13 +400,21 @@ unless the mock overrides them.
## Second-PC hardware smoke test

The example includes a finite, target-memory-read-only hardware check. Copy the
compiled example, matching `vmm.dll`, `leechcore.dll`, `info.db`, and the symbol
support DLLs from the same MemProcFS release to the acquisition PC, then run:
compiled example, matching VMMDLL/LeechCore runtime files, `info.db`, and symbol
support files from the same MemProcFS release to the acquisition PC, then run
on Windows:

```powershell
.\strata_dma_example.exe --hardware-test explorer.exe
```

On Linux, use the matching `.so` runtime and run:

```bash
LD_LIBRARY_PATH="$STRATA_DMA_RUNTIME_DIR" \
./strata_dma_example --hardware-test explorer.exe
```

It validates initialization/version discovery, the physical map, normal attach
with bounded CR3-recovery fallback, expanded process information, an MZ read,
scatter, module/section and VAD/PTE enumeration, then attempts PEB and VFS
Expand Down
42 changes: 31 additions & 11 deletions StrataDMA/StrataDMA/StrataDMA/DMA.Backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ std::string CopyString(const char* value)
return value ? value : "";
}

size_t BoundedStringLength(const char* value, size_t capacity)
{
return static_cast<size_t>(
std::find(value, value + capacity, '\0') - value);
}

DMAProcessInfo CopyProcessInfo(const VMMDLL_PROCESS_INFORMATION& native)
{
DMAProcessInfo process;
Expand Down Expand Up @@ -192,8 +198,11 @@ DMAOperationResult VmmdllBackend::ConfigGet(ULONG64 option, uint64_t& value) con
value = 0;
if (!handle_)
return NotInitialized();
return VMMDLL_ConfigGet(handle_, option, &value)
? DMAOperationResult::Success() : BackendFailure("VMMDLL_ConfigGet");
ULONG64 nativeValue = 0;
if (!VMMDLL_ConfigGet(handle_, option, &nativeValue))
return BackendFailure("VMMDLL_ConfigGet");
value = static_cast<uint64_t>(nativeValue);
return DMAOperationResult::Success();
}

DMAOperationResult VmmdllBackend::ConfigSet(ULONG64 option, uint64_t value)
Expand Down Expand Up @@ -258,9 +267,9 @@ DMAOperationResult VmmdllBackend::PrefetchPages(DWORD pid,
if (addresses.empty() || addresses.size() > std::numeric_limits<DWORD>::max())
return DMAOperationResult::Failure(DMAStatus::InvalidArgument,
"Prefetch requires one or more addresses.");
return VMMDLL_MemPrefetchPages(handle_, pid,
const_cast<PULONG64>(addresses.data()),
static_cast<DWORD>(addresses.size()))
std::vector<ULONG64> nativeAddresses(addresses.begin(), addresses.end());
return VMMDLL_MemPrefetchPages(handle_, pid, nativeAddresses.data(),
static_cast<DWORD>(nativeAddresses.size()))
? DMAOperationResult::Success() : BackendFailure("VMMDLL_MemPrefetchPages");
}

Expand All @@ -270,8 +279,11 @@ DMAOperationResult VmmdllBackend::VirtualToPhysical(DWORD pid,
physicalAddress = 0;
if (!handle_)
return NotInitialized();
return VMMDLL_MemVirt2Phys(handle_, pid, virtualAddress, &physicalAddress)
? DMAOperationResult::Success() : BackendFailure("VMMDLL_MemVirt2Phys");
ULONG64 nativeAddress = 0;
if (!VMMDLL_MemVirt2Phys(handle_, pid, virtualAddress, &nativeAddress))
return BackendFailure("VMMDLL_MemVirt2Phys");
physicalAddress = static_cast<uint64_t>(nativeAddress);
return DMAOperationResult::Success();
}

std::unique_ptr<IVmmScatterSession> VmmdllBackend::CreateScatter(
Expand Down Expand Up @@ -574,10 +586,14 @@ DMAOperationResult VmmdllBackend::ResolveSymbol(const std::string& symbolModule,
address = 0;
if (!handle_)
return NotInitialized();
return VMMDLL_PdbSymbolAddress(handle_, symbolModule.c_str(), symbol.c_str(),
&address) ? DMAOperationResult::Success()
: DMAOperationResult::Failure(DMAStatus::NotFound,
ULONG64 nativeAddress = 0;
if (!VMMDLL_PdbSymbolAddress(handle_, symbolModule.c_str(), symbol.c_str(),
&nativeAddress)) {
return DMAOperationResult::Failure(DMAStatus::NotFound,
"The PDB symbol was not found.");
}
address = static_cast<uint64_t>(nativeAddress);
return DMAOperationResult::Success();
}

DMAOperationResult VmmdllBackend::LookupSymbol(const std::string& symbolModule,
Expand Down Expand Up @@ -680,8 +696,12 @@ DMAOperationResult VmmdllBackend::EnumerateRegistryKeys(const std::string& path,
break;
DMARegistryKeyInfo key;
key.name = name.data();
#ifdef _WIN32
key.lastWriteTime = (static_cast<uint64_t>(time.dwHighDateTime) << 32) |
time.dwLowDateTime;
#else
key.lastWriteTime = static_cast<uint64_t>(time);
#endif
keys.push_back(std::move(key));
}
return DMAOperationResult::Success();
Expand Down Expand Up @@ -809,7 +829,7 @@ DMAOperationResult VmmdllBackend::ListVfs(const std::string& path,
const char* name = blob->uszMultiText + native.ouszName;
const size_t remaining = blob->cbMultiText -
static_cast<size_t>(native.ouszName);
entry.name.assign(name, strnlen_s(name, remaining));
entry.name.assign(name, BoundedStringLength(name, remaining));
entry.directory = native.cbFileSize == std::numeric_limits<uint64_t>::max();
entry.size = entry.directory ? 0 : native.cbFileSize;
if (native.ExInfo.dwVersion == VMMDLL_VFS_FILELIST_EXINFO_VERSION) {
Expand Down
Loading