2023/2/24:管理者権限で起動した場合でも一般権限で書き込みできるか判定できるよう修正
環境
Windows10 Pro 64bit 22H2
Visual Studio 2017 Community Edition
.NET Framework 4(Windows10にプレインストールされているランタイムで動作するようにするため)
Administratorグループに所属していない一般ユーザでWindowsにログインしており、デバッグの際もそのユーザでログインしている。そのため、管理者権限に昇格する場合はAdministratorグループに入っている別のユーザのユーザー名とパスワードを入力する必要がある点を参考情報として記載します。
背景
元々は自作ソフトを自動更新するためのアップデーターを作成するためにかじった知識だった。
インストーラを使って「Program Files」にインストールすると決まっていれば、管理者権限を昇格すべきかを判定せずに、問答無用で管理者権限を昇格すれば問題ないですが、
仮にインストーラを使った場合でも、UAC対策のためなのか、そのユーザでしか使わないためなのか、「C:\Users\ユーザー名\AppData\Local」という管理者権限が不要のフォルダにインストールする可能性も考えられます。
また、自作ソフトをフォルダに入れてZIP圧縮した状態のまま配布し、使う相手はZIPを解凍するだけで使える状態の場合、管理者権限は不要な場合が多い。
それらの管理者権限が不要なフォルダにあるアプリは、できるだけ管理者権限を要さないままアップデートしたい。
それで、インストールフォルダに対して管理者権限の昇格が最低限必要かどうかを判断したかったわけです。
古いPCのHDDを外部HDDケースに入れて繋ぐ場合など管理者権限があっても権限が足りない場合は、管理者権限があっても書き込めない形にはなりますが、今回の場合は既にインストールされているフォルダ内のアプリの自動更新の実装が最終目的なので、PCが壊れるといった異常事態が無い限りは昇格すれば書き込めるでしょうし、そもそも異常事態が発生した場面では別の対策をするはずなので自動更新処理の出番はないと考えます。
なので、自動更新を実装する場合
インストールフォルダが今ログインしているユーザの権限で書き込めるかどうかを確認し、
昇格せずに書き込めるのであればそのまま自動更新し、
昇格せずに書き込めないのであれば自動更新の前に昇格する
というアプローチをとろうと考えているので、そのための段階の一つとして、
引数で指定したフォルダに対して、
昇格せずに書き込める権限があるかないかを
判定する関数を作ろうと考えました。
2023/2/24追記
しかし、作った関数を使ってみてわかったのが、管理者権限で起動した状態で使った場合、本来一般権限で使用した場合にfalseが返ってくるはずだった場面でもtrueが返ってくる場合がある問題が生じたため、一部修正せざるを得なくなった。しかもその習性にはWIN32APIが必要になるため、p/Invokeをするための多量のAPI宣言が必要となります。
コードと解説
流れ
余りにも長いので、
- 判定処理本体
- トークン取得処理
- WIN32API宣言
の3つの部位に分けます
判定処理本体の流れ的は
- 現在のユーザの情報を取得(管理者権限の場合、ここでトークン処理が入る)
- ディレクトリのセキュリティ情報を取得
- セキュリティ情報をユーザ・グループ別にループ
- 現在のユーザーとセキュリティ情報でチェックしているグループとの所属判定、または、現在のユーザーとセキュリティ情報でチェックしているユーザとの同一判定
- 拒否判定のチェック
- 許可判定のチェック
になります。
トークン処理の流れは
- プロセスユーザーのメモリクォータの調整権限を取得 (OpenProcessToken、LookupPrivilegeValue、AdjustTokenPrivileges)
- デスクトップシェル(エクスプローラー)のウインドウハンドルを取得します (GetShellWindow)
- そのウィンドウハンドルに関連付けられたプロセスのプロセス ID (PID) を取得します (GetWindowThreadProcessId)
- そのプロセスを開く (OpenProcess)
- そのプロセスからアクセストークンを取得します (OpenProcessToken)
- そのトークンでプライマリトークンを作成します (DuplicateTokenEx)
- そのプライマリトークンを使用してWindowsIdentityオブジェクトを作成
になります。
資料
余りにも多いので、骨組みとなる資料だけを記載せます。
それ以外の資料は説明中に引用します。
- iPentecの「ディレクトリのアクセス権を取得する (C#プログラミング)」の「プログラム例:.NET Framework」
ディレクトリから権限情報を取得して一覧するコードですが、このコードを骨格としました。
しかし、このコードではグループ単位で出力されたものはグループ単位のままなので、グループに属しているかどうかの確認が必要である。
- DOBON.NETの「現在のユーザーが管理者か調べる」の「アプリケーションを管理者として実行しているか調べる」
このコードを改造して、グループに所属しているかどうかを確かめるコードに利用した。
- Get-Aclで取得したファイル・フォルダのアクセス権(FileSystemRights)が、数字表記される場合の変換スクリプト
MicrosoftのドキュメントにFileSystemRightsの値が2097152以上のビット値の説明が無いため、この記事から2097152以上の32ビット値のビット列の情報を得ました。ちなみに私がこのページから参照したビット列は
「(FileSystemRights)0x40000000」:Generic_write、つまり書き込み権限
「(FileSystemRights)0x10000000」:Generic_all、つまりフルコントロール
となります。
「トークン処理」の部分のコードの資料など、その他の資料は、余りにも多いため、説明の中で引用します。
判定処理
ソースコード
判定処理関数はtrueかfalseで返す形式にしています。
using System;
using System.IO;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Windows.Forms;
private bool WriteJudge(string pass) {
bool ret = false;
try {
//管理者権限かどうかを確認
WindowsIdentity wic = WindowsIdentity.GetCurrent();
WindowsPrincipal wpc = new WindowsPrincipal(wic);
admin = wpc.IsInRole(WindowsBuiltInRole.Administrator);
//現在のユーザーを表すWindowsIdentityオブジェクトを取得する
WindowsIdentity wi = admin ? GetExplorerIdentity() : WindowsIdentity.GetCurrent();
//現在のユーザーのWindowsPrincipalオブジェクトを作成する
WindowsPrincipal wp = new WindowsPrincipal(wi);
//ディレクトリのセキュリティオブジェクト取得
DirectorySecurity security = Directory.GetAccessControl(pass);
//ユーザ・グループ一覧からループ
foreach (FileSystemAccessRule rule in security.GetAccessRules(true, true, typeof(NTAccount))) {
//現在のユーザーがグループに所属しているかどうかをチェック
//(判定の対象がグループでなくても動作した。)
if (!(wp.IsInRole((rule.IdentityReference as NTAccount).Value))) {
//関係のないユーザを飛ばす
continue;
}
//許可判定
if (
(rule.AccessControlType == System.Security.AccessControl.AccessControlType.Allow) &&
(
((rule.FileSystemRights & System.Security.AccessControl.FileSystemRights.Write) == System.Security.AccessControl.FileSystemRights.Write) ||
((rule.FileSystemRights & System.Security.AccessControl.FileSystemRights.FullControl) == System.Security.AccessControl.FileSystemRights.FullControl) ||
((rule.FileSystemRights & (FileSystemRights)0x40000000) == (FileSystemRights)0x40000000) ||
((rule.FileSystemRights & (FileSystemRights)0x10000000) == (FileSystemRights)0x10000000)
)
) {
//許可されている
ret = true;
}
//拒否判定
if (
(rule.AccessControlType == System.Security.AccessControl.AccessControlType.Deny) &&
(
((rule.FileSystemRights & System.Security.AccessControl.FileSystemRights.Write) == System.Security.AccessControl.FileSystemRights.Write) ||
((rule.FileSystemRights & System.Security.AccessControl.FileSystemRights.FullControl) == System.Security.AccessControl.FileSystemRights.FullControl) ||
((rule.FileSystemRights & (FileSystemRights)0x40000000) == (FileSystemRights)0x40000000) ||
((rule.FileSystemRights & (FileSystemRights)0x10000000) == (FileSystemRights)0x10000000)
)
) {
//拒否されている
ret = false;
//ACL上では拒否が許可より優先度が高いので、拒否が出た時点でbreakした。
break;
}
}
} catch (Exception ex) {
MessageBox.Show(ex.ToString(), "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
return ret;
}
説明
順に説明します。
bool ret = false;
戻り値となる変数retは、ACLで許可判定が出た場合にtrueつまり管理者権限不要と見なし、 ACL判定で拒否または判定なしとなった場合にfalseつまり管理者権限を要すると見なしています。
ACL上では拒否も許可もない状態は許可されていない状態、つまり事実上の拒否判定と見なすので、管理者権限が無い状態で書き込めるのは許可判定が出た場合のみと考えられます。
戻り値となる変数retの初期値をfalseにしたのは、拒否も許可もなかった状態を初期値として表す意図があります。
次に、現在ログオンしているWindowsユーザに関する情報を取得します。
//管理者権限かどうかを確認
WindowsIdentity wic = WindowsIdentity.GetCurrent();
WindowsPrincipal wpc = new WindowsPrincipal(wic);
admin = wpc.IsInRole(WindowsBuiltInRole.Administrator);
//現在のユーザーを表すWindowsIdentityオブジェクトを取得する
WindowsIdentity wi = admin ? GetExplorerIdentity() : WindowsIdentity.GetCurrent();
「WindowsIdentity.GetCurrent()」で取得するユーザー情報は、自プロセスを起動したユーザーの情報なので、
「WindowsIdentity.GetCurrent()」で取得できる情報を場合分けすると、
- 一般権限で起動した場合:Windowsにログインしたユーザーの情報
- 管理者権限で起動した場合:管理者ユーザの情報(Windowsにログインしたユーザーの情報とは異なる場合がある。)
になります。つまり、管理者権限で起動したプロセスからはWindowsにログインしたユーザーの情報を得られることは保証できません。そのため管理者権限で起動した場合は、エクスプローラのプロセスからエクスプローラを起動したユーザの情報が入ったトークンを取得して、それをWindowsIdentityのコンストラクタに入れる必要がありますが、今回はその状態に持って行った「GetExplorerIdentity()」というユーザー関数で対応しています。このユーザー関数のコードは非常に長いので、この記事の「トークン処理」の部分で説明します。
それで、ここでは自プロセスが管理者権限で起動しているのかを確認し、
- 管理者権限で起動した場合:GetExplorerIdentity()。
- 一般権限で起動した場合:WindowsIdentity.GetCurrent()
という形で場合分けをしています。「GetExplorerIdentity()」ユーザー関数は管理者権限でなければ動作しないため、場合分けしています。
管理者権限で用いなければ、この部分は
//現在のユーザーのWindowsPrincipalオブジェクトを作成する
WindowsIdentity wi = WindowsIdentity.GetCurrent();
の一行だけで済んだのに…。
あとはWindowsIdentityの情報をWindowsPrincipalに渡します。
//現在のユーザーのWindowsPrincipalオブジェクトを作成する
WindowsPrincipal wp = new WindowsPrincipal(wi);
チェックしたいフォルダの権限情報を取得
//ディレクトリのセキュリティオブジェクト取得
DirectorySecurity security = Directory.GetAccessControl(pass);
次は、取得した権限情報から、フォルダに対するアクセス権限を取得するため、ユーザーやグループごとにループしています。
//ユーザ・グループ一覧からループ
foreach (FileSystemAccessRule rule in security.GetAccessRules(true, true, typeof(NTAccount))) {
もしかするとNTAccount型である必要はないのかもしれませんが、現時点では調べきれていません。
しかしこの情報からは、現在のログインユーザーがチェック対象のグループに含まれているかどうかまでは分かりませんので、今度はそれを行うための処理を行います。
//現在のユーザーがグループに所属しているかどうかをチェック
//(判定の対象がグループでなくても動作した。)
if (!(wp.IsInRole((rule.IdentityReference as NTAccount).Value))) {
//関係のないユーザを飛ばす
continue;
}
現在ログインしているユーザが「(rule.IdentityReference as NTAccount).Value」のグループまたはユーザに属しているかどうかをチェックしています。
私が動かしてみたところ、「(rule.IdentityReference as NTAccount).Value」がグループではなくユーザになっても判定してくれているので、おそらくユーザの場合は現在のウインドウズのログインユーザと「(rule.IdentityReference as NTAccount).Value」で表されるユーザが同一かどうかを判定していると思われます。
つまり、
・「(rule.IdentityReference as NTAccount).Value」がグループであれば
現在のログインユーザが「(rule.IdentityReference as NTAccount).Value」に所属しているかどうかの判定になり、
・「(rule.IdentityReference as NTAccount).Value」がユーザーであれば
現在のログインユーザと「(rule.IdentityReference as NTAccount).Value」が同一かどうかの判定になると思われます。
もし属していないと判別したのなら、そのユーザまたはグループの情報はチェックする必要はありませんので、continueで飛ばします。
ですから、これより下の行は、全て現在ログイン中のユーザか、ログイン中のユーザが属しているグループに関する権限の条件チェックになります。
//許可判定
if (
(rule.AccessControlType == System.Security.AccessControl.AccessControlType.Allow) &&
(
((rule.FileSystemRights & System.Security.AccessControl.FileSystemRights.Write) == System.Security.AccessControl.FileSystemRights.Write) ||
((rule.FileSystemRights & System.Security.AccessControl.FileSystemRights.FullControl) == System.Security.AccessControl.FileSystemRights.FullControl) ||
((rule.FileSystemRights & (FileSystemRights)0x40000000) == (FileSystemRights)0x40000000) ||
((rule.FileSystemRights & (FileSystemRights)0x10000000) == (FileSystemRights)0x10000000)
)
) {
//許可されている
ret = true;
}
逆に、ACL上では「Allow」つまり許可の権限は拒否の権限よりも優先度が低いので、次以降に拒否判定が出ることを想定して、許可が出た時点ではbreakをしないようにしている。
「(FileSystemRights)0x40000000」:Generic_write、つまり書き込み権限
「(FileSystemRights)0x10000000」:Generic_all、つまりフルコントロール
//拒否判定
if (
(rule.AccessControlType == System.Security.AccessControl.AccessControlType.Deny) &&
(
((rule.FileSystemRights & System.Security.AccessControl.FileSystemRights.Write) == System.Security.AccessControl.FileSystemRights.Write) ||
((rule.FileSystemRights & System.Security.AccessControl.FileSystemRights.FullControl) == System.Security.AccessControl.FileSystemRights.FullControl) ||
((rule.FileSystemRights & (FileSystemRights)0x40000000) == (FileSystemRights)0x40000000) ||
((rule.FileSystemRights & (FileSystemRights)0x10000000) == (FileSystemRights)0x10000000)
)
) {
//拒否されている
ret = false;
//ACL上では拒否が許可より優先度が高いので、拒否が出た時点でbreakした。
break;
}
ACL上では「Deny」つまり拒否の権限は許可の権限よりも優先度が高いので、拒否が出た時点でbreakをして処理自体を終わらせます。
また、breakの前に変数retにfalseを入れているのは、許可判定が出たために変数retにtrueが書き込まれている可能性があるためです。
「(FileSystemRights)0x40000000」:Generic_write、つまり書き込み権限
「(FileSystemRights)0x10000000」:Generic_all、つまりフルコントロール
トークン処理
トークン処理の資料は以下の記事です。
そのサイトにはサンプルプロジェクトのZIPファイルのリンクがありますが、リンク切れなので、こちらにリンクを載せます。
サンプルプロジェクトのダウンロード:RunAsDesktopUser.zip
この記事の背景ですが、
この記事の「The new “LUA bug” of Vista/Win7」の部分で、管理者権限で起動したアプリからは管理者ユーザの情報しか取得できず、ログインユーザーの情報を取得できる保証が無いという問題点と、それに付随する問題を述べ、それに対して、エクスプローラのプロセスからエクスプローラを起動したユーザーの情報(≒ログインユーザー)を取得し、そのユーザの権限で他のアプリを立ち上げる、という解決策を本家Microsoftが述べた記事です。
この資料とそのサンプルは取得したトークンを用いてCreateProcessWithTokenW関数経由でアプリを起動していましたが、今回行いたいことは、その同様のトークンをWindowsIdentityのコンストラクタに渡そうとしています。
つまり、CreateProcessWithTokenW関数の引数に渡すか、WindowsIdentityのコンストラクタに渡すかの違いだけです。
API宣言は長いので後で記述します。トークン処理だけを先に記述します。
コードのコメントは、資料にリンクされているサンプルのコードにあるコメントを翻訳しただけのものです。
ソースコード
private WindowsIdentity GetExplorerIdentity() {
WindowsIdentity ret = null;
IntPtr hProcessToken = IntPtr.Zero, hShellProcess = IntPtr.Zero, hShellProcessToken = IntPtr.Zero, hPrimaryToken = IntPtr.Zero;
IntPtr hwnd = IntPtr.Zero;
bool fret = false;
uint dwPID = 0;
int dwLastErr = 0;
try {
// このプロセスで SeIncreaseQuotaPrivilege を有効にします。 (現在のプロセスが昇格されていない場合、これは機能しません。)
fret = OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, out hProcessToken);
if (!fret) {
dwLastErr = Marshal.GetLastWin32Error();
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);
if (!fret) {
dwLastErr = Marshal.GetLastWin32Error();
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();
if (IntPtr.Zero == hwnd) {
dwLastErr = Marshal.GetLastWin32Error();
throw (new Win32Exception(dwLastErr));
}
// デスクトップシェルプロセスのPIDを取得します。
GetWindowThreadProcessId(hwnd, out dwPID);
if (0 == dwPID) {
dwLastErr = Marshal.GetLastWin32Error();
throw (new Win32Exception(dwLastErr));
}
// デスクトップシェルプロセスを開いてクエリを実行します(トークンを取得します)
hShellProcess = OpenProcess(PROCESS_QUERY_INFORMATION, false, dwPID);
if (hShellProcess == IntPtr.Zero) {
dwLastErr = Marshal.GetLastWin32Error();
throw (new Win32Exception(dwLastErr));
}
// この時点から、閉じるハンドルがあるので、必ずクリーンアップしてください。
// デスクトップシェルのプロセストークンを取得します。
fret = OpenProcessToken(hShellProcess, TOKEN_DUPLICATE, out hShellProcessToken);
if (!fret) {
dwLastErr = Marshal.GetLastWin32Error();
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
);
if (!fret) {
dwLastErr = Marshal.GetLastWin32Error();
throw (new Win32Exception(dwLastErr));
}
ret = new WindowsIdentity(hPrimaryToken);
} catch {
ret = null;
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); }
}
return ret;
}
説明
順に説明します。
IntPtr hProcessToken = IntPtr.Zero;
fret = OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, out hProcessToken);
if (!fret) {
dwLastErr = Marshal.GetLastWin32Error();
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);
if (!fret) {
dwLastErr = Marshal.GetLastWin32Error();
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();
if (IntPtr.Zero == hwnd) {
dwLastErr = Marshal.GetLastWin32Error();
throw (new Win32Exception(dwLastErr));
}
GetShellWindow関数は、「explorer.exe」のプロセスのウインドウハンドルを取得する関数です。
// デスクトップシェルプロセスのPIDを取得します。
GetWindowThreadProcessId(hwnd, out dwPID);
if (0 == dwPID) {
dwLastErr = Marshal.GetLastWin32Error();
throw (new Win32Exception(dwLastErr));
}
エクスプローラーのウインドウハンドルからエクスプローラーのプロセスIDをGetWindowThreadProcessId関数で取得します。
// デスクトップシェルプロセスを開いてクエリを実行します(トークンを取得します)
hShellProcess = OpenProcess(PROCESS_QUERY_INFORMATION, false, dwPID);
if (hShellProcess == IntPtr.Zero) {
dwLastErr = Marshal.GetLastWin32Error();
throw (new Win32Exception(dwLastErr));
}
エクスプローラーのプロセスIDからエクスプローラーのプロセスハンドルをOpenProcess関数で取得します。
PROCESS_QUERY_INFORMATION
トークン、終了コード、優先度クラスなど、プロセスに関する特定の情報を取得するために必要です ( OpenProcessToken を参照)。
引用元:MSDNの「プロセスのセキュリティとアクセス権」
// デスクトップシェルのプロセストークンを取得します。
fret = OpenProcessToken(hShellProcess, TOKEN_DUPLICATE, out hShellProcessToken);
if (!fret) {
dwLastErr = Marshal.GetLastWin32Error();
throw (new Win32Exception(dwLastErr));
}
エラー処理の際、ハンドル解放後に「IntPtr.Zero」を代入しているのは、後のfinallyの部分に関係しますので、その部分で説明します。
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
);
if (!fret) {
dwLastErr = Marshal.GetLastWin32Error();
throw (new Win32Exception((int)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)」
ret = new WindowsIdentity(hPrimaryToken);
} catch {
ret = null;
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); }
}
return ret;
DuplicateTokenEx関数で複製したトークンを用いてWindowsIdentityオブジェクトを初期化します。他の部分はWIN32API関数の処理なので戻り値でエラーを検出できましたが、この部分だけが唯一try~catch文の例外処理でしかエラー検出できない箇所です。
この場合、戻り値でエラーを検出できないということは、エラーが発生したらリソースが解放できなくなることを意味します。なのでエラーが発生した後もリソースを解放できるよう、finallyの部分でリソース解放処理を書いています。
しかしfinallyでリソース解放処理をする場合、既に開放済みのリソースが混在している可能性がありますので、他のエラー処理でリソース解放後に「IntPtr.Zero」を代入し、finallyの部分ではハンドルの値が「IntPtr.Zero」ではないことを確認するNULLチェックを行った上で解放処理を行います。
この関数内のcatch内でthrowすると、finallyの処理が終わった後、先ほど説明したWriteJudgeユーザ関数のcatch内に移動します。
こちらの他の投稿者が書いた記事によると、catch内でthrowすると、finallyを通ってから、呼び出し元関数のcatch内に移動する点が書かれていました。これはつまり、catch内でthrowしても、finallyで書いたリソース解放処理などの後処理は無視されずに行われることを意味しています。
WIN32API関数とそれに関係する構造体や定数の宣言
ソースコード
宣言だけなので、説明はしません。
[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
);
使用法
bool ret=WriteJudge("C:\Program Files");
引数に管理者権限が必要かどうかを確認したいフォルダのパスを入れ、戻り値がfalseの時は管理者権限が必要、戻り値がtrueの場合は管理者権限不要と判断する関数を作った形になります。
ただ、私のコードまたはアプローチの根本自体に間違いが無いとは言い切れませんし、もっと良いやり方もあるかもしれません。
一応記述に間違いが無いかを確認したつもりですが、ここまで分量が多いと、見落としが大量に発生している可能性が極めて高いです。