PowerShellを(比較的)安全にする
PowerShellはWindows上でできることなら何でもできます。
なんらかの脆弱性等でPowerShellをキックできてしまうと、何でもできる、ということです。
それはとても恐ろしいことなので、安全に使う方法を考えてみた。
何でもできるけど
複雑なことをするのは、そうそう単純には書けません。
PowerShellをキックする際に、引数-Commandで実行コマンドを指定できます。
が、これにはそうそう複雑なことはかけません。
環境によりますが、中括弧やらダブルクォートに制限が入るためです。
ま、嫌がらせなら制限内でもできますが、そこは諦めます。
じゃあ複雑なことをするには
スクリプトファイル.ps1の登場です。
-Commandでスクリプトファイルを指定すれば、実行できてしまいます。
そこのガードには、Set-ExecutionPolicyです。
もともと初期状態のPowerShellは、ExecutionPolicyがRestrictedになっていて、スクリプトファイルは実行できません。
なので、このままにするか、AllSignedにすることで、悪意のあるスクリプトファイルからガードできます。
しかし、AllSignedの場合、穴があります。
署名用の証明書を登録しっぱなしにすると、それで署名が可能になってしまいます。
すなわち脆弱性を使って
- .ps1ファイルを作成する
- -Commandで登録された署名用の証明書で署名する
- .ps1ファイルを実行する
これのガードのために、署名用の証明書を随時出し入れするようにしました。
Powershellスクリプトの署名と証明書の操作 - じゅんじゅんのきまぐれ
しかし別な穴が
-EncodedCommandは、スクリプトをUTF-16LEのBase64エンコードすることで、中括弧やらダブルクォートの制限を回避します。
せっかくAllSignedでガードしても、できてしまうのです。
これに対して、stuncloudさんが閃いてくれました。
EncodedCommandが渡されたら止める | たっぷす庵
これで、-EncodedCommandがガードできます。
、、、が、全ユーザーの$PROFILEに仕込みが必要です。
まあ、自分さえ防げれば、最大の脆弱性はたいてい自分なので、だいたいは良いです。
でも、サービスの穴経由だと、、、。
ということで、プロセスの起動監視と組み合わせて考えてみました。
方針は
- 自分の実行は、stuncloudさん方式でのりきる(WindowをHideした場合、選択肢の入力待ちで止まるプロセスになるけど、それは良いとする)
- 他ユーザーの-EncodedCommandはタスクキル
Register-WMIEvent -query "Select * From __InstanceCreationEvent within 3 Where TargetInstance ISA 'Win32_Process'" -sourceIdentifier "NewProcessWatcher" -Action { $p = $Event.SourceEventArgs.NewEvent.TargetInstance if ($p.Name -match 'powershell' -and $p.CommandLine -match '-EncodedCommand\s+(\S*)' -and (Get-WmiObject -Query "Select * From Win32_Process Where ProcessId=$($p.ProcessId)").GetOwner() -ne (Get-WmiObject -Query "Select * From Win32_Process Where ProcessId=$PID").GetOwner()) { Stop-Process -Force $p.ProcessId Write-Warning "EncodedCommandが別ユーザーで使われました!" Write-Host ([System.Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($Matches[1]))) } }
なんか、$p.GetOwner()が効かないから再検索したよ!
、、、ただ、起動監視なので、見つけてkillするまでの間に少し実行されてしまいます、、、。
ま、それくらいはカンベンしてください。
なお、解除したい場合は、
Unregister-Event NewProcessWatcher Remove-Job NewProcessWatcher
追記 2015/02/10
上記の方法もまあまあですが、stuncloudさんがより良い対処方法を見つけてくれました。
続・EncodedCommandが渡されたら止める | たっぷす庵
素晴らしいです。
、、、が、ちょっと欠点があるのです。
- AllUsers,AllHostsのProfileを実行するには、LocalMachineのExecutionPolicyを緩める必要がある
- AllSigned運用にするなら、正式なコード署名証明書が必要(高い、、、)
- RemoteSigned運用にするなら、EncodedCommandに関わらずkillする覚悟が必要(それでいいか)
ということで、$PROFILE.AllUsersAllHostsに以下を書くことにしました。
AllSigned運用の人はオレオレコード署名証明書のルートを入れている想定で、署名しときます。
LocalMachineはRemoteSigned。
trap { Stop-Process -Force $PID } $sfp = 'C:\Users\Public\Documents\PowerShellAllSignedEncodedCommandGo' $n = [DateTime]::Now $s = (Test-Path $sfp) -and (($lw = (Get-Item $sfp).LastWriteTime) -lt $n.AddHours(2)) -and ($lw -gt $n.AddHours(-2)) if(!$s) { $s = (Get-ExecutionPolicy) -eq 'AllSigned' if($s -and [System.Environment]::CommandLine -match '-EncodedCommand\s+(\S*)') { Write-Warning 'EncodedCommandが使われています!' Write-Host ([System.Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($Matches[1]))) $s = ((Read-Host '実行しますか?(y/n)') -eq 'y') } } if($s -and [System.Environment]::CommandLine -match 'set-executionpolicy') { $s = $false } if(!$s) { "$([DateTime]::Now) $env:USERNAME stop! $([System.Environment]::CommandLine)" >> C:\Users\Public\Documents\PowerShellMsg.txt throw 'stop PowerShell' }
スタートアップで起動するPowerShellで、C:\Users\Public\Documents\PowerShellMsg.txt を表示してます。
これで、不審なPowerShellプロセスは、次にログインした時に気づくすんぽーです。
あとは、PowerShell経由でなくExecutionPolicyを変更する方法とかがあると穴となります。
追記 2015/02/18
上記スクリプトを少し修正しました。
具体的には、特定ファイルが存在したら実行可否を問わないようにしました。
特定ファイルはハードコードで、更新日付の前後2時間が有効です。