PowerShellのセキュリティ強化 ── ログ・AMSI・言語モード・JEA

· 更新日: · · PowerShell, セキュリティ, Windows, ログ, 監査, 情報システム, 権限管理, 運用改善

更新履歴(7件・最終更新 2026年08月02日)

この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。

記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
トランスクリプト共有のACLが、記録そのものを止める設定になっていたのを直しました。`CREATOR OWNER`に`AD,RA,REA`だけを与えていましたが、Windowsで「追記しかできない」が成立するのは書き手が`FILE_APPEND_DATA`を単独で指定して開いたときだけで、`Start-Transcript`はそう開きません。PowerShellの実装はまず`FileAccess.ReadWrite`、失敗時に`FileMode.Append`+`FileAccess.Write`で開き、.NETはこれを`GENERIC_READ`/`GENERIC_WRITE`へ写します。`FILE_GENERIC_WRITE`には`FILE_WRITE_DATA`や`SYNCHRONIZE`が含まれるため、この`CreateFile`はアクセス拒否になります。読み書きは許して`DE`(削除)を与えない形に改め、この構成で守れるのは「他人の記録に触れない」「自分の記録も消せない」までで、自分の記録の上書きはACLでは塞げないことを明記しました。上書きまで塞ぐ手段として、JEAの`TranscriptDirectory`(Local Systemが書き、標準ユーザーはアクセスを持たない)、イベント転送・SIEM、追記専用で開く独自収集プロセスの3つを挙げています。
トランスクリプト共有の権限設定で、既存フォルダーを流用したときに直接付与されたACEが残る問題を直しました。`SetAccessRuleProtection($true, $false)`が外すのは継承してきたACEだけで、そのフォルダーに直接付いているACEはそのまま残ります。`New-Item -Force`は既存フォルダーでも成功するため、以前に`Domain Users`や`Everyone`へ変更権限を直接付けてあったなら、以降で`CREATOR OWNER`を絞ってもその付与だけが生き残り、利用者は他人の記録を上書き・削除できました。直接付与を一度すべて落としてから、必要なものだけを同じACLに入れて1回で適用する形にしています(空にする瞬間を作らないため)。
トランスクリプト共有の権限設計に`OWNER RIGHTS`のACEを追加しました。ファイルを作った利用者はそのファイルの所有者になり、Windowsは所有者に`READ_CONTROL`と`WRITE_DAC`を暗黙で与えます。そのため`WD`も`DE`も配っていなくても、所有者は自分でDACLを書き換えて上書き・削除の権限を付け直せてしまい、「乗っ取られたあとも記録が残る」という目的が達成できていませんでした。`OWNER RIGHTS`(`S-1-3-4`)のACEがあるとき、システムは所有者への暗黙の権限を無視します。あわせて、ACLで守れるのは同じ共有を使う利用者どうしまでで、監査証跡として守るなら利用者が触れない収集側へ流す必要があることも明記しました。
トランスクリプトの保存先の権限設定を見直しました。利用者への許可に継承を付けて書き込みを配ると、ファイル名さえ分かれば他人の記録を上書きできてしまうため、フォルダーへの作成権限と、自分が作ったファイルへの追記だけに分ける形に直しています。
対象環境と前提条件を冒頭の表にまとめ、JEAを扱う7章だけはリモート処理が前提になる旨を章の冒頭に明示しました。あわせてトランスクリプトの集約先を「書けるが消せない」共有にする手順、設定が効いたかの見極め方、多層防御の関係図、言語モードの表の拡充を追加しています。
初版公開
この記事を引用する(DOI(登録済みアーカイブ): 10.5281/zenodo.21547462)

以下のDOIは過去に登録されたアーカイブを指しており、現在の本文とは一致しない場合があります。現在の本文を参照するときは、このページのURLを使用してください。

小村 豪(2026)「PowerShellのセキュリティ強化 ── ログ・AMSI・言語モード・JEA」合同会社小村ソフト. https://comcomponent.com/blog/powershell-security-hardening-jea/

DOI(登録済みアーカイブ)
10.5281/zenodo.21547462
DOI(前回登録した版)
10.5281/zenodo.21733171

PowerShellを全面禁止するのではなく、実行内容を記録し、検査・実行範囲・権限をそれぞれ絞る。これが本記事の方針です。ログを残す機能と、実行前に検査する機能、管理作業を限定して任せる機能は、互いの代わりにはなりません。

PowerShellを止めると正規の管理業務や自動化が止まる一方、攻撃側は同等の操作を別の手段で行えます。「何が実行されたかを追えるか」「古い実行経路が残っていないか」「利用者へ必要以上の権限を渡していないか」を順に確認するほうが現実的です。

この記事では、社内のWindows環境を対象に、ログの設定 → 記録の保全 → AMSIと古いエンジン → 言語モード → JEAの設計と構成 → 展開確認の順に説明します。実行ポリシーと署名の詳細は「PowerShellの実行ポリシーとスクリプト署名」で扱い、本記事はその先を整理します。

目的別の読み方

確認したいこと 読む章 確認の着地点
何から始めるべきか 1章: 結論・導入順序 まず可視化し、必要な制限を段階的に重ねる
5.1と7でログを残したい 2章: ログの設定と確認 新しいセッションの4104と、ログサイズを確認する
集約した記録を守りたい 3章: トランスクリプトの保全 他人の記録を守ることと、自分の記録の上書きを防ぐことを分ける
検査や実行制限を整えたい 4章: AMSIと古いエンジン、5章: 実行ポリシー・言語モード 設定名だけでなく、検査・制限を支える仕組みを確認する
サービス再起動だけを委任したい 6章: JEAの設計、7章: JEAの構成と接続 何を許すかを決めてから、サーバーへ専用の接続先を登録する
展開前の抜けを確認したい 8章: チェックリスト 設定値ではなく、記録・保護・制限の実際の動作を見る

コードは本番へ一括実行する手順ではありません。パス・サーバー名・グループ名は例です。自社の値へ読み替え、必要な権限と操作する端末を確認して検証機で試してください。ログ集約共有は集約サーバーで、JEAは操作を委任する対象サーバーで構成します。

対象環境・前提条件

項目 内容
対象OS Windows 10/11、Windows Server。AMSIはWindows 10以降が前提です1
対象バージョン Windows PowerShell 5.1とPowerShell 7の両方を対象にします。ポリシーの設定キーもログの記録先も5.1と7で別なので、両方が入っている端末では両方に設定してください(第2章・第4章)
必要な権限 第2章〜第4章のログ設定・PowerShell 2.0の無効化、第7章のJEAエンドポイント登録は、いずれも管理者権限が必要です。実運用では1台ずつ手で入れるのではなくグループポリシーで配布します
ローカル/リモート 第2章〜第5章は対象の端末・サーバー内で完結します。第6〜7章のJEAだけは、PowerShellリモート処理(WinRM)が有効であることが前提です(第6章の前提条件を参照)
扱わないこと 実行ポリシーと署名の詳細は別記事(PowerShellの実行ポリシーとスクリプト署名)で扱います。本記事では位置づけだけを整理します(第5章)

図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全24件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle

1. まず結論 ── 対策の役割と導入順序を分ける

最優先はスクリプトブロックログによる可視化です。PowerShell 5.0以降には、ログ、AMSI、言語モード、JEAという異なる役割の機能が揃っています。ログは難読化されたコードも展開後の形で残し、AMSIは実行直前の内容をウイルス対策製品へ渡します。言語モードは使える言語要素を、JEAは任せる操作を限定します。2134

PowerShellを守る4つの役割実行内容の記録、内容の検査、言語要素の制限、操作の委任は互いの代替ではなく別の役割を担う。PowerShellを安全に使う記録:実行内容を残す検査:AMSIへ渡す制限:言語要素を絞る委任:JEAで操作を渡す

図1: 4つの機能は別々の役割を持ち、この並びは導入順序ではない。

1.1 記録を残してから、検査・権限・実行範囲を整える

記録は「後から追える状態」を作り、委任は利用者へ渡す権限を小さくします。検査と制限は、実行の手前で働く仕組みです。導入の優先順位は、次の表で考えます。

段階 やること 効果
1. 可視化 スクリプトブロックログ、トランスクリプション 何が起きたか分かる。事後調査が可能になる
2. 基礎的な無害化 PowerShell 2.0の無効化、最新版の維持、AMSIの確認 検査を迂回する経路を塞ぐ
3. 権限の限定 JEAによる委任、管理者権限の削減 被害の範囲を限定する
4. 実行の制限 WDAC/AppLocker + 制約付き言語モード 未承認コードの実行自体を止める

多くの現場では、可視化と権限の限定だけでも状況が大きく改善します。WDAC/AppLockerによる実行制限は導入コストが高いため、業務影響を検証しながら進める領域です。

