Cesar
Cesar

Reputation: 21

How to Access and Reconstruct HBITMAP Data from a Debugged Process

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 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 HBITMAPto 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?

-- to reproduce --

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, IntPtr hSourceHandle, IntPtr hTargetProcessHandle, out IntPtr lpTargetHandle, uint dwDesiredAccess, bool bInheritHandle, uint dwOptions);

    [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();
        int debuggedProcessId = _dte.Debugger.CurrentProcess.ProcessID;

        const uint PROCESS_DUP_HANDLE = 0x0040;
        const uint PROCESS_VM_READ = 0x0010;
        const uint PROCESS_QUERY_INFORMATION = 0x0400;

        var sourceProcessHandle = OpenProcess(PROCESS_DUP_HANDLE | PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, false, debuggedProcessId);
        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");
            foreach (Expression expr in hbm.DataMembers)
            {
                Debug.WriteLine($"\nName: {expr.Name}, Type: {expr.Type}, Value: {expr.Value}"); // => prints only:      Name: unused, Type  : , Value : <Unable to read memory>
            }

            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;
            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");
            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);
            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;
}

Upvotes: 2

Views: 108

Answers (0)

Related Questions