SetDIBitsToDeviceの安全な画像表示とメモリ消去:Windows GDIセキュリティ実践ガイド

機密画像を一瞬だけ表示し、終了後に一切の痕跡を残さない――その要件を満たすには、SetDIBitsToDeviceの表向きの仕様だけでは足りません。CPUメモリ、GDIオブジェクト、DWM/GPU、ページファイルやクラッシュダンプまで、複数層の“残留”を前提に潰していく設計と運用が必要です。本稿では実務で使える具体策とコード、検証手順まで整理します。

目次

前提と結論(要点サマリー)

  • SetDIBitsToDeviceは「呼び出し元のビット列をDCへコピーする」APIですが、内部での一時バッファやドライバ側の挙動は仕様で厳密に公開されていません。関数呼び出し後に内部メモリが必ずゼロ化される保証はありません
  • 完全消去を目指すなら、アプリ側でゼロ化→解放→GDI/DC破棄→フラッシュ→黒塗り再描画までを一連の手順として実装します。
  • GPU/フレームバッファ(DWMを含む)への転送は避けにくく、アプリだけで“絶対に残らない”を保証することは不可能SetWindowDisplayAffinity(WDA_EXCLUDEFROMCAPTURE)などの抑止、BitLocker/ページファイル/休止ファイルの暗号化などOS/ポリシー面の統制が不可欠です。
  • 運用設計(権限・スクリーンキャプチャ遮断・ダンプ無効化)と、実装(ゼロ化・フラッシュ・破棄)をセットでやって初めて“現実的に安全”に近づけます。

SetDIBitsToDeviceの振る舞いと誤解しやすいポイント

関数の役割とデータの流路

SetDIBitsToDeviceは、呼び出し元が保持するDIB(BITMAPINFOと画素配列)を、指定したHDCへ転送するためのGDI関数です。縮小・拡大や色変換の有無、ドライバ実装により、内部で一時的なワークバッファが用意される可能性はあります。多くのケースで、転送元のユーザーメモリはAPI呼び出しの間だけ参照され、その後は呼び出し元の所有に戻ります。しかし、内部バッファのゼロ化やライフサイクルは未規定であり、OSビルドやドライバ、設定により変わり得ます。

StretchDIBitsとの違い

StretchDIBitsはスケーリング(ストレッチ/シュリンク)や一部の色変換を伴う点が主な違いで、内部経路やドライバへの依存度はむしろ増える傾向があります。“内部で余分な処理やメモリを持たせたくない”という目的だけであれば、まずはSetDIBitsToDeviceを選ぶのが無難です(とはいえ、どちらでも内部の一時バッファが生じる可能性は残ります)。

「内部は必ず解放される/ゼロ化される」は前提にしない

Windowsのドキュメントに「呼び出し元が解放すべき」と書かれていないからといって、内部メモリのゼロ化まで保証しているわけではありません。“API内部は不透明で変わる可能性がある”を前提に、アプリケーション側で痕跡を最小化する作法を取るのが安全です。

CPUメモリの痕跡対策(アプリ側で確実にできること)

もっともコントロールしやすいのが、呼び出し元が保持する画素バッファ(lpvBitsなど)です。以下の順序をワンショット関数化して、漏れなく適用します。

手順推奨API/操作目的・補足
1VirtualAllocでページ境界に確保(MEM_COMMIT)し、VirtualLockでロックページングや一部のコピーを抑止し、サイズ/境界を管理しやすくする。同容量の一時ワークも同様に確保。
2memcpyでソース→一時バッファへコピー(必要に応じて復号)APIに渡す直前だけ平文にする設計。復号はJIT(直前)で行い露出時間を最短化。
3SetDIBitsToDeviceを呼び出し描画がバッチングされ得るため、直後にGdiFlush()を併用。
4SecureZeroMemoryRtlSecureZeroMemory)で一時バッファを確実に上書きコンパイラ最適化で消されないゼロ化。volatileポインタや専用APIを使用。
5VirtualUnlockVirtualFreeロック解除後に即解放。ソース側の平文も同様にゼロ化してから解放。

