PowerShellを(比較的)安全にする

PowerShellWindows上でできることなら何でもできます。
なんらかの脆弱性等でPowerShellをキックできてしまうと、何でもできる、ということです。
それはとても恐ろしいことなので、安全に使う方法を考えてみた。



何でもできるけど

複雑なことをするのは、そうそう単純には書けません。
PowerShellをキックする際に、引数-Commandで実行コマンドを指定できます。
が、これにはそうそう複雑なことはかけません。
環境によりますが、中括弧やらダブルクォートに制限が入るためです。
ま、嫌がらせなら制限内でもできますが、そこは諦めます。

じゃあ複雑なことをするには

スクリプトファイル.ps1の登場です。
-Commandでスクリプトファイルを指定すれば、実行できてしまいます。
そこのガードには、Set-ExecutionPolicyです。
もともと初期状態のPowerShellは、ExecutionPolicyがRestrictedになっていて、スクリプトファイルは実行できません。
なので、このままにするか、AllSignedにすることで、悪意のあるスクリプトファイルからガードできます。


しかし、AllSignedの場合、穴があります。
署名用の証明書を登録しっぱなしにすると、それで署名が可能になってしまいます。
すなわち脆弱性を使って

  1. .ps1ファイルを作成する
  2. -Commandで登録された署名用の証明書で署名する
  3. .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時間が有効です。