Why does ReadProcessMemory fail so often with ERROR_PARTIAL_COPY?

289 views Asked by At

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?

1

There are 1 answers

1
Bonita Montero On

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 ?