ストライド(1行あたりのバイト数)は次の式で計算します(biBitCountが24/32などの場合)。

UINT stride = ((width * bpp + 31) / 32) * 4;
SIZE_T bytes = (SIZE_T)stride * height; // heightはabs値

RAIIで“やり漏れ”を防ぐサンプル(C++)

// 例: 機密DIBを1回だけ表示し、必ずゼロ化・解放する
struct LockedPage {
    void* p = nullptr; SIZE_T size = 0;
    LockedPage(SIZE_T n) : size(n) {
        p = ::VirtualAlloc(nullptr, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
        if (p) ::VirtualLock(p, size);
    }
    ~LockedPage() {
        if (p) {
            ::RtlSecureZeroMemory(p, size);
            ::VirtualUnlock(p, size);
            ::VirtualFree(p, 0, MEM_RELEASE);
        }
    }
};

bool ShowSensitiveDIBOnce(HWND hwnd, const BITMAPINFO* pbmi, const void* srcBits) {
const LONG  width  = pbmi->bmiHeader.biWidth;
const LONG  height = std::abs(pbmi->bmiHeader.biHeight);
const WORD  bpp    = pbmi->bmiHeader.biBitCount;
const UINT  stride = ((width * bpp + 31) / 32) * 4;
const SIZE_T bytes = (SIZE_T)stride * height;


LockedPage work(bytes);
if (!work.p) return false;

// 直前復号(必要時)。ここでは単純コピー。
std::memcpy(work.p, srcBits, bytes);

HDC hdc = ::GetDC(hwnd);
if (!hdc) return false;
BOOL ok = FALSE;
do {
    // 下地を黒で塗る
    RECT rc; ::GetClientRect(hwnd, &rc);
    ::PatBlt(hdc, 0, 0, rc.right - rc.left, rc.bottom - rc.top, BLACKNESS);

    // 表示(下端からの行数に注意:DIBは下から上がデフォルト)
    ok = ::SetDIBitsToDevice(
        hdc, 0, 0, (DWORD)width, (DWORD)height, 0, 0, 0, (UINT)height,
        work.p, const_cast<BITMAPINFO*>(pbmi), DIB_RGB_COLORS);

    ::GdiFlush();
} while (false);
::ReleaseDC(hwnd, hdc);

// workデストラクタでゼロ化・解放される
return ok == TRUE;


} 

ポイント:例外や早期リターンが発生してもデストラクタで必ずゼロ化されます。Cであれば__try/__finallygoto cleanupパターンで同等の“確実な後始末”を実装してください。

GDIオブジェクトとウィンドウの後始末(フレームの押し出し)

描画直後は、ウィンドウサーフェスやDWMの合成バッファに画像が残っている可能性があります。以下の“押し出し”を行い、見える面も見えない面も黒で上書きした状態を強制的に提示します。

順序操作目的・詳細
1PatBlt(..., BLACKNESS)FillRect全面黒塗り直近フレームを黒で更新し、再描画時の“残り”を減らす。
2GdiFlush()→(DWMありなら)DwmFlush()GDI/DWMのキューを空にし、黒フレームの表示完了を待つ。
3InvalidateRect+UpdateWindow2回程度繰り返す合成・遅延の影響を減らすため、黒フレームを重ねて提示。
4DeleteObject(HBITMAP)DeleteDC(HDC)ReleaseDCGDIハンドルを即時破棄。参照を切る。
5ウィンドウ最小化→DestroyWindow(用途により)合成面の“表示対象”自体を下げる。直後にまた黒塗りしてから破棄するのが安全。

GPU・フレームバッファのリスクと現実的な抑制策

Windows Vista以降はDWMによるコンポジションが標準で、GDIの結果も最終的にGPU側サーフェスに載ります。アプリからビデオメモリを直接ゼロ化する手段は基本的にありません。したがって、以下の抑止と“押し出し”の組み合わせが現実解です。

