Skip to content

Leçon 3.5 · Triage statique· 50 min

Écrire vos premières règles YARA

Transformez vos constats de triage en règles YARA : chaînes, motifs hex, modules pe et math, performances, tests sur goodware et sur variantes.

Objectifs

  • Expliquer où s’exécutent les règles YARA dans une chaîne défensive, et ce qu’une correspondance prouve ou ne prouve pas
  • Écrire des règles avec des chaînes texte, hexadécimales et regex, des modificateurs, et des conditions utilisant les modules pe et math
  • Choisir, parmi les constats de triage, des chaînes de règle qui survivent à une recompilation sans correspondre à des logiciels sans rapport
  • Éviter les règles lentes en comprenant les atomes, les chaînes courtes et les expressions régulières non ancrées
  • Tester une règle contre les faux positifs sur un corpus de goodware et pour les vrais positifs sur des variantes, puis la versionner

Tout ce que vous avez fait jusqu’ici dans ce module produit des observations : un hash, une poignée de chaînes étranges, une table des imports qui évoque une injection, une section à l’entropie suspecte. Des observations sur un fichier sont utiles pour un incident. Une règle de détection les transforme en quelque chose qui trouve aussi le fichier suivant, sur chaque poste, dans chaque exécution en sandbox (bac à sable), à travers des années d’échantillons archivés.

YARA est le langage de référence pour ce travail. Une règle YARA décrit un motif d’octets et de structure, et le moteur YARA répond à une seule question pour chaque fichier ou région mémoire qu’il analyse : est-ce que cela correspond ? Cette leçon vous apprend à écrire des règles qui répondent bien à cette question : rapides, spécifiques à ce que vous avez analysé, et robustes face aux petits changements que les attaquants introduisent d’un build à l’autre.

Où s’exécute YARA

Une règle que vous écrivez aujourd’hui peut se retrouver à de nombreux endroits :

OùCe qui est analyséPourquoi c’est important pour la conception de la règle
Scanners de postes et de serveurs (plug-ins EDR, THOR, Loki, table YARA d’osquery)Fichiers sur disque, parfois mémoire des processusS’exécute sur des milliers de fichiers par hôte : la vitesse compte
Sandboxes et passerelles de messagerieFichiers soumis, fichiers déposés, dumps mémoireSert à la classification et à l’étiquetage automatiques
VirusTotal Livehunt et RetrohuntNouveaux téléversements, ou des mois de téléversements passésTrouve des échantillons apparentés dont vous ignoriez l’existence
Analyse mémoire (yara <rules> <pid>, yarascan de Volatility)Un processus actif ou une image mémoireAttrape les malwares packés une fois qu’ils se sont dépaquetés
Votre propre dépôt d’échantillonsVotre collectionRegroupement et « l’avons-nous déjà vu ? »

Deux conséquences en découlent. D’abord, une règle doit être assez peu coûteuse pour tourner partout, ce qui explique que les performances aient leur propre section ci-dessous. Ensuite, une correspondance YARA est une correspondance de motif, pas un verdict. Elle dit « ces octets sont présents », rien sur l’intention. Une règle visant une famille de malwares (logiciels malveillants) ne vaut que par l’unicité des motifs que vous avez choisis.

Anatomie d’une règle

text
import "pe"

rule MAL_Win_Example_Loader
{
    meta:
        description = "Detects Example loader by its config marker and decoder"
        author      = "Analyst Name"
        date        = "2026-09-29"
        version     = "1.0"

    strings:
        $cfg  = "CFGv2|" ascii
        $ua   = "ExampleAgent" ascii wide
        $hex  = { 8B 45 ?? 35 [2-4] 89 45 }
        $re   = /https?:\/\/[a-z0-9.-]{4,40}\/gate\.php/ ascii

    condition:
        uint16(0) == 0x5A4D and filesize < 1MB and 2 of them
}

Une règle a un nom et trois sections :

  • meta contient des paires clé/valeur destinées aux humains et aux outils. Le moteur les ignore lors de la correspondance, mais tout jeu de règles sérieux s’appuie dessus.
  • strings déclare les motifs, chacun nommé avec $. Une règle peut n’avoir aucune chaîne si sa condition n’utilise que la structure du fichier.
  • condition est une expression booléenne. C’est la seule section obligatoire, et la règle correspond lorsqu’elle s’évalue à vrai.

