PowerShell難読化の基礎 (3)
概要
前回の記事および前々回の記事に引き続き、今回もPowerShellの難読化の手法について記述します。
コメントアウトの挿入による難読化
前回の記事ではバッククオート記号の挿入による難読化手法を取り上げましたが、コメントアウトを挿入することによってもコードの可読性を下げることが可能です。例として前々回の記事で作成した以下のコードを考えます。
(((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 '')
以上の難読化されたコードは、1から5の数字を順に出力するものです。
PS C:\> (((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 '') 1 2 3 4 5 PS C:\>
PowerShellでは<#と#>で括ることにより、複数行をコメントアウトすることが可能です。この性質を利用して、コードに意図的にコメントアウトを挿入することにより、可読性を低下させることができます。例えば以下のように挿入します。
(((102, 111, <# #>114, 101, 97, <# ;asjfasjfaa #>99, 104, 40, 36, 105, <# caf3tr#Dfnc #>32, 105, 110, 32, 40, 49, 46, 46,<# KNbceHrc #> 53, 41, 41, 123, 32, 87, 114, 105, 116, 101, 45, 72, <# 35u89uj0n0 #>111, 115, 116, 32, 36, 105, 125) | %{ ([Int]$_ -as [char]) }) -Join<# TGceWcc #> '') | &($env:comspec<# nuEbcGR53 #>[4,15,25] -Join '')
このコードをPowerShellで実行すると、以下のように正常に1から5までの数字が出力されます。
PS C:\> (((102, 111, <# #>114, 101, 97, <# ;asjfasjfaa #>99, 104, 40, 36, 105, <# caf3tr#Dfnc #>32, 105, 110, 32, 40, 49, 46, 46,<# KNbceHrc #> 53, 41, 41, 123, 32, 87, 114, 105, 116, 101, 45, 72, <# 35u89uj0n0 #>111, 115, 116, 32, 36, 105, 125) | %{ ([Int]$_ -as [char]) }) -Join<# TGceWcc #> '') | &($env:comspec<# nuEbcGR53 #>[4,15,25] -Join '') 1 2 3 4 5 PS C:\>
コメントアウトは意味のある文字列の間に挟むことはできません。たとえば「$test = "Get-Host"; Invoke-Expression $test」というコードに、以下のようにコメントアウトを挿入するとエラーになります。
PS C:\> $test = "Get-Host"; Invoke-Expr<# aaaa #>ession $test Invoke-Expr<# : The term 'Invoke-Expr<#' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again. At line:1 char:21 + $test = "Get-Host"; Invoke-Expr<# aaaa #>ession $test + ~~~~~~~~~~~~~ + CategoryInfo : ObjectNotFound: (Invoke-Expr<#:String) [], CommandNotFoundException + FullyQualifiedErrorId : CommandNotFoundException PS C:\>
コメントアウトを挿入する場合は、以下のように語句の区切りに適切に挿入する必要があります。
PS C:\> $test = <# aaaa #>"Get-Host"; <# bbbb #>Invoke-Expression <# cccc #>$test Name : ConsoleHost Version : 5.1.17134.228 InstanceId : 75c394c9-bdca-46bb-9ddc-189ca01897f0 UI : System.Management.Automation.Internal.Host.InternalHostUserInterface CurrentCulture : en-US CurrentUICulture : en-US PrivateData : Microsoft.PowerShell.ConsoleHost+ConsoleColorProxy DebuggerEnabled : True IsRunspacePushed : False Runspace : System.Management.Automation.Runspaces.LocalRunspace PS C:\>
Base64エンコードによる難読化
JavaScriptやPHPなどでの難読化と同様に、PowerShellの難読化にもBase64エンコードが用いられます。
コマンドレットのBase64エンコード
PowerShellで実行するコマンドレットをBase64エンコードする手順から解説します。例として「Get-Date -Format G」を取り扱います。 まずはPowerShellのコマンドレットを適切な文字エンコードで読み取ります。Windows OSにおいてはUnicodeを標準として取り扱っているため、実行させたいコマンドの文字列をUnicodeのバイト列に変換する必要があります。文字列をUnicodeのバイト列に変換するには、名前空間System.TextのEncodingクラスに属しているUnicode.GetBytesメソッドを用います。
PS C:\> $com = "Get-Date -Format G" PS C:\> $unicodeBytes = [System.Text.Encoding]::Unicode.GetBytes($com) PS C:\>
コマンドレットをUnicodeのバイト列に変換したら、名前空間SystemのCovertクラスに属しているToBase64Stringメソッドにより、Base64エンコードされた文字列を取得することができます。
PS C:\> [System.Convert]::ToBase64String($unicodeBytes) RwBlAHQALQBEAGEAdABlACAALQBGAG8AcgBtAGEAdAAgAEcA PS C:\>
攻撃者はBase64エンコードされた文字列を実行する際に、powershell.exeを用いてコードの実行を試みます。powershell.exeにはBase64エンコードされた文字列をコマンドとして実行する、EncodedCommandというオプションがあります。ただし、powershell.exeのオプションは他のオプションと混同しない範囲で省略することが可能であるため、「powershell.exe -EncodedCommand ~」と用いられることはあまりなく、「powershell.exe -enc ~」のように用いられることがほとんどです。先ほどエンコードした文字列を実行すると、以下のように「Get-Date -Format G」の実行結果を得ることができます。
PS C:\> Get-Date -Format G 9/14/2018 2:14:50 PM PS C:\> powershell.exe -enc RwBlAHQALQBEAGEAdABlACAALQBGAG8AcgBtAGEAdAAgAEcA 9/14/2018 2:14:59 PM PS C:\>
UnicodeではなくASCIIで変換した場合についても示します。先ほど説明した通り、Windows OSではUnicodeを標準で取り扱うため、ASCIIのバイト列としてBase64エンコードした文字列を実行しようとしてもうまくいきません。以下に実行例を示します。
PS C:\> $com = "Get-Date -Format G" PS C:\> $asciiBytes = [System.Text.Encoding]::ASCII.GetBytes($com) PS C:\> [System.Convert]::ToBase64String($asciiBytes) R2V0LURhdGUgLUZvcm1hdCBH PS C:\> powershell.exe -enc R2V0LURhdGUgLUZvcm1hdCBH 敇慄整ⴠ潆浲瑡䜠 : The term '敇慄整ⴠ潆浲瑡䜠' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again. At line:1 char:1 + 敇慄整ⴠ潆浲瑡䜠 + ~~~~~~~~~ + CategoryInfo : ObjectNotFound: (敇慄整ⴠ潆浲瑡䜠:String) [], CommandNotFoundException + FullyQualifiedErrorId : CommandNotFoundException PS C:\>
スクリプトのBase64エンコード
スクリプトのソースコードをBase64エンコードする手順についても解説します。とはいえ、コマンドレットをBase64エンコードする手順と大差ありません。例として以下のスクリプトを用います。
PS C:\> Get-Content .\test.ps1 $namelist = @("John", "Alex", "Mike") ForEach($name in $namelist) { Write-Host ("Hello, " + $name + "!") } PS C:\> .\test.ps1 Hello, John! Hello, Alex! Hello, Mike! PS C:\>
まずは名前空間System.IOのFileクラスから、ReadAllTextメソッドを用いてスクリプトファイルの内容をすべて読み取ります。
PS C:\> $filepath = "C:\test.ps1" PS C:\> $srctext = [System.IO.File]::ReadAllText($filepath) PS C:\>
あとは先ほどのコマンドレットと同じ手順で、Base64化されたスクリプトのソースコードを得ることができます。
PS C:\> $unicodeBytes = [System.Text.Encoding]::Unicode.GetBytes($srctext) PS C:\> [System.Convert]::ToBase64String($unicodeBytes) JABuAGEAbQBlAGwAaQBzAHQAIAA9ACAAQAAoACIASgBvAGgAbgAiACwAIAAiAEEAbABlAHgAIgAsACAAIgBNAGkAawBlACIAKQANAAoAIAANAAoARgBvAHIARQBhAGMAaAAoACQAbgBhAG0AZQAgAGkAbgAgACQAbgBhAG0AZQBsAGkAcwB0ACkADQAKAHsADQAKACAAIAAgACAAVwByAGkAdABlAC0ASABvAHMAdAAgACgAIgBIAGUAbABsAG8ALAAgACIAIAArACAAJABuAGEAbQBlACAAKwAgACIAIQAiACkADQAKAH0A PS C:\> powershell.exe -enc JABuAGEAbQBlAGwAaQBzAHQAIAA9ACAAQAAoACIASgBvAGgAbgAiACwAIAAiAEEAbABlAHgAIgAsACAAIgBNAGkAawBlACIAKQANAAoAIAANAAoARgBvAHIARQBhAGMAaAAoACQAbgBhAG0AZQAgAGkAbgAgACQAbgBhAG0AZQBsAGkAcwB0ACkADQAKAHsADQAKACAAIAAgACAAVwByAGkAdABlAC0ASABvAHMAdAAgACgAIgBIAGUAbABsAG8ALAAgACIAIAArACAAJABuAGEAbQBlACAAKwAgACIAIQAiACkADQAKAH0A Hello, John! Hello, Alex! Hello, Mike! PS C:\>
Base64エンコードされたコードのデコード
Base64エンコードされたコードをデコードする場合は、エンコードする手順とは逆の行程でデコードします。Base64エンコードされたコマンドをUnicodeのバイト列に変換する場合は名前空間SystemのConvertクラスに属するFromBase64Stringメソッドを、バイト列をUnicode文字列に変換するには名前空間System.TextのEncodingクラスに属するUnicode.GetStringメソッドを用います。
PS C:\> $encoded = "JABuAGEAbQBlAGwAaQBzAHQAIAA9ACAAQAAoACIASgBvAGgAbgAiACwAIAAiAEEAbABlAHgAIgAsACAAIgBNAGkAawBlACIAKQANAAoAIAANAAoARgBvAHIARQBhAGMAaAAoACQAbgBhAG0AZQAgAGkAbgAgACQAbgBhAG0AZQBsAGkAcwB0ACkADQAKAHsADQAKACAAIAAgACAAVwByAGkAdABlAC0ASABvAHMAdAAgACgAIgBIAGUAbABsAG8ALAAgACIAIAArACAAJABuAGEAbQBlACAAKwAgACIAIQAiACkADQAKAH0A" PS C:\> $unicodeBytes = [System.Convert]::FromBase64String($encoded) PS C:\> [System.Text.Encoding]::Unicode.GetString($unicodeBytes) $namelist = @("John", "Alex", "Mike") ForEach($name in $namelist) { Write-Host ("Hello, " + $name + "!") } PS C:\>
まとめ
過去2回の記事に引き続き、今回の記事でもPowerShellの難読化手法について記述しました。次回の記事では難読化されたPowerShellの難読化を解除する手順について解説します。