  • 画面キャプチャ抑止:SetWindowDisplayAffinity(WDA_EXCLUDEFROMCAPTURE)でOS標準の画面キャプチャや一部のAPIからの取得を遮断。RDPや一部の仮想化環境では動作条件が異なるため、ローカルセッションでの適用を前提に。
  • サムネイル/ライブプレビュー対策:DwmSetWindowAttribute(DWMWA_FORCE_ICONIC_REPRESENTATION, TRUE)DWMWA_HAS_ICONIC_BITMAPを使い、自前の黒いアイコン化ビットマップを供給(WM_DWMSENDICONICLIVEPREVIEWBITMAPハンドリング)。Alt+Tabやタスクバーのミニチュアに機密画像が残らないようにする。
  • 黒フレーム複数回提示:合成ヒストリや遅延を押し出すため、黒塗り→フラッシュ→再描画を複数回。
  • ソフトウェアレンダリングの選択:要件次第で、GPU経由を避ける専用ビューア(GDIオンリー/Direct2D WARP)に切り替え、露出面を限定。

ディスプレイアフィニティ適用の最小コード

BOOL EnableNoCapture(HWND hwnd) {
    return ::SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE);
}

注意:ハードウェアベースの外部キャプチャ(外付けキャプチャカード等)や、OSの意図しないバグ/特権コードには効きません。抑止であって、機密の完全消滅を保証するものではない点を関係者と共有してください。

OS/ポリシーで補完する統制

アプリだけで取り切れない残留リスクは、OS機能と運用で抑え込みます。

対象設定/手段意図補足
ページファイルBitLockerでシステムドライブ暗号化/ローカルセキュリティポリシー「シャットダウン時に仮想メモリのページファイルをクリア」を有効スワップに残る平文を防止クリアはシャットダウン時。実行時はアプリ側のVirtualLockで露出を減らす。
休止ファイル(hiberfil)休止無効化またはドライブ暗号化メモリ内容のディスク保存を防止運用要件に応じて選択。
クラッシュダンプダンプ種別を最小化(または無効)・収集の権限を厳格化ダンプ経由の漏えいを抑止調査とのトレードオフ。機密端末は自動送信禁止
画面キャプチャ企業ポリシー(情報漏えい対策ソフト / グループポリシー)+SetWindowDisplayAffinity論理的なキャプチャ経路を遮断モバイル端末や二重化環境の統制も必須。
ディスク全体BitLocker全ドライブ暗号化オフライン攻撃対策TPM/回復キー運用をセットで。

実装レシピ:「1フレームだけ安全に表示」をプロシージャ化

  1. 入力のDIBは原則暗号化して保持し、表示直前に一時バッファへ復号。
  2. SetWindowDisplayAffinityを適用、ウィンドウ枠外やサムネイルを黒で供給。
  3. SetDIBitsToDeviceで描画→GdiFlush()
  4. 即座に黒塗り→GdiFlush()DwmFlush()
  5. 一時バッファはSecureZeroMemoryVirtualUnlockVirtualFree
  6. GDIオブジェクトとDCを破棄、必要ならウィンドウも破棄。

黒塗り・フラッシュの最小コード

void WipeWindowNow(HWND hwnd) {
    RECT rc; ::GetClientRect(hwnd, &rc);
    HDC  hdc = ::GetDC(hwnd);
    ::PatBlt(hdc, 0, 0, rc.right - rc.left, rc.bottom - rc.top, BLACKNESS);
    ::GdiFlush();
    ::ReleaseDC(hwnd, hdc);
    // DWM有効なら
    HMODULE hDwm = ::LoadLibraryW(L"dwmapi.dll");
    if (hDwm) {
        typedef HRESULT (WINAPI *PFNDWMFLUSH)();
        auto p = (PFNDWMFLUSH)::GetProcAddress(hDwm, "DwmFlush");
        if (p) p();
        ::FreeLibrary(hDwm);
    }
}

