PowerShell 的安全性強化 ── 日誌・AMSI・語言模式・JEA

· · PowerShell, 資訊安全, Windows, 日誌, 稽核, 資訊系統, 權限管理, 維運改善

由於「入侵發生時 PowerShell 遭到濫用」的報告不斷出現,有些組織因此打算在公司內全面禁止使用 PowerShell。然而無論就實際效果還是業務影響來看,這都不划算。PowerShell 本身就是 Windows 的管理基礎架構,一旦停用,維運自動化就會跟著停擺;另一方面,攻擊者依然可以透過其他手段做到同樣的事。

現實可行的方針是可視化與限制,而不是禁止。所幸 PowerShell 5.0 以後已具備一整套供防守方使用的功能:即使程式碼經過混淆,也能以展開後的形式記錄下來的指令碼區塊記錄;在執行前將內容交給防毒軟體檢查的 AMSI(Antimalware Scan Interface,讓防毒軟體檢查指令碼內容的 Windows 機制);限制可執行語法本身的語言模式;以及「只委派必要操作」的 JEA。將這些組合起來,就能在不影響業務的情況下,打造出可稽核的狀態。

本文將依效果高低排序,整理在公司內部的 Windows 環境中持續安全使用 PowerShell 所應設定的項目。執行原則與簽署的部分已在「PowerShell 的執行原則與指令碼簽署」中處理過,本文將接續處理其後的內容。

適用環境・前提條件

項目 內容
適用作業系統 Windows 10/11、Windows Server。AMSI 以 Windows 10 以後為前提1
適用版本 本文同時涵蓋 Windows PowerShell 5.1 與 PowerShell 7。原則的設定機碼與日誌的記錄位置在 5.1 與 7 中並不相同,因此兩者都已安裝的裝置必須兩邊都設定(第 3 章・第 4 章)
所需權限 第 3 章〜第 4 章的日誌設定・停用 PowerShell 2.0,以及第 7 章的 JEA 端點註冊,都需要系統管理員權限。實務上不會一台一台手動安裝,而是透過群組原則發布
本機/遠端 第 3 章〜第 6 章在目標裝置・伺服器內部即可完成。只有第 7 章的 JEA 以 PowerShell 遠端處理(WinRM)已啟用為前提(參見第 7 章開頭)
本文不涉及的部分 執行原則與簽署的細節請參閱另一篇文章(PowerShell 的執行原則與指令碼簽署)。本文僅整理其定位(第 5 章)

1. 先講結論

  • 最優先事項是啟用指令碼區塊記錄。執行過的程式碼會被記錄到事件記錄中,即使是經過混淆的程式碼,也會以展開後的形式留存(事件 ID 4104)。2
  • 同時也要啟用逐字稿記錄。包含輸入輸出在內的工作階段記錄,可以彙整到唯寫的共用資料夾中。2
  • 啟用日誌之後,也要重新檢視日誌的容量設定。若維持預設值,短時間內就會被覆寫。2
  • 透過 AMSI,執行前的指令碼會被傳遞給防毒軟體。自 PowerShell 5.0、Windows 10 以後即已可用。1
  • 應停用舊版的 PowerShell 2.0 引擎。若仍保留該引擎,就會留有切換到日誌與 AMSI 都不生效的舊引擎的空間。3
  • 執行原則並不是安全邊界。官方文件已明確說明。請勿將其作為防禦的主軸。4
  • 語言模式應作為 WDAC/AppLocker 的結果來使用。手動設定可被繞過,無法發揮安全性功能的作用。5
  • 權限委派請使用 JEA。只允許「這個指令的這個參數」,不必分配系統管理員權限就能推動維運工作。6

2. 要保護的是什麼 ── 先可視化,後限制

本文所探討的四項對策,分別負責不同的層級。首先來看整體樣貌。

