業務用のオーバーレイツールや画面キャプチャアプリをC#/VB.NETで作ると、タスク マネージャーなど管理者権限のウィンドウ上だけマウス座標が取れず困ることがあります。本記事では、アプリ側の権限やUIAccessを上げずに「低レベルマウスフックがブロックされている状態」を正確に検出する実装パターンを解説します。
C#/VB.NETの低レベルマウスフックが「突然効かなくなる」理由
まず問題の整理から始めます。典型的なケースは次のような状況です。
- WPFのC#/VB.NETアプリで、
WH_MOUSE_LL(低レベルマウスフック)を使ってマウス移動を監視している。 - デスクトップや通常のアプリ上ではフックコールバックが問題なく飛んでくる。
- しかし、タスク マネージャーや管理者として実行したアプリを前面に出すと、急にフックが呼ばれなくなり、座標が一切取れなくなる。
- 自分のアプリを管理者として起動すれば回避できるが、ユーザーに管理者権限要求をしたくない、UIAccessの署名や配置要件も避けたいという事情がある。
この現象の背景には、WindowsのUIPI(User Interface Privilege Isolation:ユーザー インターフェイス特権分離)があります。UIPIは、整合性レベル(Integrity Level)が低いプロセスから高いプロセスへのUI操作・情報取得を制限する仕組みであり、低レベルフックにも影響します。
つまり、通常権限(Medium)のプロセスから見ると、前景ウィンドウがHigh以上(管理者権限以上)になったタイミングで、OSがフックイベントを止めてしまうため、「急にマウスフックが効かなくなった」ように見えるのです。
本当にやりたいのは「ブロックの検出」であって「回避」ではない
重要なポイントは、この制限自体は正当なセキュリティ機構であり、本質的に回避はできないということです。低権限プロセスが高権限のUIを覗けないようにするのは、Windowsの設計意図そのものです。
そこで、本記事が目指すのは次のようなアプローチです。
- アプリはこれまで通り通常権限(Medium)で動かす。
- フックそのものは従来通りに設定する。
- 「今まさにフックがブロックされている状態か」を別の仕組みで検出する。
- ブロック中は、UI表示や内部ロジックを切り替えてユーザーに分かりやすく振る舞いを変える。
たとえば、「現在前景が管理者権限のウィンドウであるため、マウス追跡を一時停止しています」とバナーを出したり、ログに出力したりできます。内部的には、「低レベルフックがブロックされる状況とほぼ同義になる条件」をOSから取得し、状態として扱うイメージです。
解決のキモ:WinEvents + 整合性レベル判定
今回の解決策の中心は次の2つのAPIです。
SetWinEventHook(WinEvents)で前景ウィンドウの変化をイベントとして受け取る- 前景ウィンドウのプロセスに対して整合性レベル(Integrity Level)を取得する
フローを文章で書くと次のようになります。
SetWinEventHookでEVENT_SYSTEM_FOREGROUND(前景ウィンドウの変更イベント)だけを、WINEVENT_OUTOFCONTEXTでフックする。- 前景ウィンドウが切り替わる度に、コールバックに
HWNDが渡される。 GetWindowThreadProcessIdでそのウィンドウのプロセスIDを取得する。OpenProcess→OpenProcessToken→GetTokenInformation(TokenIntegrityLevel)の順に呼び、整合性レベルを表すSIDからRIDを取り出す。- そのRIDがMediumより高いかどうかで、低レベルフックがブロックされているかどうかを判定する。
ここで使う代表的な整合性レベルは次の通りです。
| 整合性レベル | RID(10進) | SID | 典型的な例 |
|---|---|---|---|
| Low | 4096 | S-1-16-4096 | サンドボックスされたブラウザタブ等 |
| Medium | 8192 | S-1-16-8192 | 通常のデスクトップアプリ |
| High | 12288 | S-1-16-12288 | 管理者として実行されたアプリ |
| System | 16384 | S-1-16-16384 | OSのコアプロセス |
通常権限のアプリでは、RID > Medium(8192)になったら「ブロック中」とみなす、というポリシーにすれば、実際の挙動とかなり高い精度で一致します。
WinEventsで前景ウィンドウをイベント駆動で監視する
前景ウィンドウを監視する方法として、GetForegroundWindowをタイマーでポーリングする実装もよく見かけますが、これは次のような欠点があります。
- ポーリング間隔によっては、ウィンドウ切り替えを見落としたり検出が遅くなったりする。
- 必要以上にCPUを消費する。
- UIPIによる制限で例外が発生する可能性があり、コードが煩雑になる。
代わりに、Win32のWinEventsを使うと、OS側からイベント駆動で教えてもらえるため非常に効率的です。C#での最低限のセットアップ例は次の通りです。
public class ForegroundIntegrityMonitor : IDisposable
{
private const uint EVENT_SYSTEM_FOREGROUND = 0x0003;
private const uint WINEVENT_OUTOFCONTEXT = 0x0000;
private delegate void WinEventDelegate(
IntPtr hWinEventHook,
uint eventType,
IntPtr hwnd,
int idObject,
int idChild,
uint dwEventThread,
uint dwmsEventTime);
[DllImport("user32.dll")]
private static extern IntPtr SetWinEventHook(
uint eventMin,
uint eventMax,
IntPtr hmodWinEventProc,
WinEventDelegate lpfnWinEventProc,
uint idProcess,
uint idThread,
uint dwFlags);
[DllImport("user32.dll")]
private static extern bool UnhookWinEvent(IntPtr hWinEventHook);
private readonly WinEventDelegate _callback; // GC対策でフィールドに保持
private IntPtr _hookHandle;
public ForegroundIntegrityMonitor()
{
_callback = WinEventProc;
_hookHandle = SetWinEventHook(
EVENT_SYSTEM_FOREGROUND,
EVENT_SYSTEM_FOREGROUND,
IntPtr.Zero,
_callback,
0,
0,
WINEVENT_OUTOFCONTEXT);
}
private void WinEventProc(
IntPtr hWinEventHook,
uint eventType,
IntPtr hwnd,
int idObject,
int idChild,
uint dwEventThread,
uint dwmsEventTime)
{
// ここでhwndから整合性レベルを判定する
}
public void Dispose()
{
if (_hookHandle != IntPtr.Zero)
{
UnhookWinEvent(_hookHandle);
_hookHandle = IntPtr.Zero;
}
}
}
注意点として、WinEventDelegateのインスタンスをフィールドで保持しないとGCされてしまい、コールバックが突然呼ばれなくなることがあります。これは低レベルマウスフックのデリゲートと同じ落とし穴なので、必ずフィールドに保持してください。
前景プロセスの整合性レベルを取得する手順
次に、WinEventコールバックから前景プロセスの整合性レベルを取得する手順を具体的に見ていきます。流れは次のようになります。
GetWindowThreadProcessIdでHWNDからプロセスIDを取得OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)でプロセスハンドルを開くOpenProcessToken(TOKEN_QUERY)でプロセストークンを取得GetTokenInformation(TokenIntegrityLevel)で整合性レベルSIDを取得- SIDの最後のサブオーソリティ(RID)を読み取り、Low/Medium/High/Systemを判定
アクセス権の指定をミスするとOpenProcessに失敗しがちなので、PROCESS_QUERY_LIMITED_INFORMATIONを使うのが実用的です。また、トークン取得に失敗した場合は保守的に「ブロック中」とみなすと、安全側の判定ができます。
C#での整合性レベル取得ユーティリティ
上記の手順をC#でまとめると、次のようなユーティリティメソッドになります。
public enum ProcessIntegrityLevel
{
Unknown = 0,
Low,
Medium,
High,
System
}
public static class IntegrityLevelHelper
{
private const uint PROCESS_QUERY_LIMITED_INFORMATION = 0x1000;
private const uint TOKEN_QUERY = 0x0008;
private const int SECURITY_MANDATORY_LOW_RID = 0x00001000; // 4096
private const int SECURITY_MANDATORY_MEDIUM_RID = 0x00002000; // 8192
private const int SECURITY_MANDATORY_HIGH_RID = 0x00003000; // 12288
private const int SECURITY_MANDATORY_SYSTEM_RID = 0x00004000; // 16384
private enum TOKEN_INFORMATION_CLASS
{
TokenUser = 1,
// 中略
TokenIntegrityLevel = 25
}
[StructLayout(LayoutKind.Sequential)]
private struct SID_AND_ATTRIBUTES
{
public IntPtr Sid;
public int Attributes;
}
[StructLayout(LayoutKind.Sequential)]
private struct TOKEN_MANDATORY_LABEL
{
public SID_AND_ATTRIBUTES Label;
}
[DllImport("user32.dll", SetLastError = true)]
private static extern uint GetWindowThreadProcessId(
IntPtr hWnd,
out uint lpdwProcessId);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr OpenProcess(
uint dwDesiredAccess,
bool bInheritHandle,
uint dwProcessId);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CloseHandle(IntPtr hObject);
[DllImport("advapi32.dll", SetLastError = true)]
private static extern bool OpenProcessToken(
IntPtr ProcessHandle,
uint DesiredAccess,
out IntPtr TokenHandle);
[DllImport("advapi32.dll", SetLastError = true)]
private static extern bool GetTokenInformation(
IntPtr TokenHandle,
TOKEN_INFORMATION_CLASS TokenInformationClass,
IntPtr TokenInformation,
uint TokenInformationLength,
out uint ReturnLength);
[DllImport("advapi32.dll", SetLastError = true)]
private static extern IntPtr GetSidSubAuthorityCount(IntPtr pSid);
[DllImport("advapi32.dll", SetLastError = true)]
private static extern IntPtr GetSidSubAuthority(
IntPtr pSid,
uint nSubAuthority);
public static ProcessIntegrityLevel GetForegroundWindowIntegrityLevel(IntPtr hwnd)
{
try
{
uint pid;
GetWindowThreadProcessId(hwnd, out pid);
IntPtr hProcess = OpenProcess(
PROCESS_QUERY_LIMITED_INFORMATION,
false,
pid);
if (hProcess == IntPtr.Zero)
return ProcessIntegrityLevel.Unknown;
try
{
IntPtr hToken;
if (!OpenProcessToken(hProcess, TOKEN_QUERY, out hToken))
return ProcessIntegrityLevel.Unknown;
try
{
uint length = 0;
// 1回目: 必要なバッファサイズを取得
GetTokenInformation(hToken,
TOKEN_INFORMATION_CLASS.TokenIntegrityLevel,
IntPtr.Zero,
0,
out length);
IntPtr buffer = Marshal.AllocHGlobal((int)length);
try
{
if (!GetTokenInformation(hToken,
TOKEN_INFORMATION_CLASS.TokenIntegrityLevel,
buffer,
length,
out length))
{
return ProcessIntegrityLevel.Unknown;
}
var tml = Marshal.PtrToStructure<TOKEN_MANDATORY_LABEL>(buffer);
IntPtr pSid = tml.Label.Sid;
// サブオーソリティ数
IntPtr pCount = GetSidSubAuthorityCount(pSid);
byte count = Marshal.ReadByte(pCount);
// 最後のサブオーソリティがRID
IntPtr pRid = GetSidSubAuthority(pSid, (uint)(count - 1));
int rid = Marshal.ReadInt32(pRid);
if (rid >= SECURITY_MANDATORY_SYSTEM_RID)
return ProcessIntegrityLevel.System;
if (rid >= SECURITY_MANDATORY_HIGH_RID)
return ProcessIntegrityLevel.High;
if (rid >= SECURITY_MANDATORY_MEDIUM_RID)
return ProcessIntegrityLevel.Medium;
if (rid >= SECURITY_MANDATORY_LOW_RID)
return ProcessIntegrityLevel.Low;
return ProcessIntegrityLevel.Unknown;
}
finally
{
Marshal.FreeHGlobal(buffer);
}
}
finally
{
CloseHandle(hToken);
}
}
finally
{
CloseHandle(hProcess);
}
}
catch
{
return ProcessIntegrityLevel.Unknown;
}
}
}
ここでは例外処理を簡略化して、何か失敗した場合はUnknownを返しています。運用ポリシーによっては、Unknownを「High扱い」としてブロック側に寄せる実装も有効です。
「ブロック中かどうか」をフラグとして持つC#実装例
先ほどのForegroundIntegrityMonitorとIntegrityLevelHelperを組み合わせて、現在フックがブロックされていると推定されるかどうかを通知するクラスを作ってみます。
public class MouseHookBlockStateMonitor : IDisposable
{
public bool IsBlocked { get; private set; }
public event EventHandler<bool>? BlockStateChanged;
private const uint EVENT_SYSTEM_FOREGROUND = 0x0003;
private const uint WINEVENT_OUTOFCONTEXT = 0x0000;
private delegate void WinEventDelegate(
IntPtr hWinEventHook,
uint eventType,
IntPtr hwnd,
int idObject,
int idChild,
uint dwEventThread,
uint dwmsEventTime);
[DllImport("user32.dll")]
private static extern IntPtr SetWinEventHook(
uint eventMin,
uint eventMax,
IntPtr hmodWinEventProc,
WinEventDelegate lpfnWinEventProc,
uint idProcess,
uint idThread,
uint dwFlags);
[DllImport("user32.dll")]
private static extern bool UnhookWinEvent(IntPtr hWinEventHook);
private readonly WinEventDelegate _callback;
private IntPtr _hookHandle;
public MouseHookBlockStateMonitor()
{
_callback = WinEventProc;
_hookHandle = SetWinEventHook(
EVENT_SYSTEM_FOREGROUND,
EVENT_SYSTEM_FOREGROUND,
IntPtr.Zero,
_callback,
0,
0,
WINEVENT_OUTOFCONTEXT);
// 起動時の前景ウィンドウもチェックしておくと安全
IntPtr hwnd = GetForegroundWindow();
UpdateState(hwnd);
}
[DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();
private void WinEventProc(
IntPtr hWinEventHook,
uint eventType,
IntPtr hwnd,
int idObject,
int idChild,
uint dwEventThread,
uint dwmsEventTime)
{
UpdateState(hwnd);
}
private void UpdateState(IntPtr hwnd)
{
var il = IntegrityLevelHelper.GetForegroundWindowIntegrityLevel(hwnd);
bool blocked;
switch (il)
{
case ProcessIntegrityLevel.High:
case ProcessIntegrityLevel.System:
blocked = true;
break;
case ProcessIntegrityLevel.Unknown:
// ポリシー次第でtrue/falseを決める
blocked = true; // 安全側に寄せる例
break;
default:
blocked = false;
break;
}
if (blocked != IsBlocked)
{
IsBlocked = blocked;
BlockStateChanged?.Invoke(this, blocked);
}
}
public void Dispose()
{
if (_hookHandle != IntPtr.Zero)
{
UnhookWinEvent(_hookHandle);
_hookHandle = IntPtr.Zero;
}
}
}
このクラスを使うと、アプリ側からは単にIsBlockedプロパティとBlockStateChangedイベントを監視するだけで、「いま低レベルマウスフックが効いていない状態かどうか」をほぼリアルタイムに知ることができます。
WPFアプリからの利用例
WPFアプリのメインウィンドウで、このモニタを使ってUIに状態を表示する例を示します。
public partial class MainWindow : Window
{
private MouseHookBlockStateMonitor? _blockMonitor;
public MainWindow()
{
InitializeComponent();
Loaded += MainWindow_Loaded;
Closed += MainWindow_Closed;
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
_blockMonitor = new MouseHookBlockStateMonitor();
_blockMonitor.BlockStateChanged += BlockMonitor_BlockStateChanged;
UpdateBanner(_blockMonitor.IsBlocked);
}
private void MainWindow_Closed(object? sender, EventArgs e)
{
if (_blockMonitor != null)
{
_blockMonitor.BlockStateChanged -= BlockMonitor_BlockStateChanged;
_blockMonitor.Dispose();
_blockMonitor = null;
}
}
private void BlockMonitor_BlockStateChanged(object? sender, bool isBlocked)
{
// UIスレッド上で更新
Dispatcher.Invoke(() => UpdateBanner(isBlocked));
}
private void UpdateBanner(bool isBlocked)
{
if (isBlocked)
{
BannerTextBlock.Text = "前景ウィンドウが管理者権限のため、マウス追跡を一時停止しています。";
BannerBorder.Visibility = Visibility.Visible;
}
else
{
BannerBorder.Visibility = Visibility.Collapsed;
}
}
}
ここで、XAML側に簡単なバナーを用意しておくとユーザーにとって分かりやすいUIになります。
<Border x:Name="BannerBorder"
Background="Orange"
Visibility="Collapsed"
Padding="8"
HorizontalAlignment="Stretch">
<TextBlock x:Name="BannerTextBlock"
Foreground="White"
FontWeight="Bold" />
</Border>
これだけで、「タスク マネージャーを前面にした瞬間にバナーが出る」「通常のウィンドウに戻すとバナーが消える」といった、ユーザーにとって直感的な挙動を実現できます。
VB.NETで実装する場合のポイント
VB.NETでも基本の考え方とWin32 APIはまったく同じです。宣言の書き方だけが少し変わります。たとえば、WinEventのコールバックとフック登録部分は次のように記述できます。
Private Delegate Sub WinEventDelegate(
ByVal hWinEventHook As IntPtr,
ByVal eventType As UInteger,
ByVal hwnd As IntPtr,
ByVal idObject As Integer,
ByVal idChild As Integer,
ByVal dwEventThread As UInteger,
ByVal dwmsEventTime As UInteger)
<DllImport("user32.dll")>
Private Shared Function SetWinEventHook(
ByVal eventMin As UInteger,
ByVal eventMax As UInteger,
ByVal hmodWinEventProc As IntPtr,
ByVal lpfnWinEventProc As WinEventDelegate,
ByVal idProcess As UInteger,
ByVal idThread As UInteger,
ByVal dwFlags As UInteger) As IntPtr
End Function
VB.NETではデリゲートインスタンスがGCされやすいので、C#と同様にクラスのフィールドに保持しておく必要があります。また、構造体のマーシャリングはC#のものをそのまま翻訳しづらいため、最初はC#で実装してからVBに移植するか、あるいは整合性レベル取得部分だけをC#クラスライブラリとして切り出してVBから参照する、という構成にすると安全です。
| 言語 | おすすめ構成 | メリット |
|---|---|---|
| C#のみ | WPFアプリ+Win32 APIラッパーをすべてC#で実装 | 情報量が多く、ネット上のサンプルをそのまま流用しやすい |
| VB.NETのみ | すべてVBで宣言・実装 | C#に慣れていなくても1プロジェクトで完結できる |
| C#+VB混在 | Win32ラッパーをC#クラスライブラリに切り出し、VBから参照 | 危険なマーシャリング部分をC#側に閉じ込められる |
なぜスレッド情報や例外監視ではダメなのか
インターネット上には、「フックコールバックが一定時間呼ばれなかったらブロックとみなす」「例外が発生したらブロックとみなす」といったタイマー依存の判定方法も見られます。しかし、これらは次の理由から誤検知のリスクが高く非推奨です。
- ユーザーが単にマウスを動かしていないだけでも「ブロック」と誤判定しうる。
- アプリ側の負荷やGC、他の処理による一時停止でしきい値を超えることがある。
- OSアップデートや環境差によって例外の出方が変わる可能性がある。
対して、本記事で紹介した方法は、UIPIが実際に参照している整合性レベルそのものを見ているため、OS側の設計ときわめて整合的です。特定の環境や負荷に依存しない、再現性の高い判定ができる点が大きなメリットです。
実運用での工夫ポイント
最後に、低レベルマウスフックのブロック検出をアプリに組み込む際の工夫ポイントをいくつか挙げます。
ユーザーに正直に伝えるUI
- バナーやステータスバーに「管理者権限のウィンドウ上ではマウス追跡が行えません」と明示する。
- ログに前景プロセス名・整合性レベル・ブロック状態を記録しておくと、問い合わせ対応が楽になります。
- 必要であれば、ヘルプページやツールチップで「アプリを管理者として実行すると改善する場合があります」と案内する。
整合性レベルがUnknownだった場合の扱い
GetTokenInformationに失敗したり、特殊な環境でSID解析がうまくいかなかった場合、整合性レベルをUnknownとして扱うかどうかを決める必要があります。
- セキュリティや誤取得を警戒するなら「UnknownはHigh相当(ブロック中)」とみなす。
- ユーザー体験を優先するなら「Unknownでもとりあえず許可」とする。
業務アプリであれば、まずはログに吐きつつ安全側に倒す実装(=ブロック扱い)をおすすめします。
低レベルフック自体の安定性にも配慮する
今回のテーマは「ブロック検出」ですが、低レベルフック側の安定性も同じくらい重要です。代表的な注意点を簡単にまとめておきます。
| ポイント | 内容 |
|---|---|
| デリゲートの寿命 | SetWindowsHookExに渡すデリゲートはフィールドで保持し、GCされないようにする。 |
| 処理時間 | フックコールバック内で重い処理を行わず、キューイングや非同期処理に逃がす。 |
| 多重フック | 同じプロセスで複数回SetWindowsHookExを呼ばないように管理する。 |
| 例外処理 | フックコールバック内はtry-catchで完全に囲み、例外を外に漏らさない。 |
これらを守ることで、「ブロックとは関係ない落ち方」を大幅に減らすことができます。
まとめ:権限を上げずに「ブロック状態」を見抜く
本記事では、C#/VB.NETのWPFアプリなどで低レベルマウスフック(WH_MOUSE_LL)を使う際に、「タスク マネージャーなどの高整合性プロセス上ではフックコールバックが飛んでこない」という問題に対して、権限を上げずにブロック状態を検出する方法を紹介しました。
- やりたいこと:通常権限のまま、「いまフックが効いていない状態かどうか」を知りたい。
- 解決策:
SetWinEventHookで前景ウィンドウの変化(EVENT_SYSTEM_FOREGROUND)を監視し、GetTokenInformation(TokenIntegrityLevel)で整合性レベルを取得して判定する。 - 判定ロジック:
Mediumより高い(High以上)の整合性レベルなら「フックはブロック中」とみなす。 - 理由:UIPIは整合性レベルを基準に動作しており、そこを見ればOSの挙動と整合した判断ができるため。
このアプローチを組み込むことで、フックの限界をユーザーに隠すのではなく、「OSのセキュリティ仕様としてこういう制限がある」という事実を反映したUXを提供できます。低レベルマウスフックを多用するツールを開発している場合は、ぜひ「ブロック状態の検出」と「その時の振る舞い」をセットで設計に取り込んでみてください。

コメント