1.2 設定名だけで「守れた」と判断しない

ログを有効にするだけでなく、容量と保存先まで整えます。4104と入出力を含むトランスクリプションを併用し、既定サイズのまま短期間で上書きされないようにします。2 トランスクリプト共有で守れるのは、他人の記録へのアクセスと自分の記録の削除までです。自分の記録の上書きまで防ぐ方法は3章で分けて説明します。

AMSIはPowerShell 5.0以降・Windows 10以降で利用できますが、古い2.0エンジンが残っていれば、検査とログのない経路が残ります。15 また、実行ポリシーはセキュリティ境界ではなく、手動の言語モード設定も防御の主軸にしません。63 特定の管理作業を任せるなら、利用者に管理者権限を配るのではなくJEAで操作を限定します。4

設定後に確かめるものログの有効化、AMSI対応、JEAの定義は設定の存在だけでなく目的を達成しているか確認する。設定を入れるログが実際に残るか残した記録を守れるか古い実行経路がないか必要な操作だけ任せたか

図2: 設定の有無だけでなく、記録・保全・実行経路・委任範囲まで確認する。

2. ログを残す ── 5.1と7の設定・記録先・容量を確認する

この章は、種類を選ぶ → 設定する → 新しいセッションで記録を確認する → 容量を確認するという手順です。トランスクリプションの出力先へ共有を指定する前に、3章の権限設計で集約先を準備してください。

2.1 何を残すかでログを選ぶ

PowerShellでは、モジュールログ・スクリプトブロックログ・トランスクリプションの3種類を使い分けます。イベントに残すものとファイルに残すもの、さらにバージョンごとの記録先を分けて押さえます。2

種類 記録内容 イベントID
モジュールログ 指定モジュールのパイプライン実行の詳細 4103
スクリプトブロックログ 実行されたコードのテキスト(難読化解除後) 4104
記録先のログ 5.1は Microsoft-Windows-PowerShell/Operational、7は PowerShellCore/Operational ─
トランスクリプション セッションの入出力をテキストファイルに記録 ─(ファイル出力)
ログの保存先を分けて確認するスクリプトブロックログはバージョンごとのイベントログへ、トランスクリプションはファイルへ記録する。実行内容を記録するスクリプトブロックログトランスクリプション5.1/7のイベントログ準備したファイル保存先

図3: イベントログとトランスクリプトは保存先が異なり、どちらも実際の記録を確認する。

2.2 設定先を確認してから有効にする

Windows PowerShell 5.1は、グループポリシーの「コンピューターの構成 > 管理用テンプレート > Windows コンポーネント > Windows PowerShell」で設定します。2 PowerShell 7は「管理用テンプレート > PowerShell Core」側です。テンプレートも別なので、5.1側だけを設定して終わりにしないでください。7

以下は、レジストリで直接設定する例です。通常の社内展開ではGPOで配布します。前半はスクリプトブロックログ、後半はトランスクリプションで、それぞれ5.1用と7用を設定しています。出力先の共有名は例であり、先に準備した集約先へ読み替えます。

# スクリプトブロックログを有効化する(要管理者権限。通常はGPOで配布する)
$key = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging'
New-Item -Path $key -Force | Out-Null
# -Type はレジストリプロバイダーが追加する動的パラメーター。値の型(DWord)を明示できる
Set-ItemProperty -Path $key -Name 'EnableScriptBlockLogging' -Value 1 -Type DWord

# PowerShell 7(pwsh)は別のポリシーキーを見る。両方に設定する
$key = 'HKLM:\SOFTWARE\Policies\Microsoft\PowerShellCore\ScriptBlockLogging'
New-Item -Path $key -Force | Out-Null
Set-ItemProperty -Path $key -Name 'EnableScriptBlockLogging' -Value 1 -Type DWord

# トランスクリプションを有効化し、書き込み専用の共有へ集約する
$key = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription'
New-Item -Path $key -Force | Out-Null
Set-ItemProperty -Path $key -Name 'EnableTranscripting'     -Value 1 -Type DWord
Set-ItemProperty -Path $key -Name 'OutputDirectory'         -Value '\\logsrv\pstranscripts$'
Set-ItemProperty -Path $key -Name 'EnableInvocationHeader'  -Value 1 -Type DWord

# トランスクリプションもPowerShell 7側は別キー。同じ値を書く
$key = 'HKLM:\SOFTWARE\Policies\Microsoft\PowerShellCore\Transcription'
New-Item -Path $key -Force | Out-Null
Set-ItemProperty -Path $key -Name 'EnableTranscripting'     -Value 1 -Type DWord
Set-ItemProperty -Path $key -Name 'OutputDirectory'         -Value '\\logsrv\pstranscripts$'
Set-ItemProperty -Path $key -Name 'EnableInvocationHeader'  -Value 1 -Type DWord

この例では、5.1は Windows\PowerShell 配下、7は PowerShellCore 配下へ設定します。スクリプトブロックログ・トランスクリプションとも、両方のキーに同じ値を書きます。GPOで配布する場合も、それぞれのテンプレートを確認してください。

PowerShell CoreのGPOにはWindows PowerShell側の設定を使用する選択肢もありますが、5.1の設定を入れただけで7側まで有効になったとは判断しません。採用した設定方法にかかわらず、両方の実行環境で記録を確認することが要点です。7

5.1と7の両方でログ設定を確認するWindows PowerShellとPowerShell Coreの設定先を区別し、それぞれの実行環境で記録まで確認する。利用する実行環境を確認Windows PowerShell 5.1PowerShell 75.1側の設定とログを確認Core側の設定とログを確認それぞれで記録を試す

図4: 設定を共用する方式を選ぶ場合も、5.1と7の両方で記録を確かめる。

2.3 新しいセッションで、記録されたことを確認する

スクリプトブロックログの価値は、難読化への耐性にあります。Base64でエンコードされたコマンドや、文字列連結で組み立てられたコードであっても、実行時点で展開された内容が記録されます。2 攻撃の調査で「何が実行されたか」を再現できるかどうかは、この設定の有無で決まります。

記録の確認は Get-WinEvent です(「Get-WinEventでイベントログを実務的に調べる」)。

# 直近1日のスクリプトブロックログを確認する。
# Windows PowerShell(5.1)とPowerShell 7では書き込まれるログが別なので、
# 両方を対象にする
Get-WinEvent -FilterHashtable @{
    LogName   = 'Microsoft-Windows-PowerShell/Operational', 'PowerShellCore/Operational'
    ID        = 4104
    StartTime = (Get-Date).AddDays(-1)
} -ErrorAction SilentlyContinue |
    Select-Object TimeCreated, LogName, @{ n='Script'; e={ $_.Message } } -First 20

有効化できたかの見極めは、次の3点で行います。

  1. 設定を入れたあと、新しいPowerShellセッションを開く。スクリプトブロックログは、有効化以降に開始されたセッションから記録されます。2 すでに開いているウィンドウで試して「出ない」と判断しないでください。
  2. 目印になるコマンドを新しいセッションで実行し、それが4104として返るかを見る。上のクエリで、いま実行したコード片を含むイベントが返れば有効です。1件も返らなければ、Get-ItemProperty でポリシーキーの値(EnableScriptBlockLogging が 1)と、5.1側/7側のどちらのキーに書いたかを確認します。
  3. ログの記録先を間違えていないか。pwsh で実行したなら記録先は PowerShellCore/Operational です。Microsoft-Windows-PowerShell/Operational だけを見て「記録されていない」と誤判定するのが、この設定でいちばん多い勘違いです。

上のクエリは -ErrorAction SilentlyContinue を指定しています。何も返らないことだけを、有効化失敗や「実行がなかった」証拠にはしないでください。まず各ログの存在と、どちらのセッションで確認したかを切り分けます。PowerShell 7でイベントプロバイダーが未登録なら、Microsoftの案内に沿ってPowerShell 7の管理者セッションで $PSHOME\RegisterManifest.ps1 により登録します。8

4104が出ないときの確認順序新しいセッションで目印のコマンドを実行し、対応するログを確認する。値が返らなければ設定先やログの存在とプロバイダー登録を切り分ける。はいいいえ新しいセッションを開く目印のコマンドを実行対応するログで4104を確認見つかったか実行した内容と照合設定先・ログの存在を確認7のプロバイダー登録も確認

図5: 空の結果だけで記録の有無を決めず、設定先と確認先を切り分ける。

2.4 ログサイズを設定し、変更後の値を確かめる

有効化したら必ずログサイズを見直してください。既定サイズのままだと数時間〜数日で一巡し、いざというときに残っていません。

