WPF での UIPI 制限回避:ChangeWindowMessageFilterEx API の実装と利用

背景と課題

Windows NT 6.0 以降、セキュリティ強化の一環として「ユーザーインターフェース プライバシー 隔離(UIPI)」が導入されています。これにより、低い整合性レベル(Integrity Level)を持つプロセスは、高い整合性レベルを持つプロセスのウィンドウに対して、特定の Win32 メッセージを送信することが禁止されます。

具体的には、通常ユーザー権限で動作しているアプリケーションから、管理者権限で起動された WPF ウィンドウへ SentMessageWM_COPYDATA などのメッセージを送ろうとすると、受信側はメッセージを受信できなくなります。このプロトコル的な制限を解消し、プロセス間の円滑な通信を実現するためには、Win32 API である ChangeWindowMessageFilterEx を利用して、ターゲットウィンドウのメッセージフィルタを明示的に変更する必要があります。

ChangeWindowMessageFilterEx の仕組み

この API は、特定のウィンドウに対して、どのメッセージが許可されるかというフィルタリング設定を上書きすることを目的としています。プロセス全体の設定を変更する ChangeWindowMessageFilter と異なり、こちらは対象となるウィンドウハンドル(HWND)単位で制御が可能です。

主なパラメータは以下の通りです。

  • hwnd: メッセージフィルタを変更したいウィンドウのハンドル。
  • message: 許可または禁止したいメッセージ ID(例:WM_COPYDATA)。
  • action: 実行する操作。MSGFLT_ALLOW (許可)、MSGFLT_DISALLOW (拒否)、MSGFLT_RESET (初期化)の中から選択します。
  • pChangeFilterStruct: フィルタの状態情報を受けるための構造体ポインタ(オプション)。

特に重要な点は、 action パラメータに MSGFLT_ALLOW を指定することです。これにより、低特権プロセスからのメッセージでも透過的に受け取れるようになります。

実装手順

以下に、WPF プロジェクトにおいてこの機能を実装する具体的なコード例を示します。送信側と受信側の双方で必要な処理を行います。

1. 共通の Win32 ラッパー定義

まず、P/Invoke 呼び出しや構造体の定義をカプセル化する静的クラスを作成します。

using System;
using System.Runtime.InteropServices;

namespace IpcUtility
{
    public static class NativeApi
    {
        [DllImport("user32.dll", SetLastError = true)]
        internal static extern bool ChangeWindowMessageFilterEx(
            IntPtr hWnd, 
            uint msg, 
            FilterAction action, 
            ref ChangeFilterStruct filter);

        [StructLayout(LayoutKind.Sequential)]
        internal struct ChangeFilterStruct
        {
            public uint Size;
            public uint Status;
        }

        public enum FilterAction : uint
        {
            Reset = 0,
            Allow = 1,
            Disallow = 2
        }

        public const int WM_COPYDATA = 0x004A;
        
        [StructLayout(LayoutKind.Sequential)]
        public struct CopyDataStruct
        {
            public IntPtr UserData;
            public int DataLength;
            public IntPtr DataPointer;
        }
    }
}

2. 受信側(高権限)ウィンドウの実装

メッセージを受信する側の WPF ウィンドウでは、ロード完了時に ChangeWindowMessageFilterEx を呼び出すことが必須となります。また、ウィンドウのプロシージャ(Hook)を設定し、 WM_COPYDATA をハンドリングします。

using System;
using System.Text;
using System.Windows;
using System.Windows.Interop;
using IpcUtility;

namespace HighPrivilegeApp
{
    public partial class MainWindow : Window
    {
        private HwndSource _source;

        public MainWindow()
        {
            InitializeComponent();
            Loaded += OnWindowLoaded;
        }

        private void OnWindowLoaded(object sender, RoutedEventArgs e)
        {
            // 表示用のコントロール
            var statusTextBlock = FindName("txtStatus") as System.Windows.Controls.TextBlock;
            
            _source = PresentationSource.FromVisual(this) as HwndSource;
            if (_source == null) return;

            var handle = _source.Handle;

            // 【重要】UIPI 制限を解除する設定
            EnableMessageFilter(handle, NativeApi.WM_COPYDATA, NativeApi.FilterAction.Allow);

            // メッセージフックの登録
            _source.AddHook(WndHandler);
        }

