ロック中のサブモニタに画像を表示したかった

Windowsのセキュリティ設計に負けたちゃったよ

初めに

最近、家のデスクトップにデュアルモニタを導入した。作業効率は当然上がるのだが、毎回ちょっとした違和感があったのが、ロック画面(サインイン画面)の挙動である。

ロックすると、メインモニタにはお馴染みのサインイン画面が出るのだが、サブモニタは問答無用で真っ黒になる。デスクトップに戻ると壁紙が華やかなだけに、ロック中の片方だけ暗いのが妙に間抜けというか、こちらの作業環境が中途半端に見えて気に入らない。

DisplayFusionのような有名な多機能ツールも試してみたものの、こちらは仮想デスクトップを切り替えるたびに画面が一瞬暗転する挙動が許容できず、結局アンインストール。

結局はWindowsだって人間が作ったものなのだから、どこか叩けば穴があるだろうと思い立ち、Win32 APIで自作アプリを書くことにした。

結論から言うと、これは敗北した記録である。
ただし収穫は大きかった。

やりたかったこと

ロック画面のサブモニタにも何か表示すること

自作アプリの構想

最初に立てた作戦はこうだった。Windowsにはロックされた瞬間にイベントを通知してもらえる仕組みがあるはずで、それを購読すれば、ロックを検知した瞬間にサブモニタへ全画面のウィンドウを生成して画像を表示できるのではないか。アンロックを検知したらウィンドウを綺麗に片付ける。シンプルかつ正攻法に思えた。

ロック画面そのものを書き換えるのは、Windowsのセキュリティ設計上ほぼ不可能だということは事前に調べてわかっていた。サインイン画面はSecure Desktopという別世界で動いており、一般のアプリは描画権を持たない。だからそこは諦め、ロック画面の手前に自分のウィンドウを置くという方針で逃げを打ったつもりだった。

どのような仕組みで動いているのか(技術的な話)

中身はWin32 API + GDI+で、構成は四つに分けた。

まずは、COMとGDI+を初期化して、メッセージループに入る前にセッション監視オブジェクトを起動するだけ。

その次が今回の肝である。HWND_MESSAGE を親に指定したメッセージ専用ウィンドウを作り、WTSRegisterSessionNotification という関数でロック/アンロックの通知を購読する。これが届くと WM_WTSSESSION_CHANGE というメッセージがウィンドウに飛んでくるので、WTS_SESSION_LOCKWTS_SESSION_UNLOCK で分岐する。前者でサブモニタ用のウィンドウを生成、後者で破棄する。管理者権限は要らない。

MonitorEnum は、EnumDisplayMonitors でモニタを列挙して、プライマリ以外の矩形を返すだけの薄いやつ。

BlackoutWindow は1モニタにつき1個生成される全画面ボーダーレスウィンドウで、WS_POPUP + WS_EX_TOPMOST + WS_EX_TOOLWINDOW の組み合わせで枠なし・最前面・Alt+Tabに出ない、という設定。WM_PAINT で GDI+ を使って画像をアスペクト比維持で中央描画する。ちらつき防止のためダブルバッファを噛ませた。

開発環境はVSCode + w64devkit(MinGW-w64 / GCC)。ビルドはCMakeで構成して、#pragma comment(lib, ...) ではなくCMake側でリンクライブラリを明示することでMSVCにもMinGWにも乗るようにした。MinGWで wmain を使うために -municode リンクオプションが要るのが地味なハマりポイントだった。

結果:Windowsの壁

ビルドは通った。起動するとコンソールにログが出て、Win + L でロックすると見事に

[EVENT] WTS_SESSION_LOCK
[INFO] Sub monitors found: 1
[EVENT] WTS_SESSION_UNLOCK

と検知できている。アンロックも拾えている。コードは完璧だ。

しかしロック中、サブモニタは 何も変わらず黒いまま だった。

検証のため、SessionMonitor::Initialize の最後で OnLock() を直接呼んでみた。すると、ロックしていない通常のデスクトップ状態では、サブモニタにきちんと画像が表示された。つまり描画コードに不具合は無く、ウィンドウ生成も画像読み込みも正常。

これでようやく腑に落ちた。Windowsはロックされた瞬間、ユーザーセッションのデスクトップそのものを画面から退避させているらしい。サブモニタが真っ黒に見えるのは最前面のウィンドウが無いからではなく、そもそも誰の絵も映していないからだ。WS_EX_TOPMOST で最前面に居座ったところで、その”面”自体が画面から消えている以上、こちらのウィンドウもまとめて隠されてしまう。

最初にSecure Desktopの壁として一度諦めたはずの問題に、別の入口から侵入したつもりが、結局同じ壁の同じ面を裏側から叩いていたのだった。

失敗してわかったこと

冷静に整理すると、Microsoftはここに 二重に壁を置いている。一つはサインイン画面そのものに描画する権利を奪うこと(Secure Desktop)、もう一つはロック中にユーザーセッションのデスクトップを画面から退避させること。前者だけなら隣に出して逃げられたが、後者があるせいでロック中に画面に何かを出すという行為そのものが、ユーザーセッション側のアプリには許されていない。

これはバグでも実装漏れでもなく、明確な意思を持った設計だ。攻撃者がパスワード入力欄を偽装するアプリを置けないようにするための堅牢な仕組みで、悪用と正当用途を切り分ける手段が無いから一律で塞がれている。叩けば叩くほど、これがマルウェアの典型挙動と区別が付かなくなることが見えてくる。

というわけで、ロック画面のサブモニタを画像で埋めるという当初の目的は、ユーザーセッション側のアプリでは原理的に不可能、という結論に至った。

ロック画面のサブモニタを画像で埋めるツールが世の中に存在しないのは、皆が怠けていたからではなく、皆ここに到達して諦めていたからなのだろう。同じ道を辿る人の数時間を節約できれば、この記録にも意味があるはずだ。