環境
Windows10 Pro 64bit 22H2
Visual Studio 2022 Community Edition
.NET Framework 4.6(Windows10にプレインストールされているランタイムで動作するようにするため)
Administratorグループに所属していない一般ユーザでWindowsにログインしており、デバッグの際もそのユーザでログインしている。そのため、管理者権限に昇格する場合はAdministratorグループに入っている別のユーザのユーザー名とパスワードを入力する必要がある点を参考情報として記載します。
背景
アップデーターを作成するためにかじった知識です。
「Program Files」などの管理者権限を必要とするフォルダにインストールされているアプリをアップデートする場合、一般ユーザ権限のアプリから管理者権限を付けてアップデーターを立ち上げる場合があります。逆に、アップデートを終了する場合は、管理者権限で立ち上げたアップデーターから管理者権限のない状態でアップデート対象のアプリを再起動する場合があります。
管理者権限の上げ下げが必要ない場合は問題ありません。
管理者権限を付けて他のアプリを起動する場合も特に問題ありません。
問題は管理者権限を持ったアプリから管理者権限を降格させた状態で他のアプリを起動するときです。
管理者権限の降格を「Process.Start」と「RunAs.exe」で行った場合、アプリを起動したユーザーはログインユーザーではなく、管理者権限を制限された管理者ユーザーになり、ログインユーザではなくなります。
そしてその問題を本家Microsoftが言及したのです。
現在ログインしている私のユーザーフォルダは他のユーザがアクセスできない設定にしていたため、管理者権限を制限された管理者ユーザーの権限で起動されたアプリは、私のユーザーフォルダにアクセスできないという問題に実際に直面した。それではさすがに不便です。
今回はこのMicrosoftのサイトが資料になります。
権限の操作を必要としない場合
private bool PrivilegeProcessStartWithBase(string szApp, string szCmdLine, string szCurrDir) {
try{
bool ret = false;
Process fret = null;
ProcessStartInfo app = new ProcessStartInfo();
app.FileName = szApp;
app.Arguments = szCmdLine;
app.Verb = "";
app.CreateNoWindow = true;
app.UseShellExecute = false;
fret = Process.Start(app);
ret = (fret != null);
} catch (Exception ex) {
throw;
}
return ret;
}
- 実行ファイル名や引数を囲うための「"」や、ファイルパスに使われる「\」は、「\"」や「\\」にエスケープする。
- CUIのプロジェクトでない限り、通常は「ProcessStartInfo.CreateNoWindow」は「true」にする。
- Verbオプションや特別なシェルの操作をしない限り、通常は「ProcessStartInfo.UseShellExecute」は「false」にする。
といった点を考慮すれば問題ありません。
一般権限のアプリから他のアプリを管理者権限に昇格して起動する
private bool PrivilegeProcessStartWithUtoA(string szApp, string szCmdLine, string szCurrDir) {
try{
bool ret = false;
Process fret = null;
ProcessStartInfo app = new ProcessStartInfo();
app.FileName = szApp;
app.Arguments = szCmdLine;
app.Verb = "RunAs";
app.CreateNoWindow = true;
app.UseShellExecute = true;
fret = Process.Start(app);
ret = (fret != null);
} catch (Win32Exception ex) {
// ユーザーが [いいえ] を選択すると例外が発生します。
Console.WriteLine(ex.Message);
} catch (Exception ex) {
// それ以外のエラー
throw;
}
return ret;
}
権限操作が無いときと同じく
- 実行ファイル名や引数を囲うための「"」や、ファイルパスに使われる「\」は、「\"」や「\\」にエスケープする。
- CUIのプロジェクトでない限り、通常は「ProcessStartInfo.CreateNoWindow」は「true」にする。
といった点に加えて、
- (理由は分かりませんが、)「ProcessStartInfo.UseShellExecute」を「true」にしなければ管理者権限で起動しない。
- 「ProcessStartInfo.Verb」には「"RunAs"」を入れる必要がある。
- MSDNの「ユーザー アカウント制御」のガイドラインに「ユーザーが昇格しないことを選択したため、タスクが失敗したときにエラー メッセージを表示しないでください。」とあるため、それに従ってWin32Exceptionの例外をキャッチした場合はエラーメッセージを表示しないようにしている。(ガイドラインはWindowsVistaやWindows7を想定した作りで、Windows10を想定したものではないらしい。)
といった点を考慮すれば問題ありません。
管理者権限のアプリから他のアプリを一般権限に降格して起動する
問題の箇所です。
今回はこのMicrosoftのサイトを基に説明します。
そのサイトにはサンプルプロジェクトがリンクされており、そのプロジェクト内のソースを基に作っているのですが、リンク切れなので、こちらにリンクを載せます。
サンプルプロジェクトのダウンロード:RunAsDesktopUser.zip
ただしサンプルプロジェクトはC++での手順ですので、C#ではこれらの手順をp/invokeしていくことになります。
サイトによると、一般権限で起動する手順は
- 現在のトークンで SeIncreaseQuotaPrivilege を有効にします
- デスクトップ シェルを表す HWND を取得します ( GetShellWindow )
- そのウィンドウに関連付けられたプロセスのプロセス ID (PID) を取得します ( GetWindowThreadProcessId )
- そのプロセスを開く ( OpenProcess )
- そのプロセスからアクセス トークンを取得します ( OpenProcessToken )
- そのトークンでプライマリ トークンを作成します ( DuplicateTokenEx )
- そのプライマリ トークンを使用して新しいプロセスを開始します ( CreateProcessWithTokenW )
とあります。
要するにどういうことかというと、
アプリが管理者権限で起動した場合、アプリ側からだけだと管理者ユーザーの情報しか取得できない。それじゃあエクスプローラーのプロセスを起動したユーザーの情報を取得して、そのユーザーの権限で他のアプリを起動すれば、Windowsにログインしたユーザの権限で起動したのと実質同じと見なして良いんじゃないの?という発想と思われます。
Win32API関数とそれに関連する構造体や定数の宣言
まずはAPI関数と構造体の宣言を見ていきます。
[DllImport("kernel32.dll", SetLastError = true)]
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[SuppressUnmanagedCodeSecurity]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool CloseHandle(IntPtr hObject);
[DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr GetCurrentProcess();
[StructLayout(LayoutKind.Sequential)]
struct LUID {
public uint LowPart;
public uint HighPart;
}
[DllImport("advapi32", SetLastError = true, CharSet = CharSet.Unicode)]
static extern bool LookupPrivilegeValueW(
string lpSystemName,
string lpName,
ref LUID lpLuid
);
public const string SE_INCREASE_QUOTA_NAME = "SeIncreaseQuotaPrivilege";
[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct LUID_AND_ATTRIBUTES {
public LUID Luid;
public UInt32 Attributes;
}
public const int SE_PRIVILEGE_ENABLED = 2;
public const int ANYSIZE_ARRAY = 1;
struct TOKEN_PRIVILEGES {
public int PrivilegeCount;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = ANYSIZE_ARRAY)]
public LUID_AND_ATTRIBUTES[] Privileges;
}
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool AdjustTokenPrivileges(
IntPtr TokenHandle,
[MarshalAs(UnmanagedType.Bool)] bool DisableAllPrivileges,
ref TOKEN_PRIVILEGES NewState,
UInt32 BufferLengthInBytes,
ref TOKEN_PRIVILEGES PreviousState,
out UInt32 ReturnLengthInBytes
);
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool AdjustTokenPrivileges(
IntPtr TokenHandle,
[MarshalAs(UnmanagedType.Bool)] bool DisableAllPrivileges,
ref TOKEN_PRIVILEGES NewState,
UInt32 Zero,
IntPtr Null1,
IntPtr Null2
);
[DllImport("user32.dll", SetLastError = true)] static extern IntPtr GetShellWindow();
[DllImport("user32.dll", SetLastError = true)] static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr OpenProcess(
uint processAccess,
bool bInheritHandle,
uint processId
);
public const uint PROCESS_ALL_ACCESS = 0x001F0FFF;
public const uint PROCESS_TERMINATE = 0x00000001;
public const uint PROCESS_CREATE_THREAD = 0x00000002;
public const uint PROCESS_VM_OPERATION = 0x00000008;
public const uint PROCESS_VM_READ = 0x00000010;
public const uint PROCESS_VM_WRITE = 0x00000020;
public const uint PROCESS_DUP_HANDLE = 0x00000040;
public const uint PROCESS_CREATE_PROCESS = 0x000000080;
public const uint PROCESS_SET_QUOTA = 0x00000100;
public const uint PROCESS_SET_INFORMATION = 0x00000200;
public const uint PROCESS_QUERY_INFORMATION = 0x00000400;
public const uint PROCESS_SUSPEND_RESUME = 0x00000800;
public const uint PROCESS_QUERY_LIMITED_INFORMATION = 0x00001000;
public const uint DELETE = 0x00010000;
public const uint READ_CONTROL = 0x00020000;
public const uint WRITE_DAC = 0x00040000;
public const uint WRITE_OWNER = 0x00080000;
public const uint SYNCHRONIZE = 0x00100000;
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool OpenProcessToken(
IntPtr ProcessHandle,
UInt32 DesiredAccess,
out IntPtr TokenHandle
);
public const UInt32 STANDARD_RIGHTS_REQUIRED = 0x000F0000;
public const UInt32 STANDARD_RIGHTS_READ = 0x00020000;
public const UInt32 TOKEN_ASSIGN_PRIMARY = 0x0001;
public const UInt32 TOKEN_DUPLICATE = 0x0002;
public const UInt32 TOKEN_IMPERSONATE = 0x0004;
public const UInt32 TOKEN_QUERY = 0x0008;
public const UInt32 TOKEN_QUERY_SOURCE = 0x0010;
public const UInt32 TOKEN_ADJUST_PRIVILEGES = 0x0020;
public const UInt32 TOKEN_ADJUST_GROUPS = 0x0040;
public const UInt32 TOKEN_ADJUST_DEFAULT = 0x0080;
public const UInt32 TOKEN_ADJUST_SESSIONID = 0x0100;
public const UInt32 TOKEN_READ = (STANDARD_RIGHTS_READ | TOKEN_QUERY);
public const UInt32 TOKEN_ALL_ACCESS = (
STANDARD_RIGHTS_REQUIRED | TOKEN_ASSIGN_PRIMARY |
TOKEN_DUPLICATE | TOKEN_IMPERSONATE | TOKEN_QUERY | TOKEN_QUERY_SOURCE |
TOKEN_ADJUST_PRIVILEGES | TOKEN_ADJUST_GROUPS | TOKEN_ADJUST_DEFAULT |
TOKEN_ADJUST_SESSIONID
);
public enum TOKEN_TYPE {
TokenPrimary = 1,
TokenImpersonation
}
[StructLayout(LayoutKind.Sequential)]
public struct SECURITY_ATTRIBUTES {
public int nLength;
public IntPtr lpSecurityDescriptor;
public int bInheritHandle;
}
public enum SECURITY_IMPERSONATION_LEVEL {
SecurityAnonymous,
SecurityIdentification,
SecurityImpersonation,
SecurityDelegation
}
[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public extern static bool DuplicateTokenEx(
IntPtr hExistingToken,
uint dwDesiredAccess,
IntPtr lpTokenAttributes,
SECURITY_IMPERSONATION_LEVEL ImpersonationLevel,
TOKEN_TYPE TokenType,
out IntPtr phNewToken
);
[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public extern static bool DuplicateTokenEx(
IntPtr hExistingToken,
uint dwDesiredAccess,
ref SECURITY_ATTRIBUTES lpTokenAttributes,
SECURITY_IMPERSONATION_LEVEL ImpersonationLevel,
TOKEN_TYPE TokenType,
out IntPtr phNewToken
);
[StructLayout(LayoutKind.Sequential)]
public struct PROCESS_INFORMATION {
public IntPtr hProcess;
public IntPtr hThread;
public int dwProcessId;
public int dwThreadId;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct STARTUPINFO {
public Int32 cb;
public string lpReserved;
public string lpDesktop;
public string lpTitle;
public Int32 dwX;
public Int32 dwY;
public Int32 dwXSize;
public Int32 dwYSize;
public Int32 dwXCountChars;
public Int32 dwYCountChars;
public Int32 dwFillAttribute;
public Int32 dwFlags;
public Int16 wShowWindow;
public Int16 cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
public enum LogonFlags {
/// <summary>
/// ログオンし、HKEY_USERS レジストリ キーにユーザーのプロファイルを読み込みます。
/// プロファイルがロードされた後、関数は戻ります。
/// プロファイルの読み込みには時間がかかることがあるため、
/// HKEY_CURRENT_USER レジストリ キーの情報にアクセスする必要がある場合にのみ、
/// この値を使用することをお勧めします。
/// 注: Windows Server 2003: プロファイルは、子プロセスが作成されたかどうかに関係なく、
/// 新しいプロセスが終了した後にアンロードされます。
/// </summary>
/// <remarks>LOGON_WITH_PROFILE を参照してください</remarks>
WithProfile = 1,
/// <summary>
/// ログオンしますが、ネットワーク上でのみ指定された資格情報を使用します。
/// 新しいプロセスは呼び出し元と同じトークンを使用しますが、
/// システムは LSA 内に新しいログオン セッションを作成し、
/// プロセスは指定された資格情報を既定の資格情報として使用します。
/// この値を使用して、リモートとは異なる資格情報のセットをローカルで使用するプロセスを作成できます。
/// これは、信頼関係がないドメイン間のシナリオで役立ちます。
/// システムは、指定された資格情報を検証しません。,
/// したがって、プロセスは開始できますが、ネットワーク リソースにアクセスできない可能性があります。
/// </summary>
/// <remarks>LOGON_NETCREDENTIALS_ONLY を参照してください。</remarks>
NetCredentialsOnly
}
public enum CreationFlags {
DefaultErrorMode = 0x04000000,
NewConsole = 0x00000010,
NewProcessGroup = 0x00000200,
SeparateWOWVDM = 0x00000800,
Suspended = 0x00000004,
UnicodeEnvironment = 0x00000400,
ExtendedStartupInfoPresent = 0x00080000
}
[DllImport("advapi32", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool CreateProcessWithTokenW(
IntPtr hToken,
LogonFlags dwLogonFlags,
string lpApplicationName,
string lpCommandLine,
CreationFlags dwCreationFlags,
IntPtr lpEnvironment,
string lpCurrentDirectory,
[In] ref STARTUPINFO lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation
);
処理本体
private bool PrivilegeProcessStartWithAtoU(string szApp, string szCmdLine, string szCurrDir) {
bool fret = false;
bool ret = false;
IntPtr hProcessToken = IntPtr.Zero, hShellProcess = IntPtr.Zero, hShellProcessToken = IntPtr.Zero, hPrimaryToken = IntPtr.Zero;
IntPtr hwnd = IntPtr.Zero;
uint dwPID = 0;
int dwLastErr;
STARTUPINFO si=new STARTUPINFO();
PROCESS_INFORMATION pi=new PROCESS_INFORMATION();
try {
// このプロセスで SeIncreaseQuotaPrivilege を有効にします。 (現在のプロセスが昇格されていない場合、これは機能しません。)
fret = OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, out hProcessToken);
dwLastErr = Marshal.GetLastWin32Error();
if (!fret) {
throw (new Win32Exception(dwLastErr));
}
TOKEN_PRIVILEGES tkp;
tkp.PrivilegeCount = 1;
tkp.Privileges = new LUID_AND_ATTRIBUTES[1];
fret = LookupPrivilegeValueW(null, SE_INCREASE_QUOTA_NAME, ref tkp.Privileges[0].Luid);
dwLastErr = Marshal.GetLastWin32Error();
if (!fret) {
throw (new Win32Exception(dwLastErr));
}
tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
fret = AdjustTokenPrivileges(hProcessToken, false, ref tkp, 0, IntPtr.Zero, IntPtr.Zero);
dwLastErr = Marshal.GetLastWin32Error();
if (!fret) {
throw (new Win32Exception(dwLastErr));
}
CloseHandle(hProcessToken);
hProcessToken = IntPtr.Zero;
// デスクトップ シェルを表す HWND を取得します。
// 警告: シェルが実行されていない (クラッシュまたは終了している) 場合、
// またはデフォルトのシェルがカスタム シェルに置き換えられている場合、これは失敗します。
// また、エクスプローラーが終了し、管理者特権で再起動された場合、おそらく必要なものは返されません。
hwnd = GetShellWindow();
dwLastErr = Marshal.GetLastWin32Error();
if (IntPtr.Zero == hwnd) {
throw (new Win32Exception(dwLastErr));
}
// デスクトップシェルプロセスのPIDを取得します。
GetWindowThreadProcessId(hwnd, out dwPID);
dwLastErr = Marshal.GetLastWin32Error();
if (0 == dwPID) {
throw (new Win32Exception(dwLastErr));
}
// デスクトップシェルプロセスを開いてクエリを実行します(トークンを取得します)
hShellProcess = OpenProcess(PROCESS_QUERY_INFORMATION, false, dwPID);
dwLastErr = Marshal.GetLastWin32Error();
if (hShellProcess == IntPtr.Zero) {
throw (new Win32Exception(dwLastErr));
}
// この時点から、閉じるハンドルがあるので、必ずクリーンアップしてください。
// デスクトップシェルのプロセストークンを取得します。
fret = OpenProcessToken(hShellProcess, TOKEN_DUPLICATE, out hShellProcessToken);
dwLastErr = Marshal.GetLastWin32Error();
if (!fret) {
throw (new Win32Exception(dwLastErr));
}
// シェルのプロセス トークンを複製して、プライマリ トークンを取得します。
// 実験に基づくと、これは CreateProcessWithTokenW に必要な最小限の権限セットです (現在のドキュメントとは異なります)。
const uint dwTokenRights = TOKEN_QUERY | TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID;
fret = DuplicateTokenEx(
hShellProcessToken, dwTokenRights, IntPtr.Zero,
SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation,
TOKEN_TYPE.TokenPrimary, out hPrimaryToken
);
dwLastErr = Marshal.GetLastWin32Error();
if (!fret) {
throw (new Win32Exception(dwLastErr));
}
si.cb = 96;
// 新しいトークンでターゲット プロセスを開始します。
fret = CreateProcessWithTokenW(
hPrimaryToken,
0,
szApp,
szCmdLine,
0,
IntPtr.Zero,
szCurrDir,
ref si,
out pi
);
dwLastErr = Marshal.GetLastWin32Error();
if (!fret) {
throw (new Win32Exception(dwLastErr));
}
ret = fret;
} catch {
throw;
} finally {
//クリーンアップ:
// リソースをクリーンアップする
if (hProcessToken != IntPtr.Zero) { CloseHandle(hProcessToken); }
if (hPrimaryToken != IntPtr.Zero) { CloseHandle(hPrimaryToken); }
if (hShellProcessToken != IntPtr.Zero) { CloseHandle(hShellProcessToken); }
if (hShellProcess != IntPtr.Zero) { CloseHandle(hShellProcess); }
if (si.hStdError != IntPtr.Zero) { CloseHandle(si.hStdError); }
if (si.hStdInput != IntPtr.Zero) { CloseHandle(si.hStdInput); }
if (si.hStdOutput != IntPtr.Zero) { CloseHandle(si.hStdOutput); }
if (pi.hProcess != IntPtr.Zero) { CloseHandle(pi.hProcess); }
if (pi.hThread != IntPtr.Zero) { CloseHandle(pi.hThread); }
}
return ret;
}
サンプルのソースのままの記述が大半なので、綺麗なソースではないかもしれません。(サンプルのソースにはgoto分が使われている箇所さえあった。)
コメントはサンプルのコメントを翻訳しただけのものです。
処理本体の説明
順に説明します。
Win32ExceptionのコンストラクタにGetLastError関数または「Marshal.GetLastWin32Error」のエラーコードを引数として渡すと、FormatMessage関数で取得する文字列が「Win32Exception.Message」に入るようです。つまりエラーコードをWin32Exceptionに渡せばエラーメッセージは自動で作成されるということです。なのでWin32API関数が関与する大半のエラー処理の箇所でWin32Exceptionをthrowしています。
IntPtr hProcessToken = IntPtr.Zero;
fret = OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, out hProcessToken);
dwLastErr = Marshal.GetLastWin32Error();
if (!fret) {
throw (new Win32Exception(dwLastErr));
}
一番最初のOpenProcessTokenは、アクセストークンの特権を扱える状態で自プロセスのトークンを取得する処理と思われます。
TOKEN_ADJUST_PRIVILEGES
アクセス トークンの特権を有効または無効にするために必要です。
引用元:MSDNの「Access-Token オブジェクトのアクセス権」
TOKEN_PRIVILEGES tkp;
tkp.PrivilegeCount = 1;
tkp.Privileges = new LUID_AND_ATTRIBUTES[1];
fret = LookupPrivilegeValueW(null, SE_INCREASE_QUOTA_NAME, ref tkp.Privileges[0].Luid);
dwLastErr = Marshal.GetLastWin32Error();
if (!fret) {
throw (new Win32Exception(dwLastErr));
}
tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
OpenProcessTokenでアクセストークンの特権を扱える形で自プロセスのトークンを取得しましたが、あくまでアクセストークンの特権を扱えるようになっただけで、具体的にどの特権を扱うかはOpenProcessTokenの時点では決まっていません。アクセストークンの特権の操作はAdjustTokenPrivileges関数で行うことになりますが、そのためにはどの特権を操作するのかに関する情報を関数の引数に渡す必要があります。その関数の引数に渡す変数に値を入れて準備するためにLookupPrivilegeValueW関数が用いられていますが、どうやら要求する権限に対応する識別IDを取得する処理のようです。
要求している権限はMSDNによると
SE_INCREASE_QUOTA_NAME
プロセスに割り当てられたクォータを増やすには必要です。
ユーザー権限: プロセスのメモリ クォータを調整します。引用元:MSDNの「特権定数 (承認)」
とあります。恐らくアクセストークンを複製してプライマリトークンを作成する際に何らかの形でプロセスのメモリクォータの増減が必要になると思われますが、プロセスのメモリクォータを増やすこと、アクセストークンを複製してプライマリトークンを作成する処理とが、具体的にどう関係するのかを私は分かっていません。
実際にOpenProcessToken関数、LookupPrivilegeValueW関数、その後のAdjustTokenPrivileges関数をコメントアウトして処理を進めると、コメントアウトした箇所の次の、GetShellWindow関数、GetWindowThreadProcessId関数、OpenProcess関数は処理が進み、その次のOpenProcessToken関数で失敗し、何故かGetLastErrorは0が返ってくる。逆に言えばそれまでは何ら問題なく処理できたことを意味する。なのでSE_INCREASE_QUOTA_NAMEはアクセストークンを複製してプライマリトークンを作成する処理とに関係があるだろうというところまでは推察できるが、具体的な関係を私は分かっていない。
LookupPrivilegeValueW関数が失敗した場合はOpenProcessToken関数で取得したリソースの解放を忘れないようにする点は注意が必要です。(2023/04/07:finallyで一括開放するように修正した。)
fret = AdjustTokenPrivileges(hProcessToken, false, ref tkp, 0, IntPtr.Zero, IntPtr.Zero);
dwLastErr = Marshal.GetLastWin32Error();
if (!fret) {
throw (new Win32Exception(dwLastErr));
}
CloseHandle(hProcessToken);
hProcessToken = IntPtr.Zero;
AdjustTokenPrivileges関数はアクセストークンの特権を実際に操作する関数です。
また、実験してわかったのですが、自プロセスが管理者特権で起動されていないと、この部分で失敗するようです。そもそもこの記事でこの処理をする目的は、管理者権限を持っている状態から一般ユーザの情報を取得するためであって、自プロセスが一般権限の場合はそのまま他のプロセスを起動すればよいだけです。なので自プロセスが一般権限の場合は不要の処理と言えます。
AdjustTokenPrivileges関数の処理が終わったら、一番最初のOpenProcessToken関数で取得したリソースはいらないので解放します。ただし、現在リソースをfinallyで一括開放しているので、開放済みであることを識別できるよう、IntPtr.Zeroを代入します。
// デスクトップ シェルを表す HWND を取得します。
// 警告: シェルが実行されていない (クラッシュまたは終了している) 場合、
// またはデフォルトのシェルがカスタム シェルに置き換えられている場合、これは失敗します。
// また、エクスプローラーが終了し、管理者特権で再起動された場合、おそらく必要なものは返されません。
hwnd = GetShellWindow();
dwLastErr = Marshal.GetLastWin32Error();
if (IntPtr.Zero == hwnd) {
throw (new Win32Exception(dwLastErr));
}
GetShellWindow関数は、「explorer.exe」のプロセスのウインドウハンドルを取得する関数です。
// デスクトップシェルプロセスのPIDを取得します。
GetWindowThreadProcessId(hwnd, out dwPID);
dwLastErr = Marshal.GetLastWin32Error();
if (0 == dwPID) {
throw (new Win32Exception(dwLastErr));
}
エクスプローラーのウインドウハンドルからエクスプローラーのプロセスIDをGetWindowThreadProcessId関数で取得します。
// デスクトップシェルプロセスを開いてクエリを実行します(トークンを取得します)
hShellProcess = OpenProcess(PROCESS_QUERY_INFORMATION, false, dwPID);
dwLastErr = Marshal.GetLastWin32Error();
if (hShellProcess == IntPtr.Zero) {
throw (new Win32Exception(dwLastErr));
}
エクスプローラーのプロセスIDからエクスプローラーのプロセスハンドルをOpenProcess関数で取得します。
PROCESS_QUERY_INFORMATION
トークン、終了コード、優先度クラスなど、プロセスに関する特定の情報を取得するために必要です ( OpenProcessToken を参照)。
引用元:MSDNの「プロセスのセキュリティとアクセス権」
// デスクトップシェルのプロセストークンを取得します。
fret = OpenProcessToken(hShellProcess, TOKEN_DUPLICATE, out hShellProcessToken);
dwLastErr = Marshal.GetLastWin32Error();
if (!fret) {
throw (new Win32Exception(dwLastErr));
}
OpenProcessToken関数でエクスプローラーのプロセスハンドルからエクスプローラーのプロセストークンを取得します。
OpenProcessToken関数が失敗した場合、その前に取得したリソースを解放します。(2023/04/07:finallyで一括開放するように修正した。)
TOKEN_DUPLICATE
アクセス トークンを複製するために必要です。
引用元:MSDNの「Access-Token オブジェクトのアクセス権」
// シェルのプロセス トークンを複製して、プライマリ トークンを取得します。
// 実験に基づくと、これは CreateProcessWithTokenW に必要な最小限の権限セットです (現在のドキュメントとは異なります)。
const uint dwTokenRights = TOKEN_QUERY | TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID;
fret = DuplicateTokenEx(
hShellProcessToken, dwTokenRights, IntPtr.Zero,
SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation,
TOKEN_TYPE.TokenPrimary, out hPrimaryToken
);
dwLastErr = Marshal.GetLastWin32Error();
if (!fret) {
throw (new Win32Exception(dwLastErr));
}
OpenProcessToken関数で取得したトークンをDuplicateTokenEx関数で複製します。
DuplicateTokenEx関数が失敗した場合、その前に取得したリソースを解放します。(2023/04/07:finallyで一括開放するように修正した。)
DuplicateTokenEx関数の第二引数の個々のフラグの意味は、MSDNの「Access-Token オブジェクトのアクセス権」で各自で確認してください。
SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation
The server process can impersonate the client's security context on its local system. The server cannot impersonate the client on remote systems.
(Edgeの翻訳:サーバー プロセスは、ローカル システム上のクライアントのセキュリティ コンテキストを偽装できます。サーバーは、リモート システム上のクライアントを偽装できません。)引用元:MSDNの「SECURITY_IMPERSONATION_LEVEL enumeration (winnt.h)」
2023/2/24現在ではまだ日本語訳はされていない模様。
TOKEN_TYPE.TokenPrimary
新しいトークンは、CreateProcessAsUser 関数で使用できるプライマリ トークンです。
引用元:MSDNの「DuplicateTokenEx 関数 (securitybaseapi.h)」
si.cb = 96;
CreateProcessWithTokenW関数に渡すための構造体型変数の初期化です。Googleで検索をかけたところ、C#はデフォルトの状態で0埋めに近い状態になっているらしいので、ZeroMemoryに相当する処理は明示的に入れていません。
「si.cb」にはSTARTUPINFO構造体のサイズを入れる必要がありますが、unsafeを有効にしていない場合はsizeofが使えないため、手計算して手入力しています。
// 新しいトークンでターゲット プロセスを開始します。
fret = CreateProcessWithTokenW(
hPrimaryToken,
0,
szApp,
szCmdLine,
0,
IntPtr.Zero,
szCurrDir,
ref si,
out pi
);
dwLastErr = Marshal.GetLastWin32Error();
if (!fret) {
throw (new Win32Exception(dwLastErr));
}
CreateProcessWithTokenW関数で、複製したプライマリトークンを使って、エクスプローラを起動したユーザーの権限で他のアプリを起動しています。実行ファイルのパス、コマンドライン引数、カレントディレクトリはこのCreateProcessWithTokenW関数で指定します。
ret = fret;
} catch {
throw;
} finally {
//クリーンアップ:
// リソースをクリーンアップする
if (hProcessToken != IntPtr.Zero) { CloseHandle(hProcessToken); }
if (hPrimaryToken != IntPtr.Zero) { CloseHandle(hPrimaryToken); }
if (hShellProcessToken != IntPtr.Zero) { CloseHandle(hShellProcessToken); }
if (hShellProcess != IntPtr.Zero) { CloseHandle(hShellProcess); }
if (si.hStdError != IntPtr.Zero) { CloseHandle(si.hStdError); }
if (si.hStdInput != IntPtr.Zero) { CloseHandle(si.hStdInput); }
if (si.hStdOutput != IntPtr.Zero) { CloseHandle(si.hStdOutput); }
if (pi.hProcess != IntPtr.Zero) { CloseHandle(pi.hProcess); }
if (pi.hThread != IntPtr.Zero) { CloseHandle(pi.hThread); }
}
リソースはfinallyで一括解放します。tryの部分で解放したリソースにはIntPtr.Zeroを代入して、finallyの部分でNULLチェックを行えるようにしています。
戻り値は、一番最後の関数であるCreateProcessWithTokenW関数の戻り値です。
ユーザ関数を組み合わせる
その前に説明したユーザ関数のcatch内でthrowしたExceptionは、この関数のcatchにthrowされるようになります。
private bool PrivilegeProcessStart(string szApp, string szCmdLine, string szCurrDir, bool dircheck = true) {
bool ret = false;
try{
bool judge = false, admin = false;
//自身の権限情報を取得
WindowsIdentity ident = WindowsIdentity.GetCurrent();
WindowsPrincipal princ = new WindowsPrincipal(ident);
admin = princ.IsInRole(WindowsBuiltInRole.Administrator);
judge = dircheck ? (!WriteJudge(szCurrDir)) : false;
//判定
if ((!judge) && admin) {
//一般権限への降格が必要
ret = PrivilegeProcessStartWithAtoU(szApp, szCmdLine, szCurrDir);
} else if (judge && (!admin)) {
//管理者権限への昇格が必要
ret = PrivilegeProcessStartWithUtoA(szApp, szCmdLine, szCurrDir);
} else {
//そのまま起動
ret = PrivilegeProcessStartWithBase(szApp, szCmdLine, szCurrDir);
}
} catch (Exception ex) {
MessageBox.Show(
ex.ToString(),"エラー",
MessageBoxButtons.OK, MessageBoxIcon.Error
);
}
return ret;
}
これは、要するにインストールフォルダの書き込み権限を確認し、管理者権限が不要であれば一般権限で他のアプリ起動し、管理者権限が必要であれば管理者権限を付けて他のアプリを起動する関数です。
また、コード内にある「WriteJudge」関数は、私が書いた「フォルダの書き込みに管理者権限が不要かどうかをC#で判定する」の記事で作成したユーザ関数ですので、「WriteJudge」関数の中身を知りたければその記事を参照ください。
順に説明します。
private bool PrivilegeProcessStart(string szApp, string szCmdLine, string szCurrDir, bool dircheck = true) {
先ず引数から、
- szApp:実行ファイルのパス
- szCmdLine:コマンドライン引数
- szCurrDir:カレントディレクトリを渡す部分だが、私はここにインストールディレクトリを指定する。
あれ?実行ファイルのパスを引数に渡しているのに、なぜインストールディレクトリが必要なのでしょうか? 実行ファイルはインストールディレクトリの直下にあるとは限りません。場合によっては「インストールディレクトリ/bin/実行ファイル」といった具合に、インストールディレクトリ直下よりも下の階層に実行ファイルがある場合があるので、インストールディレクトリは別に指定しています。 - dircheck:カレントディレクトリの権限のチェックをするかどうかを表すフラグ。falseであれば問答無用で一般権限で起動し、trueであればディレクトリの権限に合わせます。何故この変数を設けたのか?アップデートが終了した場合、アップデート対象のソフトを再起動するときは、一般権限で起動することになります。それはたとえ管理者権限を必要とするフォルダにインストールされたソフトであっても一緒です。ですので一番最後にアップデート対象のソフトを再起動するときのために、問答無用で一般権限で起動するオプションを付けました。
//自身の権限情報を取得
WindowsIdentity ident = WindowsIdentity.GetCurrent();
WindowsPrincipal princ = new WindowsPrincipal(ident);
admin = princ.IsInRole(WindowsBuiltInRole.Administrator);
ここでは、起動したユーザが管理者権限を持っているかどうかを確認しています。
judge = dircheck ? (!WriteJudge(szCurrDir)) : false;
私が書いた「フォルダの書き込みに管理者権限が不要かどうかをC#で判定する」の記事で作成したユーザ関数は、元々は管理者権限が不要であればtrue、必要であればfalseを返す関数ですが、WindowsPrincipal.IsInRole(WindowsBuiltInRole.Administrator)と比較しやすくするために値を反転させています。
インストールディレクトリの書き込みに管理者権限が必要かどうかをチェックしています。ただしチェックを飛ばすオプションがついていれば、管理者権限を不要と見なしています。
//判定
if ((!judge) && admin) {
//一般権限への降格が必要
ret = PrivilegeProcessStartWithAtoU(szApp, szCmdLine, szCurrDir);
} else if (judge && (!admin)) {
//管理者権限への昇格が必要
ret = PrivilegeProcessStartWithUtoA(szApp, szCmdLine, szCurrDir);
} else {
//そのまま起動
ret = PrivilegeProcessStartWithBase(szApp, szCmdLine, szCurrDir);
}
return ret;
- 一般権限での起動が必要で、現在のプロセスが管理者権限の場合:一般権限に降格させて起動
- 管理者権限での起動が必要で、現在のプロセスが一般権限の場合:管理者権限に昇格させて起動
- それ以外:そのまま起動
という場合分けを行って、この記事で説明したユーザー関数に処理を渡している部分です。
使用場面
アップデート処理は、他のアプリを立ち上げた後に自アプリを閉じるという処理を繰り返します。それはなぜかと言うと、実行ファイルが起動している間はその実行ファイルを更新できないからです。なのでアップデータを起動するとともに、アップデート対象のソフトは確実に閉じる必要があります。
閉じる処理の前に起動処理が関係するところから、使用する場面は限られます。
つまり、バージョン確認後またはアップデート終了後に起動処理関数を用い、その後Form.Close()で閉じるという方法に限られると思われます。