管理員・服務台・自動化指令碼(以及入侵的攻擊者)【委派】JEA(第 7 章)不分配系統管理員權限,只交出已許可的操作【限制】語言模式 + WDAC/AppLocker(第 6 章)限縮可執行的程式碼與可用語法【檢查】AMSI(第 4 章)將執行前的內容傳遞給防毒軟體透過停用 PowerShell 2.0 堵住繞過路徑以 PowerShell 進行的操作【記錄】指令碼區塊記錄 4104・逐字稿記錄(第 3 章)以解除混淆後的形式留存

記錄的作用是打造「事後可追溯」的狀態,委派則是縮小權限本身,檢查與限制則是在執行之前就加以攔阻。這四項對策彼此並非替代關係,而是各自負責不同的層級。在此基礎上,導入的順序仍有優先次序。

階段 要做的事 效果
1. 可視化 指令碼區塊記錄、逐字稿記錄 能掌握發生了什麼事,使事後調查成為可能
2. 基礎性的無害化 停用 PowerShell 2.0、維持最新版本、確認 AMSI 已啟用 堵住繞過檢查的路徑
3. 限縮權限 透過 JEA 委派、削減系統管理員權限 限縮受害範圍
4. 限制執行 WDAC/AppLocker + 限制語言模式 直接阻止未經核准的程式碼執行

在許多現場,光是做到 1 與 3,狀況就會大幅改善。4 的導入成本較高,是需要一邊驗證業務影響一邊推進的領域。

3. 啟用日誌 ── 4104 最為重要

PowerShell 的日誌有三個層級。2

種類 記錄內容 事件 ID
模組記錄 指定模組的管線執行詳情 4103
指令碼區塊記錄 執行過的程式碼文字(解除混淆後) 4104
記錄目的地日誌 5.1 為 Microsoft-Windows-PowerShell/Operational,7 為 PowerShellCore/Operational
逐字稿記錄 將工作階段的輸入輸出記錄到文字檔案 ─(檔案輸出)

以上皆可在群組原則的「電腦設定 > 系統管理範本 > Windows 元件 > Windows PowerShell」中設定,也可以直接透過登錄檔設定。2

# 啟用指令碼區塊記錄(需要系統管理員權限。通常透過 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

PowerShell 7 看的並不是 Windows\PowerShell 底下,而是 PowerShellCore 底下的原則。如果只設定了 5.1 端就以為完成,以 pwsh 執行的指令碼記錄就會整個缺漏。無論是指令碼區塊記錄還是逐字稿記錄,都請在兩邊的機碼寫入相同的值(透過 GPO 發布時,也要分別在各自的範本中設定)。

指令碼區塊記錄的價值在於對混淆手法的抵抗力。即使是以 Base64 編碼的命令,或是透過字串串接組成的程式碼,執行當下展開後的內容都會被記錄下來。2 攻擊調查時能否還原「究竟執行了什麼」,取決於是否啟用了這項設定。

查驗記錄要用 Get-WinEvent(參見「用 Get-WinEvent 實務調查事件記錄檔」)。

# 確認最近一天的指令碼區塊記錄。
# 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

判斷是否成功啟用,依下列三點來進行。

  1. 設定完成後,開啟一個新的 PowerShell 工作階段。指令碼區塊記錄只會從啟用之後才開始的工作階段起被記錄。2 請不要在已經開啟的視窗中測試,並因此誤判為「沒有記錄」。
  2. 在新工作階段中執行一個容易辨識的命令,確認是否能以 4104 的形式返回。若上面的查詢能返回包含剛才執行的程式碼片段的事件,就代表已生效。若一筆都沒有返回,請用 Get-ItemProperty 確認原則機碼的值(EnableScriptBlockLogging 是否為 1),以及究竟是寫入了 5.1 端還是 7 端的機碼。
  3. 記錄目的地日誌是否選錯。若是以 pwsh 執行,記錄目的地就是 PowerShellCore/Operational。只看 Microsoft-Windows-PowerShell/Operational 就誤判為「沒有記錄」,是這項設定中最常見的誤解。

啟用之後務必重新檢視日誌容量。若維持預設容量,幾小時到幾天內就會被寫滿一輪覆寫,等到真正需要時反而沒有留存。

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

