Examinar arquivos em busca de vírus no .NET com código aberto
Examine um arquivo na área de preparação da aplicação com ClamAV antes de aceitá-lo. Este programa .NET 10 para console envia o arquivo a um daemon local e retorna resultados distintos para arquivos limpos, arquivos infectados e erros do mecanismo de análise. Ele nunca limpa, modifica ou exclui o arquivo de entrada.
Introdução
Somente um resultado limpo de uma análise concluída permite a próxima etapa de processamento. Um arquivo ausente, um limite de tamanho, um daemon indisponível, uma resposta malformada, um tempo limite excedido ou um cancelamento deve deixar o arquivo sem aceitação. Um resultado limpo descreve a análise do mecanismo com sua configuração e suas assinaturas atuais; ele não garante que um arquivo seja inofensivo.
Ferramentas antivírus de código aberto para .NET
O ClamAV fornece o mecanismo de análise. O cliente TCP integrado ao .NET pode se comunicar pelo protocolo INSTREAM documentado, portanto este exemplo não precisa de uma biblioteca NuGet que encapsule o mecanismo de análise. Mantenha o daemon no mesmo host: sua interface TCP não tem autenticação.
Configurar o ClamAV
Os comandos abaixo usam Bash no Linux. Instale o SDK do .NET 10 atual, na versão 10.0.111 ou mais recente da série .NET 10, e o ClamAV 1.5.4 seguindo as instruções para sua plataforma. O programa foi testado com o SDK 10.0.111 e o ClamAV 1.5.4; consulte a política de suporte do ClamAV antes de implantar essa versão.
Configure clamd e freshclam para usar o mesmo diretório de banco de dados e aguarde a conclusão
do download inicial das assinaturas. Se sua distribuição executa um serviço FreshClam, deixe que
ele gerencie as atualizações em vez de iniciar um segundo atualizador. Mantenha as configurações
de banco de dados, usuário e registro de logs do pacote em clamd.conf.
Substitua os valores existentes dessas diretivas pelo fragmento abaixo; não acrescente cópias
conflitantes:
TCPAddr 127.0.0.1
TCPSocket 3310
StreamMaxLength 25M
MaxFileSize 26M
MaxScanSize 26M
AlertExceedsMax yes
O limite do fluxo corresponde ao limite de 25 MiB do cliente. Os limites do mecanismo também
restringem o conteúdo extraído;
AlertExceedsMax faz com que os limites correspondentes gerem alertas que este programa trata como erros do
mecanismo de análise. Revise os outros limites para arquivos contêineres e a política para arquivos
criptografados de acordo com sua carga de trabalho. Consulte a
referência de configuração dessa versão.
Inicie ou reinicie clamd pelo gerenciador de serviços da sua plataforma com a configuração editada.
Crie a aplicação em um diretório com permissão de escrita fora de uma árvore de compilação .NET
existente. Cole este bloco Bash inteiro. Ele recusa configurações de SDK/compilação em diretórios
ancestrais e um diretório FileScan já existente, e só entra no projeto depois que a criação e a
restauração são concluídas com sucesso:
(
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
Uma falha na criação ou na restauração pode deixar um diretório FileScan parcial enquanto seu shell
permanece no diretório original. Examine o erro e preserve os arquivos existentes. Se este bloco
criou o projeto com sucesso, mas a restauração falhou, corrija o problema no SDK ou na fonte de
pacotes e tente novamente a partir daquele diretório original:
dotnet restore FileScan/FileScan.csproj && cd FileScan
Se a própria criação do projeto falhou, escolha um novo local vazio antes de repetir a configuração.
Não use --force para sobrescrever um projeto existente.
Implementar a análise de arquivos em C#
Substitua Program.cs por este programa completo. Ele transmite no máximo 25 MiB em blocos de 64 KiB,
limita a resposta do daemon e cancela todas as operações de rede e de arquivo após 30 segundos ou
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; }
}
}
No diretório do projeto, crie um arquivo de texto sintético e analise-o. Isso substitui
sample.txt; use esse nome apenas para dados de teste descartáveis. Uma falha na gravação impede a
execução da análise:
printf 'ordinary test text\n' > sample.txt &&
dotnet run -- sample.txt
Com um daemon funcionando, isso imprime Clean.
Os códigos de saída são 0 para arquivo limpo, 1 para arquivo infectado e
2 para erros do mecanismo de análise. Verifique o código de saída antes de aceitar um arquivo.
Uma conexão recusada deve produzir ScannerError, nunca Clean.
Manter a análise não destrutiva
O programa abre o arquivo de entrada somente para leitura. Mantenha os arquivos em preparação em um diretório pertencente à aplicação e impeça gravações simultâneas; processe os mesmos bytes que foram analisados. Uma API de antivírus que combina análise com remediação é inadequada quando a aplicação precisa preservar os arquivos originais.
Testar análises com resultados limpos, infectados e de erro
Baixe o arquivo de teste EICAR oficial como
eicar.com.txt no diretório do projeto e execute:
dotnet run -- eicar.com.txt
Espere Infected e o código de saída 1. O EICAR é um arquivo inofensivo
para testar antivírus; outros antivírus na sua máquina podem colocá-lo em quarentena independentemente
deste programa. Pare o daemon local e repita dotnet run -- sample.txt: espere
ScannerError e o código de saída 2.
Teste também um arquivo ausente, uma entrada maior que 25 MiB e um arquivo contêiner cujo conteúdo
extraído exceda o limite configurado do mecanismo. Esses casos devem retornar
ScannerError, e não Clean. Um arquivo vazio e um arquivo comum de exatamente 25 MiB estão dentro do
limite do cliente. Compare os hashes dos arquivos de entrada antes e depois de cada análise. Um
daemon privado com um banco de assinaturas exclusivo para testes verifica a integração, mas não
comprova a cobertura do banco de dados de malware usado em produção.
Boas práticas
Mantenha as assinaturas de produção atualizadas, revise os limites do mecanismo e retenha os arquivos em preparação de acordo com a política de retenção da aplicação. Erros de análise exigem uma decisão entre tentar novamente ou rejeitar; eles não autorizam a publicação. Não inclua credenciais nem diagnósticos do mecanismo de análise nas respostas visíveis ao usuário.
Conclusão
Para realizar a análise em um pipeline de processamento de arquivos, consulte o Virus Scan Robot da Transloadit.