Les noms de règles suivent les règles des identifiants (lettres, chiffres, tirets bas, sans commencer par un chiffre) et doivent être uniques dans un jeu de règles compilé.

Chaînes texte et modificateurs

Une chaîne texte correspond littéralement, en respectant la casse et en ASCII par défaut. Les modificateurs changent cela :

ModificateurEffetUsage typique
asciiCorrespond à la forme à un octet par caractère (par défaut s’il est seul)Chaînes C, texte de configuration
wideCorrespond à l’UTF-16LE : chaque caractère ASCII suivi de 00API Windows W, .NET, littéraux L"..."
ascii wideCorrespond à l’une ou l’autre formeQuand vous ne savez pas comment le prochain build la stockera
nocaseInsensible à la casseChemins PDB, noms de fichiers, en-têtes HTTP
fullwordNe correspond que si la chaîne est délimitée par des caractères non alphanumériquesNoms courts qui apparaissent aussi dans des mots plus longs
xor / base64Correspond aux encodages XOR sur un octet ou base64 de la chaîneChaînes légèrement encodées

fullword demande de la prudence : « evil » avec fullword correspond à C:\evil\x.exe mais pas à devils. Il échoue aussi sur Agent/1.0 si vous avez cherché Agent/ avec fullword, car le caractère suivant est un chiffre.

Attention : wide signifie seulement « UTF-16LE avec des caractères ASCII ». Il ne gère pas le vrai texte non ASCII. Et aucun des deux modificateurs n’aide pour les chaînes que le programme construit à l’exécution, comme les stackstrings ou les chaînes chiffrées par XOR avec une clé multi-octets. Pour celles-là, ciblez plutôt le code de décodage, ou analysez la mémoire après le décodage.

Chaînes hexadécimales

Les chaînes hexadécimales correspondent à des octets bruts : c’est ainsi que l’on cible du code ou des structures binaires :

SyntaxeSignificationExemple
??N’importe quel octet{ E8 ?? ?? ?? ?? } (un call rel32)
?5, 4?Joker sur un quartet{ 4? 8B 05 } (n’importe quel préfixe REX)
[n-m]Saut : entre n et m octets arbitraires{ 6A 40 [0-8] FF 15 }
( A | B )Alternatives{ 8D (40 | 50) 5A }
~XXN’importe quel octet sauf XX{ 85 C0 ~74 }

Mettez des jokers sur les parties que le compilateur ou l’éditeur de liens choisit pour vous : offsets relatifs des appels et des sauts, déplacements relatifs à RIP, offsets de pile. Gardez littérales les parties que l’auteur a choisies : constantes, clés XOR, valeurs magiques, forme d’une boucle de décodage.

Expressions régulières

Les regex s’écrivent entre /.../ et utilisent une syntaxe proche de Perl. C’est le type de chaîne le plus souple, et le plus facile à rendre lent. Une bonne regex commence par une suite littérale que le moteur peut indexer (/LabAgent\/[0-9]\.[0-9]/), pas par une classe de caractères ou .*.

Conditions

Les conditions combinent des références aux chaînes avec de l’arithmétique et l’inspection du fichier :

text
condition:
    uint16(0) == 0x5A4D                // "MZ" at offset 0
    and uint32(uint32(0x3C)) == 0x4550 // "PE\0\0" at e_lfanew
    and filesize < 2MB
    and ( $cfg or 2 of ($s*) )
    and #ua >= 2                       // $ua occurs at least twice
    and $hex in (0 .. 4096)            // within the first 4 KB

uint16(0) == 0x5A4D est l’idiome que vous écrirez le plus souvent. uint16 lit une valeur 16 bits petit-boutiste : les octets 4D 5A (« MZ ») se lisent donc 0x5A4D. Placer la vérification de l’en-tête et filesize en premier permet au moteur d’écarter à peu de frais la plupart des fichiers qui ne correspondent pas, et empêche une règle PE de se déclencher sur un fichier texte qui cite vos chaînes (votre propre rapport de triage, par exemple).

