Python para Ferramentas de Segurança: Um Esboço de Scanner de Portas e Quebrador de Hash
Uma introdução prática e amigável para iniciantes sobre como escrever pequenas ferramentas de segurança em Python — um scanner de portas concorrente e um quebrador de hash por dicionário, com ética embutida.
Ética & responsabilidade: Apenas escaneie sistemas que você possui ou tenha permissão explícita e por escrito para testar. Um scanner de portas apontado para o alvo errado, em muitas jurisdições, é tratado da mesma forma que qualquer outra tentativa de acesso não autorizado. As ferramentas abaixo são para aprendizado, para defender sua própria infraestrutura e para uso dentro de engajamentos autorizados. Se você não tem certeza se tem permissão para apontar uma ferramenta em um host, a resposta é não.
Nota: Este é um conteúdo de placeholder criado para demonstrar o blog. Substitua-o pela sua própria escrita.
Há um momento, no início de qualquer carreira em segurança, quando você percebe que a maioria das ferramentas das quais você depende — nmap, hashcat, dirb, sqlmap — são elas mesmas apenas programas. Uma vez que isso fica claro, a próxima pergunta óbvia é: "Eu poderia escrever uma pequena versão disso eu mesmo?". A resposta é sim, e fazer isso é uma das maneiras mais rápidas de realmente entender o que as ferramentas reais estão fazendo. Este post percorre duas pequenas ferramentas em Python: um scanner de portas concorrente e um quebrador de hash por dicionário.
Ferramenta 1 — Um Scanner de Portas Concorrente
O objetivo é modesto: dado um host e um intervalo de portas, relatar quais portas estão aceitando conexões TCP. A versão ingênua — abrir um socket, tentar conectar, repetir — funciona, mas é dolorosamente lenta porque faz tudo uma porta de cada vez. A solução é a concorrência, e a API mais moderna e limpa do Python para isso é concurrent.futures.ThreadPoolExecutor.
"""portscanner.py — um pequeno scanner de portas TCP concorrente para uso autorizado."""
from __future__ import annotations
import argparse
import socket
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass
@dataclass
class PortResult:
port: int
open: bool
def probe(host: str, port: int, timeout: float = 1.0) -> PortResult:
"""Tenta uma conexão TCP em (host, port). Retorna um PortResult."""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(timeout)
# connect_ex retorna 0 em sucesso, um errno caso contrário.
result = s.connect_ex((host, port))
return PortResult(port=port, open=(result == 0))
def scan(host: str, ports: range, workers: int = 100) -> list[PortResult]:
results: list[PortResult] = []
with ThreadPoolExecutor(max_workers=workers) as pool:
futures = {pool.submit(probe, host, p): p for p in ports}
for future in as_completed(futures):
results.append(future.result())
results.sort(key=lambda r: r.port)
return results
def main() -> None:
parser = argparse.ArgumentParser(description="Pequeno scanner de portas TCP.")
parser.add_argument("host", help="host de destino (apenas escaneie o que você possui)")
parser.add_argument("--start", type=int, default=1)
parser.add_argument("--end", type=int, default=1024)
parser.add_argument("--workers", type=int, default=100)
args = parser.parse_args()
print(f"Escaneando {args.host} portas {args.start}-{args.end} "
f"com {args.workers} workers...")
for r in scan(args.host, range(args.start, args.end + 1), args.workers):
if r.open:
print(f" {r.port:>5}/tcp aberta")
if __name__ == "__main__":
main()
Algumas notas sobre as escolhas:
connect_exé usado em vez deconnectpara que uma conexão recusada retorne um código de erro em vez de levantar uma exceção. Exceções em 1.000 portas somam.- O socket é aberto com um bloco
withpara que os descritores de arquivo sejam liberados prontamente. No Linux, o limite padrão de fd é 1024 por processo; um scanner descuidado pode atingi-lo rapidamente. - Threads são adequadas aqui porque o trabalho é limitado por E/S — estamos esperando por tempos de conexão, queimando CPU. Para trabalho limitado por CPU,
ProcessPoolExecutorseria a ferramenta certa. - 100 workers é um padrão razoável. Muito mais alto e você começa a disparar regras de IDS ou a esgotar portas efêmeras do lado do scanner.
Uma regra geral rápida para a escolha entre threads e processos:
| A carga de trabalho é… | Use | Por quê |
|---|---|---|
| Limitada por E/S | ThreadPoolExecutor |
Threads liberam o GIL durante chamadas de sistema de E/S |
| Limitada por CPU | ProcessPoolExecutor |
Processos contornam o GIL entre núcleos |
| Misturada | Threads, com cuidado | A maioria das ferramentas de segurança é pesada em E/S |
| CPU + nativo | multiprocessing ou ext. C |
Quando a taxa de hashing do Python é o gargalo |
Execute em sua própria máquina:
# Escaneia as portas 1-1024 do localhost com 100 workers.
python3 portscanner.py 127.0.0.1
# Escaneia uma faixa mais ampla com mais workers.
python3 portscanner.py 127.0.0.1 --start 1 --end 65535 --workers 500
Uma versão mais agradável desta ferramenta permitiria carregar uma lista de alvos de um arquivo YAML:
# targets.yaml — lista de alvos de escaneamento autorizados.
targets:
- host: 127.0.0.1
ports: [22, 80, 443, 8080]
- host: 192.168.1.10
ports: [1, 1024]
Estou deixando o código de carregamento YAML como um exercício porque é o tipo de tarefa pequena e satisfatória que faz uma ferramenta parecer sua.
O Que Esta Ferramenta Não Faz (Ainda)
Um scanner real — nmap, masscan, zmap — faz muito mais. Algumas coisas para considerar se você quiser estendê-la:
- Fingerprinting de serviço. Depois que uma porta está aberta, envie uma sonda específica do protocolo e analise o banner. É assim que
nmap -sVfunciona. - Escaneamento SYN. Este scanner faz um
connect()completo, que é registrado pelo alvo. O escaneamento SYN (nmap -sS) envia apenas um SYN e lê o SYN+ACK, depois envia um RST. É mais rápido e furtivo, mas requer sockets brutos e geralmente root. - Limitação de taxa. Um scanner educado limita seus pacotes por segundo para não causar acidentalmente um DoS no alvo. Para uma ferramenta de aprendizado, isso é exagero; para qualquer coisa apontada para infraestrutura real, é obrigatório.
Ferramenta 2 — Um Esboço de Quebrador de Hash por Dicionário
A segunda ferramenta é um ataque de dicionário contra um único hash não salgado. A configuração: você tem um hash (digamos, de um desafio de CTF ou de uma imagem forense do seu próprio sistema) e um wordlist, e quer encontrar o texto plano que produziu o hash.
"""hashcracker.py — ataque de dicionário contra um único hash não salgado.
Apenas para aprendizado. Ferramentas reais de quebra de senha (hashcat, john)
são ordens de magnitude mais rápidas porque rodam em GPUs e usam SIMD.
"""
from __future__ import annotations
import argparse
import hashlib
import sys
from concurrent.futures import ProcessPoolExecutor, as_completed
def hash_word(word: str, algorithm: str) -> str:
"""Retorna o digest hexadecimal de `word` sob o algoritmo dado."""
h = hashlib.new(algorithm)
h.update(word.encode("utf-8"))
return h.hexdigest()
def crack(target: str, wordlist: str, algorithm: str = "sha256") -> str | None:
"""Retorna a primeira palavra cujo hash corresponde a `target`, ou None."""
with open(wordlist, "r", encoding="utf-8", errors="ignore") as f:
words = [line.rstrip("\n") for line in f]
with ProcessPoolExecutor() as pool:
futures = {
pool.submit(hash_word, w, algorithm): w for w in words
}
for future in as_completed(futures):
word = futures[future]
if future.result() == target.lower():
# Cancela o resto o melhor possível; eles já estão em execução.
return word
return None
def main() -> None:
parser = argparse.ArgumentParser(description="Quebrador de hash por dicionário.")
parser.add_argument("hash", help="hash de destino (hex)")
parser.add_argument("wordlist", help="caminho para o wordlist")
parser.add_argument("--algo", default="sha256", help="nome do algoritmo hashlib")
args = parser.parse_args()
print(f"Quebrando hash {args.algo} com {args.wordlist}...")
result = crack(args.hash, args.wordlist, args.algo)
if result is None:
print("Nenhuma correspondência encontrada.")
sys.exit(1)
print(f"Correspondência: {result}")
if __name__ == "__main__":
main()
Gere um hash de destino a partir de uma palavra conhecida e tente:
# Gera um sha256 de "letmein" — a senha que estamos tentando recuperar.
echo -n "letmein" | sha256sum
# 9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee6845628c12b338f3b8c4b8c4b8c (ilustrativo)
# Executa o quebrador contra um wordlist.
python3 hashcracker.py \
9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee6845628c12b338f3b8c4b8c4b8c \
/usr/share/wordlists/rockyou.txt \
--algo sha256
Algumas coisas a notar:
- Isso usa
ProcessPoolExecutor, não threads, porque o hashing é limitado por CPU. Threads não ajudariam aqui devido ao GIL. - A correspondência é insensível a maiúsculas/minúsculas no digest hexadecimal porque algumas ferramentas emitem maiúsculas e outras minúsculas.
- Não há tratamento de salt. Se o hash que você está visando foi produzido com um salt, você precisaria saber o salt e incorporá-lo em
hash_word. O salting derrota a pré-computação (tabelas-arco-íris) mas não os ataques de dicionário em si — apenas torna cada palpite mais caro. - Isso é drasticamente mais lento que
hashcat.hashcatusa GPUs e pode fazer bilhões de hashes SHA-256 por segundo em hardware de consumo. O ponto deste script é a compreensão, não o desempenho.
Por Que Escrever Estes Você Mesmo?
Há algumas boas razões para escrever pequenas versões de ferramentas conhecidas, mesmo versões melhores já existindo:
- Você aprende o que a ferramenta realmente está fazendo. Um scanner de portas para de ser mágico no momento em que você escreve
connect_excontra uma porta e observa o timeout. - Você aprende a ler o código de outras pessoas. Depois de ter escrito um quebrador de hash, ler o código-fonte do
hashcatse torna viável. O vocabulário é compartilhado. - Você constrói o hábito de ferramentas pequenas e afiadas. A maior parte do trabalho real de segurança é cola — um script que pega a saída de uma ferramenta, a transforma e a alimenta para outra. Escrever ferramentas pequenas do zero é como você se torna bom em cola.
Para Onde Ir a Partir Daqui
Se estes dois foram interessantes, os próximos projetos naturais são:
- Um forçador de diretórios para HTTP, similar ao
gobuster. A estrutura é a mesma: um alvo, um wordlist, um pool de workers e uma função de sonda que retorna aberto/fechado. - Um parser de logs que lê um log de acesso do Apache/Nginx e sinaliza padrões suspeitos (user agents anômalos, picos de 4xx, scans de único IP).
- Um IDS simples que observa um pcap e alerta sobre escaneamentos SYN — um ótimo exercício na leitura de flags TCP.
O projeto OWASP Python Security e os guias de concorrência da Real Python são bons lugares para continuar cavando. E, como sempre, a documentação dos módulos hashlib e socket vale a pena ler de ponta a ponta pelo menos uma vez — não são longos e eles desmistificam muito.