Get-WinEvent -ListLog 'Microsoft-Windows-PowerShell/Operational', 'PowerShellCore/Operational' |
    Select-Object LogName, MaximumSizeInBytes, RecordCount
wevtutil sl Microsoft-Windows-PowerShell/Operational /ms:1073741824   # 1GBに拡張する例
wevtutil sl PowerShellCore/Operational               /ms:1073741824   # PowerShell 7側も忘れずに

# 反映されたかを確認する。MaximumSizeInBytes が 1073741824 になっていれば成功
Get-WinEvent -ListLog 'Microsoft-Windows-PowerShell/Operational', 'PowerShellCore/Operational' |
    Select-Object LogName, MaximumSizeInBytes

上のコードは、現在値の確認 → 1GBへの変更例 → 変更後の値の確認という一組です。wevtutil sl は成功しても何も表示しないため、表示の有無ではなく MaximumSizeInBytes を設定前後で見比べます。管理者権限で実行していないと変更が拒否されるので、値が変わらないときはまずそこを疑ってください。

ログ容量の変更は値で確認するログサイズは現在値、変更、変更後の値を確認し、コマンドが何も表示しなかったことを成功判定に使わない。現在の最大サイズを取得必要なサイズへ変更変更後の最大サイズを取得想定した値と比較

図6: 容量変更はコマンドの表示ではなく、設定前後の値で確認する。

3. 記録を守る ── トランスクリプト共有の権限と限界

3.1 先に、守れる範囲を決める

ここでいう集約先は、他人の記録を自由に読み書きできる共有ではありません。また、本文やコードでいう「書き込み専用」は、自分の記録に対する上書きまで禁止する意味ではありません。JEAの TranscriptDirectory とは書き手が違うため、同じ権限設計をそのまま当てはめないでください。

トランスクリプトの出力先は、利用者が書き込みはできて削除できない共有にするのが要点です。ローカルに置くと、侵害された端末では消されてしまいます。

権限は、共有アクセス許可とNTFSのアクセス許可の両方を設定します(実効権限は両者の厳しいほうになります)。要点は、利用者に「フォルダーへのファイル作成」と「自分が作ったファイルの読み書き」だけを与え、削除(DE)とサブフォルダー・ファイルの削除(DC)を与えないことです。

先に、この構成で何が守れて何が守れないかをはっきりさせておきます。守れるのは「他人の記録には一切触れない」と「自分の記録も消せない」の2点です。守れないのは「自分の記録の上書き」で、これはACLだけでは塞げません(理由は後述します)。それでも、乗っ取られたアカウントから他人の証跡を消せなくなるだけで、被害の切り分けは大きく変わります。1台の端末が落ちても、他の端末の記録は残ります。

共有ACLで守れる範囲この共有ACLは他人の記録へのアクセスと自分の記録の削除を防ぐが、自分の記録の上書きは防げない。利用者が書く記録用共有他人の記録に触れさせない自分の記録も削除させない自分の記録の上書きは残るより強い保全は3.4節へ

図7: 削除を防ぐことと、上書きを防ぐことは別の条件になる。

3.2 共有とNTFSの両方で権限を設定する

このコードは、指定したフォルダーの既存のアクセス許可を外して入れ直します。業務用の共有へそのまま適用せず、ログ用の検証フォルダーで試してください。EXAMPLE\Domain Users、EXAMPLE\ログ管理者、保存パスは自社の値へ読み替えます。

# ログ集約サーバー側で実行する(要管理者権限)
$path = 'D:\PSTranscripts'
$null = New-Item -Path $path -ItemType Directory -Force