wevtutil sl 即使成功也不會顯示任何內容,因此比對設定前後的 MaximumSizeInBytes 是唯一的確認方式。若不是以系統管理員權限執行,變更會被拒絕,值若沒有改變,請先從這一點開始懷疑。

將逐字稿的彙整位置設為「能寫入但無法刪除」的共用資料夾

逐字稿的輸出位置,重點在於設為使用者能寫入、但無法刪除的共用資料夾。若放在本機,一旦裝置遭入侵,記錄就會被刪除。

權限需要同時設定共用存取權限與 NTFS 存取權限(有效權限會取兩者中較嚴格的一方)。重點在於,只給使用者「在資料夾中建立檔案」與「讀寫自己建立的檔案」這兩項權限,不給予刪除(DE)與刪除子資料夾・檔案(DC)的權限

在此先明確說明這種組態能保護什麼、不能保護什麼。能守住的有兩點:「完全無法碰觸他人的記錄」以及「自己的記錄也無法刪除」。守不住的是「覆寫自己的記錄」,這一點光靠 ACL 無法堵住(原因後述)。即便如此,光是讓遭到接管的帳戶無法刪除他人的證據,對受害範圍的釐清就會有很大的差別。即使有一台裝置淪陷,其他裝置的記錄依然會留存下來。

# 在日誌彙整伺服器端執行(需要系統管理員權限)
$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,再一次套用。
# 若把「清空」和「重新加入」拆成兩次 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(附加),但那樣逐字稿將一行都寫不進去(後述)
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        # 確認設定結果

若沿用既有的資料夾,務必徹底清查直接附加的 ACE。SetAccessRuleProtection($true, $false) 拿掉的只是繼承而來的 ACE,直接附加在該資料夾上的 ACE 會原封不動地保留下來。7 New-Item -Force 即使對既有資料夾也會成功,因此如果重複使用一個曾經有過「以前暫時把變更權限加給 Domain Users」這類經歷的資料夾,即使之後把 CREATOR OWNER 限縮下來,那筆直接附加的權限依然會存活,使用者仍然能覆寫・刪除他人的逐字稿。上面的程式碼之所以先全部拿掉再重新加入,正是為此。若是建立全新的資料夾則不需要這道手續,但加上這道手續也不會有損失。套用之後請親眼確認 icacls $path 的輸出,檢查是否列出了非預期的主體。

這裡 (OI) 的加法決定了對策成敗。若在給使用者的許可上加上 (OI) 並分配 WD,該 ACE 就會繼承給底下的所有檔案,只要知道檔名,就能覆寫・截斷他人的逐字稿。7 那樣一來,「即使裝置或帳戶遭到接管,記錄依然留存」的目的就無法達成。因此如上所示,將對資料夾的建立權限(不繼承)與透過 CREATOR OWNER 只對自己建立的檔案進行讀寫分開處理。

而且請不要漏掉 OWNER RIGHTS 這一行。建立檔案的使用者,會成為該檔案的擁有者。因為 Windows 會隱含賦予擁有者 READ_CONTROLWRITE_DAC,8 所以即使 ACE 沒有分配 DE,擁有者依然能自行改寫 DACL,重新賦予自己刪除的權限。若維持「自己的記錄自己能刪除」的狀態,限縮 CREATOR OWNER 就毫無意義。放入 OWNER RIGHTS(S-1-3-4)的 ACE 之後,系統就會忽略對擁有者隱含賦予的 READ_CONTROL / WRITE_DAC,到這裡「能寫入但無法刪除」才真正成立。8

為什麼不只限制為 AD(附加)

或許會覺得「既然不想被覆寫,CREATOR OWNER 只給 AD 就好」。但這樣一來,逐字稿將一行都留不下來。

