How to Access and Reconstruct HBITMAP Data from a Debugged Process?

Cesar 20 Reputation points
2024-11-04T01:34:23.8833333+00:00

I am developing a Visual Studio 2022 extension to extract HBITMAP objects from the memory of a debugged process for analysis.

Ideally, I want to access and reconstruct (when necessary) the raw bitmap data directly, without needing to convert each HBITMAP to PNG format.

What I’ve Tried:

Duplicating the HBITMAP handle using DuplicateHandle—but it fails with the error:

The handle is invalid.

Reading the HBITMAP data using ReadProcessMemory—but it fails with:

Only part of a ReadProcessMemory or WriteProcessMemory request was completed.

Converting HBITMAP to PNG (works, but costly).

I was able to convert HBITMAP to a PNG byte array and read it in my extension, but this involves extra overhead for each bitmap conversion, which I want to avoid.

Question:

Are there alternative approaches I could try to read and reconstruct an HBITMAP from the debugged process’s memory directly?

Or, is there a more efficient way to handle and transfer the HBITMAP data between processes that could avoid the conversion to PNG?

Visual Studio extension source:

using EnvDTE;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Drawing;
using System.Drawing.Imaging;
using Task = System.Threading.Tasks.Task;
using System.ComponentModel;

namespace VSIX;

[PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)]
[Guid(VSIXPackage.PackageGuidString)]
[ProvideAutoLoad(UIContextGuids.NoSolution, PackageAutoLoadFlags.BackgroundLoad)]
public sealed class VSIXPackage : AsyncPackage
{
    public const string PackageGuidString = "f9a8aea3-f579-4816-9cb5-4ae3a5d68ef7";
    private DebugWatcher _debugWatcher;

    protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
    {
        await this.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
        _debugWatcher = new DebugWatcher();
    }
}

public class DebugWatcher
{
    private DTE _dte;
    private DebuggerEvents _debuggerEvents;

    public DebugWatcher()
    {
        _ = InitializeAsync();
    }

    private async Task InitializeAsync()
    {
        await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
        _dte = await ServiceProvider.GetGlobalServiceAsync(typeof(DTE)) as DTE;
        if (_dte == null)
            return;
        _debuggerEvents = _dte.Events.DebuggerEvents;
        _debuggerEvents.OnEnterBreakMode += OnEnterBreakMode;
    }



    [DllImport("kernel32.dll")]
    private static extern uint GetLastError();

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern IntPtr OpenProcess(
        uint dwDesiredAccess,
        bool bInheritHandle,
        int dwProcessId
    );

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool CloseHandle(IntPtr hObject);

    [DllImport("gdi32.dll")]
    private static extern bool DeleteObject(IntPtr hObject);

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool DuplicateHandle(
        IntPtr hSourceProcessHandle,      // Handle to source process
        IntPtr hSourceHandle,             // Handle to duplicate
        IntPtr hTargetProcessHandle,      // Handle to target process
        out IntPtr lpTargetHandle,        // Duplicate handle
        uint dwDesiredAccess,             // Access rights
        bool bInheritHandle,              // Handle inheritance option
        uint dwOptions                    // Optional actions
    );

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool ReadProcessMemory(
        IntPtr hProcess,
        IntPtr lpBaseAddress,
        [Out] byte[] lpBuffer,
        int dwSize,
        out IntPtr lpNumberOfBytesRead);