Les opérateurs d’ensemble sont ce qui rend les règles robustes : any of them, all of them, 2 of ($s*), any of ($a*, $b1). $s* s’étend à toutes les chaînes dont le nom commence par $s : nommer les chaînes selon leur rôle ($s_ pour les chaînes fortes, $w_ pour les faibles, $code_ pour le code) garde les conditions lisibles.

Les modules pe et math

Les modules exposent la structure analysée du fichier aux conditions. Le module pe transforme en champs les en-têtes étudiés dans En-têtes PE et Sections PE :

text
import "pe"
import "math"

rule SUSP_PE_Injection_Import_Triad
{
    condition:
        pe.imports("kernel32.dll", "VirtualAllocEx")
        and pe.imports("kernel32.dll", "WriteProcessMemory")
        and pe.imports("kernel32.dll", "CreateRemoteThread")
}

rule SUSP_PE_HighEntropy_Text
{
    condition:
        uint16(0) == 0x5A4D
        and for any i in (0 .. pe.number_of_sections - 1) : (
            pe.sections[i].name == ".text"
            and math.entropy(pe.sections[i].raw_data_offset,
                             pe.sections[i].raw_data_size) >= 7.2
        )
}

rule SUSP_PE_FewSections_NoText
{
    condition:
        uint16(0) == 0x5A4D
        and pe.number_of_sections <= 3
        and not for any s in pe.sections : ( s.name == ".text" )
}

Champs et fonctions utiles :

ExpressionRenvoie
pe.imports("dll", "Func")Vrai si cette fonction est importée (le nom de DLL est insensible à la casse)
pe.imphash()Le hash d’imports vu dans Identifier et hacher les fichiers
pe.number_of_sectionsNombre de sections d’après l’en-tête du fichier
pe.sections[i].name, .raw_data_offset, .raw_data_sizeDonnées par section
pe.pdb_pathLe chemin PDB issu de l’enregistrement de débogage CodeView, s’il existe
pe.is_dll(), pe.timestamp, pe.entry_pointInformations d’en-tête
math.entropy(offset, size)Entropie de Shannon d’une plage d’octets, de 0,0 à 8,0

Ce sont exactement les signaux de Lire les capacités à partir des imports et de Détecter les packers et l’entropie, exprimés sous forme de règles. Des règles comme les trois ci-dessus sont des règles de chasse (hunting) : elles signalent des traits suspects, pas une famille, et correspondront à certains logiciels légitimes. C’est acceptable tant que leurs noms et leurs métadonnées le disent.

Attention : pe.imphash() n’est pas plus unique que la table des imports. Dans le lab ci-dessous, le programme du lab et un programme printf d’une ligne, compilés avec la même chaîne mingw-w64, produisent le même imphash, car tous deux n’importent que le runtime C. L’imphash identifie une famille lorsque les imports propres à l’auteur dominent la table, pas pour les petits programmes.

Performances : les atomes et pourquoi les règles deviennent lentes

YARA ne recherche pas chaque chaîne à chaque position. À la compilation, il extrait de chaque chaîne un court atome (jusqu’à quatre octets) qui doit être présent pour que la chaîne corresponde, et injecte tous les atomes dans un unique automate d’Aho-Corasick. Analyser un fichier revient à faire une seule passe sur ses octets à la recherche d’atomes ; la chaîne complète n’est vérifiée que là où un atome est trouvé. Les performances dépendent donc de la qualité des atomes :

  • Les chaînes courtes ou banales font de mauvais atomes. Une chaîne de 2 octets, ou une chaîne hexadécimale comme { 8D ?? ?? }, donne un atome qui se déclenche en permanence, et chaque occurrence coûte une vérification. YARA avertit : string "$a" may slow down scanning.
  • Les octets courants font de mauvais atomes. 00 00 00 00, FF FF FF FF, 90 90, CC CC apparaissent partout dans les fichiers PE.
  • Les regex sans préfixe littéral sont coûteuses. /[a-z]+\/[0-9]\.[0-9]/ déclenche le même avertissement ; /LabAgent\/[0-9]\.[0-9]/ non.
  • Les grands sauts et les nombreuses alternatives multiplient le travail. Préférez [0-16] à [0-512] et séparez les variantes très différentes en chaînes distinctes.
  • Les conditions sont évaluées après l’analyse : une condition peu coûteuse n’économise donc pas la recherche des chaînes. Mais filesize et les vérifications d’en-tête réduisent quand même l’analyse coûteuse des modules et l’évaluation des boucles.

