Comment créer un moteur antivirus
Introduction
En parcourant les forums de techniciens, je vois souvent des personnes (et beaucoup peu expérimentées) demander « Comment faire un antivirus », parfois avec des langages peu adaptés (bat, PHP, …) et ayant une mauvaise idée de ce qu’est un antivirus, et comment il devrait être construit.
J’ai également vu de nombreux « logiciels antivirus » fabriqués par des enfants, avec très peu de personnes encore à l’école et environ 4 heures par jour de codage sur plusieurs semaines. Je ne dis pas que les enfants ne sont pas qualifiés, mais je dis que la construction d’un moteur antivirus nécessite soit beaucoup de personnes qualifiées avec un emploi à temps plein, plus beaucoup de temps pour publier un logiciel décent ou beaucoup d’argent pour les payer 🙂 (au cas où ils ne seraient pas bénévoles).
Donc, je vais couvrir ici les directives pour un codage antivirus de base, pour Windows et en C / C++. On peut trouver ici les pointeurs pour concevoir un moteur antivirus, ou simplement apprendre comment la plupart d’entre eux sont construits.
Protection
Pour une bonne protection, un Antivirus doit avoir au moins un pilote, pour pouvoir exécuter du code dans le noyau et avoir globalement accès aux API du noyau. À partir de Vista, Microsoft a compris que l’industrie des antivirus avait besoin de clés pour entrer dans le noyau et activer les filtres à des endroits stratégiques, tels que le système de fichiers, le registre et le réseau. Ne soyez pas stupéfait si la création d’un antivirus pour les systèmes pré-Vista peut être une vraie douleur, car il n’a pas été conçu pour cela.
- Cependant, sur les systèmes Pré-Vista, les sociétés antivirus utilisaient des fonctionnalités de type rootkit pour protéger les portes (même si cela n’était pas du tout recommandé par Microsoft) et pouvoir protéger votre système. Ils ont utilisé ce que nous appelons des « Hooks » (détours d’API à des fins de filtrage).
- Sur Vista+, Microsoft a fourni des API pour insérer notre pilote de bas niveau entre les appels utilisateur et les API du noyau. De cette façon, il est facile d’enregistrer un produit antivirus dans le noyau. De plus, ce type de système basé sur l’enregistrement nous permet de répartir la sécurité de notre système en couches, où plusieurs produits ayant des objectifs différents peuvent cohabiter. Ce n’était pas le cas pour les hooks, car la mise en œuvre était totalement dépendante du produit.
NOTE: Je ne couvrirai pas les solutions de contournement avec des hooks pour les systèmes pré-Vista, car c’est facile à trouver sur Internet, et parce qu’il faudrait un chapitre entier pour expliquer comment accrocher, où accrocher et ainsi de suite But Mais il faut savoir que c’est la même idée que les API du noyau, sauf que vous devez implémenter vous-même ce que Microsoft a fourni sur les systèmes Vista+.
Pour en savoir plus sur le codage des pilotes, vous pouvez vérifier que les liens utiles:
http://msdn.microsoft.com/en-us/library/windows/hardware/gg490655.aspx
http://www.codeproject.com/Articles/9504/Driver-Development-Part-1-Introduction-to-Drivers
Pour en savoir plus sur les hooks, vous pouvez vérifier cet exemple de base:
http://www.unknowncheats.me/forum/c-and-c/59147-writing-drivers-perform-kernel-level-ssdt-hooking.html
Process
La première chose à protéger l’utilisateur est le lancement de processus malveillants. C’est la chose de base. L’antivirus doit enregistrer un rappel PsSetCreateProcessNotifyRoutineEx. Ce faisant, à chaque création de processus, et avant que le thread principal ne commence à s’exécuter (et ne provoque des choses malveillantes), le rappel antivirus est notifié et reçoit toutes les informations nécessaires.
Il reçoit le nom du processus, l’objet fichier, le PID, etc. Comme le processus est en attente, le pilote peut demander à son service d’analyser la mémoire du processus pour détecter tout élément malveillant. Il fonde quelque chose, le pilote définira simplement CreationStatus sur FALSE et retournera.
NTSTATUS PsSetCreateProcessNotifyRoutineEx( _In_ PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine, _In_ BOOLEAN Remove);
VOID CreateProcessNotifyEx( _Inout_ PEPROCESS Process, _In_ HANDLE ProcessId, _In_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo);
typedef struct _PS_CREATE_NOTIFY_INFO { SIZE_T Size; union { ULONG Flags; struct { ULONG FileOpenNameAvailable :1; ULONG Reserved :31; }; }; HANDLE ParentProcessId; CLIENT_ID CreatingThreadId; struct _FILE_OBJECT *FileObject; PCUNICODE_STRING ImageFileName; PCUNICODE_STRING CommandLine; NTSTATUS CreationStatus;} PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO;
Threads
Dans la même idée que pour les processus, les threads peuvent être un moyen pour les objets malveillants de causer des dommages. Par exemple, on peut injecter du code dans un processus légitime et démarrer un thread distant sur ce code dans le contexte du processus (facile à suivre? 🙂 ). De cette façon, un processus légitime peut faire des choses malveillantes.
Nous pouvons filtrer les nouveaux threads avec le rappel PsSetCreateThreadNotifyRoutine. Chaque fois qu’un thread est créé, l’antivirus est notifié avec le TID et le PID. Ainsi, il est capable de regarder le code d’adresse de début du thread, de l’analyser et d’arrêter le thread ou de le reprendre.
NTSTATUS PsSetCreateThreadNotifyRoutine( _In_ PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine);
VOID(*PCREATE_THREAD_NOTIFY_ROUTINE) ( IN HANDLE ProcessId, IN HANDLE ThreadId, IN BOOLEAN Create );
Images
La troisième menace dynamique concerne les images qui peuvent être chargées en mémoire. Une image est un fichier PE, un fichier EXE, une DLL ou un fichier SYS. Pour être informé des images chargées, enregistrez simplement PsSetLoadImageNotifyRoutine. Ce rappel nous permet d’être averti lorsque l’image est chargée dans la mémoire virtuelle, même si elle n’est jamais exécutée. Nous pouvons alors détecter lorsqu’un processus tente de charger une DLL, de charger un pilote ou de déclencher un nouveau processus.
Le rappel obtient des informations sur le chemin d’image complet (utile pour l’analyse statique), et le plus important à mon avis, l’adresse de base de l’image (pour l’analyse en mémoire). Si l’image est malveillante, l’antivirus peut utiliser de petites astuces pour éviter l’exécution, comme analyser l’image en mémoire et aller au point d’entrée, puis appeler l’opcode d’assemblage « ret » pour l’annuler.
NTSTATUS PsSetLoadImageNotifyRoutine( _In_ PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine);
VOID (*PLOAD_IMAGE_NOTIFY_ROUTINE)( __in_opt PUNICODE_STRING FullImageName, __in HANDLE ProcessId, __in PIMAGE_INFO ImageInfo );
typedef struct _IMAGE_INFO { union { ULONG Properties; struct { ULONG ImageAddressingMode : 8; //code addressing mode ULONG SystemModeImage : 1; //system mode image ULONG ImageMappedToAllPids : 1; //mapped in all processes ULONG Reserved : 22; }; }; PVOID ImageBase; ULONG ImageSelector; ULONG ImageSize; ULONG ImageSectionNumber;} IMAGE_INFO, *PIMAGE_INFO;
Système de fichiers
Une fois que chaque élément dynamique est sécurisé, un antivirus devrait être en mesure d’avertir l’utilisateur des éléments malveillants à la volée, pas seulement lorsqu’ils sont sur le point de démarrer. Un antivirus devrait pouvoir analyser des fichiers lorsque l’utilisateur ouvre un dossier, une archive ou lorsqu’il est téléchargé sur le disque. De plus, un antivirus devrait pouvoir se protéger, en interdisant à tout programme de supprimer ses fichiers.
La façon de faire tout cela est d’installer un pilote dans le système de fichiers, et plus particulièrement un minifiltre d’un filtre hérité (à l’ancienne). Ici, nous allons parler de minifilter.
Un minifiltre est un type de pilote spécifique, capable d’enregistrer des rappels à chaque opération de lecture/écriture effectuée sur le système de fichiers (fonctions principales IRP). Un IRP (Interrupt Request Paquet) est un objet utilisé pour décrire une opération de lecture/ écriture sur le disque, qui est transmise avec la pile de pilotes. Le minifiltre sera simplement inséré dans cette pile et recevra cet IRP pour décider quoi en faire (opération autoriser / refuser).
Pour un petit exemple de minifiltre, veuillez vérifier ce lien utile ou celui-ci. Les directives Microsoft sont ici.
Vous trouverez également 2 exemples de la documentation WDK ici et ici.
Un rappel de minifiltre de base ressemble à ceci. Il existe 2 types de rappel, Pré-opération et Post-opération, qui sont capables de filtrer avant ou après la requête. Voici un pseudo code de pré-opération:
FLT_PREOP_CALLBACK_STATUS PreOperationCallback (__inout PFLT_CALLBACK_DATA Data, __in PCFLT_RELATED_OBJECTS FltObjects, __deref_out_opt PVOID *CompletionContext){ ... if ( all_good ) { return FLT_PREOP_SUCCESS_NO_CALLBACK; } else { // Access denied Data->IoStatus.Information = 0; Data->IoStatus.Status = STATUS_ACCESS_DENIED; return FLT_PREOP_COMPLETE; } }
Registre
Le registre est l’un des endroits les plus critiques à protéger. Il existe de nombreuses façons pour un logiciel malveillant de garder la main persistante sur le système en enregistrant une seule (ou quelques) clés / valeurs dans le registre. Les endroits les plus connus sont les clés d’exécution et les services. C’est également l’endroit où l’antivirus peut être vaincu (avec le système de fichiers), en supprimant simplement ses clés de pilote / service, afin qu’il ne redémarre plus au démarrage du système.
Ce n’est pas une nécessité réelle pour un antivirus de protéger les lieux de redémarrage, la plupart d’entre eux ne le font pas. Mais ils doivent protéger leurs clés de registre d’installation, pour éviter d’être facilement vaincus par des malwares. Cela peut être fait en enregistrant CmRegisterCallback.
Le rappel donne suffisamment d’informations pour obtenir le nom complet de la clé, le type d’accès (Créer, Renommer, Supprimer, …) et le PID de l’appelant. De cette façon, il est facile d’accorder ou non l’accès à l’appel, en définissant le champ d’état du rappel Post-opération.
NTSTATUS CmRegisterCallbackEx(_In_ PEX_CALLBACK_FUNCTION Function,_In_ PCUNICODE_STRING Altitude,_In_ PVOID Driver,_In_opt_ PVOID Context,_Out_ PLARGE_INTEGER Cookie,_Reserved_ PVOID Reserved);
EX_CALLBACK_FUNCTION RegistryCallback;NTSTATUS RegistryCallback( _In_ PVOID CallbackContext, _In_opt_ PVOID Argument1, _In_opt_ PVOID Argument2){ ... }Argument1 = typedef enum _REG_NOTIFY_CLASS { RegNtDeleteKey, RegNtPreDeleteKey = RegNtDeleteKey,...Argument2 = typedef struct _REG_POST_OPERATION_INFORMATION { PVOID Object; NTSTATUS Status; PVOID PreInformation; NTSTATUS ReturnStatus; PVOID CallContext; PVOID ObjectContext; PVOID Reserved;} REG_POST_OPERATION_INFORMATION, *PREG_POST_OPERATION_INFORMATION;
Réseau (Pare-feu)
Pour garder les portes de l’ensemble du trafic internet qui peut être énorme sur certains systèmes (serveurs, utilisateurs de bande passante énormes) sans être ralenti par le changement de contexte qui a lieu en zone utilisateur, il est totalement déconseillé d’installer un pare-feu qui n’a pas de pilote sous-jacent, à l’exception de certains filtres de navigateur web qui peuvent suffire au trafic http, mais qui ne protégeront pas contre les communications in/out de logiciels malveillants.
Afin d’avoir une implémentation correcte du pare-feu, il faut coder un pilote de filtrage IP de bas niveau NDIS, TDI ou une autre méthode. NDIS / TDI est un peu difficile à faire, et nécessiterait beaucoup de connaissances (plus que d’autres filtres à mon avis).
Quoi qu’il en soit, voici quelques conseils pour commencer à coder un tel pilote, les directives Microsoft et l’ancien tutoriel codeproject (mais toujours bon à lire), un exemple de pare-feu NDIS et un exemple de pare-feu TDI. Voici également un bon article sur l’astuce de contournement du pare-feu NDIS, et une petite explication sur la pile de pilotes réseau,
Protection de l’espace utilisateur
La protection de l’espace utilisateur n’est pas une nécessité, mais peut être un module supplémentaire contre les banquiers Troyens, et plus spécifiquement contre les espions de processus. Ils sont généralement injectés dans tous les processus, pour plusieurs raisons.
Tout d’abord, ils sont capables (à la demande) de tuer le processus s’il a été identifié comme un logiciel malveillant (cela ne devrait pas se produire, car les AV sont censés l’arrêter avant qu’il ne démarre). Il est toujours plus facile d’arrêter un processus lorsque vous êtes dans son contexte.
Deuxièmement, ils sont capables de protéger les processus critiques, comme les navigateurs Web, contre l’accrochage de malwares capables de détourner et de filtrer les appels d’API afin de recueillir des mots de passe, des informations bancaires et de rediriger le flux Internet vers les serveurs de logiciels malveillants. Ils ne surveillent que la modification de l’IAT, l’épissage, et peuvent également définir eux-mêmes des hooks pour éviter LoadLibray d’une DLL de malware, et ainsi interdire certaines méthodes d’injection de code.
Un moyen simple d’injecter une DLL protector dans tous les processus consiste à utiliser la clé de registre AppInitDll pour enregistrer la DLL protector. Il chargera la DLL dans chaque processus démarré sur le système, dès qu’ils lieront l’Utilisateur32.image dll (la plupart d’entre eux le font).
Moteur d’analyse
Le moteur d’analyse est l’une des parties les plus importantes, il est responsable de l’analyse des échantillons de fichiers / mémoire provenant des pilotes. If doit être rapide (même avec une énorme base de données), et devrait pouvoir gérer la plupart des types de fichiers (exécutables auto-extraits, Archives – RAR, ZIP, fichiers emballés – UPX, UP) et devrait donc avoir de nombreux modules pour le faire:
- Unpacker : Ce module doit être capable de détecter et de décompresser la plupart des packers connus (UPX, Armadillo, ASPack, …)
- Moteur de signature: La base de données d’un antivirus contient des millions de signatures, et le moteur doit pouvoir les rechercher rapidement dans un échantillon. Ainsi, un algorithme très puissant devrait en faire partie. Quelques exemples : AhoCorasick, RabinKarp, algorithmes de correspondance de chaînes.
- Bac à sable: Ce module n’est pas nécessaire, mais serait un plus pour pouvoir exécuter des échantillons dans une mémoire limitée, sans effet sur le système. Cela pourrait aider à décompresser des échantillons contenant des packers inconnus et aider le moteur heuristique (voir après) à détecter les appels d’API qui pourraient être considérés comme suspects ou malveillants. Un bon bac à sable ici.
- Moteur heuristique: Comme indiqué ci-dessus, le moteur heuristique ne recherche pas de signatures, mais recherche plutôt un comportement suspect (ie. exemple qui ouvre une connexion sur le site hxxp://malware_besite.com). Cela peut être fait par analyse statique, ou via un bac à sable.
Syntaxe de signature
La syntaxe de signature est le « dictionnaire » du langage compris par le moteur de signature. C’est un moyen de formaliser quel est le modèle à trouver, comment le rechercher et où le rechercher dans l’échantillon. La syntaxe doit être assez simple à comprendre pour les chercheurs, assez puissante pour gérer tous les cas d’utilisation et facile à analyser pour de meilleures performances du moteur.
VirusTotal a développé une bonne syntaxe et un bon moteur (projet Yara), qui est open source. Cela devrait être un bon pointeur pour créer votre propre syntaxe, ou simplement l’utiliser. Voici également un bon blog sur la façon de créer des signatures pour les antivirus.
Exemple de signature:
rule silent_banker : banker{ meta: description = "This is just an example" thread_level = 3 in_the_wild = true strings: $a = {6A 40 68 00 30 00 00 6A 14 8D 91} $b = {8D 4D B0 2B C1 83 C0 27 99 6A 4E 59 F7 F9} $c = "UVODFRYSIHLNWPEJXQZAKCBGMT" condition: $a or $b or $c}
Auto-protection
L’auto-protection est très importante pour un antivirus, pour éviter d’être vaincu par un logiciel malveillant et continuer à protéger l’utilisateur. C’est pourquoi un antivirus devrait pouvoir protéger sa propre installation et conserver sa persistance au redémarrage.
Il y a plusieurs endroits à protéger: Fichiers, Clés de registre, Processus / Threads, Mémoire.
- La protection des fichiers est implémentée dans le minifiltre, avec des règles particulières sur les fichiers de l’antivirus (Pas d’accès en suppression, renommage, déplacement, écriture).
- La protection du registre est intégrée au filtre du registre, l’accès étant refusé pour les clés de registre du pilote et du service.
- Les threads de pilotes sont protégés, car il est tout à fait impossible de décharger le module du noyau sans planter le système
- Pour pouvoir protéger le service, qui est un processus utilisateur, 2 solutions:
- Le plus simple serait d’ajouter des règles pour les échecs dans le gestionnaire de services et de définir chaque règle d’échec sur « redémarrer le service ». De cette façon, lorsque le service n’est pas arrêté par le gestionnaire de services, il redémarre. Bien entendu, le service ne devrait pas pouvoir accepter de commandes tant que le système ne redémarre pas (ou ne s’arrête pas).
- La deuxième méthode, plus générique, consisterait à définir des rappels sur les poignées de processus avec ObRegisterCallbacks.
En définissant le ObjectType sur PsProcessType et l’Opération sur OB_OPERATION_HANDLE_CREATE, vous recevez un rappel avant et après l’opération, et vous pouvez renvoyer ACCESS_DENIED dans ReturnStatus si le handle de processus interrogé a GrantedAccess qui possède des droits de fin de processus (ou des droits d’écriture de processus, ou tout ce qui peut entraîner un crash / kill), et bien sûr si le processus en est un que l’antivirus doit protéger (son service par exemple ).
Bien sûr, il faut également protéger la poignée en double et le PsThreadType pour éviter toute méthode de terminaison nécessitant de saisir une poignée sur le processus ou un thread. Voici un petit exemple d’utilisation de ce rappel.
NTSTATUS ObRegisterCallbacks( _In_ POB_CALLBACK_REGISTRATION CallBackRegistration, _Out_ PVOID *RegistrationHandle);
typedef struct _OB_CALLBACK_REGISTRATION { USHORT Version; USHORT OperationRegistrationCount; UNICODE_STRING Altitude; PVOID RegistrationContext; OB_OPERATION_REGISTRATION *OperationRegistration;} OB_CALLBACK_REGISTRATION, *POB_CALLBACK_REGISTRATION;
typedef struct _OB_OPERATION_REGISTRATION { POBJECT_TYPE *ObjectType; OB_OPERATION Operations; POB_PRE_OPERATION_CALLBACK PreOperation; POB_POST_OPERATION_CALLBACK PostOperation;} OB_OPERATION_REGISTRATION, *POB_OPERATION_REGISTRATION;
VOID ObjectPostCallback( _In_ PVOID RegistrationContext, _In_ POB_POST_OPERATION_INFORMATION OperationInformation);
typedef struct _OB_POST_OPERATION_INFORMATION { OB_OPERATION Operation; union { ULONG Flags; struct { ULONG KernelHandle :1; ULONG Reserved :31; }; }; PVOID Object; POBJECT_TYPE ObjectType; PVOID CallContext; NTSTATUS ReturnStatus; POB_POST_OPERATION_PARAMETERS Parameters;} OB_POST_OPERATION_INFORMATION, *POB_POST_OPERATION_INFORMATION;
typedef union _OB_POST_OPERATION_PARAMETERS { OB_POST_CREATE_HANDLE_INFORMATION CreateHandleInformation; OB_POST_DUPLICATE_HANDLE_INFORMATION DuplicateHandleInformation;} OB_POST_OPERATION_PARAMETERS, *POB_POST_OPERATION_PARAMETERS;
typedef struct _OB_POST_CREATE_HANDLE_INFORMATION { ACCESS_MASK GrantedAccess;} OB_POST_CREATE_HANDLE_INFORMATION, *POB_POST_CREATE_HANDLE_INFORMATION;
GUI (Interface utilisateur graphique)
C’est la partie visible de l’iceberg. À mon avis, l’une (peut-être LA) partie la plus importante si vous souhaitez vendre votre produit. Les utilisateurs adorent ce qui est beau, facile à utiliser, intuitif. Même si ce n’est pas efficace à 100%. L’interface graphique doit être sexy.
L’interface graphique n’est qu’une coquille vide, elle ne fait que des traitements graphiques et envoie/reçoit des commandes au cœur (le service). Il affiche également les barres de progression, ce qui est analysé, fournit la configuration, et donc Here Voici l’interface utilisateur Avast. Sexy, non? 🙂
Architecture
L’architecture globale pourrait ressembler à ceci:
- GUI : Aucun droit d’administration, FAIBLE
- Garde DLL(s): protection du navigateur Web, MOYEN
- Service: Droits d’administrateur. Sert de passerelle vers le code du noyau et prend des décisions avec une base de données, STRONG
- Pilote (s): Filtres du noyau, STRONG
L’interface graphique n’a besoin d’aucun droit administratif, elle ne prend que les actions de l’utilisateur et les transmet au service. Il affiche également l’état du produit. Rien de plus, ce n’est pas son but. Si l’interface graphique est tuée, ce n’est pas un problème car le service devrait pouvoir la redémarrer.
Les DLL de garde (le cas échéant) sont massivement injectées dans tous les processus et devraient pouvoir rechercher des hooks IAT et / ou des threads malveillants. Ils devraient être assez difficiles à décharger ou à vaincre. Ils ne sont pas critiques mais importants.
Le service est le cœur du produit. Il devrait être impossible à tuer, ou au moins devrait pouvoir se redémarrer automatiquement lors de la mise à mort. Le service est responsable de la communication entre tous les modules du produit, il envoie des commandes aux pilotes, prend des commandes de l’utilisateur et interroge la base de données pour une analyse d’échantillon. C’est le cerveau.
Les pilotes du noyau sont également critiques. Ce sont les tentacules qui recueillent des informations sur tout ce qui se passe sur le système et les transmettent au service pour décision. Ils peuvent également refuser l’accès aux lieux gardés, sur la base de la décision du service.
Conclusion
Créer un moteur antivirus solide, fiable et stable est une tâche compliquée, qui nécessite des personnes expérimentées avec une très forte connaissance de la programmation du noyau Windows, de la programmation des applications Windows, de la conception graphique, de l’architecture logicielle, de l’analyse des logiciels malveillants,
Construire des pilotes stables est également une tâche compliquée, car un petit grain de sable peut faire planter tout le système. Il a besoin de tests, de tests et de beaucoup de tests. Lorsque votre moteur est terminé et fonctionne, il vous suffit d’engager des chercheurs pour analyser les malwares et ajouter des signatures à votre base de données 🙂 Qu’attendez-vous? Vas-y! Links
Liens
- http://www.symantec.com/connect/articles/building-anti-virus-engine