在 Windows 上,「只能附加」之所以成立,僅限於寫入者以單獨指定 FILE_APPEND_DATA 的方式開啟檔案的情況。Microsoft 的參考文件將 FILE_APPEND_DATA 定義為「(對本機檔案而言,若在不伴隨 FILE_WRITE_DATA 的情況下指定此旗標,寫入操作將不會覆寫既有資料)」。9 也就是說,唯附加是開啟方式的性質,並非只靠在 ACE 中放入 AD 就能實現。

Start-Transcript 的寫入端,並不會以唯附加的方式開啟。PowerShell 的實作是先以 FileMode.OpenOrCreate + FileAccess.ReadWrite 開啟,只有在失敗時才會改以 FileMode.Append + FileAccess.Write 重新開啟。10 在 .NET 端,FileAccess.Read/Write 會分別映射為 GENERIC_READ/GENERIC_WRITE,而 FileMode.Append 內部也只是先替換為 FileMode.OpenOrCreate,再將游標移到檔尾而已。11 並不存在單獨要求 FILE_APPEND_DATA 的路徑。

FILE_GENERIC_WRITE 包含 FILE_WRITE_DATAFILE_WRITE_ATTRIBUTESFILE_WRITE_EAREAD_CONTROLSYNCHRONIZE,FILE_GENERIC_READ 則包含 FILE_READ_DATAFILE_READ_ATTRIBUTESFILE_READ_EAREAD_CONTROLSYNCHRONIZE9 存取檢查看的是所要求的所有權利,因此只擁有 AD,RA,REA 的使用者,這個 CreateFile 會被拒絕。即使共用端允許變更(Change),也會在 NTFS 端被擋下。本來是想保護記錄,結果卻變成連記錄本身都停止了。

因此本節的 ACL 並非單獨使用 AD,而是設計成「允許讀寫,但不給予 DE(刪除)・WDAC(變更權限)・WO(變更擁有者)」的形式。擁有者能夠覆寫・截斷自己的逐字稿。請認清這一點無法只靠 ACL 堵住。

若想連覆寫都堵住,就只能輸出到寫入者無法碰觸的位置。有三個選項。

  • 若使用 JEA,可以用工作階段組態檔案的 TranscriptDirectory(第 7 章)。寫入這項輸出的並非連線進來的使用者,而是 Local System,Microsoft 的文件也要求「標準使用者不應擁有這個資料夾的存取權」「僅限稽核用的安全性管理員」。12 由於使用者原本就不需要權限,本節的煩惱就此消失
  • 匯入 Windows 事件轉送或 SIEM。指令碼區塊記錄(4104)會出現在事件記錄中,因此可以作為轉送對象
  • 建立一個以唯附加方式開啟的自訂收集處理程序。只要自行寫到單獨要求 FILE_APPEND_DATA 開啟的程度,ACE 端的 AD 才會第一次具有意義

另外,ACL 所能保護的,終究只是防範「寫入該共用資料夾的使用者」。檔案伺服器的管理員可以取得擁有權,若裝置端已被完全掌控,攻擊者甚至能在寫入共用資料夾之前就動手腳。請將本節的權限設計理解為,只保證「使用同一個共用資料夾的使用者之間無法互相刪除」。

icacls 的權限代號(WDADDEDC 等)與繼承指定((OI)(CI)(IO))的意義,Windows 命令參考中有完整清單。7 另外,這套組態務必先在驗證機上試過一次,再進行部署。由於逐字稿是由執行中的工作階段持續寫入同一個檔案,若權限限縮得太緊,記錄本身就會失敗。實際確認「使用者的裝置能夠寫入」「使用者無法讀取・改寫他人既有的檔案」「使用者無法刪除自己的檔案」這三點,才是確實的做法。

4. 堵住繞過路徑 ── PowerShell 2.0 與 AMSI

透過 AMSI(Antimalware Scan Interface),在 PowerShell 5.0 以後,指令碼的內容會在執行前一刻傳遞給防毒軟體,並以解除混淆後的狀態接受檢查。1 只要使用包含 Microsoft Defender 在內的支援產品,不需額外設定即可運作。

