Diário de debug como consertei e testei um scanner de malware em Python com YARA e VirusTotal#
Se tem uma coisa que eu aprendi rápido administrando servidores é que scripts fantásticos encontrados na internet raramente funcionam de primeira no meu ambiente. Às vezes a proposta é excelente, a ideia é útil, a arquitetura parece boa, mas a execução real esbarra em versão de Python, dependência ausente, regex quebrada, função fora de classe, código duplicado e detalhes que só aparecem quando eu coloco o script para rodar em um servidor de verdade.
Recentemente eu precisei fazer uma varredura profunda em um diretório web:
/home/user/public_html
O objetivo era caçar possíveis webshells, ofuscações, arquivos PHP maliciosos, uso suspeito de eval, cadeias de base64_decode, compressão com gzip ou zlib, além de artefatos que pudessem indicar comprometimento em ambiente de hosting.
Para isso, peguei um script Python robusto chamado encfind.py. A proposta dele era muito boa: usar expressões regulares para detectar assinaturas conhecidas, calcular entropia para identificar blocos ofuscados, integrar regras YARA e consultar hashes na API do VirusTotal.
No papel, parecia o cenário perfeito. Na prática, o primeiro Enter já me lembrou que ferramenta de segurança só vira ferramenta operacional depois de ser depurada, testada e validada no ambiente real.
O projeto final está documentado aqui:
https://domain.com/projetos/br/encfind-malware-webshell-scanner.html
O que eu queria resolver#
Meu objetivo não era apenas "rodar um script". Eu queria ter uma ferramenta de análise estática para usar em resposta a incidente e auditoria operacional de aplicações web, principalmente em bases PHP.
O scanner precisava me ajudar a encontrar:
- webshells conhecidos;
- uso perigoso de
eval; - cadeias de
base64_decode; - strings ofuscadas;
- arquivos com entropia alta;
- payloads simulando comportamento de malware;
- indicadores acionados por YARA;
- reputação de hash consultada no VirusTotal;
- achados priorizados por risco.
Em ambiente de hosting, isso faz diferença. Um diretório public_html pode ter milhares de arquivos, plugins, temas, caches, backups, uploads antigos e trechos de código legítimos misturados com artefatos suspeitos. Eu precisava de algo que reduzisse o tempo entre "suspeito de comprometimento" e "tenho evidência técnica para priorizar".
Desafio 1: O fantasma do Python antigo#
Logo na primeira tentativa, rodando como root, recebi este erro:
[root@server scripts]# python3 encfind.py
Traceback (most recent call last):
File "encfind.py", line 25, in <module>
from dataclasses import dataclass
ModuleNotFoundError: No module named 'dataclasses'
O erro era direto, mas a decisão precisava ser cuidadosa. O módulo dataclasses foi introduzido nativamente no Python 3.7. O servidor estava usando Python 3.6 como padrão no comando:
python3
Eu poderia atualizar o Python do sistema, mas em servidor de produção isso raramente é minha primeira escolha. Python do sistema costuma ser dependência de ferramentas do próprio sistema operacional, scripts de painel, automações e pacotes nativos. Atualizar sem necessidade pode resolver um problema pequeno e criar cinco problemas silenciosos.
Minha solução foi instalar o backport oficial do dataclasses via pip3:
pip3 install dataclasses
Isso corrigiu a dependência sem trocar o Python global do sistema. Foi a solução mais limpa para aquele contexto, porque manteve compatibilidade com Python 3.6 e liberou o script para seguir.
Por que eu não atualizei o Python do sistema#
Essa decisão é importante. Quando estou em servidor cPanel, DirectAdmin, VPS de cliente ou ambiente com dependências legadas, eu evito atualizar runtime global sem necessidade.
O risco de mexer no Python do sistema inclui:
- quebrar scripts administrativos;
- afetar pacotes do sistema;
- alterar comportamento de automações existentes;
- mudar path de módulos;
- causar conflitos entre
pip, pacotes RPM/DEB e bibliotecas do SO.
Neste caso, o erro era resolvido por backport. Primeiro eu resolvi no nível da dependência, não no nível do runtime inteiro.
Desafio 2: O parêntese perdido na regex#
Depois do dataclasses, rodei de novo. O script avançou um pouco mais, mas parou em outro traceback:
sre_constants.error: missing ), unterminated subpattern at position 28
Esse erro aconteceu quando o script tentava compilar um dicionário de assinaturas de webshell usando a biblioteca re.
Fui direto ao dicionário WEBSHELL_SIGNATURES e encontrei a assinatura quebrada:
'file_upload': r'move_uploaded_file\s*\(.*\$_(FILES',
O problema estava no trecho:
$_(FILES
O parêntese antes de FILES foi interpretado pela engine de regex como abertura de grupo de captura, mas ele não era fechado. Como a string tinha exatamente aquele deslocamento até o erro, a mensagem position 28 fazia sentido.
Minha correção foi fechar o grupo e revisar a legibilidade do pattern:
'file_upload': r'move_uploaded_file\s*\(.*\$_(FILES)',
Esse ajuste permitiu que o Python compilasse as assinaturas novamente.
A leitura operacional desse erro#
Esse é o tipo de falha que parece simples depois que a gente encontra, mas derruba a ferramenta inteira antes mesmo do scan começar.
Quando uma regex inválida fica dentro de um dicionário global, e esse dicionário é compilado na inicialização, o scanner não consegue operar parcialmente. Ele falha antes de analisar qualquer arquivo.
Por isso, depois de corrigir a assinatura, eu não apenas rodei o script de novo. Eu também revisei o bloco de assinaturas para procurar padrões com:
- parênteses não fechados;
- escapes inconsistentes;
- grupos abertos sem fechamento;
- uso ambíguo de
$_; - strings raw com intenção diferente do que a regex executa.
Em scanner de segurança, assinatura quebrada é uma falha de disponibilidade da própria ferramenta.
Desafio 3: O erro de atributo na matchlist#
Depois das duas correções, finalmente o script iniciou. Ele exibiu banner, mostrou quantidade de workers e começou a escanear o diretório:
[root@server public_html]# encfind.py
🔍 Scan em: /home/user/public_html
Workers: 6 | Entropy: 4.5
Até esse ponto, parecia que o problema tinha acabado. Mas ao finalizar o scan e tentar imprimir o relatório na tela, veio outro erro:
Traceback (most recent call last):
File "/usr/local/bin/encfind.py", line 1010, in main
results.display()
AttributeError: 'MatchList' object has no attribute 'display'
Esse erro foi muito claro: o objeto results, uma instância de MatchList, não tinha o método display().
Fui olhar o código-fonte e encontrei algo pior do que uma função ausente. O arquivo estava estruturalmente bagunçado. Provavelmente por cópia, colagem ou edição acumulada, o código inteiro havia sido duplicado dentro do mesmo arquivo. Havia classes repetidas, blocos soltos e a função que deveria mostrar os resultados estava fora do escopo da classe.
Ou seja, o scanner até conseguia coletar resultados, mas falhava na etapa de apresentação.
Faxina estrutural no código#
Minha solução foi fazer uma limpeza pesada no arquivo.
Eu removi repetições estruturais, reorganizei as classes, validei o escopo dos métodos e garanti que display() ficasse dentro da classe MatchList.
O problema não era apenas "criar uma função display qualquer". Era colocar a função no lugar correto da orientação a objetos, para que a chamada:
results.display()
fizesse sentido no objeto retornado pelo pipeline de scan.
O esqueleto final ficou nessa linha:
#!/usr/bin/env python3
# encfind.py - Detector avançado de código malicioso
# Código limpo e corrigido
import os
import sys
import re
import base64
import zlib
import json
import csv
import argparse
import math
import hashlib
import time
import random
import itertools
import multiprocessing as mp
from typing import List, Dict, Optional
from dataclasses import dataclass
from datetime import datetime
from collections import Counter
import urllib.request
import urllib.parse
import ssl
No blog eu não vou colar a ferramenta inteira linha por linha porque o projeto completo já está publicado e versionado na página do projeto, mas o ponto técnico é este: eu tratei a ferramenta como código de produção. Dependência corrigida, regex validada, duplicidade removida, classes reorganizadas e método de exibição devolvido para o objeto correto.
O que o scanner precisava entregar#
Depois das correções, o encfind.py precisava fazer mais do que "rodar sem erro". Ele precisava entregar valor técnico.
Eu validei as seguintes capacidades:
- varrer recursivamente
/home/user/public_html; - usar múltiplos workers;
- aplicar threshold de entropia, no caso exibindo
Entropy: 4.5; - detectar assinaturas de webshell;
- identificar padrões de ofuscação;
- decodificar cadeias simples quando possível;
- consultar VirusTotal com a flag
--vt; - integrar YARA quando configurado;
- imprimir resultados sem quebrar no final;
- apresentar evidências claras para triagem.
Em resposta a incidente, ferramenta que quebra no relatório é tão problemática quanto ferramenta que não escaneia. A coleta pode estar correta, mas se a saída não chega ao operador, a operação perde rastreabilidade.
A prova de fogo com assinatura simulada#
Com o script funcionando, eu precisava garantir que ele realmente encontrava ameaças. Eu não joguei malware real no servidor. Em vez disso, criei arquivos inofensivos contendo assinaturas e padrões que o scanner deveria detectar.
Criei um arquivo chamado:
teste_shell.php
dentro do diretório web, simulando comportamento suspeito:
<?php
// Assinatura simulada do China Chopper
$mock_chopper = "eval(base64_decode(\$_POST['cmd']));";
// Simulando ofuscação com str_rot13 para testar o decodificador da árvore (AST)
$ofuscado = str_rot13('echo "Isto é um teste de ofuscação";');
eval($ofuscado);
?>
Esse arquivo tinha elementos importantes para o teste:
- string com
eval(base64_decode(...)); - referência a
$_POST['cmd']; - uso de
str_rot13; - execução dinâmica via
eval; - padrão parecido com assinatura de webshell, sem ser malware real.
O objetivo era testar regex, heurística, entropia e lógica de decodificação com um payload controlado.
Por que usei uma amostra controlada#
Em servidor de produção, eu não coloco malware real apenas para testar ferramenta. Isso cria risco operacional e pode acionar antivírus, WAF, backup, scanner do provedor, auditoria ou bloqueio indevido.
Uma amostra controlada é suficiente para validar:
- se o scanner lê o arquivo;
- se encontra strings perigosas;
- se classifica execução dinâmica;
- se interpreta ofuscação;
- se imprime resultado;
- se gera score ou evidência.
Esse tipo de teste é mais seguro e mais fácil de repetir.
Teste com EICAR para VirusTotal#
Como o script tinha integração com VirusTotal, eu também quis validar a comunicação externa e a lógica de consulta por hash.
Usei o padrão EICAR, que é a string universal de teste de antivírus:
X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*
Criei um arquivo .txt simples com essa string.
Esse teste é seguro porque o EICAR não é malware real. Ele existe justamente para validar mecanismos de detecção, antivírus, pipelines de segurança e integrações sem colocar uma ameaça verdadeira no ambiente.
Execução final do scan#
Depois de criar os arquivos de teste, rodei:
encfind.py -d /home/user/public_html/ --vt
Esse comando varreu o diretório e ativou a consulta ao VirusTotal.
O resultado foi exatamente o que eu esperava. O terminal destacou o teste_shell.php, apontou a ofuscação, indicou risco na cadeia de execução e conseguiu trabalhar com o padrão simulado.
No arquivo com EICAR, a integração com VirusTotal funcionou: a API retornou o hash e a detecção massiva, com pontuação máxima de risco dentro da lógica do script. Isso comprovou duas coisas:
- a comunicação externa estava funcionando;
- o cache e a lógica de enriquecimento por hash estavam operacionais.
O que eu validei tecnicamente no resultado#
Depois do scan, eu não olhei apenas se apareceu "vermelho" no terminal. Eu validei o comportamento esperado.
Para o arquivo PHP simulado, eu queria ver:
- detecção de padrão
eval; - detecção de cadeia
base64_decode; - reconhecimento de entrada via
$_POST; - identificação de ofuscação via
str_rot13; - score coerente com risco;
- caminho completo do arquivo;
- evidência suficiente para ação humana.
Para o EICAR, eu queria ver:
- hash calculado;
- consulta VirusTotal executada;
- retorno de detecção;
- classificação de risco alta;
- ausência de erro de API;
- funcionamento de cache.
Esse cuidado evita falso conforto. Uma ferramenta pode "rodar" e ainda assim não detectar o que deveria. Meu critério foi rodar, detectar, explicar e permitir triagem.
Comandos e caminhos do atendimento#
O fluxo principal ficou assim:
# tentativa inicial
python3 encfind.py
# correção do Python 3.6 sem atualizar runtime do sistema
pip3 install dataclasses
# execução do scanner no diretório web
encfind.py -d /home/user/public_html/ --vt
Arquivo analisado:
/home/user/public_html/teste_shell.php
Diretório de varredura:
/home/user/public_html
O aprendizado prático#
O que parecia ser uma execução simples de cinco minutos virou uma sessão real de debugging envolvendo dependência de versão do Python, sintaxe de expressão regular, estrutura de orientação a objetos e validação de integração externa.
Eu saí de um script que quebrava no import, depois quebrava na compilação de regex, depois quebrava no relatório, para uma ferramenta funcional que escaneia diretórios web, identifica padrões suspeitos, usa heurística, integra VirusTotal e pode apoiar triagem de incidentes em ambiente de hosting.
Esse tipo de ajuste é exatamente o que separa "baixei uma ferramenta" de "coloquei uma ferramenta em condição operacional". Em segurança, não basta o script existir. Eu preciso entender as dependências, corrigir as falhas, testar com amostras controladas e validar o resultado antes de confiar nele em um incidente real. No fim, o EncFind ficou como uma peça prática no meu arsenal para auditar arquivos suspeitos em servidores web, principalmente quando preciso transformar uma pasta cheia de PHP em evidências priorizadas para decisão técnica.
Este artigo foi útil?
Deixe uma reação rápida para apoiar o conteúdo:
Este post está licenciado sob CC BY-NC.



Comentários
Participe da discussão abaixo.
0 comentários