    private void OnEnterBreakMode(dbgEventReason Reason, ref dbgExecutionAction ExecutionAction)
    {
        ThreadHelper.ThrowIfNotOnUIThread();

        // Get the process being debugged (source process)
        int debuggedProcessId = _dte.Debugger.CurrentProcess.ProcessID;
        
        // Process access rights
        const uint PROCESS_DUP_HANDLE        = 0x0040;
        const uint PROCESS_VM_READ           = 0x0010;
        const uint PROCESS_QUERY_INFORMATION = 0x0400;
            
        // Open the debugged process with required access rights
        var sourceProcessHandle = OpenProcess(
            PROCESS_DUP_HANDLE | PROCESS_VM_READ | PROCESS_QUERY_INFORMATION,
            false,
            debuggedProcessId
        );
        
        // Get our current VS extension process (target process)
        var targetProcess       = System.Diagnostics.Process.GetCurrentProcess();
        var targetProcessHandle = targetProcess.Handle;

        if (sourceProcessHandle == null || targetProcessHandle == null)
            return;

        try
        {
            /*
                    --- Reading from HBITMAP ----           <- fail
             */
            Expression hbm = _dte.Debugger.GetExpression("hbm"); // HBITMAP hbm
            foreach (Expression expr in hbm.DataMembers)
            {
                Debug.WriteLine($"\nName: {expr.Name}, " +   // => prints only:      Name: unused, Type  : , Value : <Unable to read memory>
                                $"Type  : {expr.Type}, " +   //
                                $"Value : {expr.Value}");    //
            }

            string address = hbm.Value.Replace("0x", "");
            if (!long.TryParse(hbm.Value.Replace("0x", ""), System.Globalization.NumberStyles.HexNumber, null, out long hbmLong))
                return;
            IntPtr bitmapHandle = new IntPtr(hbmLong);

            const int BITMAPINFOHEADER_SIZE = 40; // sizeof(BITMAPINFOHEADER)
            byte[] headerBuffer = new byte[BITMAPINFOHEADER_SIZE];
            if (!ReadProcessMemory(sourceProcessHandle, bitmapHandle, headerBuffer, BITMAPINFOHEADER_SIZE, out var bytesReadHeader))
            {
                uint error = GetLastError();
                Debug.WriteLine($"Error: {new Win32Exception((int)error).Message}"); // Error: Only part of a ReadProcessMemory or WriteProcessMemory request was completed
            }

            const uint DUPLICATE_SAME_ACCESS = 0x00000002;
            
            bool success = DuplicateHandle(
                sourceProcessHandle,                // Source process (debugged process)
                bitmapHandle,                       // The bitmap handle we want to duplicate
                targetProcessHandle,                // Target process (our VS extension)
                out IntPtr duplicatedBitmapHandle,  // Where the new handle will be stored
                0,                                  // Access (0 because we're using DUPLICATE_SAME_ACCESS)
                false,                              // Don't inherit handle
                DUPLICATE_SAME_ACCESS               // Copy same access rights
            );

            if (!success)
            {
                uint error = GetLastError();
                Debug.WriteLine($"Error: {new Win32Exception((int)error).Message}"); // Error: The handle is invalid                         
            }



            /*
                    --- Reading from std::vector<uint8_t> ----           <- works
             */
            Expression pngData = _dte.Debugger.GetExpression("pngData"); // std::vector<uint8_t> pngData

            int size = int.Parse(pngData.DataMembers.Item("[capacity]")?.Value);                
            address  = pngData.DataMembers.Item("[allocator]").DataMembers.Item("[Raw View]")
                          .DataMembers.Item("_Myval2").DataMembers.Item("_Myfirst").Value;
            
            if (!long.TryParse(address.Replace("0x", ""), System.Globalization.NumberStyles.HexNumber, null, out long addressLong))
                return;
            
            byte[] buffer = new byte[size];
            if (!ReadProcessMemory(sourceProcessHandle, new IntPtr(addressLong), buffer, buffer.Length, out var bytesRead))
                return;
            
            MemoryStream ms = new MemoryStream(buffer);
            Image image     = Image.FromStream(ms); // necessary to install package: System.Drawing.Common
            image.Save($"C:\\Users\\Cesar\\Downloads\\VS.png", ImageFormat.Png);
        }
        catch (ArgumentException)
        {
            Debug.WriteLine("ArgumentException");
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"Error evaluating expression: {ex.Message}");
        }        
    }
}

Debug process source:

#include <iostream>
#include <Windows.h>
#include <gdiplus.h>
#include <vector>
using namespace Gdiplus;
using namespace DllExports;
#pragma comment (lib,"Gdiplus.lib")

// => cleanups omitted for brevity
std::vector<uint8_t> hBitmapToPngArray(HBITMAP hBitmap, const wchar_t* pngPath)
{
    std::vector<uint8_t> pngData;

    IStream* pStream = nullptr;
    if (FAILED(CreateStreamOnHGlobal(NULL, TRUE, &pStream)))
        return pngData;

    std::unique_ptr<Gdiplus::Bitmap> bitmap(new Gdiplus::Bitmap(hBitmap, NULL));
    if (!bitmap || bitmap->GetLastStatus() != Gdiplus::Ok)
        return pngData;

    CLSID pngClsid; UINT num  = 0;  UINT size = 0;

    GetImageEncodersSize(&num, &size);
    if (size == 0)
        return pngData;

    std::vector<BYTE> buffer(size);
    Gdiplus::ImageCodecInfo* pImageCodecInfo = (Gdiplus::ImageCodecInfo*)buffer.data();

    GetImageEncoders(num, size, pImageCodecInfo);
    for (UINT j = 0; j < num; ++j)
    {
        if (wcscmp(pImageCodecInfo[j].MimeType, L"image/png") == 0)
        {
            pngClsid = pImageCodecInfo[j].Clsid;
            break;
        }
    }

    Gdiplus::Status status = bitmap->Save(pStream, &pngClsid, NULL);
    if (status != Gdiplus::Ok)
        return pngData;

    STATSTG statstg = { 0 };
    if (FAILED(pStream->Stat(&statstg, STATFLAG_DEFAULT)))
        return pngData;

    LARGE_INTEGER seekPos = { 0 };
    pStream->Seek(seekPos, STREAM_SEEK_SET, NULL);
    pngData.resize(statstg.cbSize.LowPart);
    ULONG bytesRead;
    bitmap->Save(pngPath, &pngClsid, NULL);
    pStream->Release();

    return pngData;
}



int main()
{
    GdiplusStartupInput gdiplusStartupInput;
    ULONG_PTR gdiplusToken;
    GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);

    HWND hwnd = FindWindow(NULL, L"Untitled - Notepad");
    if (!hwnd)
    {
        MessageBox(NULL, L"Window not found!", L"Error", MB_ICONERROR);
        return 0;
    }

    RECT rc;
    GetWindowRect(hwnd, &rc);
    int width  = rc.right - rc.left;
    int height = rc.bottom - rc.top;

    HDC hdcWindow = GetDC(hwnd);
    HDC hdcMemDC  = CreateCompatibleDC(hdcWindow);

    HBITMAP hbm = CreateCompatibleBitmap(hdcWindow, width, height);
    SelectObject(hdcMemDC, hbm);

    if (!PrintWindow(hwnd, hdcMemDC, PW_RENDERFULLCONTENT))
    {
        MessageBox(NULL, L"PrintWindow failed!", L"Error", MB_ICONERROR);
        return 0;
    }

    std::vector<uint8_t> pngData = hBitmapToPngArray(hbm, L"C:\\Users\\Cesar\\Downloads\\process.png");                                 
    while (true)  
    {
        Sleep(100);
    }
    return 0;
}
Visual Studio
Visual Studio
A family of Microsoft suites of integrated development tools for building applications for Windows, the web and mobile devices.
5,313 questions
C#
C#
An object-oriented and type-safe programming language that has its roots in the C family of languages and includes support for component-oriented programming.
11,155 questions
C++
C++
A high-level, general-purpose programming language, created as an extension of the C programming language, that has object-oriented, generic, and functional features in addition to facilities for low-level memory manipulation.
3,807 questions
Visual Studio Extensions
Visual Studio Extensions
Visual Studio: A family of Microsoft suites of integrated development tools for building applications for Windows, the web and mobile devices.Extensions: A program or program module that adds functionality to or extends the effectiveness of a program.
235 questions
{count} votes

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.