Traitez l’avertissement du compilateur comme un test en échec. Le lab le montre se déclencher sur une vraie règle, avec la correction.

Des constats de triage aux chaînes de règle

Les meilleures chaînes sont des choses que l’auteur a choisies et qu’il trouverait fastidieux de changer :

Constat de triageBon matériau de règle ?Remarques
User-agent, noms de mutex, de pipe ou d’événement personnalisésOuiCodés en dur, souvent réutilisés d’une version à l’autre
Marqueurs de configuration, chaînes de format, noms de commandesOuip. ex. "cmd=%s&id=%08x", "LABCFG{"
Chemin PDB issu du répertoire de débogageOui, comme élément à l’appuiLaisse fuiter des noms d’utilisateurs et de projets ; voir En-têtes PE
Code distinctif : décodeurs, préparations de clé, constantesOui, avec des jokersSurvit aux changements de chaînes ; peut casser d’un compilateur à l’autre
Fautes de frappe et tournures étranges dans les messagesOuiÉtonnamment stables d’un build à l’autre
Domaines et IP de C2FaibleChangent constamment ; mieux comme IOC (indicateurs de compromission) que comme chaînes de règle
Chaînes de bibliothèques, du compilateur et du runtimeNonPartagées par tous les programmes compilés de la même manière
Noms d’API WindowsNon, pris seulsUtilisez plutôt pe.imports() ou une règle capa
Hashs du fichier entierPas dans YARAUtilisez des IOC de hash ; une règle doit généraliser

Chaînes et chaînes obfusquées est la source de la plupart de ces candidates. Pour chacune, posez-vous deux questions : le prochain build la contiendrait-il encore ? et un programme légitime pourrait-il la contenir ? Une chaîne qui passe les deux tests est une chaîne forte : quelques-unes suffisent à elles seules pour une correspondance. Une chaîne qui ne passe que le premier est faible : utile seulement en combinaison.

Cette distinction devient la condition. Un schéma courant est « trois des chaînes fortes, ou une chaîne forte plus des éléments à l’appui », ce qui tolère que l’auteur supprime ou modifie un artefact sans pour autant correspondre sur des éléments génériques seuls.

Nommage, métadonnées et versionnement

Un jeu de règles est une base de code partagée. Les conventions des jeux de règles publics largement utilisés (voir le guide de style YARA de Florian Roth) fonctionnent bien :

  • Préfixe selon l’objectif : MAL_ pour une famille de malwares, HKTL_ pour les outils offensifs (hack tools), SUSP_ pour les traits suspects, HUNT_ pour les règles de chasse larges qui ne sont pas destinées au blocage.
  • Puis plateforme, famille et détail distinctif : MAL_Win_LabImplant_Strings, MAL_Win_LabImplant_Decoder.
  • Métadonnées minimales : description, author, date, reference (l’identifiant de votre rapport ou une analyse publique), une ou plusieurs valeurs hash d’échantillons sur lesquels la règle a été testée, et une étiquette de partage comme tlp.
  • Versionnez-la : incrémentez version et modified à chaque modification, gardez les règles dans Git, et conservez les hashs des échantillons de test à côté de la règle, pour que n’importe qui puisse relancer les tests de vrais positifs.

Astuce : Rédigez la description comme la réponse à « que signifie une correspondance ? » Un analyste SOC qui lit une alerte à 3 heures du matin doit savoir si elle signifie « famille de malwares connue » ou « packer inhabituel, à examiner ».

Lab : détecter l’implant du lab