        private IntPtr WndHandler(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
        {
            if (msg == NativeApi.WM_COPYDATA)
            {
                // メッセージデータのパース
                NativeApi.CopyDataStruct cds = Marshal.PtrToStructure<NativeApi.CopyDataStruct>(lParam);
                
                string payload = "";
                try
                {
                    byte[] buffer = new byte[cds.DataLength];
                    Marshal.Copy(cds.DataPointer, buffer, 0, cds.DataLength);
                    payload = Encoding.Unicode.GetString(buffer);
                }
                catch (Exception ex)
                {
                    payload = $"Error: {ex.Message}";
                }

                // UI スレッドでの更新
                Dispatcher.Invoke(() =>
                {
                    var txt = FindName("txtStatus") as System.Windows.Controls.TextBlock;
                    txt.Text = $"受信: {payload}";
                });

                handled = true;
            }
            return IntPtr.Zero;
        }

        private static bool EnableMessageFilter(IntPtr targetHandle, uint messageId, NativeApi.FilterAction act)
        {
            var config = new NativeApi.ChangeFilterStruct
            {
                Size = (uint)Marshal.SizeOf(typeof(NativeApi.ChangeFilterStruct)),
                Status = 0
            };

            bool success = NativeApi.ChangeWindowMessageFilterEx(targetHandle, messageId, act, ref config);
            if (!success)
            {
                int err = Marshal.GetLastWin32Error();
                throw new System.ComponentModel.Win32Exception(err);
            }
            return success;
        }

        protected override void OnClosed(EventArgs e)
        {
            _source?.RemoveHook(WndHandler);
            base.OnClosed(e);
        }
    }
}

3. 送信側(低権限)クライアントの実装

送信側は標準的な SendMessage API を使用しますが、宛先のウィンドウ名を取得する必要があります。

using System;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows;
using IpcUtility;

namespace LowPrivilegeApp
{
    public partial class SenderWindow : Window
    {
        private const string TargetWindowTitle = "HighPrivilegeApp.MainWindow"; // 実際のタイトルに合わせる

        public SenderWindow()
        {
            InitializeComponent();
        }

        private async void OnSendButton_Click(object sender, RoutedEventArgs e)
        {
            // 対象ウィンドウの検索
            IntPtr targetHwnd = FindWindowByCaption(null, TargetWindowTitle);
            if (targetHwnd == IntPtr.Zero)
            {
                MessageBox.Show("受信側ウィンドウが見つかりません。", "エラー");
                return;
            }

            string messageToSend = "TestMessage_123";
            SendDataViaCopyData(targetHwnd, messageToSend);
        }

        private void SendDataViaCopyData(IntPtr destinationHwnd, string content)
        {
            byte[] dataBytes = Encoding.Unicode.GetBytes(content);

            NativeApi.CopyDataStruct cds = new NativeApi.CopyDataStruct
            {
                UserData = IntPtr.Zero,
                DataLength = dataBytes.Length,
                DataPointer = Marshal.AllocHGlobal(dataBytes.Length)
            };

            try
            {
                Marshal.Copy(dataBytes, 0, cds.DataPointer, dataBytes.Length);
                User32_SendMessage(destinationHwnd, NativeApi.WM_COPYDATA, IntPtr.Zero, ref cds);
            }
            finally
            {
                if (cds.DataPointer != IntPtr.Zero)
                {
                    Marshal.FreeHGlobal(cds.DataPointer);
                }
            }
        }

        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        private static extern IntPtr FindWindowByCaption(string lpClassName, string lpWindowName);

        [DllImport("user32.dll")]
        private static extern IntPtr User32_SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, ref NativeApi.CopyDataStruct lParam);
    }
}

動作確認のポイント

上記の実装により、以下の条件でデータ転送が可能になります。

  • 受信側: 管理者権限(High Integrity)で実行されている。
  • 送信側: 一般ユーザー権限(Medium Integrity)で実行されている。
  • 通信手段: WM_COPYDATA メッセージを利用。

受信側ウィンドウのコンストラクタや Loaded イベントで EnableMessageFilter が正しく呼ばれていない場合、依然としてメッセージは到達しないため、この部分が実装上の最も重要な箇所となります。

7月6日 18:15 投稿