序:なぜ今、VBScriptにおける「自己昇格」を極めるのか
モダンなITインフラにおいて、PowerShellや各種オーケストレーションツールの導入が進む現在であっても、Windows環境におけるVBScript(WSH: Windows Script Host)のプレゼンスが完全に消失することはありません。
依存するランタイムが一切存在しない「ゼロ・ディペンデンシー」、あらゆるWindows OSにデフォルトで組み込まれている「絶対的なポータビリティ」、そしてミリ秒単位で起動する「圧倒的な軽量さ」。これらは、エンタープライズの自動化やレガシーシステムの保守、キッティング現場において、今なおVBScriptが最強の「ファースト・ペンギン」であり続ける理由です。
しかし、現代のセキュアなWindows OS環境において、システム設定の変更、レジストリ操作、Active Directory連携、特定サービスの制御といった管理タスクを実行するには、UAC(ユーザーアカウント制御)による管理者特権(Administrator Privilege)が不可欠です。
一般ユーザー権限で起動されたスクリプトが、手動で「管理者として実行」し直されることを期待する設計は、プロフェッショナルの仕事ではありません。ユーザーに余計な手順を踏ませず、かつセキュリティ警告を最小限に抑えながら、「自身が管理者権限で動いているかを判別し、未昇格であれば自動的にUACプロンプトを呼び出して自身を再起動する」というブートストラップ構造の確立こそが、本稿のテーマです。
リファレンスをなぞっただけのコードでは、パスに含まれるスペースで破綻し、引数は消失し、UACキャンセル時に無限ループに陥ります。本稿では、それらすべてのエッジケースを排除した、堅牢極まる「極限のブートストラップ・テンプレート」を提示し、その内部構造を徹底的に解剖します。
—
アーキテクチャ設計:UAC自動昇格を成立させる3つの技術的要件
堅牢なブートストラップを設計するにあたり、クリアすべき技術的課題は以下の3点に集約されます。
1. 非破壊かつ超高速な「権限判定」
外部プロセス(`whoami` など)をフォークさせる手法は、プロセスの起動オーバーヘッドが発生するだけでなく、エンドポイントセキュリティ製品(EDR等)に「不審な挙動」として検知されるリスクを高めます。プロセスを起動せず、Win32 API(COM経由)の挙動のみで権限を100%判別するロジックが必要です。
2. 引数(Arguments)および実行パスの完全な復元
再帰呼び出し(再起動)を行う際、最初に渡された引数群を1文字の狂いもなく、スペースやダブルクォーテーションを適切にエスケープした状態で引き継がねばなりません。
3. UAC昇格特有の「カレントディレクトリ変質問題」の解決
`ShellExecute` で `runas`(管理者として実行)を呼び出すと、プロセスの作業ディレクトリ(カレントディレクトリ)が、スクリプトの配置場所から `C:\Windows\System32` へ強制的に変更されます。これにより、相対パスを前提とした処理がすべて破壊されます。この仕様をあらかじめ予期し、コード側で能動的にカレントディレクトリを復元する必要があります。
—
極限のブートストラップ・テンプレート(完全コード)
以下に、上記の課題をすべて解決し、メモリリークの排除とエラーハンドリングを極限まで突き詰めた、VBScriptの完全なブートストラップ・コードを示します。
Option Explicit
‘ ==============================================================================
‘ SYSTEM ARCHITECT TEMPLATE: UAC Self-Elevation Bootstrap
‘ Minimum OS Target : Windows 7 / Windows Server 2008 R2 or later
‘ Thread Safety : Single-Threaded Apartment (STA) / COM-Active
‘ ==============================================================================
Call Main()
Sub Main()
‘ 1. 権限チェック
If Not IsUserAnAdmin() Then
‘ 管理者権限がない場合、自己昇格(UAC呼び出し)を実行
Call ElevateProcess()
WScript.Quit 0
End If
‘ 2. カレントディレクトリの補正(System32への変質を防止)
Call RestoreCurrentDirectory()
‘ ————————————————————————–
‘ [管理者権限でのメイン処理領域]
‘ ここから下に、本来実行したい特権タスクを記述します。
‘ ————————————————————————–
WScript.Echo “=== 管理者権限での実行が担保されました ===”
WScript.Echo “実行パス: ” & WScript.ScriptFullName
WScript.Echo “作業フォルダ: ” & CreateObject(“WScript.Shell”).CurrentDirectory
‘ 引数の確認デモ
Dim i, args
Set args = WScript.Arguments
If args.Count > 0 Then
WScript.Echo “引き継がれた引数一覧:”
For i = 0 To args.Count – 1
WScript.Echo ” Arg(” & i & “): [” & args(i) & “]”
Next
Else
WScript.Echo “引数はありません。”
End If
‘ ————————————————————————–
End Sub
‘ ==============================================================================
‘ SUB-ROUTINES / FUNCTIONS
‘ ==============================================================================
”’
”’
”’
Function IsUserAnAdmin()
On Error Resume Next
Dim objShell
Set objShell = CreateObject(“WScript.Shell”)
‘ HKEY_USERS\S-1-5-19 (LocalServiceのSID) は管理者権限でのみ書き込み・読み込みの完全制御が可能。
‘ このキーへのダミー読み込みを試みることで、最速かつ軽量に権限を判定する。
objShell.RegRead “HKEY_USERS\S-1-5-19\”
If Err.Number = 0 Then
IsUserAnAdmin = True
Else
IsUserAnAdmin = False
End If
Err.Clear
Set objShell = Nothing
End Function
”’
”’
Sub ElevateProcess()
Dim objShellApp, objArgs, strArgList, i, strArg
‘ 引数の再構築(スペースやダブルクォーテーションを適切にエスケープ)
Set objArgs = WScript.Arguments
strArgList = “”
For i = 0 To objArgs.Count – 1
strArg = objArgs(i)
‘ 引数内にスペースまたはダブルクォーテーションが存在する場合のエスケープ処理
If InStr(strArg, ” “) > 0 Or InStr(strArg, “”””) > 0 Then
strArg = “””” & Replace(strArg, “”””, “”””””) & “”””
End If
strArgList = strArgList & strArg & ” ”
Next
strArgList = Trim(strArgList)
‘ Shell.Application オブジェクトを使用した自己再帰実行
Set objShellApp = CreateObject(“Shell.Application”)
On Error Resume Next
‘ 動詞 “runas” により、OSに対してUACプロンプトの出力を要求
‘ WScript.FullName は wscript.exe または cscript.exe のフルパス
objShellApp.ShellExecute _
WScript.FullName, _
“””” & WScript.ScriptFullName & “”” ” & strArgList, _
“”, _
“runas”, _
1
‘ UACで「いいえ」を選択された(キャンセルされた)場合のエラーハンドリング
If Err.Number <> 0 Then
‘ エラー番号 70 (書き込み権限なし) や -2147023673 (ユーザーキャンセル) 等をハンドリング
WScript.Echo “エラー: 管理者権限への昇格が拒否されました。プログラムを終了します。”
Err.Clear
End If
‘ COMオブジェクトの明示的解放(参照カウントをゼロにし、即座にメモリを返却)
Set objShellApp = Nothing
Set objArgs = Nothing
End Sub
”’
”’ スクリプトが実際に配置されているディレクトリへ復元します。
”’
Sub RestoreCurrentDirectory()
Dim objFSO, objShell, strScriptFolder
On Error Resume Next
Set objFSO = CreateObject(“Scripting.FileSystemObject”)
Set objShell = CreateObject(“WScript.Shell”)
‘ スクリプト自身の親フォルダパスを取得
strScriptFolder = objFSO.GetParentFolderName(WScript.ScriptFullName)
‘ カレントディレクトリを補正
objShell.CurrentDirectory = strScriptFolder
‘ メモリ解放
Set objFSO = Nothing
Set objShell = Nothing
On Error GoTo 0
End Sub
—
深淵なる解説:COMオブジェクトの制御とエッジケースの完全排除
上記のコードが、なぜインターネット上に散見される「お絵かきスクリプト」と一線を画すのか。チーフアーキテクトの視点から、その設計思想を解説します。
1. レジストリプローブによる超高速・低負荷な権限検知
多くのVBScriptサンプルでは、管理者権限の判別に `whoami /groups` や `net session` などの外部コマンドを実行(`WshShell.Run` や `WshShell.Exec`)し、その戻り値や標準出力を解析しています。
しかし、これは以下の理由からアンチパターンです。
- プロセスフォークのオーバーヘッド:`cmd.exe` や `whoami.exe` を起動するたびに、OSカーネル内ではスレッド生成、メモリ割り当て、セキュリティトークンの検証が走り、数百ミリ秒の遅延が生じます。
- EDR/アンチウイルスとの干渉:セキュリティ監視が厳しいエンタープライズ環境では、スクリプトからの「不審な外部システムコマンド実行」は即座にアラート対象となり、プロセスが隔離される原因になります。
本コードで採用した `HKEY_USERS\S-1-5-19\` へのアクセスは、OS内部で完結する超高速なレジストリプローブです。`S-1-5-19` は「Local Service」アカウントの固定SIDであり、この配下のレジストリツリーは、管理者権限を持つプロセスからしかアクセス(読み書き)できません。
外部プロセスを一切起動せず、COMのメモリ空間内だけで完結するため、実行速度は1ミリ秒以下、セキュリティ製品を刺激することもありません。
2. `ShellExecute`における引数(Arguments)の完全な復元とエスケープ
スクリプトがコマンドライン(CUI)やバッチ処理、タスクスケジューラから引数付きで呼び出された場合、自己昇格時にそれらの引数を確実に引き継ぐ必要があります。
単に `WScript.Arguments` をループで結合するだけでは、以下のような引数が渡されたときに構文が崩壊します。
- `”C:\Program Files\App”`(スペースを含むパス)
- `”User=””Administrator”””`(エスケープされたダブルクォーテーションを含む引数)
‘ エスケープ処理の要諦
If InStr(strArg, ” “) > 0 Or InStr(strArg, “”””) > 0 Then
strArg = “””” & Replace(strArg, “”””, “”””””) & “”””
End If
この処理により、引数にスペースが含まれている場合は全体をダブルクォーテーションで囲み、かつ内部のダブルクォーテーションはVBScriptの仕様に従って2重(`””`)にエスケープされます。これにより、昇格後のプロセスへ、オリジナルの引数が完全に復元された状態で引き渡されます。
3. UAC昇格時の「カレントディレクトリ変質問題」とその克服
Windowsのセキュリティアーキテクチャの仕様上、`runas` 動詞で昇格したプロセスの初期作業ディレクトリは、呼び出し元の場所に関わらず、システムディレクトリである `C:\Windows\System32` に設定されます。
これが引き起こす致命的なバグが、「相対パスによるファイル読み書きの失敗」です。
スクリプトと同じフォルダにある設定ファイル(`config.ini` など)を単に `config.ini` として参照しようとすると、プログラムは `C:\Windows\System32\config.ini` を探しに行き、当然ファイルが見つからずに異常終了します。
本テンプレートでは、`RestoreCurrentDirectory` サブルーチンにより、昇格直後に `FileSystemObject` を用いてスクリプト自身の絶対パス(`WScript.ScriptFullName`)から親ディレクトリを取得し、`WScript.Shell` の `CurrentDirectory` を上書き補正しています。これにより、開発者はカレントディレクトリの変質を一切意識することなく、安全に相対パスを使用できます。
4. COM参照カウントの即時解放(`Nothing`)がもたらす決定論的動作
VBScript(ActiveX/COMアーキテクチャ)において、オブジェクトの寿命は「参照カウント法」で管理されています。
Set objShellApp = Nothing
「スクリプトが終了すればどうせ解放される」という怠惰な思考は、エンタープライズ領域のコードには許されません。特に、UAC昇格のようにプロセスが分岐・終了するようなクリティカルな局面においては、不要になったCOMオブジェクトを即座に明示的解放(`Nothing`を代入)し、内部のCOMサーバの参照カウントをデクリメントすることが重要です。
これを怠ると、ゾンビプロセスとして `wscript.exe` がメモリ上に残留したり、システムリソース(ファイルハンドルやレジストリキー)がロックされたままになり、後続の処理に予測不可能なエラー(ファイル共有違反など)を引き起こす原因となります。
—
結:レガシーを「枯れた超兵器」として飼い慣らす
VBScriptは古い技術です。しかし、「古い」ということは「仕様が完全に枯れており、OSのアップデートによる予期せぬ破壊的仕様変更の影響を受けにくい」という、システム運用における最大のメリットと同義です。
今回提示した「自己昇格ブートストラップ」をスクリプトの先頭に組み込むことで、あなたの作成するVBScript資産は、モダンなWindows 11やWindows Server 2022のセキュリティモデルに完全に対応した「堅牢な自動化ツール」へと進化します。
アーキテクチャの特性を理解し、エッジケースを先回りして潰す。これこそが、レガシーを「負の遺産」にせず、システムを支配するためのプロフェッショナルの技術です。