Vous allez compiler un programme inoffensif qui contient le type d’artefacts que transporte un vrai implant (un user-agent, un nom de mutex, un nom de pipe en UTF-16, un marqueur de configuration, un nom de PDB et une minuscule boucle de décodage), mais qui se contente de les afficher. Il n’a ni réseau, ni persistance, et ne crée aucun mutex. Il vous faut mingw-w64, UPX et YARA (brew install yara, apt install yara, ou la version Windows disponible sur la page GitHub VirusTotal/yara ; pip install yara-python vous donne les bindings Python).

  1. Créez lab_implant.c :

    c
    /* lab_implant.c - a harmless "implant" for YARA practice.
     * It does nothing but print strings that look like implant artefacts. */
    #include <windows.h>
    #include <stdio.h>
    
    #ifndef LAB_VERSION
    #define LAB_VERSION "1.0"
    #endif
    #ifndef LAB_INTERVAL
    #define LAB_INTERVAL "3600"
    #endif
    
    static const char    USER_AGENT[] = "LabAgent/" LAB_VERSION " (Windows NT 10.0; lab-build)";
    static const char    MUTEX_NAME[] = "Global\\LabDemoMutex";
    static const wchar_t PIPE_NAME[]  = L"\\\\.\\pipe\\lab-demo-pipe";
    static const char    CONFIG[]     = "LABCFG{interval=" LAB_INTERVAL ";jitter=20;mode=demo}";
    
    /* An "encoded" blob and a tiny decoder, so the binary has distinctive code. */
    unsigned char encoded_blob[] = { 0x3e, 0x3e, 0x31, 0x32, 0x7f };
    
    __attribute__((noinline))
    void lab_decode(unsigned char *buf, size_t len) {
        for (size_t i = 0; i < len; i++)
            buf[i] ^= (unsigned char)(0x5A + i);
    }
    
    int main(void) {
        lab_decode(encoded_blob, sizeof encoded_blob);
        printf("user-agent : %s\n", USER_AGENT);
        printf("mutex name : %s\n", MUTEX_NAME);
        printf("pipe name  : %ls\n", PIPE_NAME);
        printf("config     : %s\n", CONFIG);
        printf("decoded    : %.5s\n", (char *)encoded_blob);
        return 0;
    }
  2. Compilez deux « versions », comme diffèrent les publications d’un auteur : v1 non optimisée, v2 optimisée avec un nouveau numéro de version et un nouvel intervalle. --pdb fait écrire à l’éditeur de liens un enregistrement de débogage CodeView qui nomme un fichier PDB, comme le fait MSVC. MSVC stocke le chemin de build complet (contenant souvent un nom d’utilisateur) ; GNU ld stocke le nom que vous lui passez.

    bash
    mkdir -p build/v1 build/v2
    x86_64-w64-mingw32-gcc -O0 -s -Wl,--pdb=build/v1/lab_implant.pdb \
        -o build/v1/lab_implant.exe lab_implant.c
    x86_64-w64-mingw32-gcc -O2 -s -DLAB_VERSION='"1.1"' -DLAB_INTERVAL='"900"' \
        -Wl,--pdb=build/v2/lab_implant.pdb -o build/v2/lab_implant.exe lab_implant.c
  3. Faites le triage de v1 comme dans les leçons précédentes : strings -a, GNU strings -el ou FLOSS (pour le nom de pipe en UTF-16), et vos scripts d’imports et de hachage. Localisez ensuite le décodeur dans un désassemblage. C’est la boucle qui ajoute 0x5A :

    bash
    x86_64-w64-mingw32-objdump -d build/v1/lab_implant.exe | grep -B8 -A8 '0x5a('

    Dans mon build v1, la boucle contient ces octets (sortie d’objdump, adresses supprimées) :

    text
    44 8d 40 5a     lea    0x5a(%rax),%r8d
    48 8b 55 10     mov    0x10(%rbp),%rdx
    48 8b 45 f8     mov    -0x8(%rbp),%rax
    48 01 d0        add    %rdx,%rax
    44 31 c1        xor    %r8d,%ecx
    89 ca           mov    %ecx,%edx
    88 10           mov    %dl,(%rax)
  4. Écrivez la première règle évidente, naive.yar, à partir du user-agent exact et des octets de code exacts :

    text
    rule LabImplant_naive
    {
        strings:
            $ua   = "LabAgent/1.0 (Windows NT 10.0; lab-build)"
            $code = { 44 8D 40 5A 48 8B 55 10 48 8B 45 F8 48 01 D0 44 31 C1 }
    
        condition:
            all of them
    }
    bash
    yara -s naive.yar build/v1/lab_implant.exe build/v2/lab_implant.exe
    text
    LabImplant_naive build/v1/lab_implant.exe
    0x2200:$ua: LabAgent/1.0 (Windows NT 10.0; lab-build)
    0x8bc:$code: 44 8D 40 5A 48 8B 55 10 48 8B 45 F8 48 01 D0 44 31 C1

    Elle correspond à v1 et manque complètement v2. Le changement de version a cassé $ua, et -O2 a réécrit la boucle, donc $code a disparu aussi. Vos offsets peuvent différer ; le résultat, non.

  5. Généralisez. Comparez le désassemblage de v2 (le même grep sur build/v2/lab_implant.exe) : l’optimiseur a produit 8d 50 5a 30 14 01 48 83 c0 01 (lea 0x5a(%rax),%edx; xor %dl,(%rcx,%rax,1); add $0x1,%rax). Une première tentative pour couvrir les deux boucles avec un seul motif, { 8D (40 | 50) 5A [0-12] (30 | 31) }, correspond aux deux builds, mais sa compilation avec les avertissements activés signale string "$decode" may slow down scanning : son meilleur atome est constitué de trois octets courants. Séparez-le plutôt en deux motifs plus longs, un par compilation. Le résultat, lab_implant.yar :

    text
    import "pe"
    
    rule MAL_Win_LabImplant_Strings
    {
        meta:
            description = "Detects the benign LabImplant training program by its embedded artefacts"
            author      = "Your Name"
            date        = "2026-09-29"
            modified    = "2026-09-29"
            version     = "1.2"
            reference   = "TR-2026-0929-01 (internal triage report)"
            hash1       = "<sha256 of your build/v1/lab_implant.exe>"
            hash2       = "<sha256 of your build/v2/lab_implant.exe>"
            tlp         = "CLEAR"
    
        strings:
            $s_ua      = /LabAgent\/[0-9]{1,2}\.[0-9]{1,2} \(Windows NT/ ascii
            $s_mutex   = "Global\\LabDemoMutex" ascii wide
            $s_pipe    = "\\\\.\\pipe\\lab-demo-pipe" wide
            $s_cfg     = "LABCFG{interval=" ascii
            $pdb       = "lab_implant.pdb" ascii nocase fullword
            $decode_o0 = { 44 8D 40 5A [8-16] 44 31 C1 89 CA 88 10 }  // -O0 loop
            $decode_o2 = { 8D 50 5A 30 14 01 48 83 C0 01 }             // -O2 loop
    
        condition:
            uint16(0) == 0x5A4D
            and filesize < 2MB
            and (
                3 of ($s_*)
                or ($pdb and 1 of ($s_*))
                or (1 of ($decode_*) and 2 of ($s_*))
            )
    }

    Notez l’échappement : dans une chaîne texte YARA, \\ représente une barre oblique inverse, si bien que "\\\\.\\pipe\\..." correspond au littéral \\.\pipe\....

  6. Testez les vrais positifs sur les deux versions :

    bash
    yara -s lab_implant.yar build/v1/lab_implant.exe build/v2/lab_implant.exe
    text
    MAL_Win_LabImplant_Strings build/v1/lab_implant.exe
    0x2200:$s_ua: LabAgent/1.0 (Windows NT
    0x2230:$s_mutex: Global\LabDemoMutex
    0x2260:$s_pipe: \\x00\\x00.\x00\\x00p\x00i\x00p\x00e\x00\\x00l\x00a\x00b\x00-\x00d\x00e\x00m\x00o\x00-\x00p\x00i\x00p\x00e\x00
    0x22a0:$s_cfg: LABCFG{interval=
    0x2e34:$pdb: lab_implant.pdb
    0x8bc:$decode_o0: 44 8D 40 5A 48 8B 55 10 48 8B 45 F8 48 01 D0 44 31 C1 89 CA 88 10
    MAL_Win_LabImplant_Strings build/v2/lab_implant.exe
    0x2500:$s_ua: LabAgent/1.1 (Windows NT
    0x24d0:$s_mutex: Global\LabDemoMutex
    0x24a0:$s_pipe: \\x00\\x00.\x00\\x00p\x00i\x00p\x00e\x00\\x00l\x00a\x00b\x00-\x00d\x00e\x00m\x00o\x00-\x00p\x00i\x00p\x00e\x00
    0x2460:$s_cfg: LABCFG{interval=
    0x3034:$pdb: lab_implant.pdb
    0xa80:$decode_o2: 8D 50 5A 30 14 01 48 83 C0 01

    -s affiche chaque chaîne correspondante avec son offset. Lisez-le d’un œil critique : la règle correspondrait encore si l’un des artefacts disparaissait.

  7. Testez l’effet du packing. Packez v1 et analysez-le à la fois avec votre règle et avec les règles structurelles de la section pe ci-dessus (enregistrez-les sous pe_hunting.yar) :

    bash
    upx -q -o build/v1/lab_implant_upx.exe build/v1/lab_implant.exe
    yara lab_implant.yar build/v1/lab_implant_upx.exe
    yara pe_hunting.yar build/v1/lab_implant_upx.exe

    La règle sur les chaînes ne trouve rien : les chaînes sont compressées. Seule SUSP_PE_FewSections_NoText se déclenche (UPX laisse des sections nommées UPX0, UPX1, UPX2). C’est pourquoi les règles sur fichiers et l’analyse mémoire se complètent, et pourquoi Détecter les packers et l’entropie précède l’écriture de règles dans le triage.

  8. Testez les faux positifs. Sous Windows, analysez le répertoire système ; sous Linux ou macOS, analysez /usr/bin avec une copie de la règle dont la vérification d’en-tête est remplacée par true (sinon le test MZ rejette d’office tout fichier ELF ou Mach-O) :

    bash
    # Windows (cmd, in the lab VM)
    yara -r lab_implant.yar C:\Windows\System32 2>nul
    # Linux / macOS
    sed 's/uint16(0) == 0x5A4D/true/' lab_implant.yar > lab_implant_nohdr.yar
    yara -r lab_implant_nohdr.yar /usr/bin 2>/dev/null

    Attendez-vous à n’obtenir aucune sortie. Toute correspondance est une chaîne que vous pensiez unique et qui ne l’est pas ; ouvrez le fichier, comprenez pourquoi, et resserrez la règle. Dans une vraie équipe, cette étape s’exécute automatiquement sur un large corpus de goodware à chaque modification de règle.

  9. Vérifiez l’affirmation sur l’imphash faite dans l’avertissement ci-dessus :

    bash
    printf '#include <stdio.h>\nint main(int c, char **v){printf("argc=%%d %%s\\n", c, v[0]);return 0;}\n' > hello2.c
    x86_64-w64-mingw32-gcc -O2 -s -o hello2.exe hello2.c
    python3 -c "import pefile,sys; [print(f, pefile.PE(f).get_imphash()) for f in sys.argv[1:]]" \
        build/v1/lab_implant.exe build/v2/lab_implant.exe hello2.exe

Questions : Quel artefact unique supprimeriez-vous de la source pour que la règle échoue, et la condition le permet-elle ? Pourquoi $s_mutex n’a-t-il correspondu que sous sa forme ASCII alors que la règle demande ascii wide ? Si l’auteur plaçait toutes les chaînes derrière le décodeur, quelles parties de la règle fonctionneraient encore, et qu’ajouteriez-vous ? Pourquoi l’imphash de hello2.exe est-il identique à celui de l’implant, et qu’est-ce que cela vous apprend sur les règles d’imphash pour les petits programmes ?

À retenir

  • Une règle YARA, c’est meta + strings + condition ; une correspondance signifie « le motif est présent », et le nom et la description de la règle doivent dire ce que cela implique.
  • Utilisez ascii wide, nocase, fullword, les jokers, les sauts et les alternatives pour absorber les différences que vous attendez entre builds, et rien de plus.
  • Commencez les règles PE par uint16(0) == 0x5A4D et une borne filesize ; utilisez les modules pe et math pour la structure, les imports et l’entropie.
  • Construisez les chaînes à partir d’artefacts choisis par l’auteur (noms, marqueurs, chemins PDB, code du décodeur) et combinez-les avec N of ($s*) pour qu’un seul changement ne casse pas la règle.
  • Tenez compte de l’avertissement « may slow down scanning » : les mauvais atomes et les regex non ancrées pèsent sur chaque analyse, partout.
  • Une règle n’est terminée que lorsqu’elle a correspondu à toutes les variantes connues, n’a rien trouvé dans un corpus de goodware, et a été versionnée avec les hashs sur lesquels elle a été testée.