問題在於,舊版引擎並不具備這套機制。若 Windows PowerShell 2.0 引擎仍維持啟用,就會留有透過 powershell.exe -Version 2 切換到日誌與 AMSI 都不生效的環境的空間。這項功能已經是非建議使用,建議加以停用。3

# 確認 PowerShell 2.0 引擎的狀態並將其停用(需要系統管理員權限,有時需要重新開機)
Get-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2* |
    Select-Object FeatureName, State

Disable-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2Root -NoRestart

另外,若已導入 PowerShell 7(pwsh),設定機碼與日誌記錄目的地都與 5.1 不同(原則位於 PowerShellCore 底下,日誌則是 PowerShellCore/Operational)。在兩者並存的環境中,請對兩邊都執行設定、日誌容量調整與調查查詢。關於版本共存,請參閱「Windows PowerShell 5.1 與 PowerShell 7 的差異」。

5. 別搞錯執行原則的定位

在此重新明確說明一次。執行原則並不是安全邊界。官方文件明確指出,這是為了防止使用者無意間執行指令碼的安全性功能,並非用來防止惡意操作的機制。4

話雖如此,它並非毫無價值。簽署制度另有一種價值,那就是「能夠確認發布的模組未遭竄改」(參見「PowerShell 模組的公司內部發布與更新」)。重要的是正確理解其角色再加以運用,「設成 AllSigned 就安全了」這種認知才是最危險的。

6. 語言模式 ── 與應用程式控制搭配使用

PowerShell 的工作階段擁有語言模式,可用的語言元素會受到限制。5

模式 可用內容 實務上的定位
FullLanguage 所有語言元素(預設) 一般工作階段
ConstrainedLanguage 所有 Cmdlet 都能運作,迴圈・條件分支・字串展開・屬性參照也都能使用。但可用的 .NET 型別限定於許可清單,Add-Type 只能載入已簽署的組件 在 WDAC/AppLocker 底下自動切換的模式。互動操作與一般管理工作大致上都能照常進行
RestrictedLanguage 可以執行命令,但無法使用指令碼區塊。變數僅限 $PSCulture$PSUICulture$true$false$null,比較運算子僅限 -eq-gt-lt,不可進行指派・屬性參照・方法呼叫 用於讀取模組資訊清單(.psd1)的模式,並非設計給人以互動方式使用
NoLanguage 指令碼語言本身遭停用。無法使用指令碼與變數,只能呼叫 Cmdlet 與原生命令 JEA 工作階段組態(RestrictedRemoteServer)的預設值。用於建立「只能執行既定命令」窗口的模式

這三種限制模式並非階段性的關係,而是用途不同。ConstrainedLanguage 的作用是「在人們撰寫並使用指令碼的環境中,只阻止危險型別的使用」,而NoLanguage 則是「一開始就不讓人寫指令碼」,後者只有在像 JEA 這種受限窗口中才能成立。5

目前的模式可以用下面的方式確認。

$ExecutionContext.SessionState.LanguageMode

重要的是設定方式。限制語言模式的運作方式,是在透過 WDAC(Windows Defender Application Control)或 AppLocker 組態允許清單方式的應用程式控制之後,由 PowerShell 自動切換而成。5 透過環境變數等方式手動設定,很容易被繞過,無法發揮安全性功能的作用。請理解若不導入應用程式控制,只單獨限縮語言模式,是投入的心力與效果不成比例的做法

7. JEA ── 委派「僅限必要的操作」

在現實中左右受害程度的,多半是權限的廣度。若處於「把系統管理員權限交給服務台」「維運人員全員都在 Domain Admin 群組中」這種狀態,一台裝置遭入侵就會演變成全公司遭入侵。

JEA(Just Enough Administration)是一種不交出系統管理員權限、只委派特定操作的機制。6 非常適合「只想把應用程式服務的重新啟動工作交給服務台」這類需求。

只有這一章的前提不同 ── JEA 建立在遠端處理之上

