.net

【UAC】管理者権限昇格時に共有フォルダにアクセスできない対策

管理者権限でアプリケーションを実行した際に、共有フォルダなどのネットワークパスへのアクセスができないという事象が発生した。
出た例外は↓

System.IO.IOException: ログオン失敗: ユーザー名を認識できないか、またはパスワードが間違っています。

状態としては、エクスプローラー上でネットワークパスにアクセスし、認証を通してある。
よってアプリケーションの実行者が、ログインしているWindowsアカウントなら問題なくアクセスできるはずだ。しかし例外となる。


原因

原因を探ってみることにした。
一般的に.Netで管理者権限を要する場合、app.manifestを作成し、↓のようにrequestedExecutionLevel=requireAdministratorにして対応する。

<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
  <assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
    <security>
      <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
        <!-- UAC マニフェスト オプション
             Windows のユーザー アカウント制御のレベルを変更するには、
             requestedExecutionLevel ノードを以下のいずれかで置換します。

        <requestedExecutionLevel  level="asInvoker" uiAccess="false" />
        <requestedExecutionLevel  level="requireAdministrator" uiAccess="false" />
        <requestedExecutionLevel  level="highestAvailable" uiAccess="false" />

            requestedExecutionLevel 要素を指定すると、ファイルおよびレジストリの仮想化が無効にされます。
            アプリケーションが下位互換性を保つためにこの仮想化を要求する場合、この要素を
            削除します。
        -->
        <requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
      </requestedPrivileges>
    </security>
  </trustInfo>

  :
  
</assembly>

上記設定後、アプリケーションを実行すると、UAC(ユーザーアカウント制御)の画面が表示され、[はい]を選択すると管理者権限で実行される、という流れだ。

どうやらこの UACで管理者権限昇格して実行したプロセスと、通常のプロセスでは異なるユーザーセッションとなり、通常のプロセスで接続しているネットワークのセッションが引き継がれないようだ。

くまった!!


対策方法

対策方法は一応↓の3つある。

  • UACを無効にする
  • レジストリの値を変更する
  • WindowsAPI(WNetAddConnection)を使用する

一応と書いたのは、最初の2つの方法は推奨できないからだ。
UACを無効にすることは他人に提供する手段ではないし、レジストリの値を変更することは、他のアプリで本来食い止めたいケースを食い止められなくなる。

本来あるべき姿は、認証されていない場合は、認証ダイアログを表示してユーザーに入力を促す であろう。
それを実現するのが、3つ目の「WindowsAPI(WNetAddConnection)を使用する」方法だ。
.Netでユーザー認証を行う仕組みは、標準では用意されておらず、WindowsAPIを呼んで認証を通すことになる。
エクスプローラーでもお馴染みの↓の認証ウィンドウだ。

Windowsセキュリティ ユーザー認証ダイアログ


サンプルソース

実際に、WindowsAPIを使った認証サンプルを書いてみる。
WNetAddConnection2で認証を通し、共有フォルダにあるmemo.txtの中身を表示する というサンプル

Form1.cs

using System;
using System.IO;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            //共有フォルダのパス
            const string NETWORK_DIR = @"\\192.168.0.100\share";
            try
            {
                // 接続を試みる
                WinAPIHelper.NetAddConnect(NETWORK_DIR);

                string readText = string.Empty;
                string path = Path.Combine(NETWORK_DIR, "memo.txt");
                using (StreamReader sr = new StreamReader(path))
                {
                    readText = sr.ReadToEnd();
                }
                MessageBox.Show(readText);
            }
            catch(Exception ex)
            {
                MessageBox.Show(ex.ToString());
            }
            finally
            {
                // 最後に接続を閉じる
                WinAPIHelper.NetCancelConnect(NETWORK_DIR);
            }
        }
    }
}

WinAPIHelper.cs

using System;
using System.Runtime.InteropServices;

namespace WindowsFormsApplication1
{
    class WinAPIHelper
    {
        private const int RESOURCETYPE_ANY = 0x0;
        private const int CONNECT_INTERACTIVE = 0x8;

        [StructLayout(LayoutKind.Sequential)]
        public struct NETRESOURCE
        {
            public int dwScope;
            public int dwType;
            public int dwDisplayType;
            public int dwUsage;
            [MarshalAs(UnmanagedType.LPWStr)]
            public string lpLocalName;
            [MarshalAs(UnmanagedType.LPWStr)]
            public string lpRemoteName;
            [MarshalAs(UnmanagedType.LPWStr)]
            public string lpConmment;
            [MarshalAs(UnmanagedType.LPWStr)]
            public string lpProvider;
        }

        [DllImport("mpr.dll", EntryPoint = "WNetCancelConnection2", CharSet = CharSet.Unicode)]
        public static extern int WNetCancelConnection2(string lpName,
                                                       int dwFlags,
                                                       bool tForce);

        [DllImport("mpr.dll", EntryPoint = "WNetAddConnection2", CharSet = CharSet.Unicode)]
        public static extern int WNetAddConnection2(ref NETRESOURCE lpNetResource,
                                                    string lpPassword,
                                                    string lpUserName,
                                                    int dwFlags);

        /// <summary>
        /// 切断する
        /// </summary>
        /// <param name="connect"></param>
        /// <returns></returns>
        public static void NetCancelConnect(string connect)
        {
            // 接続が残っていた場合はクリアする
            WNetCancelConnection2(connect, RESOURCETYPE_ANY, true);
        }

        /// <summary>
        /// 接続を試みる
        /// </summary>
        /// <param name="connect"></param>
        public static void NetAddConnect(string connect)
        {
            NETRESOURCE netResource = new NETRESOURCE()
            {
                dwScope = 0,
                dwType = RESOURCETYPE_ANY,
                dwDisplayType = 0,
                dwUsage = 0,
                lpLocalName = "",
                lpRemoteName = connect,
                lpProvider = ""
            };

            // 接続が残っていた場合に備えてクリアする
            NetCancelConnect(connect);
            // パスワード(第二引数)、ユーザー(第三引数)はnullとする
            int ret = WNetAddConnection2(ref netResource, null, null, CONNECT_INTERACTIVE);

            if (ret != 0)
                throw new Exception($"WNetAddConnection2 error. return value={ret}");
        }
    }
}

実行してみると、↓の認証ダイアログが表示さる。

Windowsセキュリティ ユーザー認証ダイアログ

接続できるユーザー、パスワードを入力する。

読取成功メッセージ

例外とならず、無事に共有フォルダのファイルにアクセスできた。
注意点として、NetCancelConnectは実行した後、数十秒~数分接続できる状態が残るようだ。その間は認証ダイアログが表示されなかった。

今日はココマデ!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です