Analyser les fichiers dans .NET avec un antivirus open source
Analysez avec ClamAV un fichier placé en zone de transit et géré par l’application avant de l’accepter. Ce programme console .NET 10 envoie le fichier à un démon local et renvoie des verdicts distincts : sain, infecté ou erreur de l’analyseur. Il ne nettoie, ne modifie ni ne supprime jamais le fichier d’entrée.
Introduction
Seule une analyse terminée avec un verdict sain autorise l’étape de traitement suivante. Un fichier manquant, une limite de taille, un démon indisponible, une réponse mal formée, un délai d’attente dépassé ou une annulation doivent empêcher l’acceptation du fichier. Un verdict sain décrit l’analyse du moteur avec sa configuration et ses signatures actuelles ; il ne garantit pas qu’un fichier est inoffensif.
Outils antivirus open source pour .NET
ClamAV fournit l’analyseur. Le client TCP intégré à .NET peut communiquer selon son protocole INSTREAM documenté ; cet exemple n’a donc besoin d’aucune bibliothèque NuGet enveloppant l’analyseur. Gardez le démon sur le même hôte : son interface TCP ne comporte pas d’authentification.
Configurer ClamAV
Les commandes ci-dessous utilisent Bash sous Linux. Installez le SDK .NET 10 actuel, en version 10.0.111 ou ultérieure de la série .NET 10, ainsi que ClamAV 1.5.4 en suivant ses instructions par plateforme. Le programme a été testé avec le SDK 10.0.111 et ClamAV 1.5.4 ; consultez la politique de support de ClamAV avant de déployer cette version.
Configurez clamd et freshclam pour utiliser
le même répertoire de base de données, et attendez la fin du téléchargement initial des signatures.
Si votre distribution exécute un service FreshClam, laissez-le gérer les mises à jour plutôt que
de lancer un second outil de mise à jour. Conservez les paramètres de base de données, d’utilisateur
et de journalisation du paquet dans clamd.conf.
Remplacez les valeurs existantes de ces directives par ce fragment ; n’ajoutez pas de copies
conflictuelles :
TCPAddr 127.0.0.1
TCPSocket 3310
StreamMaxLength 25M
MaxFileSize 26M
MaxScanSize 26M
AlertExceedsMax yes
La limite du flux correspond au plafond de 25 MiB du client. Les limites du moteur bornent aussi
le contenu décompressé ; AlertExceedsMax fait en sorte que les limites
correspondantes déclenchent des alertes que ce programme traite comme des erreurs de l’analyseur.
Examinez les autres limites des archives et la politique relative aux fichiers chiffrés selon
votre charge de travail. Consultez la
référence de configuration versionnée.
Démarrez ou redémarrez clamd avec la configuration modifiée via
le gestionnaire de services de votre plateforme.
Créez l’application dans un répertoire accessible en écriture, en dehors d’une arborescence de
compilation .NET existante. Collez ce bloc Bash en entier. Il refuse toute configuration du SDK
ou de compilation dans les répertoires parents, ainsi qu’un répertoire
FileScan existant, et n’entre dans le projet qu’après la réussite de
sa création et de la restauration :
(
cd -P . || exit 1
scanner_parent=$PWD
while :; do
for scanner_config in global.json Directory.Build.props Directory.Build.targets; do
if [ -e "$scanner_parent/$scanner_config" ] || [ -L "$scanner_parent/$scanner_config" ]; then
printf 'Choose a directory outside an existing .NET build tree.\n' >&2
exit 1
fi
done
[ "$scanner_parent" = / ] && break
scanner_parent=${scanner_parent%/*}
scanner_parent=${scanner_parent:-/}
done
command -v dotnet >/dev/null &&
mkdir FileScan &&
dotnet new console --framework net10.0 --name FileScan --output FileScan --no-restore &&
dotnet restore FileScan/FileScan.csproj
) && cd FileScan
Un échec de création ou de restauration peut laisser un répertoire
FileScan partiellement créé, tandis que votre shell reste dans son
répertoire initial. Examinez l’erreur et préservez les fichiers existants. Si ce bloc a créé
le projet avec succès, mais que la restauration a échoué, corrigez le problème de SDK ou de source
de paquets, puis réessayez depuis ce répertoire initial :
dotnet restore FileScan/FileScan.csproj && cd FileScan
Si la création du projet elle-même a échoué, choisissez un nouvel emplacement vide avant de
recommencer la configuration. N’utilisez pas --force pour écraser
un projet existant.
Implémenter l’analyse de fichiers en C#
Remplacez Program.cs par ce programme complet. Il transmet au maximum
25 MiB par blocs de 64 KiB, borne la réponse du démon et annule toutes les opérations réseau
et de fichiers après 30 secondes ou à l’appui sur Ctrl+C.
using System.Buffers.Binary;
using System.Net;
using System.Net.Sockets;
using System.Text;
public enum Verdict { Clean, Infected, ScannerError }
public static class Program
{
public static async Task<int> Main(string[] args)
{
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(30));
Console.CancelKeyPress += (_, e) => { e.Cancel = true; deadline.Cancel(); };
if (args.Length != 1) { Console.Error.WriteLine("Pass one local file."); return 2; }
var verdict = await FileScanner.ScanAsync(args[0], 3310, deadline.Token);
Console.WriteLine(verdict);
return verdict switch { Verdict.Clean => 0, Verdict.Infected => 1, _ => 2 };
}
}
public static class FileScanner
{
private const int MaxBytes = 25 * 1024 * 1024;
public static async Task<Verdict> ScanAsync(string path, int port, CancellationToken ct)
{
try
{
await using var file = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
if (file.Length > MaxBytes) return Verdict.ScannerError;
using var client = new TcpClient();
await client.ConnectAsync(IPAddress.Loopback, port, ct);
using var stream = client.GetStream();
await stream.WriteAsync(Encoding.ASCII.GetBytes("zINSTREAM\0"), ct);
var buffer = new byte[64 * 1024];
var header = new byte[4];
long sent = 0;
int count;
while ((count = await file.ReadAsync(buffer, ct)) != 0)
{
sent += count;
if (sent > MaxBytes) return Verdict.ScannerError;
BinaryPrimitives.WriteUInt32BigEndian(header, (uint)count);
await stream.WriteAsync(header, ct);
await stream.WriteAsync(buffer.AsMemory(0, count), ct);
}
BinaryPrimitives.WriteUInt32BigEndian(header, 0);
await stream.WriteAsync(header, ct);
var reply = new List<byte>();
var next = new byte[1];
while (reply.Count < 4096)
{
if (await stream.ReadAsync(next, ct) != 1) return Verdict.ScannerError;
if (next[0] == 0)
{
if (await stream.ReadAsync(next, ct) != 0) return Verdict.ScannerError;
string result = Encoding.UTF8.GetString(reply.ToArray());
if (result == "stream: OK") return Verdict.Clean;
if (result.StartsWith("stream: ", StringComparison.Ordinal) &&
result.EndsWith(" FOUND", StringComparison.Ordinal) && result.Length > 14 &&
!result.Contains("Heuristics.Limits.Exceeded", StringComparison.Ordinal))
return Verdict.Infected;
return Verdict.ScannerError;
}
reply.Add(next[0]);
}
return Verdict.ScannerError;
}
catch (Exception) { return Verdict.ScannerError; }
}
}
Depuis le répertoire du projet, créez un fichier texte synthétique et analysez-le. Cette opération
remplace sample.txt ; utilisez ce nom uniquement pour des données de test
jetables. Un échec d’écriture empêche le lancement de l’analyse :
printf 'ordinary test text\n' > sample.txt &&
dotnet run -- sample.txt
Si le démon fonctionne, le programme affiche Clean.
Les codes de sortie sont 0 pour un fichier sain,
1 pour un fichier infecté et 2
pour une erreur de l’analyseur. Vérifiez le code de sortie avant d’accepter un fichier. Un refus
de connexion doit produire ScannerError, jamais Clean.
Maintenir une analyse non destructive
Le programme ouvre le fichier d’entrée en lecture seule. Gardez les fichiers de transit dans un répertoire géré par l’application et empêchez les écritures concurrentes ; traitez les mêmes octets que ceux qui ont été analysés. Une API antivirus qui associe analyse et remédiation ne convient pas lorsque l’application doit préserver les fichiers originaux.
Tester les analyses saines, infectées et en échec
Téléchargez le fichier de test EICAR officiel sous le nom
eicar.com.txt dans le répertoire du projet, puis exécutez :
dotnet run -- eicar.com.txt
Le résultat attendu est Infected,
avec le code de sortie 1. EICAR est un fichier de test antivirus
inoffensif ; d’autres logiciels antivirus sur votre machine peuvent le mettre en quarantaine
indépendamment de ce programme. Arrêtez votre démon local et répétez dotnet run -- sample.txt :
le résultat attendu est ScannerError,
avec le code de sortie 2.
Testez également un fichier manquant, un fichier d’entrée de plus de 25 MiB et une archive dont
le contenu décompressé dépasse la limite configurée du moteur. Ces cas doivent renvoyer
ScannerError, et non Clean. Un fichier vide et
un fichier ordinaire de 25 MiB exactement respectent la limite du client. Comparez les empreintes
du fichier d’entrée avant et après chaque analyse. Un démon privé doté d’une base de signatures
réservée aux tests permet de vérifier l’intégration, mais n’établit pas la couverture de la base
de données de logiciels malveillants utilisée en production.
Bonnes pratiques
Maintenez les signatures de production à jour, examinez les limites du moteur et conservez les fichiers de transit conformément à la politique de conservation de votre application. Les erreurs d’analyse exigent une décision de nouvelle tentative ou de rejet ; elles n’autorisent pas la publication. N’incluez pas les informations d’authentification ni les diagnostics de l’analyseur dans les réponses visibles par les utilisateurs.
Conclusion
Pour analyser les fichiers au sein d’un pipeline de traitement de fichiers, consultez le Virus Scan Robot de Transloadit.
