
Chargeur de dylib Mach-O en mémoire pour Python macOS standard ; déchiffre, mappe et exécute des payloads sans dlopen ni écriture sur disque, avec incorporation facultative d'un fichier de couverture chiffré.
dyen charge un dylib Mach-O en mémoire à l'aide du Python macOS standard et appelle un point d'entrée exporté sans dlopen sur ce dylib. L'objectif est que l'image ne soit jamais un fichier autonome sur la cible et ne soit jamais enregistrée auprès de dyld.
par cenobyte [email protected] 2026
https://github.com/cenobyte-vincit/dyen
dyen est un chargeur Mach-O en mémoire n'utilisant que la bibliothèque standard (loader.py) pour le /usr/bin/python3 standard sur macOS. Il lit un dylib simple, un blob DYEN v1 autonome (l'enveloppe chiffrée), ou un fichier de couverture avec ce blob ajouté à la fin du fichier (EOF), mappe la tranche hôte avec mmap anonyme, applique les correctifs classiques de rebase/bind, et appelle dyld_main via un pointeur de fonction ctypes.
encrypt_dyld.py est l'outil de préparation : il encapsule un dylib en blob DYEN sur stdout, ou copie un fichier de couverture vers avec le blob ajouté. La charge utile fournie affiche uniquement . est exclusivement macOS ; le chiffreur fonctionne sous Linux ou macOS.
dyld_<basename>dyld/dyld.chello worldloader.pyLe chargeur intégré lit un chemin local ou stdin. Un dropper ou un stage 1 récupérerait le fichier de couverture (ou les octets bruts) sur le réseau et les transmettrait à read_dyld_bytes(), de sorte que le dylib chiffré n'atterrisse jamais en tant que fichier propre sur la cible.
.dyen, ou fichier de couverture.hello world). Les diagnostics vont sur stderr./usr/bin/python3 standard (stdlib uniquement)loader.py est exclusivement macOS. La compilation du dylib PoC nécessite clang d'Apple sur un Mac. encrypt_dyld.py fonctionne sous Linux ou macOS avec un dylib précompilé.
clang et make (pour compiler dyld/dyld.c)encrypt_dyld.pyruff, pylint, cppcheck (brew install ruff pylint cppcheck)make
Cette commande compile build/libdyld.dylib (universel x86_64 + arm64), écrit build/libdyld.dyen, et intègre le même dylib chiffré dans pdf/dyld_c4611_sample_explain.pdf. Le PDF généré n'est pas versionné ; make l'écrit et make clean le supprime. Le fichier de couverture source est pdf/c4611_sample_explain.pdf.
Le dylib simple est un artefact intermédiaire. À l'exécution, le chargeur déchiffre un blob DYEN avant le mappage, soit depuis un fichier .dyen autonome, soit depuis un fichier de couverture avec le dyld chiffré ajouté à la fin.
# Standalone DYEN blob to stdout (Makefile redirects to build/libdyld.dyen)
python3 encrypt_dyld.py build/libdyld.dylib CHANGEMEASAP > build/libdyld.dyen
# Hide encrypted dyld inside a cover file -> pdf/dyld_c4611_sample_explain.pdf
python3 encrypt_dyld.py build/libdyld.dylib CHANGEMEASAP pdf/c4611_sample_explain.pdf
# Use this Mac's serial number as the encryption key instead of a password (local testing)
python3 encrypt_dyld.py build/libdyld.dylib "$(python3 loader.py --serial)" > build/libdyld.dyen
# Use the target Mac's serial number as the encryption key (fictitious serial)
python3 encrypt_dyld.py build/libdyld.dylib C07XH4V2MD6T > build/libdyld.dyen
encrypt_dyld.py fonctionne sous Linux ou macOS. loader.py fonctionne uniquement sous macOS.
La CLI du chargeur prend un chemin de fichier ou lit stdin. Elle n'a pas d'option de mot de passe. Le déchiffrement essaie d'abord le numéro de série matériel de ce Mac, puis DEFAULT_DYLD_KEY (CHANGEMEASAP). Si le numéro de série ne peut pas être lu, seule la clé codée en dur est essayée. La CLI charge toujours l'entrée et appelle dyld_main. Copiez loader.py sur la cible.
/usr/bin/python3 loader.py build/libdyld.dylib
/usr/bin/python3 loader.py pdf/dyld_c4611_sample_explain.pdf
/usr/bin/python3 loader.py build/libdyld.dyen
cat build/libdyld.dylib | /usr/bin/python3 loader.py
cat pdf/dyld_c4611_sample_explain.pdf | /usr/bin/python3 loader.py
cat build/libdyld.dyen | /usr/bin/python3 loader.py
Un fichier de couverture sans blob ajouté échoue :
/usr/bin/python3 loader.py pdf/c4611_sample_explain.pdf
loader.py --serial affiche le numéro de série matériel de ce Mac (macOS uniquement) :
python3 loader.py --serial
Définissez LOADER_DEBUG à n'importe quelle valeur pour obtenir des traces détaillées sur stderr :
LOADER_DEBUG=1 /usr/bin/python3 loader.py build/libdyld.dyen
loader.py et encrypt_dyld.py :
| Code | Signification |
|---|---|
| 0 | Succès |
| 1 | Erreur d'utilisation, échec de déchiffrement, numéro de série manquant ou erreur d'E/S |
LOADER_DEBUG (n'importe quelle valeur) affiche des traces détaillées sur stderr : source d'entrée, vérifications d'en-tête/magic, détection du blob DYEN, déchiffrement, map/bind, résolution de symboles. make transmet LOADER_DEBUG=1 à encrypt_dyld.py par défaut ; make LOADER_DEBUG= supprime les traces de l'étape de chiffrement.
DEFAULT_DYLD_KEY dans loader.py vaut CHANGEMEASAP. Le chiffrement nécessite un mot de passe explicite ou un argument de numéro de série Mac passé à encrypt_dyld.py.
Le nombre d'itérations (PBKDF2_ITERATIONS dans loader.py) suit la recommandation actuelle de l'OWASP Password Storage Cheat Sheet pour PBKDF2-HMAC-SHA256 (600 000 itérations). PKCS #5 / RFC 8018 définit PBKDF2 mais laisse le nombre d'itérations au déployeur ; NIST SP 800-63B autorise PBKDF2 sans imposer de nombre spécifique. OWASP publie des cibles pratiques qui augmentent avec le temps à mesure que le matériel devient plus rapide ; cette valeur doit donc être réévaluée périodiquement. Il s'agit d'une recommandation d'étirement de mot de passe appliquée ici à la clé de test, et non d'une constante de protocole permanente.
Hôte de compilation :
make test
make lint # cppcheck (C) + ruff + pylint (Python)
make check # cppcheck only
make lint-python # ruff + pylint
make test exécute les linters, compile le dylib PoC, le chiffre (.dyen autonome et intégration PDF), puis exécute les tests d'intégration avec /usr/bin/python3. Chaque test de chargement vérifie que stdout contient hello world. Ces tests s'exécutent sur un Mac colocalisé. Ils ne constituent pas une preuve d'un environnement d'exécution propre.
CrowdStrike Falcon macOS détecte cette technique. Les autres EDR ne sont pas testés.
Le chargeur n'implémente que le rebase/bind classique LC_DYLD_INFO. Il ne gère pas les correctifs chaînés, Objective-C ou Swift. dlopen n'est utilisé que pour les dépendances système signées par Apple (par exemple /usr/lib/libSystem.B.dylib). La charge utile n'est jamais enregistrée auprès de dyld.
La dissimulation par ajout en fin de fichier (EOF) est visible dans un hex dump.
Armement : modifiez DYENCRYPT avant une utilisation opérationnelle. La valeur magique PoC dans DYLD_BLOB_MAGIC (loader.py) est une signature statique. Les règles YARA, les scanners de fichiers et les heuristiques simples de rfind peuvent s'en servir comme signature. Remplacez-la par une valeur unique de votre choix (maintenez la synchronisation entre le chiffreur et le chargeur ; la longueur n'est pas fixe, mais une valeur plus longue est préférable pour les fichiers de couverture intégrés).
Le dylib fourni dans l'arborescence n'affiche que hello world.