第 3 章〜第 6 章都能靠本機設定完成,相對地,JEA 本身就是建立在 PowerShell 遠端處理(WinRM)這套機制之上13 所謂「以 JEA 委派」,指的是在目標伺服器上註冊一個專用的連線目的地(工作階段組態),並讓使用者連線到那裡。自家公司是否能夠重現,首先就取決於這一點。

所需要的有以下三點。

前提 內容
PowerShell 版本 JEA 可在 PowerShell 5.0 以後使用13
啟用遠端處理 目標伺服器上的 PowerShell 遠端處理必須已啟用。Windows Server 2012 以後預設為啟用,若未啟用,則以系統管理員權限的 PowerShell 執行 Enable-PSRemoting13
部署位置 角色功能檔案(.psrc)須置於目標伺服器上模組的 RoleCapabilities 資料夾,工作階段組態(.pssc)則在目標伺服器上註冊。並非放在使用者裝置上的東西6

也就是說,以下的步驟 1〜3,都是在被委派的那一端伺服器上,以系統管理員權限進行的作業。使用者裝置端所需要的,只有能夠連線到那台伺服器而已。遠端處理本身的組態與安全設定,已整理在「PowerShell Remoting(WinRM)入門」中。

步驟 1:用角色功能檔案(.psrc)定義允許的操作

角色功能檔案必須放置在 PowerShell 模組的 RoleCapabilities 資料夾中。若只是建立資料夾,並不會被辨識為模組,後述的 RoleDefinitions 也無法用名稱解析到它。因此,首先要建立作為容器的模組(擁有資訊清單的資料夾)。6

# 建立用來存放角色功能的模組(需要資訊清單)
$moduleRoot = 'C:\Program Files\WindowsPowerShell\Modules\KsJea'
$null = New-Item -Path "$moduleRoot\RoleCapabilities" -ItemType Directory -Force
# 模組資料夾中,至少需要一個與資料夾同名的檔案
$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
# 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 = @()
}

光靠 VisibleCmdletsValidateSet,無法安全地限縮會改變狀態的命令。透過 ParametersValidateSet 進行的限制,只有在該參數實際被繫結時才會被評估。由於 Restart-Service 能透過管線接收 ServiceController,

Get-Service WinRM | Restart-Service

若這樣寫,-Name 就不會被繫結,ValidateSet 就會被跳過。原本以為「只能重新啟動 KsAppServiceSpooler」的端點,結果變成任何服務都能重新啟動

同樣的事情在其他參數集中也會發生。即使替 Get-WinEventLogName 加上 ValidateSet,

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

若這樣寫,LogName 就不會被繫結,限制也就不會生效。由於 JEA 工作階段是以虛擬管理員身分執行,這種情況下甚至能讀取到 Security 日誌。

因此上面的範例並未將 Restart-Service 放入 VisibleCmdlets,而是只公開只能透過引數接收目標的包裝函式(Restart-KsAppService)。只要輸入路徑只剩引數這一條,就沒有繞過的空間。

透過 ParametersValidateSet 進行的限制,只有在「該參數被繫結時」才會生效。對於能透過管線輸入或其他參數集迴避繫結的命令,限制本身就會失效。原則上,想限縮引數的命令都應該用包裝函式包起來,請這樣理解。

這裡有兩點容易踩到的規格。第一,VisibleFunctions 不會建立函式。只是列出名稱的函式並不存在於工作階段中,使用者看到的會是「沒有這個命令」。自訂函式要先用 FunctionDefinitions 定義,再同時列在 VisibleFunctions 中。14 若數量增多,拆分成指令碼模組,再透過 VisibleFunctions 公開該模組的函式,會更容易管理。

第二,函式本體不受 JEA 的限制。14 若想以本來的行為使用 Select-Object 這類 JEA 會替換掉的受限命令,就要像上面那樣以 Microsoft.PowerShell.Utility\Select-Object 的完整命名方式呼叫。反過來說,這也代表函式內部什麼都做得到,因此絕對要避免把來自使用者的輸入直接傳給 Invoke-Expression 這類寫法。