テスト・監査手順(実務用)

「本当に残っていないか」を確認するためのミニマルな手順です。社内監査で再現可能な形にしておくとよいでしょう。

  • ユーザーモードヒープ検査:表示直後にプロセスのダンプを取得(ローカル・限定端末)。既知のシグネチャ(先頭数バイト)で検索し、一時バッファをゼロ化できているかを確認。
  • ページファイル検査:休止/スワップの構成を固定し、メモリ圧迫→表示→即座に強制休止/スワップ誘発→復帰後にスワップファイルの平文出現がないかを検査(暗号化前提)。
  • サムネイル/キャプチャ:Alt+Tab/タスクバー上に黒いサムネイルが出ること、標準のキャプチャ(Snipping)で取得できないことを確認。
  • GPU押し出し:黒フレームを複数回提示したうえで、同一プロセス・同一ウィンドウで新規フレームを重ね、旧フレームが再利用されないか目視・比較。

よくある質問(FAQ)

  • Q: SetDIBitsToDeviceは内部で確保したメモリを必ずゼロ化しますか?
    A: ドキュメントには明記がなく、OS/ドライバ/ビルドで変わる可能性があります。期待しないのが前提です。呼び出し元の一時バッファをゼロ化し、GDI/DC/ウィンドウを速やかに破棄してください。
  • Q: StretchDIBitsの方が安全ですか?
    A: 安全性という観点では一長一短です。StretchDIBitsは追加処理が介在しやすく、内部経路が複雑化しがちです。スケーリング不要ならSetDIBitsToDeviceが素直です。
  • Q: GdiFlushDwmFlushは必須?
    A: 目に見える“残り”を減らすうえで非常に有用です。バッチング/遅延を抑え、黒フレームを確実に提示できます。
  • Q: スクリーンショットを完全に禁止できますか?
    A: SetWindowDisplayAffinityで多くの論理的キャプチャは遮断できますが、外部ハードウェアや特権ツールには無力です。抑止+暗号化+運用統制の多層防御が前提です。
  • Q: 画像データは常に暗号化すべき?
    A: 機密等級が高い場合は、保管時は暗号化、表示直前のみ復号(JIT)がおすすめです。露出時間を最小化できます。

NGパターン(避けるべき実装)

  • ヒープに平文を長時間保持し、ゼロ化せずにfree/HeapFreeだけを呼ぶ。
  • GDIオブジェクト/メモリDC/ウィンドウを破棄せずにハンドルリークさせる。
  • 黒塗りもフラッシュもせずに即ウィンドウ破棄(合成面に直前フレームが残り得る)。
  • サムネイル対策なしでAlt+Tabやライブプレビューを許す。
  • ページファイル/休止/ダンプの運用統制が未整備の端末で機密表示を許す。

実装チェックリスト(配布前の最終確認)

  • 一時バッファはVirtualAllocVirtualLockで確保し、ゼロ化してから解放している。
  • SetDIBitsToDevice直後にGdiFlush()、黒塗り→GdiFlushDwmFlushを実施している。
  • すべてのHBITMAP/HDC/HWNDが確実に破棄される(RAII/Finallyで担保)。
  • ウィンドウにはSetWindowDisplayAffinity(WDA_EXCLUDEFROMCAPTURE)を適用した。
  • Alt+Tab/タスクバーのサムネイルは黒画像に置換される。
  • BitLocker/ページファイルクリア/休止/ダンプのポリシーが端末で有効。
  • 内部監査手順(ダンプ検索・キャプチャ不可の検証)がドキュメント化されている。

