-1

The following program tries to scan read/write pages of a foreign application with ReadProcessMemory():

#include <Windows.h>
#include <iostream>
#include <vector>
#include <charconv>
#include <cstring>
#include <vector>
#include <stdexcept>
#include <sstream>
#include <cctype>
#include <fstream>
#include <cmath>

using namespace std;

vector<vector<MEMORY_BASIC_INFORMATION>> pageTree( HANDLE hProcess, DWORD dwMask );

using XHANDLE = unique_ptr<void, decltype([]( HANDLE h ) { h && h != INVALID_HANDLE_VALUE && CloseHandle( h ); })>;

int main( int argc, char **argv )
{
    if( argc < 2 )
        return EXIT_FAILURE;
    try
    {
        DWORD dwProcessId = [&]() -> DWORD
            {
                DWORD dwRet;
                if( from_chars_result fcr = from_chars( argv[1], argv[1] + strlen( argv[1] ), dwRet ); fcr.ec != errc() || *fcr.ptr )
                    throw invalid_argument( "process-id unparseable" );
                return dwRet;
            }();
        XHANDLE hProcess( [&]() -> HANDLE
            {
                HANDLE hRet = OpenProcess( PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, dwProcessId );
                if( !hRet )
                    throw system_error( (int)GetLastError(), system_category(), "can't open process" );
                return hRet;
            }() );
        vector<vector<MEMORY_BASIC_INFORMATION>> vvmbi = pageTree( hProcess.get(), PAGE_READWRITE );
        vector<char> processRegion;
        size_t
            succs = 0, partialErrs = 0, errs = 0,
            total = 0, read = 0, skipped = 0;
        for( vector<MEMORY_BASIC_INFORMATION> const &vmbi : vvmbi )
            for( MEMORY_BASIC_INFORMATION const &vmbi : vmbi )
            {
                processRegion.resize( vmbi.RegionSize );
                size_t actuallyRead;
                bool succ = ReadProcessMemory( hProcess.get(), vmbi.BaseAddress, to_address( processRegion.begin() ), vmbi.RegionSize, &actuallyRead );
                succs += succ;
                partialErrs += !succ && GetLastError() == ERROR_PARTIAL_COPY;
                errs += !succ;
                bool bytesCopied = succ || GetLastError() == ERROR_PARTIAL_COPY;
                actuallyRead = bytesCopied ? actuallyRead : 0;
                total += processRegion.size(),
                read += actuallyRead;
                skipped += bytesCopied ? processRegion.size() - actuallyRead : processRegion.size();
            }
        cout << "successes: " << succs << endl;
        cout << "partial errs: " << partialErrs << endl;
        cout << "errs: " << errs << endl;
        cout << "read: " << read << endl;
        cout << "skipped: " << skipped;
        auto pct = []( double a, double b ) -> double { return trunc( a / b * 1000.0 + 0.5 ) / 10.0; };
        cout << " (" << pct( (double)(ptrdiff_t)skipped, (double)(ptrdiff_t)total ) << "%)" << endl;
    }
    catch( exception const &exc )
    {
        cout << exc.what() << endl;
    }
}

template<typename Fn>
    requires requires( Fn fn, MEMORY_BASIC_INFORMATION &mbi ) { { fn( mbi ) } -> std::convertible_to<bool>; }
void enumProcessMemory( HANDLE hProcess, Fn fn );

vector<vector<MEMORY_BASIC_INFORMATION>> pageTree( HANDLE hProcess, DWORD dwMask )
{
    vector<vector<MEMORY_BASIC_INFORMATION>> vvmbis;
    enumProcessMemory( hProcess, [&]( MEMORY_BASIC_INFORMATION &mbi ) -> bool
        {
            if( !(mbi.AllocationProtect & dwMask) )
                return true;
            if( !vvmbis.size() || vvmbis.back().back().BaseAddress != mbi.BaseAddress )
                vvmbis.emplace_back( vector<MEMORY_BASIC_INFORMATION>() );
            vvmbis.back().emplace_back( mbi );
            return true;
        } );
    return vvmbis;
}

