Analiza archivos en busca de virus en .NET con código abierto
Analiza con ClamAV un archivo del área de preparación de la aplicación antes de aceptarlo. Este programa de consola de .NET 10 envía el archivo a un demonio local y devuelve resultados diferenciados: limpio, infectado o error del analizador. Nunca limpia, modifica ni elimina la entrada.
Introducción
Solo un análisis completado con resultado limpio permite el siguiente paso de procesamiento. Si falta un archivo, se alcanza un límite de tamaño, el demonio no está disponible, la respuesta está mal formada, se agota el tiempo de espera o se cancela la operación, el archivo debe quedar sin aceptar. Un resultado limpio describe el análisis del motor con su configuración y sus firmas actuales; no garantiza que un archivo sea inofensivo.
Herramientas antivirus de código abierto para .NET
ClamAV proporciona el analizador. El cliente TCP integrado de .NET puede comunicarse mediante su protocolo INSTREAM documentado, por lo que este ejemplo no necesita una biblioteca envolvente del analizador de NuGet. Mantén el demonio en el mismo host: su interfaz TCP carece de autenticación.
Configura ClamAV
Los siguientes comandos usan Bash en Linux. Instala el SDK de .NET 10 actual, versión 10.0.111 o posterior dentro de la serie .NET 10, y ClamAV 1.5.4 siguiendo sus instrucciones para cada plataforma. El programa se probó con el SDK 10.0.111 y ClamAV 1.5.4; consulta la política de soporte de ClamAV antes de desplegar esa versión.
Configura clamd y freshclam para usar el mismo directorio de bases de datos y deja que termine la descarga inicial de firmas.
Si tu distribución ejecuta un servicio FreshClam, deja que gestione las actualizaciones en lugar de
iniciar un segundo actualizador. Conserva la configuración de base de datos, usuario y registros del
paquete en clamd.conf.
Sustituye los valores existentes de estas directivas por este fragmento; no añadas copias en conflicto:
TCPAddr 127.0.0.1
TCPSocket 3310
StreamMaxLength 25M
MaxFileSize 26M
MaxScanSize 26M
AlertExceedsMax yes
El límite del flujo coincide con el máximo de 25 MiB del cliente. Los límites del motor también
acotan el contenido desempaquetado;
AlertExceedsMax hace que los límites correspondientes generen alertas que este programa trata como errores del analizador.
Revisa los demás límites de los archivos contenedores y la política de archivos cifrados para tu
carga de trabajo. Consulta la
referencia de configuración por versión.
Inicia o reinicia clamd mediante el gestor de servicios de tu plataforma con la configuración editada.
Crea la aplicación en un directorio con permisos de escritura fuera de un árbol de compilación
.NET existente. Pega este bloque de Bash completo. Rechaza configuraciones de SDK o compilación en
directorios superiores y un directorio FileScan existente, y solo entra en el proyecto cuando la creación y la restauración se completan correctamente:
(
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
Una creación o restauración fallida puede dejar un directorio FileScan parcial mientras tu shell permanece en su directorio original.
Examina el error y conserva los archivos existentes. Si este bloque creó el proyecto correctamente,
pero la restauración falló, resuelve el problema del SDK o del origen de paquetes y vuelve a
intentarlo desde ese directorio original:
dotnet restore FileScan/FileScan.csproj && cd FileScan
Si falló la creación del proyecto en sí, elige una nueva ubicación vacía antes de repetir la
configuración. No uses --force para sobrescribir un proyecto existente.
Implementa el análisis de archivos en C#
Sustituye Program.cs por este programa completo. Transmite como máximo 25 MiB en bloques de 64 KiB,
limita la respuesta del demonio y cancela todas las operaciones de red y de archivos tras
30 segundos o al pulsar 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; }
}
}
Desde el directorio del proyecto, crea un archivo de texto sintético y analízalo. Esto reemplaza
sample.txt; usa ese nombre solo para datos de prueba desechables.
Si la escritura falla, el análisis no se ejecuta:
printf 'ordinary test text\n' > sample.txt &&
dotnet run -- sample.txt
Con un demonio operativo, se imprime Clean.
Los códigos de salida son 0 para un resultado limpio, 1 para uno infectado y 2 para errores del analizador.
Comprueba el código de salida antes de aceptar un archivo. Una conexión rechazada debe producir
ScannerError, nunca Clean.
Mantén el análisis no destructivo
El programa abre la entrada solo para lectura. Mantén los archivos del área de preparación en un directorio propiedad de la aplicación e impide escrituras concurrentes; procesa los mismos bytes que se analizaron. Una API antivirus que combina análisis y remediación no es adecuada cuando la aplicación debe conservar los archivos originales.
Prueba análisis limpios, infectados y fallidos
Descarga el archivo de prueba oficial EICAR como
eicar.com.txt en el directorio del proyecto y ejecuta:
dotnet run -- eicar.com.txt
El resultado esperado es Infected con el código de salida 1.
EICAR es un archivo inofensivo para probar antivirus; otro software antivirus de tu equipo puede
ponerlo en cuarentena independientemente de este programa.
Detén tu demonio local y repite dotnet run -- sample.txt: el resultado esperado es
ScannerError con el código de salida 2.
Prueba también con un archivo inexistente, una entrada de más de 25 MiB y un archivo contenedor cuyo
contenido desempaquetado supere el límite configurado del motor. Esos casos deben devolver
ScannerError, no Clean. Un archivo vacío y un archivo ordinario de exactamente 25 MiB están dentro del límite del cliente.
Compara los hashes de entrada antes y después de cada análisis. Un demonio privado con una base de
datos de firmas exclusiva para pruebas verifica la integración, pero no permite determinar la
cobertura de la base de datos de malware en producción.
Buenas prácticas
Mantén actualizadas las firmas de producción, revisa los límites del motor y conserva los archivos del área de preparación según la política de retención de tu aplicación. Los errores de análisis exigen decidir entre reintentar o rechazar; no autorizan la publicación. No incluyas credenciales ni información de diagnóstico del analizador en las respuestas visibles para el usuario.
Conclusión
Para realizar análisis dentro de un pipeline de procesamiento de archivos, consulta el Virus Scan Robot de Transloadit.