步驟 2:用工作階段組態檔案(.pssc)定義要把哪個角色分配給誰

# SessionType: 受限遠端伺服器(預設為 NoLanguage)
# RunAsVirtualAccount: 以虛擬管理員帳戶執行
# TranscriptDirectory: 記錄執行內容
$pssc = @{
    Path                = '.\KsHelpDesk.pssc'
    SessionType         = 'RestrictedRemoteServer'
    RunAsVirtualAccount = $true
    TranscriptDirectory = 'C:\JeaTranscripts'
    RoleDefinitions     = @{ 'EXAMPLE\HelpDesk' = @{ RoleCapabilities = 'KsHelpDesk' } }
}
New-PSSessionConfigurationFile @pssc

步驟 3:進行註冊

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

使用者端則以下列方式連線。

Enter-PSSession -ComputerName 'appsrv01' -ConfigurationName 'KsHelpDesk'
# 只能使用已許可的命令。Restart-Service 也僅限指定的服務

JEA 的要點有三個。6

  • 使用者不擁有系統管理員權限。執行是在虛擬帳戶端進行的
  • 工作階段以受限遠端伺服器的形式組態。預設語言模式受到限制,無法執行任意程式碼
  • 透過逐字稿記錄執行內容。可以稽核誰做了什麼

另外,如前所述,角色功能檔案必須位於模組的 RoleCapabilities 資料夾底下,並以該模組能從 $env:PSModulePath 被找到為前提。若 RoleDefinitions 中指定的角色名稱無法解析,請先用 Get-Module -ListAvailable 確認該模組是否可見。6

已註冊的端點可以用 Get-PSSessionConfiguration 列出。如同開頭所述,JEA 是以遠端執行的組態為前提,因此若無法連線,請先從 WinRM 端而不是 JEA 的定義開始排查(參見「PowerShell Remoting(WinRM)入門」)。

8. 實務檢核清單

項目 優先度 狀態
已啟用指令碼區塊記錄(4104) 透過 GPO 全公司發布215
已擴增 PowerShell 相關日誌的容量 維持預設值,幾天內就會被清除
已啟用逐字稿記錄,並彙整到唯寫共用資料夾 不放在裝置本機2
已停用 PowerShell 2.0 引擎 日誌・AMSI 的繞過路徑3
防毒軟體支援 AMSI 預設為啟用1
沒有把執行原則誤認為安全邊界 簽署另有其價值4
已盤點系統管理員權限的授予範圍 決定受害範圍的是權限的廣度
已透過 JEA 委派例行作業 直接關係到削減系統管理員權限6
已評估 WDAC/AppLocker 語言模式限制須與此搭配5
也已將日誌設定發布到 PowerShell 7 端 5.1 與 7 的設定不同

9. 總結

  • 禁止 PowerShell 的實際效果不高,反而會使業務停擺。方針應該是「可視化與限制」。
  • 最優先事項是啟用指令碼區塊記錄(4104)。即使是經過混淆的程式碼,也會以展開後的形式記錄下來。請務必連同擴增日誌容量一起實施。
  • 逐字稿記錄的要點,在於彙整到使用者無法刪除的共用資料夾中。
  • 應停用 PowerShell 2.0 引擎,以免留下日誌與 AMSI 都不生效的路徑。
  • 執行原則並不是安全邊界。簽署作為竄改偵測手段有其價值,但請不要把它當作防禦的主軸。
  • 權限的廣度決定了受害的廣度。透過 JEA 只委派「必要的操作」,就能在不分配系統管理員權限的情況下推動維運工作。

範例程式碼下載

本文所提到的程式碼,已整理成可直接執行的形式提供下載。內含日誌設定的啟用,以及 JEA 端點的建立。

下載範例程式碼(zip)

本文的範例因為依賴 Windows 與租用戶環境,並未實際執行驗證。雖然已對所有檔案進行語法解析與 PSScriptAnalyzer 的靜態分析,但實際運作請務必在您自己的驗證機上確認。