# 共有を作る。利用者側は変更(書き込み)まで、管理はログ担当者だけ
New-SmbShare -Name 'pstranscripts$' -Path $path `
    -ChangeAccess 'EXAMPLE\Domain Users' -FullAccess 'EXAMPLE\ログ管理者'

# 親フォルダーからの継承を切る(第2引数 $false = 継承していたACEを引き継がない)。
# 継承で降りてくる CREATOR OWNER のフルコントロールが残っていると、
# 「自分が作ったファイルは自分で消せる」状態になり、集約の意味がなくなる。
#
# ただし SetAccessRuleProtection が外すのは「継承してきたACE」だけで、
# そのフォルダーに直接付与されたACEは残ります。上の New-Item -Force は
# 既存フォルダーでも成功するので、以前に Domain Users や Everyone へ
# 変更権限を直接付けてあったなら、それは以降の行をすべて実行しても
# 生き残ります。利用者は他人のトランスクリプトを上書き・削除できたまま。
# そこで、直接付与を一度すべて落としてから、必要なものだけ入れ直します
$acl = Get-Acl -Path $path
$acl.SetAccessRuleProtection($true, $false)

foreach ($ace in @($acl.Access)) { [void]$acl.RemoveAccessRuleSpecific($ace) }

# 落とし切ったうえで、管理側とSYSTEMを同じ $acl に入れてから1回で適用する。
# 「空にする」と「入れ直す」を別々に Set-Acl すると、誰も触れない瞬間ができる
foreach ($id in @('EXAMPLE\ログ管理者', 'SYSTEM')) {
    $acl.AddAccessRule([System.Security.AccessControl.FileSystemAccessRule]::new(
        $id, 'FullControl', 'ContainerInherit, ObjectInherit', 'None', 'Allow'))
}
Set-Acl -Path $path -AclObject $acl

# 利用者にはフォルダーへの「作成」だけを許可する。(OI) を付けないのが要点で、
# この許可は配下のファイルには継承されない
#   WD=ファイルの作成  AD=フォルダーの作成  X=フォルダーの走査  RA=属性の読み取り
#   (OI)を付けると「既存ファイルへのデータ書き込み」まで配ってしまう
icacls $path /grant 'EXAMPLE\Domain Users:(CI)(WD,AD,X,RA)'

# 書き手が「自分の作ったファイルだけ」を読み書きできるようにする。
# CREATOR OWNER はファイル作成時に「その作成者」へ置き換わるので、
# 他人のファイルには効かない。DE(削除)は与えない
#   RD,WD,AD = データの読み取り・書き込み・追加   RA,WA  = 属性の読み書き
#   REA,WEA  = 拡張属性の読み書き                 RC     = アクセス許可の読み取り
#   S        = 同期。GENERIC_READ/GENERIC_WRITE に含まれるので落とせない
# AD(追記)だけに絞りたくなるが、それではトランスクリプトが1行も書けない(後述)
icacls $path /grant 'CREATOR OWNER:(OI)(IO)(RD,WD,AD,REA,WEA,RA,WA,RC,S)'

# ここが要点。上の行だけでは足りません。
# ファイルを作った利用者は、そのファイルの「所有者」になります。
# Windows は所有者に READ_CONTROL と WRITE_DAC を暗黙で与えるため、
# WD も DE も配っていないのに、所有者は自分でDACLを書き換えて
# 自分に上書き・削除の権限を付け直せます。つまり、乗っ取られたアカウントは
# 自分の証跡を消せる。OWNER RIGHTS のACEがあるとき、システムは
# 所有者への暗黙の READ_CONTROL / WRITE_DAC を無視します
icacls $path /grant 'OWNER RIGHTS:(OI)(IO)(RA,REA)'

icacls $path        # 設定結果を確認する
ACLの設定で確認する3つの点継承と直接付与の整理、作成と既存ファイルへの権限の分離、所有者による権限の付け直し対策を順に確認する。以前の許可を整理する作成権限とファイル権限を分けるOWNER RIGHTSを確認する実際の利用者で試す

図8: 既存の許可・継承・所有者の権利を、それぞれ確認する。

確認1: 以前のアクセス許可が残っていないか

既存のフォルダーを流用するときは、直接付与されたACEを必ず洗ってください。SetAccessRuleProtection($true, $false) が外すのは継承してきたACEだけで、そのフォルダーに直接付いているACEはそのまま残ります。9 New-Item -Force は既存フォルダーでも成功するので、「前に一時的に Domain Users へ変更権限を付けた」といった経緯があるフォルダーを再利用すると、以降で CREATOR OWNER を絞ってもその直接付与だけが生き残り、利用者は他人のトランスクリプトを上書き・削除できます。上のコードで一度すべて落としてから入れ直しているのはこのためです。新しいフォルダーを作るなら不要ですが、入れておいて損はありません。適用後に icacls $path の出力を目で確認し、意図しない主体が並んでいないかを見てください。

確認2: ファイル作成と既存ファイルへの書き込みを分けているか

ここで (OI) の付け方が対策の成否を分けます。利用者への許可に (OI) を付けて WD を配ると、そのACEが配下の全ファイルに継承され、名前さえ分かれば他人のトランスクリプトを上書き・切り詰めできてしまいます。9 それでは「端末やアカウントが乗っ取られたあとも記録が残る」という目的が達成できません。上のように、フォルダーへの作成権限(継承させない)と、CREATOR OWNER 経由で自分が作ったファイルだけへの読み書きに分けます。

作成権限を全ファイルへ配らないフォルダーへの作成権限とCREATOR OWNERによる作成者自身のファイルの読み書きを分け、他人のファイルへの書き込みを許さない。フォルダーへファイル作成新しい記録を作るCREATOR OWNERで作成者へ自分のファイルだけ読み書き全ファイルへWDを継承しない

図9: 新しい記録を作れることを、他人の記録を書き換えられることへ広げない。

確認3: 所有者が自分で削除権限を付け直せないか

そして、OWNER RIGHTS の行を落とさないでください。ファイルを作った利用者は、そのファイルの所有者になります。Windowsは所有者に READ_CONTROL と WRITE_DAC を暗黙で与えるため、10 ACEで DE を配っていなくても、所有者は自分でDACLを書き換えて、自分に削除の権限を付け直せます。「自分の記録は自分で消せる」状態のままでは、CREATOR OWNER を絞った意味がありません。OWNER RIGHTS(S-1-3-4)のACEを置くと、システムは所有者への暗黙の READ_CONTROL / WRITE_DAC を無視するので、ここで初めて「書けるが消せない」が成立します。10

所有者の権限変更も考慮するファイル作成者が所有者としてDACLを変更できる経路をOWNER RIGHTSのACEで制限する。ファイル作成者は所有者暗黙の権利でDACLを変更可能削除権限の付け直しが問題OWNER RIGHTSのACEを適用暗黙の権利に頼る変更を制限

図10: 削除権限を配らないだけでなく、所有者が自分で付け直す経路も扱う。

3.3 なぜ AD(追記)だけに絞らないのか

この節は、上書きを許している理由を知りたい方向けの補足です。結論だけ先に言うと、Start-Transcript が要求するアクセス権を満たさないと、保護以前に記録できなくなるからです。

「追記専用」はACLの名前ではなく、ファイルの開き方で決まる

「上書きされたくないのだから CREATOR OWNER は AD だけでいい」と考えたくなります。そうすると、トランスクリプトが1行も残りません。

Windowsで「追記しかできない」が成立するのは、書き手が FILE_APPEND_DATA を単独で指定してファイルを開いたときだけです。Microsoftのリファレンスは FILE_APPEND_DATA を「(ローカルファイルでは、FILE_WRITE_DATA を伴わずにこのフラグを指定した場合、書き込み操作は既存のデータを上書きしない)」と定義しています。11 つまり追記専用は開き方の性質であって、ACEに AD を置くだけで実現するものではありません。

Start-Transcript が要求する権利を確認する

そして Start-Transcript の書き込み口は、追記専用では開きません。PowerShellの実装は、まず FileMode.OpenOrCreate + FileAccess.ReadWrite で開き、失敗したときだけ FileMode.Append + FileAccess.Write で開き直します。12 .NET側では FileAccess.Read/Write がそれぞれ GENERIC_READ/GENERIC_WRITE に写され、FileMode.Append も内部で FileMode.OpenOrCreate に置き換えてから末尾へシークするだけです。13 FILE_APPEND_DATA を単独で要求する経路はありません。

FILE_GENERIC_WRITE には FILE_WRITE_DATA・FILE_WRITE_ATTRIBUTES・FILE_WRITE_EA・READ_CONTROL・SYNCHRONIZE が、FILE_GENERIC_READ には FILE_READ_DATA・FILE_READ_ATTRIBUTES・FILE_READ_EA・READ_CONTROL・SYNCHRONIZE が含まれます。11 アクセスチェックは要求したすべての権利を見るので、AD,RA,REA しか持たない利用者のこの CreateFile は拒否されます。共有側で変更(Change)を許していても、NTFS側で落ちます。記録を守るつもりで、記録そのものを止めることになります。

だからこの節のACLは、AD 単独ではなく「読み書きは許すが DE(削除)・WDAC(権限変更)・WO(所有者変更)は与えない」という形にしてあります。所有者は自分のトランスクリプトを上書き・切り詰めできます。ここはACLでは塞げない、と割り切ってください。

追記だけの許可では記録できない理由Start-Transcriptが要求する読み書きのアクセス権をADだけの許可では満たせず、ファイルを開けない。ADだけでは不足満たすStart-Transcriptでファイルを開く読み書きの権利を要求すべての要求を満たすかアクセス拒否:記録できない記録できるが上書きも可能

図11: 追記専用はファイルの開き方で決まり、ACLにADを置くだけでは実現しない。

3.4 上書きも防ぐなら、利用者が触れない収集先へ

上書きまで塞ぎたいなら、書き手が触れない場所へ出すしかありません。選択肢は3つです。

  • JEAなら、セッション構成ファイルの TranscriptDirectory を使う(7章)。この出力を書くのは接続してきた利用者ではなく Local System で、Microsoftのドキュメントも「標準ユーザーはこのフォルダーへのアクセスを持たないこと」「監査する security 管理者だけに絞ること」を求めています。14 利用者にそもそも権限が要らないので、この節の悩みごと消えます
  • Windowsイベント転送やSIEMへ取り込む。スクリプトブロックログ(4104)はイベントログに出るので、転送の対象にできます
  • 追記専用で開く独自の収集プロセスを立てる。FILE_APPEND_DATA を単独で要求して開くところまで自分で書けば、ACE側の AD が初めて意味を持ちます

なお、ACLで守れるのはあくまで「その共有に書く利用者」からです。ファイルサーバーの管理者は所有権を取得できますし、端末側が完全に掌握されていれば、そもそも共有へ書き出す前に細工できます。この節の権限設計は、「同じ共有を使う利用者どうしで消し合えない」を担保するものだと理解してください。

利用者が触れない収集先を選ぶ上書きまで防ぎたい場合はJEAの記録先、イベント転送やSIEM、追記専用の独自収集など、書き手から記録を分離する。書き手から記録を分離したいJEA:Local Systemが記録イベント転送/SIEMへ集約追記専用の独自収集利用者のアクセスを制限

図12: 通常の共有とJEAの記録先では書き手が異なるため、同じACLを当てはめない。

3.5 実際の利用者権限で、書き込み・閲覧・削除を試す

icacls の権限の記号(WD AD DE DC など)と継承指定((OI) (CI) (IO))の意味は、Windowsコマンドのリファレンスに一覧があります。9 なお、この構成は必ず検証機で1回試してから展開してください。トランスクリプトは実行中のセッションが同じファイルへ書き足していくため、権限を絞りすぎると記録そのものが落ちます。「利用者の端末から書けること」「利用者が他人の既存ファイルを読めず、書き換えられないこと」「利用者が自分のファイルを削除できないこと」の3点を、実際に確認するのが確実です。

4. 検査の経路を整える ── AMSIとPowerShell 2.0

4.1 AMSIは実行直前の内容を検査へ渡す

AMSI(Antimalware Scan Interface) により、PowerShell 5.0以降ではスクリプトの内容が実行直前にウイルス対策製品へ渡され、難読化を解除した状態で検査されます。1 Microsoft Defenderを含む対応製品を使っていれば、追加設定なしで機能します。

AMSIと対応製品の役割AMSIが実行直前のスクリプト内容を対応するウイルス対策製品へ渡し、検査する仕組みを示す。PowerShellの実行直前の内容AMSI対応するウイルス対策製品内容を検査

図13: AMSIは内容を検査製品へ渡す仕組みで、対応した実行環境と製品を確認する。

4.2 古いエンジンが残っているかを確認し、無効化する

問題は、古いエンジンにはこの仕組みがないことです。Windows PowerShell 2.0エンジンが有効なままだと、powershell.exe -Version 2 によってログもAMSIも効かない環境に切り替えられる余地が残ります。この機能は非推奨であり、無効化が推奨されています。5

次のコードは、前半が状態確認、後半が無効化です。確認結果を読んでから変更し、全行を無条件に続けて実行しないでください。この手順は2.0のオプション機能が残っている環境のためのもので、機能が見つからない環境へ追加し直す手順ではありません。

# PowerShell 2.0エンジンの状態を確認し、無効化する(要管理者権限・再起動が必要な場合あり)
Get-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2* |
    Select-Object FeatureName, State

Disable-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2Root -NoRestart
古いエンジンは存在を確認して無効化するPowerShell 2.0のオプション機能が残っている環境を対象に状態を確認し、必要な無効化を行う。存在しない機能を追加し直さない。有効ない/無効2.0のオプション機能を確認残っているか無効化し必要な再起動を扱う追加・有効化し直さない利用する5.1と7を確認

図14: 古い機能を確認するコードと、無効化するコードは判断を挟んで使う。

4.3 7を導入しても、5.1側の確認を省かない

なお、PowerShell 7(pwsh)を導入している場合、設定キーもログの記録先も5.1とは別です(ポリシーは PowerShellCore 配下、ログは PowerShellCore/Operational)。両方が使われる環境では、設定・ログサイズ・調査クエリのすべてを両方に対して行ってください。バージョンの共存については「Windows PowerShell 5.1とPowerShell 7の違い」を参照してください。

5. 実行範囲を絞る ── 実行ポリシーと言語モードを混同しない

5.1 実行ポリシーは、悪意ある操作を止める境界ではない

この章では、悪意ある操作を止めることと、配布物の完全性を確認することを分けます。

改めて明確にしておきます。実行ポリシーはセキュリティ境界ではありません。公式ドキュメントは、ユーザーが意図せずスクリプトを実行してしまうことを防ぐための安全機能であり、悪意ある操作を防ぐものではないと明記しています。6

とはいえ無価値ではありません。署名運用には「配布したモジュールが改ざんされていないことを確認できる」という別の価値があります(「PowerShellモジュールの社内配布と更新」)。役割を正しく理解して使うことが重要で、「AllSignedにしたから安全」という理解が最も危険です。

署名の価値と実行制限を分ける実行ポリシーをセキュリティ境界とは扱わず、署名の完全性確認とアプリケーション制御による実行制限を別の役割として理解する。目的を分ける配布物の完全性を確認するコードの実行範囲を制限する署名運用の価値アプリ制御と言語モードAllSignedだけで安全とはしない

図15: 署名の価値を認めることと、実行ポリシーを防御の主軸にすることは違う。

5.2 制限の強弱だけでなく、用途を比べる

PowerShellのセッションには言語モードがあり、使える言語要素が制限されます。3

モード 使えるもの 実務での位置づけ
FullLanguage すべての言語要素(既定) 通常のセッション
ConstrainedLanguage コマンドレットはすべて動き、ループ・条件分岐・文字列展開・プロパティ参照も使える。ただし使える.NET型が許可リストに限定され、Add-Type は署名済みアセンブリしか読み込めない WDAC/AppLocker配下で自動的に切り替わるモード。対話操作や通常の管理作業はおおむねそのまま行える
RestrictedLanguage コマンドは実行できるがスクリプトブロックが使えない。変数は $PSCulture $PSUICulture $true $false $null のみ、比較演算子は -eq -gt -lt のみで、代入・プロパティ参照・メソッド呼び出しは不可 モジュールマニフェスト(.psd1)の読み込みに使われるモード。人が対話で使う想定のものではない
NoLanguage スクリプト言語そのものが無効。スクリプトも変数も使えず、コマンドレットとネイティブコマンドの呼び出しだけができる JEAのセッション構成(RestrictedRemoteServer)の既定。「決められたコマンドを叩くだけ」の窓口を作るためのモード

3つの制限モードは段階ではなく、用途が違います。ConstrainedLanguageは「人がスクリプトを書いて使う環境を、危険な型の利用だけ止める」もの、NoLanguageは「そもそもスクリプトを書かせない」もので、後者はJEAのような限定的な窓口でしか成立しません。3

言語モードは用途で選ぶ制約付き言語モードは通常の管理作業で使える言語要素を絞り、NoLanguageはJEAのような決められた操作の窓口で用いる。利用のしかたを見るスクリプトを書いて管理する決めたコマンドだけを使うアプリ制御+ConstrainedLanguageJEAのNoLanguageセッション

図16: 制限モードは単純な強弱の段階ではなく、利用する場面が異なる。

5.3 現在のモードを確認する

現在のモードは次で確認できます。

$ExecutionContext.SessionState.LanguageMode

5.4 モードの表示だけでなく、制限を強制する仕組みを見る

重要なのは設定方法です。制約付き言語モードは、WDAC(Windows Defender Application Control)やAppLockerで許可リスト方式のアプリケーション制御を構成した際に、PowerShellが自動的に切り替わる形で機能します。3 環境変数などで手動設定する方法は回避が容易であり、セキュリティ機能としては機能しません。アプリケーション制御を導入せずに言語モードだけを絞るのは、労力の割に効果がないと理解してください。

言語モードは強制する仕組みまで確認するモードを表示するだけでなく、WDACやAppLockerによる制御に基づいているかを確認し、手動設定を防御として扱わない。現在のLanguageModeを確認制限を強制する仕組みを確認WDAC/AppLockerの構成手動設定だけには頼らない

図17: 言語モードの表示と、回避できない形で制限する仕組みを分けて確認する。

6. JEAを設計する ── 接続先・許可範囲・関数の責任

現実の被害を左右するのは、多くの場合権限の広さです。「ヘルプデスクに管理者権限を渡している」「運用担当が全員Domain Adminに入っている」という状態では、1台の侵害が全社の侵害になります。

JEA(Just Enough Administration)は、管理者権限を渡さずに特定の操作だけを委任する仕組みです。4 「アプリケーションサービスの再起動だけをヘルプデスクに任せる」といった要求に、ぴったり合います。

6章では前提と許可範囲を決め、7章でファイルを作って登録・接続します。「何を許すか」の定義(.psrc)と、「誰に渡してどう実行するか」の定義(.pssc)は別の役割です。

6.1 前提: JEAは専用のリモート接続先を作る

第2章〜第5章がローカルの設定で完結するのに対し、JEAはPowerShellリモート処理(WinRM)の仕組みそのものの上に作られています。15 「JEAで委任する」とは、対象のサーバーに専用の接続先(セッション構成)を登録し、そこへ接続させることです。自社で再現できるかどうかは、まずここで決まります。

必要になるのは次の3点です。

前提 内容
PowerShellのバージョン JEAはPowerShell 5.0以降で利用できます15
リモート処理の有効化 対象サーバーでPowerShellリモート処理が有効であること。Windows Server 2012以降は既定で有効で、無効な場合は管理者権限のPowerShellで Enable-PSRemoting を実行します15
配置場所 役割機能ファイル(.psrc)は対象サーバー上のモジュールの RoleCapabilities フォルダーに、セッション構成(.pssc)は対象サーバー上で登録します。利用者の端末に置くものではありません4

つまり、7章の手順1〜3は委任される側のサーバーで、管理者権限で行う作業です。利用者の端末で必要なのは、そのサーバーへ接続できることだけです。リモート処理そのものの構成と安全な設定は「PowerShell Remoting(WinRM)入門」にまとめています。

以下の例は、配置パスと Get-EventLog に合わせ、対象サーバーのWindows PowerShell 5.1側で構成する例として読みます。PowerShell 7では Get-EventLog は標準のコマンドとして提供されないため、7側のエンドポイントへ無変更で適用できる例ではありません。16 記事全体で5.1と7を扱うことと、個々のサンプルが両方でそのまま使えることは別です。

JEAで渡すのは管理者権限ではなく操作利用者は専用のエンドポイントへ接続し、仮想アカウント側が許可された管理作業を実行する。管理者ではない利用者対象サーバーの専用接続先役割で許可した操作仮想アカウント側で実行実行内容を記録

図18: 対象サーバーのJEA接続先を通じて、必要な操作だけを委任する。

6.2 値の制限だけに頼らず、入力経路を絞る

次の2例は、パラメーター制限だけに頼った構成の問題を説明するもので、導入時に実行する手順ではありません。

VisibleCmdlets の ValidateSet だけでは、状態を変えるコマンドを安全に絞れません。Parameters と ValidateSet による制限は、そのパラメーターが実際に束縛されたときにしか評価されません。Restart-Service は ServiceController をパイプラインで受け取れるため、

Get-Service WinRM | Restart-Service

と書かれると -Name が束縛されず、ValidateSet は素通りします。「KsAppService と Spooler だけ再起動できる」つもりのエンドポイントで、任意のサービスを再起動できてしまうわけです。

同じことは別のパラメーターセットでも起きます。Get-WinEvent に LogName の ValidateSet を付けても、

Get-WinEvent -ProviderName Microsoft-Windows-Security-Auditing -MaxEvents 1

と書かれれば LogName は束縛されず、制限は働きません。JEAセッションは仮想管理者として動くため、この場合Securityログまで読めてしまいます。

そのため7章の構成例では Restart-Service を VisibleCmdlets に入れず、引数でしか対象を受け取らないラッパー関数(Restart-KsAppService)だけを公開しています。入力経路が引数しかなければ、迂回のしようがありません。

Parameters と ValidateSet による制限は、「そのパラメーターが束縛されたとき」にしか働きません。パイプライン入力や別のパラメーターセットで束縛を回避できるコマンドでは、制限そのものが無効になります。引数を絞りたいコマンドは、原則としてラッパー関数で包むと考えてください。

パラメーター制限が働く条件ValidateSetは対象のパラメーターが束縛されたときに評価されるため、パイプライン入力や別のパラメーターセットを持つコマンドは入力経路を確認する。許可する値を絞りたいすべての入力経路を確認パイプライン入力別のパラメーターセット制限した引数が束縛されるか限定したラッパー関数で受ける

図19: 特定の引数への制限が、別の入力経路でも働くとは限らない。

6.3 関数の定義と公開は別々に必要

役割機能ファイルでは、VisibleFunctions は関数を作らない点にも注意してください。名前を挙げただけの関数はセッションに存在せず、利用者には「そんなコマンドはない」と見えます。独自関数は FunctionDefinitions で定義したうえで、VisibleFunctions にも挙げてください。17 数が増えるならスクリプトモジュールに切り出し、そのモジュールの関数を VisibleFunctions で公開するほうが管理しやすくなります。

関数の定義と利用者への公開を分けるFunctionDefinitionsなどで用意した関数をVisibleFunctionsで公開する。名前を指定するだけでは関数は作られない。FunctionDefinitionsで定義セッションに関数が存在VisibleFunctionsで公開JEA利用者が呼べる

図20: 関数を用意することと、利用者へ見せることの両方が必要になる。

6.4 公開する関数の本体にも責任を持つ

関数の本体はJEAの制約を受けません。17 Select-Object などJEAが差し替える制約付きコマンドを本来の挙動で使いたい場合は、7章の例のように Microsoft.PowerShell.Utility\Select-Object と完全修飾で呼びます。裏を返せば、関数の中では何でもできるということなので、利用者からの入力を Invoke-Expression に渡すような書き方は絶対に避けてください。

JEAの利用者と関数本体の境界JEA利用者の制限と関数本体の実行範囲は異なり、関数側で利用者の入力を安全に扱う必要がある。JEA利用者の入力公開する関数で受ける限定した対象だけを渡す管理コマンドを呼び出す関数本体は別途レビュー

図21: 利用者を制限しても関数本体の安全性までは保証されないため、入力の扱いを設計する。

7. JEAを構成する ── 定義・登録・接続を順に行う

手順1〜3は対象サーバーの管理者が行い、その後で利用者が専用エンドポイントへ接続します。以下はWindows PowerShell 5.1側の構成例です。役割名、グループ、保存先、公開する操作を6章で決めてから作成してください。

JEA構成ファイルを役割ごとに用意する役割機能ファイルをモジュールへ配置し、セッション構成から役割名で参照する。登録するのはセッション構成ファイルで、その後で利用者が接続する。役割名で参照psrc:モジュールへ配置pssc:利用者・実行・記録psscをサーバーへ登録利用者が専用接続先へ接続

図22: psrcはモジュールへ配置してpsscから参照し、サーバーへ登録するのはpsscである。

手順1: 役割機能ファイル(.psrc)で許可する操作を定義する

まず、役割ファイルを置くモジュールを作る

役割機能ファイルは、PowerShellモジュールの RoleCapabilities フォルダーに置かれている必要があります。フォルダーを作っただけではモジュールとして認識されず、後述の RoleDefinitions から名前で解決できません。そのため、まず入れ物となるモジュール(マニフェストを持つフォルダー)を作ります。4

# 役割機能を入れるモジュールを作る(マニフェストが必要)
$moduleRoot = 'C:\Program Files\WindowsPowerShell\Modules\KsJea'
$null = New-Item -Path "$moduleRoot\RoleCapabilities" -ItemType Directory -Force
# モジュールフォルダーには、フォルダーと同名のファイルが1つ以上必要
$null = New-Item -Path "$moduleRoot\KsJea.psm1" -ItemType File -Force
New-ModuleManifest -Path "$moduleRoot\KsJea.psd1" -RootModule 'KsJea.psm1'

# 役割機能ファイルの雛形を作る(ファイル名が役割名になる)。
# 役割名は PSModulePath 上の全モジュールから「名前だけ」で解決されるため、
# 'HelpDesk' のような一般的な名前は他のモジュールの .psrc と衝突しうる。
# 衝突するとどちらが選ばれるかの保証がなく、意図しない権限が与えられる。
# 組織の接頭辞を付けた一意な名前にする
New-PSRoleCapabilityFile -Path "$moduleRoot\RoleCapabilities\KsHelpDesk.psrc"

# モジュールとして見えているかを確認する
Get-Module -Name KsJea -ListAvailable
役割名は検出可能なモジュールから解決される役割機能ファイルは検出可能なモジュールのRoleCapabilities配下へ置き、他の役割名と衝突しない一意の名前を選ぶ。対象サーバーのPSModulePath検出可能なKsJeaモジュールRoleCapabilitiesフォルダー一意のKsHelpDesk.psrcRoleDefinitionsで名前を指定

図23: 配置先だけでなく、モジュールとして見えることと役割名の一意性を確認する。

次に、作成した役割ファイルへ公開する操作を書く

次は、作成した KsHelpDesk.psrc に記述する内容の抜粋であり、コンソールに貼って実行する設定コマンドではありません。GUID = '....' は省略表記です。雛形が生成した実際のGUIDは残し、公開コマンドと関数の定義を編集してください。

読みどころは、Restart-Service を直接公開せず、許可するサービス名を引数で受け取る Restart-KsAppService だけを公開する点です。値の制限だけに頼らない理由は6章で説明しました。

# KsHelpDesk.psrc(抜粋)── 「何を、どこまで」許可するかを宣言する
@{
    GUID           = '....'
    # 読み取り専用のコマンドはそのまま見せてよい
    VisibleCmdlets = @(
        'Get-Service',
        'Get-EventLog'
    )
    # 状態を変える Restart-Service は、あえて VisibleCmdlets に入れない(後述)
    # VisibleFunctions は「セッションに読み込まれている関数」を絞り込むだけで、
    # 関数を定義はしない。独自関数は FunctionDefinitions で中身を書き、
    # そのうえで VisibleFunctions にも名前を挙げる(両方必要)
    VisibleFunctions = @('Get-KsAppStatus', 'Restart-KsAppService')
    FunctionDefinitions = @(
        @{
            Name        = 'Get-KsAppStatus'
            ScriptBlock = {
                # 関数の本体は既定の言語モードで動くため、JEAの制約を受けない。
                # 利用者の入力をそのまま危険なコマンドへ渡さないこと
                Get-Service -Name 'KsAppService' |
                    Microsoft.PowerShell.Utility\Select-Object Name, Status, StartType
            }
        },
        @{
            # 再起動は「引数でしか対象を受け取らない」関数として公開する
            Name        = 'Restart-KsAppService'
            ScriptBlock = {
                param(
                    [Parameter(Mandatory)]
                    [ValidateSet('KsAppService', 'Spooler')]
                    [string] $Name
                )
                Microsoft.PowerShell.Management\Restart-Service -Name $Name
            }
        }
    )
    VisibleExternalCommands = @()
}

手順2: セッション構成ファイル(.pssc)で、誰にどの役割を割り当てるかを定義する

.psrc が「何を許すか」なのに対し、ここでは誰にその役割を渡し、どのアカウントで実行し、どこへ記録するかを定義します。EXAMPLE\HelpDesk は対象の利用者グループへ読み替えます。

TranscriptDirectory は3章の利用者が直接書く共有とは別です。Local Systemに読み書きの権限があり、標準ユーザーはアクセスできず、監査する管理者だけが読める保存先を用意してください。14

# SessionType: 制限付きリモートサーバー(既定でNoLanguage)
# RunAsVirtualAccount: 仮想管理者アカウントとして実行
# TranscriptDirectory: 実行内容を記録
$pssc = @{
    Path                = '.\KsHelpDesk.pssc'
    SessionType         = 'RestrictedRemoteServer'
    RunAsVirtualAccount = $true
    TranscriptDirectory = 'C:\JeaTranscripts'
    RoleDefinitions     = @{ 'EXAMPLE\HelpDesk' = @{ RoleCapabilities = 'KsHelpDesk' } }
}
New-PSSessionConfigurationFile @pssc
JEAの記録はLocal Systemが書くJEAのTranscriptDirectoryはLocal Systemが書き込む。標準ユーザーにアクセスを持たせず、監査する管理者を限定する。JEAセッションの実行内容Local Systemが記録TranscriptDirectory限定した監査管理者が確認標準ユーザーにアクセスを与えない

図24: JEAの保存先には通常の利用者が直接書く共有とは異なる権限を設定する。

手順3: サーバーへ登録する

ここまでのファイルを用意してから、同じ対象サーバーでセッション構成を登録します。ファイルを作っただけでは、利用者の接続先にはなりません。

Register-PSSessionConfiguration -Name 'KsHelpDesk' -Path .\KsHelpDesk.pssc -Force

利用者側: 登録した専用エンドポイントへ接続する

ここからは利用者側の操作です。サーバー名と構成名を指定し、既定の接続先ではなく、作成したJEAの接続先を選びます。

Enter-PSSession -ComputerName 'appsrv01' -ConfigurationName 'KsHelpDesk'
# 許可されたコマンドしか使えない。再起動は公開した Restart-KsAppService で指定サービスのみ

JEAの要点は3つです。4

  • 利用者は管理者権限を持たない。実行は仮想アカウント側で行われる
  • セッションは制限付きリモートサーバーとして構成される。既定で言語モードが制限され、任意のコードを実行できない
  • トランスクリプトで実行内容が記録される。誰が何をしたかが監査できる
専用の構成名を指定して接続する対象サーバーへ構成を登録した後、利用者はComputerNameとConfigurationNameでその専用エンドポイントを選ぶ。サーバー:構成を登録利用者:サーバー名を指定KsHelpDesk構成を指定許可された操作だけを利用

図25: サーバー側の登録と利用者側の接続を別の作業として確認する。

接続や役割の解決で困ったときの確認場所

なお、役割機能ファイルは前述のとおりモジュールの RoleCapabilities フォルダー配下にある必要があり、そのモジュールが $env:PSModulePath から見つかることが前提です。RoleDefinitions で指定した役割名が解決できないときは、まず Get-Module -ListAvailable でモジュールが見えているかを確認してください。4

登録したエンドポイントは Get-PSSessionConfiguration で一覧できます。冒頭に書いたとおりJEAはリモート実行の構成が前提なので、接続できないときはJEAの定義ではなくWinRM側から切り分けてください(「PowerShell Remoting(WinRM)入門」)。

8. 展開前の実務チェックリスト

設定値を入れたことと、期待どおり動くことは別です。ログは新しいセッションでの4104と容量、共有は利用者権限での読み書き・削除、JEAは専用エンドポイントと公開する操作を、それぞれの節で確認してからチェックします。

表の「書き込み専用共有」は、3章の権限設計を指します。自分の記録の上書きまで防ぐ構成ではない点を、運用の判断に含めてください。

項目 優先度 状態
スクリプトブロックログ(4104)を有効化した 高 GPOで全社配布218
PowerShell関連ログのサイズを拡張した 高 既定のままだと数日で消える
トランスクリプションを有効化し、書き込み専用共有に集約した 高 端末ローカルに置かない2
PowerShell 2.0エンジンを無効化した 高 ログ・AMSIの迂回経路5
ウイルス対策製品がAMSIに対応している 高 既定で有効1
実行ポリシーをセキュリティ境界と誤解していない 中 署名は別の価値6
管理者権限の付与範囲を棚卸しした 高 被害範囲を決めるのは権限の広さ
定型作業をJEAで委任した 中 管理者権限の削減に直結4
WDAC/AppLockerを検討した 中 言語モード制限はこれとセット3
PowerShell 7側にもログ設定を配布した 中 5.1と7で設定は別
設定値と動作結果を対にして確認するログ、保存先、JEAは設定しただけでなく、それぞれ記録・権限・接続と操作を確認してから展開する。検証機で設定する新しいセッションでログ確認利用者権限で共有を確認専用エンドポイントで操作確認業務影響を確認して展開

図26: 管理者の設定画面だけでなく、実際に使う側の動作を確かめてから展開する。

9. まとめ

ここまでの判断を、導入時に見直す要点としてまとめます。

  • PowerShellの禁止は実効性が低く、業務を止めます。方針は「可視化と制限」です。
  • 最優先はスクリプトブロックログ(4104)の有効化。難読化されたコードも展開後の形で記録されます。ログサイズの拡張とセットで実施してください。
  • トランスクリプションは、利用者が消せない共有に集約します。ただし、自分の記録の上書きまで防ぐには、JEAの TranscriptDirectory やイベント転送など、利用者が触れない収集先を使います。
  • PowerShell 2.0エンジンは無効化します。ログもAMSIも効かない経路を残さないためです。
  • 実行ポリシーはセキュリティ境界ではありません。署名は改ざん検知として価値がありますが、防御の主軸にはしないでください。
  • 権限の広さが被害の広さを決めます。JEAで「必要な操作だけ」を委任すれば、管理者権限を配らずに運用が回せます。
禁止ではなく可視化と制限を重ねる実行内容を残し、記録を保全してから古い実行経路をなくし、必要な操作の委任と実行制限を組み合わせる。実行内容を残す残した記録を守る古い実行経路を残さない必要な操作だけを委任する業務影響を見て実行制限を重ねる

図27: 記録・保全・検査・委任・制限を、それぞれの役割に合わせて組み合わせる。

サンプルコードのダウンロード

この記事で扱ったコードは、そのまま動かせる形にまとめて配布しています。ログ設定の有効化と、JEAエンドポイントの作成が入っています。

サンプルコードをダウンロード(zip)

この記事のサンプルは、Windowsやテナントに依存するため実行検証はしていません。構文解析とPSScriptAnalyzerによる静的解析までは全ファイルに対して実施していますが、動作は必ずご自身の検証機で確認してください。

# 構文解析 + 静的解析(Windows以外でも実行できる)
./Invoke-SampleTests.ps1

設定値(パス、サーバー名、テナントIDなど)は例です。そのまま本番環境で実行せず、自社の環境に合わせて読み替えてください。

関連記事

関連する相談領域

合同会社小村ソフトでは、Windows運用環境のセキュリティ設定レビュー、権限委任(JEA)を含む運用設計、監査ログの整備と活用の相談を扱っています。

参考リンク

  1. Microsoft Learn, Antimalware Scan Interface (AMSI). AMSIがアプリケーションとサービスから任意のアンチマルウェア製品へコンテンツを渡して検査させる仕組みであること、PowerShellを含むWindowsのスクリプトエンジンが統合されており、難読化されたスクリプトも実行時の内容で検査できることについて。 ↩ ↩2 ↩3 ↩4 ↩5

  2. Microsoft Learn, about_Logging_Windows. モジュールログ・スクリプトブロックログ・トランスクリプションの3種類のログ機能、グループポリシーおよびレジストリ(HKLM\SOFTWARE\Policies\Microsoft\Windows\PowerShell配下)による有効化、スクリプトブロックログがイベントID 4104として難読化解除後のコードを記録すること、モジュールログがイベントID 4103として記録されること、トランスクリプションのOutputDirectoryやEnableInvocationHeaderの設定について。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8

  3. Microsoft Learn, about_Language_Modes. FullLanguage・ConstrainedLanguage・RestrictedLanguage・NoLanguageの各モードで利用できる言語要素、$ExecutionContext.SessionState.LanguageModeによる現在のモードの確認、WDACやAppLockerによるアプリケーション制御が有効な環境でPowerShellが制約付き言語モードで動作すること、手動での言語モード設定がセキュリティ機能として意図されていないことについて。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  4. Microsoft Learn, Just Enough Administration (JEA) の概要. JEAが管理者権限を付与せずに特定の管理作業のみを委任する仕組みであること、役割機能ファイル(.psrc)によるVisibleCmdlets・パラメーターとValidateSetによる制限、セッション構成ファイル(.pssc)のRestrictedRemoteServer・RunAsVirtualAccount・TranscriptDirectory・RoleDefinitions、Register-PSSessionConfigurationによる登録、JEAセッションで言語モードが制限されることについて。役割機能ファイルをPowerShellモジュールのRoleCapabilitiesフォルダーに配置する必要があること(モジュールとして検出できる状態にしておくこと、New-PSRoleCapabilityFileによる作成)はJEA Role Capabilitiesを参照。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8

  5. Microsoft Learn, Windows PowerShell 2.0 の非推奨化. Windows PowerShell 2.0エンジンが非推奨であり、無効化が推奨されること、Windowsのオプション機能として提供され有効・無効を切り替えられることについて。 ↩ ↩2 ↩3

  6. Microsoft Learn, about_Execution_Policies. 実行ポリシーがセキュリティ境界ではなく、ユーザーが意図せずスクリプトを実行することを防ぐための安全機能であること、複数の回避手段が存在することについて。 ↩ ↩2 ↩3

  7. Microsoft Learn, about_Group_Policy_Settings. PowerShell 7のグループポリシーが「管理用テンプレート > PowerShell Core」に置かれること、管理用テンプレートをインストールする必要があること、Windows PowerShell側のポリシー値を使用する選択肢について。 ↩ ↩2

  8. Microsoft Learn, about_Logging_Windows (PowerShell 7). PowerShell 7のイベントログが PowerShellCore/Operational であること、Windowsでは書き込み前にイベントプロバイダーの登録が必要であり、管理者権限で $PSHOME\RegisterManifest.ps1 を使用することについて。 ↩

  9. Microsoft Learn, icacls. ファイルとフォルダーのアクセス制御リストを表示・変更するコマンドであること、/grantによる権限付与と/remove:gによる付与済み権限の削除、権限マスクに指定できる詳細な権限の記号(DE=削除、DC=サブフォルダーとファイルの削除、WD=データの書き込み/ファイルの作成、AD=データの追加/サブフォルダーの作成、WA=属性の書き込み、WEA=拡張属性の書き込み、RA=属性の読み取り、X=実行/走査、F=フルアクセスなど)、継承の指定((OI)=オブジェクト継承、(CI)=コンテナー継承)について。共有の作成とアクセス許可の指定はNew-SmbShare(-FullAccess / -ChangeAccess / -ReadAccess)を、継承の無効化はObjectSecurity.SetAccessRuleProtection(第1引数で継承を保護し、第2引数で継承済みACEを引き継ぐかを指定する)を参照。このメソッドが指定できるのは継承ACEの扱いだけで、そのオブジェクトに直接付与されたACEは対象外です。直接付与を落とすにはObjectSecurity.RemoveAccessRuleSpecificなどで個別に外します。 ↩ ↩2 ↩3

  10. Microsoft Learn, 特殊なIDグループ(Special Identity Groups). OWNER RIGHTS(S-1-3-4)がオブジェクトの現在の所有者を表すグループであり、このSIDを持つACEがオブジェクトに適用されているとき、システムは所有者への暗黙の READ_CONTROL と WRITE_DAC を無視することについて。裏を返すと、このACEが無ければ所有者は暗黙にDACLを書き換えられます。 ↩ ↩2

  11. Microsoft Learn, File Access Rights Constants. FILE_APPEND_DATA が「For a file object, the right to append data to the file. (For local files, write operations will not overwrite existing data if this flag is specified without FILE_WRITE_DATA.)」と定義されており、追記専用の書き込みは「FILE_WRITE_DATA を伴わずに FILE_APPEND_DATA を指定して開いた」ときに成立すること、FILE_WRITE_DATA / FILE_ADD_FILE、FILE_DELETE_CHILD の意味について。ジェネリックアクセス権のマッピング(FILE_GENERIC_WRITE = FILE_APPEND_DATA + FILE_WRITE_ATTRIBUTES + FILE_WRITE_DATA + FILE_WRITE_EA + STANDARD_RIGHTS_WRITE + SYNCHRONIZE、FILE_GENERIC_READ = FILE_READ_ATTRIBUTES + FILE_READ_DATA + FILE_READ_EA + STANDARD_RIGHTS_READ + SYNCHRONIZE)はFile Security and Access Rightsを参照。 ↩ ↩2

  12. PowerShell, TranscriptionOption.FlushContentToDisk(src/System.Management.Automation/engine/hostifaces/MshHostUserInterface.cs). トランスクリプトの書き込み口が new FileStream(this.Path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read) で開かれ、失敗した場合のみ new FileStream(this.Path, FileMode.Append, FileAccess.Write, FileShare.Read) で開き直されること、FILE_APPEND_DATA を単独で要求する経路が無いことについて。 ↩

  13. .NET, SafeFileHandle.Open(src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs). FileAccess.Read / FileAccess.Write がそれぞれ GENERIC_READ / GENERIC_WRITE に写されること、FileMode.Append が CreateFile を呼ぶ前に FileMode.OpenOrCreate へ置き換えられるだけで、アクセスマスクに FILE_APPEND_DATA 単独が使われることはないことについて。 ↩

  14. Microsoft Learn, JEA セッション構成. セッション構成ファイルの TranscriptDirectory にフォルダーを指定すると自動でトランスクリプトが記録されること、「Transcripts are written to the folder by the Local System account, which requires read and write access to the directory. Standard users should have no access to the folder. Limit the number of security administrators that have access to audit the transcripts.」(標準ユーザーはこのフォルダーへのアクセスを持つべきではなく、監査する管理者だけに絞る)について。 ↩ ↩2

  15. Microsoft Learn, JEA の前提条件. JEAがPowerShell 5.0以降で利用できること、PowerShellリモート処理がJEAの土台であり、JEAを使う前にリモート処理が有効かつ適切に保護されている必要があること、Windows Server 2012以降ではPowerShellリモート処理が既定で有効であり、有効でない場合は管理者権限のPowerShellウィンドウでEnable-PSRemotingを実行して有効化できることについて。 ↩ ↩2 ↩3

  16. Microsoft Learn, Differences between Windows PowerShell 5.1 and PowerShell 7.x. Get-EventLog を含む *-EventLog コマンドレットがPowerShell 7の標準コマンドから削除され、Windowsでのイベントの取得には Get-WinEvent を使用することについて。 ↩

  17. Microsoft Learn, JEA Role Capabilities. 独自関数をFunctionDefinitionsで定義したうえでVisibleFunctionsにも名前を挙げる必要があること(「Don’t forget to add the name of your custom functions to the VisibleFunctions field so they can be run by the JEA users.」)、関数の本体(スクリプトブロック)がシステム既定の言語モードで実行されJEAの制約を受けないこと、JEAが差し替える制約付きコマンドを本来の実装で使うには完全修飾名(Microsoft.PowerShell.Utility\Select-Object)が必要なこと、関数が多い場合はスクリプトモジュールに切り出してVisibleFunctionsで公開する方法、モジュールフォルダーにフォルダーと同名のファイルが必要なことについて。 ↩ ↩2

  18. Microsoft Learn, Set-ItemProperty. レジストリプロバイダー使用時に-Typeが動的パラメーターとして追加され、レジストリ値のデータ型(String / ExpandString / Binary / DWord / MultiString / QWord など)を指定できることについて。値が存在しない場合に作成されることを含む。 ↩

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

セキュリティ対策として、社内でPowerShellを禁止すべきでしょうか?
実効性が低く、副作用が大きいため推奨しません。PowerShellはWindowsの管理基盤そのもので、実体は.NET上の機能です。実行ファイルをブロックしても、同じAPIを別の手段から呼ぶことは可能なため、攻撃側にとっての障害にはなりにくい一方、正規の管理業務と自動化は確実に止まります。現実的な方針は、禁止ではなく「可視化と制限」です。スクリプトブロックログで何が実行されたかを記録し、古いバージョンを無効化し、必要に応じて言語モードやJEAで実行できる範囲を絞ります。
スクリプトブロックログを有効にすると、ログが大量に出て困りませんか?
確かに増えるので、ログのサイズ設定とセットで有効化してください。Microsoft-Windows-PowerShell/Operationalログの最大サイズを既定のままにしておくと、短期間で上書きされて肝心なときに残っていない、という事態になります。運用としては、ログサイズを十分に確保し、必要ならSIEMやイベント転送で集約します。なお、より詳細な「呼び出しの開始・停止」まで記録する設定もありますが、こちらは出力量が非常に多いため、通常は既定のスクリプトブロックログのみを有効にします。
実行ポリシーをAllSignedにすれば、セキュリティ対策になりますか?
実行ポリシーはセキュリティ境界ではありません。公式ドキュメントも、ユーザーが意図せず危険なスクリプトを実行することを防ぐための仕組みであって、悪意ある操作を止めるものではないと明記しています。回避方法が複数存在するためです。署名運用には、配布物の完全性を確認できるという別の価値がありますが、防御の主軸はログによる可視化、AMSIによる検査、言語モードやJEAによる権限の制限に置いてください。
制約付き言語モード(ConstrainedLanguage)は、手動で設定してよいものですか?
環境変数などで手動設定するのは推奨されません。回避が容易で、セキュリティ機能として機能しないためです。制約付き言語モードは、WDAC(Windows Defender Application Control)やAppLockerによるアプリケーション制御を構成した結果として、PowerShellが自動的に切り替わる形で使うのが本来の設計です。アプリケーション制御を導入せずに言語モードだけを絞っても、実効性のある防御にはなりません。
ヘルプデスク担当者に、サーバーの特定サービスの再起動だけを任せたいのですが。
JEA(Just Enough Administration)がその用途のための仕組みです。役割機能ファイルで「このコマンドの、このパラメーターの、この値だけ許可する」と定義し、セッション構成として登録します。利用者は管理者権限を持たないまま、仮想アカウント経由で許可された操作だけを実行できます。セッションは制限付きリモートサーバーとして構成され、既定では言語モードも制限されるため、任意のコードを実行される余地がありません。実行内容はトランスクリプトとして記録できます。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る