trsing’s diary

勉強、読んだ本、仕事で調べたこととかのメモ。

WindowsでサービスからGUIを立ち上げる

下記リンクを辿れば必要なことはだいたい記述されている。

stackoverflow.com

なので以下メモ書き。

前提知識

セッションに関する知識があれば何をやっているか理解しやすい。 次の資料がわかりやすかった。

www.mbsd.jp

ざっくりと書くと

  • サービスにはSessionId0が割り当てられる
  • ユーザごとに個別のSessionId(1以上)が割り当てられる1
  • SessionはWindowStationを含み、WindowStationはDesktopを含む
  • ユーザが普段目にするのは自分のセッションのDesktop

説明

https://stackoverflow.com/a/24122826 であげられているソースについて

サービスからGUIを立ち上げるにはログオンしているユーザのセッションのdesktopに対してプロセスを起動できれば良い。

実際、このコードでやってるのは

  1. ログオンユーザのセッションIDを取得
  2. 取得したセッションIDからトークンを取得
  3. WindowStationとDesktopを指定してプロセスをスタート

ちょいと細かく書くと

  • l.174~l.195:アクティブユーザのsessionを取得
    WTSEnumerateSessions:Remote Desktop Session Host server上のsessionリストを取得

  • l.197:user token取得
    WTSQueryUserToken:session IDからprimary access tokenを取得

  • l.200:user tokenを複製2
    DuplicateTokenEx:tokenを複製する。primary token/impersonation tokenを作ることができる。

  • l.229:デフォルトデスクトップを表示先に指定
    startInfo.lpDesktop = "winsta0\\default";

  • l.231:ユーザの環境変数を取得
    CreateEnvironmentBlock:指定したユーザの環境変数を取得

  • l.236:指定したトークンでプロセスを立ち上げ
    CreateProcessAsUser:tokenで指定されたユーザのsecurity contextでプロセスを立ち上げる。

なおこのサービスはLocalSystem accountで動かすことが前提のもよう。

WTSQueryUserToken の説明

To call this function successfully, the calling application must be running within the context of the LocalSystem account and have the SE_TCB_NAME privilege.

https://docs.microsoft.com/ja-jp/windows/win32/api/wtsapi32/nf-wtsapi32-wtsqueryusertoken

LocalSystem account以外でサービスを実行する場合

やることはたいして変わらない

  1. サービスを実行するユーザーにCreateProcessAsUserを実行するための権限を与える3
  2. ログオンユーザのプロセスを取得
  3. プロセスからトークンを取得
  4. WindowStationとDesktopを指定してプロセスをスタート

サービスを実行するユーザーとログオンユーザが同じ場合

動作環境

Windows 10 Pro、バージョン20H2、OSビルド19042.867
.Net Framework 4.7.2

ソースコード

GetSessionUserTokenの部分を次のようにすれば良い4

    foreach (System.Diagnostics.Process p in System.Diagnostics.Process.GetProcesses())
    {
        if (p.ProcessName.Equals("explorer"))
            explorerHandle = p.Handle;
    }
    if (OpenProcessToken(explorerHandle, TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY, out var hImpersonationToken))
    {
        var bResult = DuplicateTokenEx(hImpersonationToken, 0, IntPtr.Zero,
                (int)SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, (int)TOKEN_TYPE.TokenPrimary,
                ref phUserToken);
        CloseHandle(hImpersonationToken);
    }

stackoverflowには

The problem with Shrike's answer is that it does not work with a user connected over RDP.

とあるが手元の環境ではリモートデスクトップ経由で問題なく動作した。Windows10だとうまくいく?ユーザー一つしかないから?

  • remote desktop経由でセッションを確認
C:\Users\******>query session
 セッション名      ユーザー名               ID  状態    種類        デバイス
 services                                  0  Disc
>rdp-tcp#1         ******                 14  Active
 console                                  15  Conn
  • ローカルでセッションを確認
C:\Users\******>query session
 セッション名      ユーザー名               ID  状態    種類        デバイス
 services                                  0  Disc
>console           ******                 14  Active

サービスを実行するユーザーとログオンユーザが異なる場合

上記コードではうまくいかなかった。権限周りでの設定がなんやかんやいりそう。


  1. XP以前は最初にログオンしたユーザにSessionId0が割り当てられた。サービスとユーザを同じセッションにするとセキュリティ上問題があるのでVista以降は分けられたとか。調べてみると面白い。

  2. WTSQueryUserTokenでprimary access tokenが得られるので動かすだけなら不要。とはいえ複製しといた方が無難。

  3. ローカルセキュリティポリシーでローカルポリシー->ユーザー権利の割り当て->プロセスレベルトークンの置き換え。「プロセスレベルトークンの置き換え」の説明:このセキュリティ設定は、あるサービスから別のサービスを開始するために、CreateProcessAsUser() アプリケーション プログラミング インターフェイス (API) を呼び出すことができるユーザー アカウントを決定します。

  4. DuplicateTokenExはなくても動きはする。