template<typename Fn>
    requires requires( Fn fn, MEMORY_BASIC_INFORMATION &mbi ) { { fn( mbi ) } -> std::convertible_to<bool>; }
void enumProcessMemory( HANDLE hProcess, Fn fn )
{
    MEMORY_BASIC_INFORMATION mbi;
    for( char *last = nullptr; ; last = (char *)mbi.BaseAddress + mbi.RegionSize )
    {
        size_t nBytes = VirtualQueryEx( hProcess, last, &mbi, sizeof mbi );
        if( nBytes != sizeof mbi )
            if( DWORD dwErr = GetLastError(); dwErr == ERROR_INVALID_PARAMETER )
                break;
            else
                throw system_error( (int)dwErr, system_category(), "can't query process pages" );
        if( !fn( mbi ) )
            break;
    }
}

This is the result from scanning explorer.exe:

successes: 316
partial errs: 282
errs: 282
read: 139862016
skipped: 4452511744 (97%)

I.e. 316 copies from the foreign address space are successful, 282 are errors with partial reads, the same number are errors at all (i.e. all errors are partial reads), and the given number of bytes are read and skipped. The total memory that has skipped is 97%.

Why does ReadProcessMemory() fail so often, or what am I doing wrong here?

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Bonita Montero
  • 2,817
  • 9
  • 22
  • In any case, the enumeration code is only looking at the `mbi.AllocationProtect` field, but it is ignoring the `mbi.State` and `mbi.Protect` fields. Not all pages are safe to read from, for example uncommitted pages, or page guards. In particular, [ReadProcessMemory would return FALSE and GetLastError would return ERROR_PARTIAL_COPY when the copy hits a page fault](https://stackoverflow.com/a/4457745/65863). – Remy Lebeau May 18 '22 at 05:56
  • I'm filtering for memory regions which are PAGE_READWRITE, aint that sufficient ? – Bonita Montero May 18 '22 at 05:59
  • you are looking at the wrong field for that filtering. The `mbi.AllocationProtect` field contains the flags used when the page was allocated, not its *current* flags, which can be changed after allocation. Use the `mbi.Protect` field instead. Also, `PAGE_READWRITE` can be combined with other flags, including `PAGE_GUARD`, which [you should never try to read from](https://stackoverflow.com/a/22364636/65863). So, at the very least, you need to update the filter to ignore pages with `PAGE_GUARD` even if they claim to be readable. – Remy Lebeau May 18 '22 at 06:09

1 Answers1

1

Remy was mostly right. Here's the corrected code with a filter-callback on pageTree instead of a protection mask.

#include <Windows.h>
#include <iostream>
#include <vector>
#include <charconv>
#include <cstring>
#include <vector>
#include <stdexcept>
#include <sstream>
#include <cctype>
#include <fstream>
#include <cmath>

using namespace std;

template<typename FilterFn>
    requires requires( FilterFn fn, MEMORY_BASIC_INFORMATION &mbi ) { { fn( mbi ) } -> std::convertible_to<bool>; }
vector<vector<MEMORY_BASIC_INFORMATION>> pageTree( HANDLE hProcess, FilterFn filterFn );

using XHANDLE = unique_ptr<void, decltype([]( HANDLE h ) { h && h != INVALID_HANDLE_VALUE && CloseHandle( h ); })>;

int main( int argc, char **argv )
{
    if( argc < 2 )
        return EXIT_FAILURE;
    try
    {
        DWORD dwProcessId = [&]() -> DWORD
            {
                DWORD dwRet;
                if( from_chars_result fcr = from_chars( argv[1], argv[1] + strlen( argv[1] ), dwRet ); fcr.ec != errc() || *fcr.ptr )
                    throw invalid_argument( "process-id unparseable" );
                return dwRet;
            }();
        XHANDLE hProcess( [&]() -> HANDLE
            {
                HANDLE hRet = OpenProcess( PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, dwProcessId );
                if( !hRet )
                    throw system_error( (int)GetLastError(), system_category(), "can't open process" );
                return hRet;
            }() );
        vector<vector<MEMORY_BASIC_INFORMATION>> vvmbi = pageTree( hProcess.get(),
            []( MEMORY_BASIC_INFORMATION &mbi ) -> bool
            {
                return mbi.State == MEM_COMMIT;
            } );
        vector<char> processRegion;
        size_t
            succs = 0, partialErrs = 0, errs = 0,
            total = 0, read = 0, skipped = 0;
        for( vector<MEMORY_BASIC_INFORMATION> const &vmbi : vvmbi )
            for( MEMORY_BASIC_INFORMATION const &vmbi : vmbi )
            {
                processRegion.resize( vmbi.RegionSize );
                size_t actuallyRead;
                bool succ = ReadProcessMemory( hProcess.get(), vmbi.BaseAddress, to_address( processRegion.begin() ), vmbi.RegionSize, &actuallyRead );
                succs += succ;
                partialErrs += !succ && GetLastError() == ERROR_PARTIAL_COPY;
                errs += !succ;
                bool bytesCopied = succ || GetLastError() == ERROR_PARTIAL_COPY;
                actuallyRead = bytesCopied ? actuallyRead : 0;
                total += processRegion.size(),
                read += actuallyRead;
                skipped += bytesCopied ? processRegion.size() - actuallyRead : processRegion.size();
            }
        cout << "successes: " << succs << endl;
        cout << "partial errs: " << partialErrs << endl;
        cout << "errs: " << errs << endl;
        cout << "read: " << read << endl;
        cout << "skipped: " << skipped;
        auto pct = []( double a, double b ) -> double { return trunc( a / b * 1000.0 + 0.5 ) / 10.0; };
        cout << " (" << pct( (double)(ptrdiff_t)skipped, (double)(ptrdiff_t)total ) << "%)" << endl;
    }
    catch( exception const &exc )
    {
        cout << exc.what() << endl;
    }
}

template<typename Fn>
    requires requires( Fn fn, MEMORY_BASIC_INFORMATION &mbi ) { { fn( mbi ) } -> std::convertible_to<bool>; }
void enumProcessMemory( HANDLE hProcess, Fn fn );

template<typename FilterFn>
    requires requires( FilterFn fn, MEMORY_BASIC_INFORMATION &mbi ) { { fn( mbi ) } -> std::convertible_to<bool>; }
vector<vector<MEMORY_BASIC_INFORMATION>> pageTree( HANDLE hProcess, FilterFn filterFn )
{
    vector<vector<MEMORY_BASIC_INFORMATION>> vvmbis;
    enumProcessMemory( hProcess, [&]( MEMORY_BASIC_INFORMATION &mbi ) -> bool
        {
            if( !filterFn( mbi ) )
                return true;
            if( !vvmbis.size() || vvmbis.back().back().BaseAddress != mbi.BaseAddress )
                vvmbis.emplace_back( vector<MEMORY_BASIC_INFORMATION>() );
            vvmbis.back().emplace_back( mbi );
            return true;
        } );
    return vvmbis;
}

template<typename Fn>
    requires requires( Fn fn, MEMORY_BASIC_INFORMATION &mbi ) { { fn( mbi ) } -> std::convertible_to<bool>; }
void enumProcessMemory( HANDLE hProcess, Fn fn )
{
    MEMORY_BASIC_INFORMATION mbi;
    for( char *last = nullptr; ; last = (char *)mbi.BaseAddress + mbi.RegionSize )
    {
        size_t nBytes = VirtualQueryEx( hProcess, last, &mbi, sizeof mbi );
        if( nBytes != sizeof mbi )
            if( DWORD dwErr = GetLastError(); dwErr == ERROR_INVALID_PARAMETER )
                break;
            else
                throw system_error( (int)dwErr, system_category(), "can't query process pages" );
        if( !fn( mbi ) )
            break;
    }
}

Unfortunately I still get about 6% skipped memory:

successes: 2159
partial errs: 225
errs: 225
read: 706748416
skipped: 42897408 (5.7%)

Why is that ?

Bonita Montero
  • 2,817
  • 9
  • 22
  • Did you try debugging the failed memory blocks for common patterns of behavior? When `ReadProcessMemory()` fails, look at the contents of all the `vmbi` fields and see if there are any commonalities. I still think you are accessing pages you should not be accessing. – Remy Lebeau May 18 '22 at 06:30