Code source sur GitHub
Une signature fixe un seul point de l'espace d'encodage
Une règle hexadécimale YARA est un prédicat syntaxique sur un tampon d'octets. Ce qu'elle cherche à détecter est un objet sémantique, par exemple une fonction qui renvoie 7. Entre ces deux plans se trouve la propriété déterminante de x86-64 : un encodage de longueur variable dans lequel la correspondance entre suites d'octets et effets architecturaux est massivement plusieurs-vers-un. Placer 7 dans RAX puis revenir admet, prudemment, plus d'une douzaine d'encodages valides.
Une signature fixe un point dans cet espace. Tout le travail d'un mutateur consiste à rejoindre un autre point. L'hypothèse intuitive — insérer un NOP, changer le hash et la taille, et la règle d'identité est neutralisée — mesure donc le mauvais plan. La question n'est jamais de savoir si des octets ont bougé. Elle est de savoir si le point exact fixé par la règle apparaît encore quelque part dans le tampon, et si la règle fixe un point ou une famille.
Il s'agit d'une analyse de laboratoire x86-64 destinée à l'évaluation autorisée de détections, avec Aether 1.0.0 et Zydis v4.1.1 comme implémentation de référence reproductible. Elle ne revendique ni une sémantique .text arbitraire, ni une réécriture binaire générale, ni le contournement d'un quelconque produit de sécurité.
Six octets, un NOP, toujours une correspondance
La fonction de test fait six octets : B8 07 00 00 00 C3, soit mov eax, 7; ret. La règle pédagogique correspond exactement à ces octets, contigus. C'est une règle de laboratoire, pas une signature de production ; son intérêt est de rendre visible le plan d'identité.
La première capture SAFE avec la seed 42 a produit 90 B8 07 00 00 00 C3 : un NOP d'un octet devant l'original inchangé. Le hash a changé, la taille a changé, la valeur de retour tient toujours, et la règle correspond encore, car l'aiguille de six octets est intacte un octet plus loin.
La raison est mécanique. YARA réduit le motif à de courts atomes, typiquement des sous-chaînes d'au plus quatre octets comme 07 00 00 00, les place dans un automate Aho-Corasick et n'exécute la vérification complète qu'aux positions où un atome est trouvé. L'automate est indépendant de la position : un décalage d'un octet ne change rien. Corollaire : la vraie force d'une règle d'identité est la qualité de son atome le plus rare, pas sa longueur. Remplacer la valeur immédiate par des jokers, B8 ?? ?? ?? ?? C3, oblige YARA à s'ancrer sur les atomes d'un octet B8 et C3, omniprésents dans du vrai .text, et transforme la règle en une vérification lente et peu précise.
Conclure que l'insertion de code inutile a neutralisé la signature n'est donc pas acceptable sans tester la règle elle-même. Cette expérience a mesuré du remplissage, pas la destruction d'une règle.
{ B8 07 00 00 00 C3 } // contiguous identity Un changement d'encodage, et la règle échoue
La seed 5 emprunte un autre chemin dans la même politique SAFE et produit 48 C7 C0 07 00 00 00 C3. La chaîne d'origine n'apparaît plus, car l'instruction a changé de classe d'encodage : un préfixe REX dont le bit W sélectionne une taille d'opérande de 64 bits, le groupe d'opcodes MOV C7 /0, un ModR/M en mode registre direct qui sélectionne RAX, la même valeur immédiate 7, puis ret. Le résultat équivaut à mov rax, 7; ret.
Cette affirmation a une limite précise. B8 étend sa valeur immédiate de 32 bits dans RAX par des zéros, tandis que REX.W + C7 /0 l'étend avec son signe. Pour la valeur positive 7, les deux donnent la même valeur sur 64 bits. Le champ pure_rax d'Aether est toutefois un oracle de valeur de retour sur 32 bits. Avec une valeur immédiate comme 0x80000000, les deux formes différeraient dans les 32 bits supérieurs, et l'oracle ne pourrait pas le voir. L'affirmation sémantique est délibérément limitée à ce cas à valeur immédiate positive et au domaine de sortie déclaré sur 32 bits.
La seed seule ne détermine pas non plus la sortie ; une capture publiée doit préciser un processus neuf, le premier morph initialisé et le build exact. Avec cette discipline, la comparaison est nette :
- Original B8 … C3 : règle d'identité HIT, pure_rax 7
- Seed 42 (90 + original) : règle d'identité HIT, pure_rax 7 → 7 OK
- Seed 5 (48 C7 C0 … C3) : règle d'identité MISS, pure_rax 7 → 7 OK
Ce qui rend le résultat crédible
Ce n'est pas un argument selon lequel la détection serait impossible. Une règle qui reconnaît les deux familles d'encodage peut toujours détecter les deux. C'est un argument selon lequel un fragment d'identité unique est incomplet.
Aether décode chaque instruction avec Zydis et la classe comme Control, Memory, Identity, PureRax ou une autre catégorie ; seules les instructions classées entrent dans un catalogue de substitutions borné sémantiquement, et ret n'est jamais diversifié comme pur. Les octets que Zydis ne peut pas décoder restent opaques : un mauvais décodage réduit donc la surface de transformation au lieu d'inventer une réécriture. Chaque sortie est re-décodée et vérifiée par une comparaison pure de la valeur de retour sur 32 bits, un simulateur indépendant de l'hôte avec des paires d'entrées fixes et, sur x86-64 ou dans le laboratoire Docker amd64, une exécution native. En mode Lab ou Industry, une divergence entraîne un échec fermé (fail-closed).
Le benchmark du laboratoire de l'auteur sur Apple Silicon portait sur 5 096 entrées de corpus (1 000 synthétiques, 4 096 issues d'ELF). Il rapporte 10 192 morphs réussis, 5 020 vérifications pures et 4 862 vérifications natives sans aucune rupture, ainsi que 94,89 % de sorties uniques par SHA-256. Sur la matrice ciblée de 128 seeds, la règle d'identité de laboratoire a correspondu 30 fois (23,44 %) et échoué 98 fois (76,56 %), sans rupture pure. Ce sont des mesures propres à ce build et à cet hôte, pas des chiffres produit permanents.
Les couches qui doivent concorder
La leçon n'est pas d'utiliser plus de signatures, ni que le morphing l'emporte. Elle est de tester les règles sur le plan qu'elles observent réellement. L'exemple de six octets traverse cinq couches, et prendre l'une d'elles pour le système entier transforme un dump d'octets convaincant en affirmation fausse. Un re-décodage structurel ne prouve pas la sémantique machine complète, un oracle pur validé ne prouve pas qu'un EDR n'a pas alerté, et un MISS YARA ne prouve pas qu'un encodage est absent de toute famille de détection.
- Décodage : quels octets et quel graphe de flot de contrôle avons-nous réellement extraits ?
- Analyse : cette transformation est-elle autorisée par le modèle actuel ?
- Abaissement : les branchements visent-ils toujours les blocs logiques prévus ?
- Oracle : le contrat de sortie déclaré a-t-il tenu dans ce domaine ?
- Détection : quelle observable a changé, et quel capteur la voit encore ?
Transformer l'expérience en CI de détection
Le schéma utile : générer des jumeaux sémantiques légitimes dans un domaine étroitement déclaré, exécuter la règle et l'oracle, conserver les preuves, puis réparer le capteur qui a échoué. Exécutez le job sur au moins trois plans : l'oracle sémantique rejette les mutations qui violent le contrat déclaré, l'identité est enregistrée sous forme d'octets exacts, de taille et de hash, et le détecteur nommé s'exécute avec sa règle exacte. Une observation ClamAV ou EDR est une quatrième mesure distincte, pas un substitut au résultat YARA.
Conservez la source exacte de la règle YARA de laboratoire, le rapport de benchmark JSON, les listings décodés avant et après, ainsi que la version et la date du détecteur. Nommez l'oracle au lieu d'écrire simplement validé : Apple Silicon peut exécuter la simulation mais pas du x86-64 en natif ; utilisez donc le laboratoire Docker amd64 lorsque l'exécution matérielle est requise.
Quand une règle échoue, la correction doit suivre le plan concerné. Une règle d'octets contigus peut nécessiter des alternatives couvrant la famille d'encodage, du contexte ou un second signal ; une règle couvrant à la fois B8 ?? … C3 et 48 C7 C0 ?? … C3 peut reconnaître les deux formes. Un détecteur de taille ou d'entropie voit peut-être déjà l'expansion moyenne d'environ 2,19× et n'a besoin que d'un calibrage. Un capteur comportemental doit être testé sur l'exécution, l'allocation, le timing et la mémoire, plutôt que déduit d'un hash statique.
Key Takeaways
Un NOP n'a pas cassé la règle ; un seul encodage équivalent, si. L'expérience démontre une faiblesse d'une règle d'identité contiguë, pas un échec universel de la détection, et elle ne vaut qu'à l'intérieur de son domaine déclaré : un cas à valeur immédiate positive, un oracle de retour sur 32 bits, un build nommé et une règle nommée.
La bonne réponse n'est pas de promettre l'invulnérabilité, mais de rendre les hypothèses d'une règle exécutables et couvertes par des tests de régression. Aether est destiné à un usage en laboratoire autorisé sous l'Aether Research License. L'article complet sur Medium contient chaque mesure et chaque réserve, et les documents SCOPE, SCORECARD, LIBRARY et SECURITY du dépôt doivent être lus avant de considérer un résultat comme une affirmation de déploiement.
Christian Schnapka
Macstab Engineering
Christian Schnapka conçoit les outils d'ingénierie ouverts qui sous-tendent Macstab. Il publie les versions longues de ces enquêtes sur Medium — chaque mesure, chaque réserve et chaque échec compris.