PowerShell難読化の基礎 (1)
概要
悪意のあるコードを実行させようとした場合、平文でそのまま実行させようとしても、何かしらのセキュリティ機構に検知されてしまいます。そこで攻撃者は、難読化を施すことでセキュリティ機構を潜り抜けて、悪意のあるコードの実行を試みます。近年のPowerShellによる攻撃でも、悪意のあるスクリプトに難読化処理を施すことは珍しくありません。本記事では、難読化されたPowerShellスクリプトへの対処のために、難読化に関する基本的な事項を記述します。
コードの評価
難読化されたコードは、実行中に意味のあるコードに形を変えて、最後に「コードを評価して実行する関数」の引数に渡されることで実行されます。例えばJavaScriptやPHPなどでは、eval関数によってコードを評価して実行します。PowerShellの場合はInvoke-Expressionコマンドレットやアンパサンド記号がそれに相当します。
Invoke-Expressionによるスクリプトの実行
それではInvoke-Expressionの挙動を確認しましょう。まずは以下のコードを考えます。変数codeに「foreach文で1から5まで表示するコード」を格納して、最後に変数codeを呼び出しています。
$code = 'foreach($i in (1..5)){ Write-Host $i}' $code
このコードを実行した場合の結果は以下の通り、変数codeに格納されたコードがそのまま表示されるのみです。
PS C:\> $code = 'foreach($i in (1..5)){ Write-Host $i}' PS C:\> $code foreach($i in (1..5)){ Write-Host $i}
次に、以下のコードを考えます。先ほどのコードの最後の、変数codeを呼び出す個所にInvoke-Expressionコマンドレットを追加したのみです。
$code = 'foreach($i in (1..5)){ Write-Host $i}' Invoke-Expression $code
結果は以下の通りです。Invoke-Expressionコマンドレットによって変数codeの中身のコードが評価され、実行されていることがわかります。
PS C:\> $code = 'foreach($i in (1..5)){ Write-Host $i}' PS C:\> Invoke-Expression $code 1 2 3 4 5
以上のようにInvoke-Expressionコマンドレットの引数として変数コードを明示的に与える他に、以下のようにパイプ演算子を利用しても同様の結果が得られます。
$code = 'foreach($i in (1..5)){ Write-Host $i}' | Invoke-Expression
パイプ演算子でInvoke-Expressionコマンドレットにコードを渡す場合は、変数codeをわざわざ宣言しなくても同じ結果が得られます。
'foreach($i in (1..5)){ Write-Host $i}' | Invoke-Expression
Invoke-Expressionコマンドレットはiexというコマンドにエイリアスされているため、iexを用いても結果は同じです。実際のサイバー攻撃では、「Invoke-Expression」とするよりもiexとする方が一般的です。
'foreach($i in (1..5)){ Write-Host $i}' | iex
アンパサンド記号によるスクリプトの実行
Invoke-Expressionコマンドレットとは挙動に差異がありますが、アンパサンド記号(&)も近い働きをします。アンパサンド記号はのファイル、コマンドレット、関数、スクリプトファイルを実行するために用います。
アンパサンド記号はファイルや関数を実行する関数であるため、複雑なPowerShellスクリプトのソースコードを引数に渡しても、実行できません。例えば以下のコードを実行した場合はエラーになります。
$code = 'foreach($i in (1..5)){ Write-Host $i}' & $code
以下のように変数codeの部分を関数化してしまえば実行可能です。
function code { foreach($i in (1..5)){ Write-Host $i } } & code
アンパサンド記号はコマンドレットや関数を実行するため、以下のようにInvoke-Expressionコマンドレットを評価してコードを実行させることは可能です。
$comlet = 'Invoke-Expression' 'foreach($i in (1..5)){ Write-Host $i}' | & $comlet
このように、PowerShellの難読化においてアンパサンド記号は、Invoke-Expressionコマンドレットを実行させるために用いることが一般的です。
PowerShellスクリプトの難読化
Invoke-Expressionコマンドレットの難読化
Invoke-Expressionコマンドレットを難読化することから始めましょう。先に記述した通り、Invoke-Expressionコマンドレットは基本的にはそのまま使われることはなく、iexというエイリアスされたコマンドとして用いることが攻撃者の間では一般的です。 よく用いられる手法としては、環境変数を活用する手法が挙げられます。環境変数の中でも「どのWindows OSでも変わらないことが保証されているもの」を用います。PowerShellの難読化によく用いられる環境変数として、COMSPECを例として取り上げます。COMSPECはcmd.exeの絶対パスが格納されています。
PS C:\> $env:comspec C:\WINDOWS\system32\cmd.exe
PowerShellでもその他のプログラミング言語と同様に、文字列を配列型変数として取り扱うことができます。環境変数COMSPECの出力結果のうち、先頭の文字「C」が第0番目の要素、その次の「:」が第1番目の要素、と数えていくと第4番目の要素がiexの「I」となることが分かります。PowerShellでは大文字小文字の区別は無く、iでもIでも構わないため、これをInvoke-Expressionコマンドレットの構成要素として用います。
PS C:\> $env:comspec[4] I
同じ要領に従うと、環境変数COMSPECから以下のようにiexの残りの文字を取り出すことができます。
PS C:\> $env:comspec[15] e PS C:\> $env:comspec[25] x
PowerShellでは、配列の要素番号はカンマ区切りで指定することで、配列型変数として出力することが可能です。
PS C:\> $env:comspec[4,15,25] I e x
この配列型変数を、Joinで連結すればiexに相当する文字列を得ることができます。
PS C:\> $env:comspec[4,15,25] -Join '' Iex
よって、アンパサンド記号と組み合わせると、以下のようにコードを記述することで、先の例に挙げたforeach文のコードが実行できます。
'foreach($i in (1..5)){ Write-Host $i}' | &($env:comspec[4,15,25] -Join '')
実際に以上のコードをPowerShellで実行すると、1から5の数字が順番に出力されるでしょう。
コードの難読化
先述のInvoke-Expressionコマンドレットと同様の手法でも原理的には可能ですが、コードの難読化には別の手法を取り上げます。簡単な手法としては、文字列をASCIIコードとして使う手法があります。例えば、アルファベット大文字のAは、アスキーコードでは65 (0x41) であるため、65という数字をCharacter型に型変換すると「A」と出力されます。
PS C:\> [char] 65 A
逆も然りで、Aという数字をASCIIコードに型変換すると「65」と出力されます。
PS C:\> [Text.Encoding]::ASCII.GetBytes("A") 65
それでは前例と同じく、以下のコードをこの手法で難読化してみましょう。
foreach($i in (1..5)){ Write-Host $i}
一つずつASCIIコード化しても構いませんが、手間がかかります。以下のコードを実行することで、すべての文字のASCIIコードが取得可能です。
'foreach($i in (1..5)){ Write-Host $i}' | %{ [Text.Encoding]::ASCII.GetBytes($_) }
出力結果を配列型変数に定義すると以下の通りです。
(102, 111, 114, 101, 97, 99, 104, 40, 36, 105, 32, 105, 110, 32, 40, 49, 46, 46, 53, 41, 41, 123, 32, 87, 114, 105, 116, 101, 45, 72, 111, 115, 116, 32, 36, 105, 125)
この配列を文字列に変換するには、以下のように実行します。
(102, 111, 114, 101, 97, 99, 104, 40, 36, 105, 32, 105, 110, 32, 40, 49, 46, 46, 53, 41, 41, 123, 32, 87, 114, 105, 116, 101, 45, 72, 111, 115, 116, 32, 36, 105, 125) | %{ ([Int]$_ -as [char]) }
これをJoinで結合すれば、目的のコードが得られます。
((102, 111, 114, 101, 97, 99, 104, 40, 36, 105, 32, 105, 110, 32, 40, 49, 46, 46, 53, 41, 41, 123, 32, 87, 114, 105, 116, 101, 45, 72, 111, 115, 116, 32, 36, 105, 125) | %{ ([Int]$_ -as [char]) }) -Join ''
よって先ほどのInvoke-Expressionコマンドレットの難読化と合わせると、難読化されたスクリプトは以下のようになります。
(((102, 111, 114, 101, 97, 99, 104, 40, 36, 105, 32, 105, 110, 32, 40, 49, 46, 46, 53, 41, 41, 123, 32, 87, 114, 105, 116, 101, 45, 72, 111, 115, 116, 32, 36, 105, 125) | %{ ([Int]$_ -as [char]) }) -Join '') | &($env:comspec[4,15,25] -Join '')
まとめ
本記事ではPowerShellを難読化するための基本的な手法について取り上げました。次回の記事も引き続き、難読化の手法を取り上げる予定です。