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 processus | S’exécute sur des milliers de fichiers par hôte : la vitesse compte |
| Sandboxes et passerelles de messagerie | Fichiers soumis, fichiers déposés, dumps mémoire | Sert à la classification et à l’étiquetage automatiques |
| VirusTotal Livehunt et Retrohunt | Nouveaux téléversements, ou des mois de téléversements passés | Trouve 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émoire | Attrape les malwares packés une fois qu’ils se sont dépaquetés |
| Votre propre dépôt d’échantillons | Votre collection | Regroupement 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
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 :
metacontient 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.stringsdé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.conditionest 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 :
| Modificateur | Effet | Usage typique |
|---|---|---|
ascii | Correspond à la forme à un octet par caractère (par défaut s’il est seul) | Chaînes C, texte de configuration |
wide | Correspond à l’UTF-16LE : chaque caractère ASCII suivi de 00 | API Windows W, .NET, littéraux L"..." |
ascii wide | Correspond à l’une ou l’autre forme | Quand vous ne savez pas comment le prochain build la stockera |
nocase | Insensible à la casse | Chemins PDB, noms de fichiers, en-têtes HTTP |
fullword | Ne correspond que si la chaîne est délimitée par des caractères non alphanumériques | Noms courts qui apparaissent aussi dans des mots plus longs |
xor / base64 | Correspond aux encodages XOR sur un octet ou base64 de la chaîne | Chaî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 :
widesignifie 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 :
| Syntaxe | Signification | Exemple |
|---|---|---|
?? | 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 } |
~XX | N’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 :
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 KBuint16(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 :
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 :
| Expression | Renvoie |
|---|---|
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_sections | Nombre de sections d’après l’en-tête du fichier |
pe.sections[i].name, .raw_data_offset, .raw_data_size | Données par section |
pe.pdb_path | Le chemin PDB issu de l’enregistrement de débogage CodeView, s’il existe |
pe.is_dll(), pe.timestamp, pe.entry_point | Informations 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 programmeprintfd’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 CCapparaissent 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
filesizeet 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 triage | Bon matériau de règle ? | Remarques |
|---|---|---|
| User-agent, noms de mutex, de pipe ou d’événement personnalisés | Oui | Codés en dur, souvent réutilisés d’une version à l’autre |
| Marqueurs de configuration, chaînes de format, noms de commandes | Oui | p. ex. "cmd=%s&id=%08x", "LABCFG{" |
| Chemin PDB issu du répertoire de débogage | Oui, comme élément à l’appui | Laisse fuiter des noms d’utilisateurs et de projets ; voir En-têtes PE |
| Code distinctif : décodeurs, préparations de clé, constantes | Oui, avec des jokers | Survit aux changements de chaînes ; peut casser d’un compilateur à l’autre |
| Fautes de frappe et tournures étranges dans les messages | Oui | Étonnamment stables d’un build à l’autre |
| Domaines et IP de C2 | Faible | Changent constamment ; mieux comme IOC (indicateurs de compromission) que comme chaînes de règle |
| Chaînes de bibliothèques, du compilateur et du runtime | Non | Partagées par tous les programmes compilés de la même manière |
| Noms d’API Windows | Non, pris seuls | Utilisez plutôt pe.imports() ou une règle capa |
| Hashs du fichier entier | Pas dans YARA | Utilisez 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 valeurshashd’échantillons sur lesquels la règle a été testée, et une étiquette de partage commetlp. - Versionnez-la : incrémentez
versionetmodifiedà 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
descriptioncomme 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).
-
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; } -
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.
--pdbfait é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 -
Faites le triage de v1 comme dans les leçons précédentes :
strings -a, GNUstrings -elou 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 ajoute0x5A: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) -
É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.exetext 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 C1Elle correspond à v1 et manque complètement v2. Le changement de version a cassé
$ua, et-O2a réécrit la boucle, donc$codea disparu aussi. Vos offsets peuvent différer ; le résultat, non. -
Généralisez. Comparez le désassemblage de v2 (le même
grepsurbuild/v2/lab_implant.exe) : l’optimiseur a produit8d 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 signalestring "$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\.... -
Testez les vrais positifs sur les deux versions :
bash yara -s lab_implant.yar build/v1/lab_implant.exe build/v2/lab_implant.exetext 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-saffiche chaque chaîne correspondante avec son offset. Lisez-le d’un œil critique : la règle correspondrait encore si l’un des artefacts disparaissait. -
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
peci-dessus (enregistrez-les souspe_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.exeLa règle sur les chaînes ne trouve rien : les chaînes sont compressées. Seule
SUSP_PE_FewSections_NoTextse déclenche (UPX laisse des sections nomméesUPX0,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. -
Testez les faux positifs. Sous Windows, analysez le répertoire système ; sous Linux ou macOS, analysez
/usr/binavec une copie de la règle dont la vérification d’en-tête est remplacée partrue(sinon le testMZrejette 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/nullAttendez-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.
-
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) == 0x5A4Det une bornefilesize; utilisez les modulespeetmathpour 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.