# 語法解析 + 靜態分析(非 Windows 環境也能執行)
./Invoke-SampleTests.ps1

設定值(路徑、伺服器名稱、租用戶 ID 等)僅為範例。請勿直接在正式環境中執行,請依照貴公司的環境自行調整。

相關文章

相關諮詢領域

合同會社小村軟體(KomuraSoft LLC)承接 Windows 維運環境的安全性設定審查、包含權限委派(JEA)在內的維運設計,以及稽核日誌的整備與運用等相關諮詢。

參考連結

  1. Microsoft Learn, Antimalware Scan Interface (AMSI)。關於 AMSI 是讓應用程式與服務能夠將內容傳遞給任意防毒軟體進行檢查的機制,以及包含 PowerShell 在內的 Windows 指令碼引擎已與其整合,即使是經過混淆的指令碼也能以執行時的內容接受檢查的說明。  2 3 4

  2. Microsoft Learn, about_Logging_Windows。關於模組記錄・指令碼區塊記錄・逐字稿記錄這三種日誌功能、透過群組原則與登錄檔(位於 HKLM\SOFTWARE\Policies\Microsoft\Windows\PowerShell 底下)進行啟用、指令碼區塊記錄以事件 ID 4104 記錄解除混淆後的程式碼、模組記錄以事件 ID 4103 記錄,以及逐字稿記錄的 OutputDirectory 與 EnableInvocationHeader 設定的說明。  2 3 4 5 6 7 8 9

  3. Microsoft Learn, Windows PowerShell 2.0 的淘汰。關於 Windows PowerShell 2.0 引擎已不建議使用,建議加以停用,以及它是以 Windows 選用功能的形式提供、可切換啟用・停用的說明。  2 3

  4. Microsoft Learn, about_Execution_Policies。關於執行原則並非安全邊界,而是為了防止使用者無意間執行指令碼的安全性功能,以及存在多種繞過方式的說明。  2 3

  5. Microsoft Learn, about_Language_Modes。關於 FullLanguage・ConstrainedLanguage・RestrictedLanguage・NoLanguage 各模式可使用的語言元素、透過 $ExecutionContext.SessionState.LanguageMode 確認目前的模式、在已啟用 WDAC 或 AppLocker 應用程式控制的環境中 PowerShell 會以限制語言模式運作,以及手動設定語言模式並非以安全性功能為設計初衷的說明。  2 3 4 5

  6. 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

  7. 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

  8. Microsoft Learn, 特殊識別群組(Special Identity Groups)。關於 OWNER RIGHTS(S-1-3-4)是代表物件目前擁有者的群組,當帶有這個 SID 的 ACE 套用在物件上時,系統會忽略對擁有者隱含賦予的 READ_CONTROLWRITE_DAC的說明。反過來說,若沒有這個 ACE,擁有者就能隱含地改寫 DACL。  2

  9. 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_FILEFILE_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

  10. 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 的路徑的說明。 

  11. .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 的說明。 

  12. 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.」(標準使用者不應擁有這個資料夾的存取權,僅限於負責稽核的管理員)的說明。 

  13. Microsoft Learn, JEA 的前提條件。關於 JEA 可在 PowerShell 5.0 以後使用、PowerShell 遠端處理是 JEA 的基礎,使用 JEA 之前必須先啟用遠端處理並妥善加以保護、Windows Server 2012 以後 PowerShell 遠端處理預設為啟用,若未啟用可在具系統管理員權限的 PowerShell 視窗中執行 Enable-PSRemoting 來啟用的說明。  2 3

  14. 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

  15. 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)正是為此用途設計的機制。透過角色功能檔案定義「只允許這個指令、這個參數、這個值」,並將其註冊為工作階段組態。使用者不需要擁有系統管理員權限,就能透過虛擬帳戶執行獲得許可的操作。工作階段會以受限遠端伺服器的形式組態,且預設語言模式也會受到限制,因此沒有執行任意程式碼的空間。執行內容還可以記錄為逐字稿。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