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 (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;
}