まとめ:“ゼロ化の技術”+“露出面の削減”+“運用統制”で現実解を作る

SetDIBitsToDeviceは手軽で高速ですが、内部実装は仕様で固められていません。したがって、アプリが直接持つ平文は自分でゼロ化し、描画面は黒で押し出し、サムネイルやキャプチャの経路はOS機能で抑止する――この3点セットが不可欠です。さらに、ページファイル/休止/クラッシュダンプの運用を固めて、アプリに依存しない漏えい経路を塞いでください。これらをテンプレート化・自動テスト化することで、「表示が終わった時点で画像データをシステムに一切残さない」という目標に、現実的なコストで近づけます。


付録:安全なDIB表示の完全版コード(抜粋)

実運用ではエラーハンドリング・ログ・タイミング制御(タイムボックス)・JIT復号などを追加してください。

class SensitiveViewer {
public:
    explicit SensitiveViewer(HWND w) : hwnd(w) {
        ::SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE);
        BOOL v = TRUE;
        HMODULE h = ::LoadLibraryW(L"dwmapi.dll");
        if (h) {
            typedef HRESULT (WINAPI *PFN)(HWND, DWORD, LPCVOID, DWORD);
            auto setAttr = (HRESULT (WINAPI*)(HWND, DWORD, LPCVOID, DWORD))
                           ::GetProcAddress(h, "DwmSetWindowAttribute");
            if (setAttr) {
                setAttr(hwnd, /*DWMWA_FORCE_ICONIC_REPRESENTATION*/ 7, &v, sizeof(v));
                setAttr(hwnd, /*DWMWA_HAS_ICONIC_BITMAP*/ 10, &v, sizeof(v));
            }
            ::FreeLibrary(h);
        }
    }


bool Show(const BITMAPINFO* pbmi, const void* src) {
    const LONG  w = pbmi->bmiHeader.biWidth;
    const LONG  h = std::abs(pbmi->bmiHeader.biHeight);
    const WORD  bpp = pbmi->bmiHeader.biBitCount;
    const UINT  stride = ((w * bpp + 31) / 32) * 4;
    const SIZE_T bytes = (SIZE_T)stride * h;

    // ワーク確保(RAII)
    LockedPage work(bytes);
    if (!work.p) return false;

    // JIT復号の代わりにここではコピー
    std::memcpy(work.p, src, bytes);

    HDC dc = ::GetDC(hwnd);
    if (!dc) return false;

    // まず黒で下地
    RECT rc; ::GetClientRect(hwnd, &rc);
    ::PatBlt(dc, 0, 0, rc.right - rc.left, rc.bottom - rc.top, BLACKNESS);

    BOOL ok = ::SetDIBitsToDevice(dc, 0, 0, (DWORD)w, (DWORD)h, 0, 0, 0, (UINT)h,
                                  work.p, const_cast<BITMAPINFO*>(pbmi), DIB_RGB_COLORS);
    ::GdiFlush();
    ::ReleaseDC(hwnd, dc);

    // 即座に黒で押し出し
    WipeWindowNow(hwnd); // 前掲関数
    // 再度黒を重ねる(合成履歴対策)
    WipeWindowNow(hwnd);

    return ok == TRUE;
}


private:
HWND hwnd;
}; 

実務メモ(チーム共有用)

  • 「完全消去」はハードウェア・OS・ドライバ・ポリシーの総合結果。アプリ単独では保証できないことを周知。
  • テストと監査を定例化(ダンプ検索・サムネ黒画・キャプチャ遮断の確認)。
  • インシデント対応:誤表示時は即黒塗り→フラッシュ→プロセス終了の緊急ルートを用意。
  • ログには機密内容を残さない(サイズ/ハッシュのみ)。

以上を踏まえ、SetDIBitsToDeviceを使った短時間表示でも、実装・運用の両輪で“現実的に安全”へ寄せることができます。

コメント

コメントする

目次