Criando um Port Scanner Multithread
Aprenda a usar threads nos processos longos do dia - a - dia e ainda leve um port scanner TCP, para procurar por portas abertas na sua rede e se proteger de ataques
Neste artigo aprenderemos a fazer um portscanner Multi - Thread. Por isso nosso aprendizado terá 2 focos: comunicação TCP-IP e Multithread.
O que é um portscanner? É um programa que serve para administradores de rede, ou atacantes, encontrarem portas TCP-IP abertas em um determinado computador da rede.
Essas portas podem, posteriormente, ser fechadas através de um firewall ou desativando-se a programa que a abriu.
Vamos fazer um programa simples, que testa apenas as portas TCP, deixando as UDP de lado.
Esse programa simplesmente tentará se conectar à porta para saber se a mesma está aberta. Se não conectar é porque está fechada.
Uma porta, por padrão, sempre está aberta para "Saída", ou seja, quando um cliente quiser se conectar a um servidor através dela. Para você poder abrir uma página no seu Browser, a porta 80 deve estar aberta para saída. Para você servir uma página HTML no seu computador, a porta 80 deve estar aberta para entrada. Mas não basta apenas a porta estar aberta, para um computador servir qualquer coisa em qualquer porta ele precisa de um programa servidor instalado e rodando. O programa abre a porta, e fica "Escutando" nela por mensagens em determinado protocolo. Protocolo é o conjunto de mensagens/comandos que cliente e servidor trocam. Geralmente são palavras, ou acrônimos de palavras, na forma de strings. Por exemplo, ainda o caso da página na web, o computador servidor deve ter rodando um programa como o IIS da Microsoft ou o Apache. Um browser no cliente se comunicaria com esse servidor através do protocolo HTTP.
Aos Programas que abrem portas e as escutam, esperando que clientes se conectem nós damos o nome de Serviços. Exemplos: HTTP, FTP, SMTP, Telnet e assim por diante.
Um firewall pode fechar ou abrir portas tanto para entrada como para saída. Existem firewalls físicos, ou de hardware, que são na verdade computadores com um sistema operacional, uma ou mais interfaces de rede e um complexo sistema de administração de pacotes e permissões. Existem também firewalls de software, ou lógicos, que utilizam-se de funções avançadas dos drivers de rede (tipip.sys) e possuem drivers que rodam em kernel-mode para manipulação (hooking) em baixo nível dessas funções. Um exemplo desse tipo seria o próprio firewall do windows ou o Zone Alarm.
No nosso programa não levaremos em consideração as configurações de firewall lógico ou físico. Nem verificaremos se a porta está aberta para entrada ou saída. Verificaremos apenas se a porta está aberta para entrada. Se estiver é porque existe algum serviço rodando nessa porta.
Esse artigo não pretende esgotar nenhum dos dois assuntos: nem comunicação via TCP/IP nem Thredas, afinal cada um desses assuntos por si só já são suficientes para escrever um livro cada. Pretendo aqui esboçar algo básico sobre threads que seria bom se todos os programadores conhecessem.
Daremos uma atenção especial ao uso de Threads, ou seja, esse programa será um breve exemplo de como se usar Threads.
No nosso programa usaremos o Delphi 7, qualquer adaptação que precisaremos fazer para que funcione nas outras versões do Delphi veremos mais a frente. Usaremos tanto o componente TidTcpClient da unit idTcpClient, paleta indyClients como o TCPClient da unit Sockets. Não colaremos o compenete numa form, mas instanciaremo-los manualmente em runtime, já que estaremos trabalhando numa unit sem form.
Tentar se conectar em uma porta para verificar se ela está aberta é uma tarefa demorada, que pode demorar um segundo ou mais dependendo da sua conexão, caso a porta esteja aberta, ou do timeout que você configurou. Usando threads nós podemos consultar milhares de portas de uma única vez, sendo que rapidamente conseguiremos verificar quais estão abertas. Conforme as portas forem "respondendo" nós já vamos sendo notificados se a porta está aberta ou não. As que demorarem ou estão fechadas ou a conexão foi muito lenta resultando num timeout. Mesmo assim o tempo do timeout será o limite para sabermos quais portas estão fechadas também.
Para varrer todas as portas faremos o seguinte: colocaremos um TEdit onde digitaremos o IP e dois outros TEdits onde colocaremos a porta inicial e a final. Varreremos as portas de duas maneiras, com um loop e com um TTimer com intervalo de 1 ms, para mostrar que a diferença está mesmo é no uso de Threads.
Poderíamos colocar dois Edits para varrer um intervalo de endereços IP, mas deixo isso a cargo do leitor.
Coloque os labels necessários para rotular todos os campos. E coloque três botões: Um para iniciar a varredura por loop, um para iniciar a varredura por um TTimer e outro para parar. Colocaremos ainda um CheckBox para dizer se as threads vão ser sincronizadas (eu já explico) e três CheckBoxes para dizer se haverá repaint e processMessages entre os loops. Isso é necessário para se evitar que a tela pareça congelada.
Sua form ficará como a Figura 1.
Figura 1. Layout
Repare que eu coloquei um TIdAntiFreeze para impedir congelamentos na form, caso você estiver usando o TIdTcp. Para evitar flicker (piscadas) nos controles da form durante os loops sete a propriedade DoubleBuffered da form para true. Você pode fazer isso no evento onCreate do formulario.
Nós colocamos também um TSpinEdit para definir de quantos em quantos "ciclos" nossa form dará um ProcessMessages, ou Repaint, ou ambos ou nenhum!
ProcessMessages é um método de TApplication responsável por processar as mensagens do windows, como o clique no Fechar, por exemplo. Se for chamado periodicamente durante um loop fará com que o formulário não trave durante o loop, permitindo que você clique em outros botões ou até mesmo no Fechar (X).
Repaint, como o próprio nome diz, Repinta a tela.
O Básico de Threads
Todos nós aprendemos na faculdade o que são Threads, mas não custa nada reforçar um resumo do resumo do básico: Threads são tarefas ou linhas de execução de um software, gerenciados pelo sistema operacional, com controle de prioridade, que podem executar simultaneamente (quando o computador possui múltiplos processadores ou processador de múltiplos núcleos), ou simular a execução simultânea através de semáforos e compartilhamento/Gerenciamento do tempo do processador. Para o programador do último nível ou para o usuário o efeito final é de tarefas sendo executadas simultaneamente.
No Delphi. toda thread tem que ser Descendente da classe TThread. A classe TThread tem um constructor especial que recebe como parâmetro um boolean. Se você passar verdadeiro, a thread vai ser criada em modo Suspenso, ou seja, não vai rodar, vai esperar que você execute o método Resume. Se você passar false a Thread começará a executar logo que criada. Mas o que a Thread executa realmente? Ela executa o método "Execute". Simples não? A princípio esse método não faz nada, a não ser que você o sobrecarregue. Ou seja, precisamos saber o básico de Herança e Polimorfismo se quisermos brincar com Threads.
Para conhecer melhor a classe TThread, de uma fuçada na unit Classes do Delphi. É lá que ela se encontra.
Você pode se perguntar: Onde eu uso isso?
Bom, imagine aquele relatório demorado de se gerar, ou a impressão de uma nota fiscal enorme, ou uma transferência de ftp de um arquivo monstruoso. Tudo isso você pode fazer a partir de uma thread secundária, deixando a sua thread principal, ou seja, o resto do seu programa, livre para execução de outras coisas. As telas não vão congelar e o usuario poderá usar outras funções do sistema sem ter que esperar. Você pode colocar um messagebox ou showmessage no final da thread para avisar que ela terminou.
Propriedades e métodos das Threads no Delphi:
- Resume -> Despausa a Thread
- Suspend -> Pausa ou "Suspende" a Thread
- Terminate -> Termina a Thread
- Synchronize -> Passando um método como parâmetro, a thread espera pelas outras threads terminarem a execução deste método para depois ela mesma iniciar a execução deste. Isso garante que as threads vão executar esse método uma de cada vez. Internamente executa os métodos dentro de critical sections.
- ReturnValue -> Define um valor que a thread deve retornar quando terminar sua execução.
- Terminated -> True se a thread estiver terminada
- FreeOnTerminate -> Destroi a thread automaticamente depois que ela terminar. Ideal se você tiver múltiplas threads sem que variáveis apontem para elas. Isso é essencial para se proteger de memory leaks. Obviamente qualquer referência que você tiver a uma thread auto-destruída será inválida e você não poderá acessar suas propriedades.
Então temos de sobrescrever (override) o método Execute. na Listagem 1você vê a unit uThreads que eu criei para isso. Preste atenção ao método execute, onde eu crio um componente para conexão via TCP/IP e tento me conectar ao IP e Porta que foram passados como parâmetro.
Repare também que eu passo já no constructor todos os parâmetros que a thread precisará para executar. Logicamente você pode experimentar outras maneiras de se fazer a mesma coisa. Espero que o código e os comentários nele estejam fáceis de entender.
Listagem 1. UThreads
unit uThreads;
interface
uses Classes, Sockets, StdCtrls, Forms, Windows;
type
TPortScanner = class(TThread)
private
FPorta: string;
FHost: string;
FMemo: TMemo;
FSincronizado: Boolean;
procedure Testar;
protected
procedure Execute; override;
public
constructor Create(
host: string;
porta: string;
memo: TMemo;
sincronizado: Boolean);
end;
implementation
uses SysUtils, SyncObjs;
var
FLock: TRTLCriticalSection;
{ TPortScanner }
constructor TPortScanner.Create(
host: string;
porta: string;
memo: TMemo;
sincronizado: Boolean);
begin
inherited Create(True);
FHost := host; //ip onde vamos nos conectar
FPorta := porta; //porta na qual vamos nos conectar
FMemo := memo; //um instância de um memo para que seja adicionado o numero da porta
FSincronizado := sincronizado; //define se o metodo "testar" será executado sincronizado
FreeOnTerminate := True; // Libera o objeto após terminar.
Priority := tpTimeCritical; { Configura sua prioridade na lista de processos do Sistema operacional. }
Resume; // Inicia o Thread.
end;
procedure TPortScanner.Execute;
begin
inherited;
//Aqui podemos definir, conforme os parâmetros do constructor, se o método
//testar vai ser executado sincronizado ou não
if FSincronizado then
Synchronize(Testar)
else
Testar;
end;
procedure TPortScanner.Testar;
var
bCon: Boolean;
begin
bcon := false;
//conexão por TidTcpClient
{
pode ser chato debugar a aplicação com o TidTcpClient porque ele
dispara uma exception qando ocorre um timeout
Outro motivo por eu não usar nesse programa o idTcpClient é que ele
possui um memory leak no Delphi 7. O FastMM4 acusa uma critical section em aberto
}
{
with TIdTCPClient.Create(nil) do
try
try
Host := FHost;
Port := StrToInt(FPorta);
//primeiro eu executo Connect e depois verifico se está conectado
Connect(1000); //um timeout de 1000 ms ou 1 s
bCon := Connected;
except
end;
finally
if bCon then
begin
Disconnect;
try
EnterCriticalSection(FLock);
FMemo.Lines.Add(FPorta);
finally
LeaveCriticalSection(FLock);
end;
end;
Free;
end;
}
//conexão por TTCPClient
with TTcpClient.Create(nil) do
try
try
RemoteHost := FHost;
RemotePort := FPorta;
Connect;
bCon := Connected;
except
end;
finally
if bCon then
begin
Disconnect;
try
EnterCriticalSection(FLock);
FMemo.Lines.Add(FPorta);
finally
LeaveCriticalSection(FLock);
end;
end;
Free;
end;
end;
initialization
InitializeCriticalSection(FLock);
finalization
DeleteCriticalSection(FLock);
end.
Repare aqui que nós fizemos a conexão de duas maneiras: por TIdTcpCliente e por TTcpClient. Resolvi não usar os componentes da paleta indy aqui por dois motivos: Primeiro que é chato debugar porque cada timeout dá um exception. (lógico que tem como contornar isso, mas estou com preguiça). Outro motivo é que o FastMM4 acusa um memory leak, uma critical section aberta, só de você ter as units no indy no seu uses. É alguma seção crítica que ele cria numa seção de initialization, mas esquece de destruir no finalization.
Por falar em seção crítica, percebeu o uso delas aqui? Pois é, como estamos acessando um mesmo memo através de várias threads e um memo é um componente da VCL, que usa bastante a API do windows, tem vários recursos dentro de um memo que não são thread - safe e não podem ser acessados ou executados ao mesmo tempo. Por isso criamos critical section para isolar o exato momento em que adicionamos uma string nova no memo, tornando nosso código thread - safe. Para poder usar critical sections corretamente:
- Crie uma e apenas uma variável do tipo TRTLCriticalSection. Ela tem que ser única na sua aplicação, ou pelo menos no seu contexto. Deve ficar atento para sair sempre da mesma critical section que entrou, ou fechar sempre a mesma que abriu.
- Não use critical sections diferentes para controlar acesso a um mesmo recurso.
- Use InitializeCriticalSection para inicializar sua seção critica.
- Depois use EnterCriticalSection para entrar na seção crítica.
- Faça o que tem de fazer logo depois de EnterCriticalSection e ...
- Logo depois saia com LeaveCriticalSection
- No final de tudo delete sua critical section com DeleteCriticalSection
É ideal Inicializarmos a seção crítica na inicialization de uma unit qualquer, na uniti principal ou na form principal. E deletar a critical section na seção finalization da ultima unit a ser descarregada. A inicialização e finalização podem ficar na mesma unit, contanto que as outras units não tenham threads que acessem recursos protegidos por esta critical section.
Sempre saia da seção critica, LeaveCriticalSection, dentro de um finally.
Se você usar o critical section de maneira correta vai garantir que o trecho de código entre EnterCriticalSection e LeaveCriticalSection será executado por uma thread de cada vez.
Em outra ocasião aprenderemos a usar o TCriticalSection do Delphi, que faz o controle das critical sections automaticamente. É muito mais fácil, mas você não pode usar vários objetos TCriticalSection, você deve ter apenas um, singleton e global. O que não pode ocorrer de jeito nenhum é ter na memoria critical sections diferentes para proteger um mesmo recurso se ENTRECRUZANDO. Por isso que sua variavel TRTLCriticalSection não pode ser um membro ou propriedade da própria classe da sua thread, e muito menos inicializada ou deletada pela sua thread, senão cada instância da thread teria uma TRTLCriticalSection diferente.
Repare também que colocamos nossa TRTLCriticalSection na seção implementation, assim ninguém fará besteira a partir das outras units mexendo, reinicializando ou deletando essa critical section específica.
Agora vamos ver a nossa unit principal, conforme os códigos das Listagens 2 e 3.
Listagem 2. Unit Principal
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls,
ExtCtrls,
Sockets, Spin;
type
TForm1 = class(TForm)
mLista: TMemo;
btTesta: TButton;
edServer: TEdit;
Label1: TLabel;
lbStatus: TLabel;
btChega: TButton;
Timer1: TTimer;
btTimer: TButton;
edPortaIni: TEdit;
edPortaFin: TEdit;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
cbSincron: TCheckBox;
cbRefresh: TCheckBox;
edPMT: TSpinEdit;
cbProcess: TCheckBox;
cbRepaint: TCheckBox;
procedure btTestaClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure btChegaClick(Sender: TObject);
procedure Timer1Timer(Sender: TObject);
procedure btTimerClick(Sender: TObject);
procedure cbRefreshClick(Sender: TObject);
private
FAbort: Boolean; //para saber se vamos parar no meio do loop
FPortaTimer: Integer; //o contador interno do timer
procedure Testar(porta: Integer);
public
function PORTA_INI : integer;
function PORTA_FIN : integer;
function PROCESS_MESSAGES_TIMEOUT : integer;
end;
var
Form1: TForm1;
implementation
uses uThreads;
{$R *.dfm}
procedure TForm1.btTestaClick(Sender: TObject);
var port: Integer;
begin
FAbort := False;
FPortaTimer := PORTA_INI;
for port := PORTA_INI to PORTA_FIN do
begin
if FAbort then
begin
Break; //close;
end;
try
Testar(port);
except
end;
if cbRefresh.Checked then
if( port mod PROCESS_MESSAGES_TIMEOUT) = 0 then
begin
if cbProcess.Checked then
Application.ProcessMessages;
if cbRepaint.Checked then
Repaint;
end;
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
Self.DoubleBuffered := True; //para evitar o flicker da tela
FAbort := False; // inicializa como falso para começar a processar
FPortaTimer := PORTA_INI; //Inteiro que define a porta atual do controle por timer
end;
procedure TForm1.btChegaClick(Sender: TObject);
begin
FAbort := True;
end;
procedure TForm1.Testar(porta: Integer);
begin
lbStatus.Caption := 'testando ' + IntToStr(porta);
TPortScanner.Create(
edServer.Text,
IntToStr(porta),
mLista,
cbSincron.Checked);
end;
procedure TForm1.Timer1Timer(Sender: TObject);
begin
if FAbort then
begin
Timer1.Enabled := False; //close;
end;
testar(FPortaTimer);
Inc(FPortaTimer);
if FPortaTimer >= PORTA_FIN then
Timer1.Enabled := False;
end;
procedure TForm1.btTimerClick(Sender: TObject);
begin
FAbort := False;
FPortaTimer := PORTA_INI;
Timer1.Enabled := True;
end;
function TForm1.PORTA_FIN: integer;
begin
Result := StrToInt(edPortaFin.Text);
end;
function TForm1.PORTA_INI: integer;
begin
Result := StrToInt(edPortaIni.Text);
end;
function TForm1.PROCESS_MESSAGES_TIMEOUT: integer;
begin
Result := StrToInt(edPMT.Text);
end;
procedure TForm1.cbRefreshClick(Sender: TObject);
begin
if not cbRefresh.Checked then
begin
cbProcess.Checked := False;
cbRepaint.Checked := False;
cbProcess.Enabled := False;
cbRepaint.Enabled := False;
end
else
begin
cbProcess.Enabled := True;
cbRepaint.Enabled := True;
end;
end;
end.
Listagem 3. DFM
object Form1: TForm1
Left = 529
Top = 233
Width = 409
Height = 332
Caption = 'Form1'
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'MS Sans Serif'
Font.Style = []
OldCreateOrder = False
OnCreate = FormCreate
PixelsPerInch = 96
TextHeight = 13
object Label1: TLabel
Left = 8
Top = 8
Width = 42
Height = 13
Caption = 'Servidor:'
end
object lbStatus: TLabel
Left = 208
Top = 248
Width = 30
Height = 13
Caption = 'Status'
end
object Label2: TLabel
Left = 8
Top = 56
Width = 33
Height = 13
Caption = 'Portas:'
end
object Label3: TLabel
Left = 64
Top = 76
Width = 7
Height = 13
Caption = 'A'
end
object Label4: TLabel
Left = 8
Top = 104
Width = 37
Height = 13
Caption = 'Refresh'
end
object mLista: TMemo
Left = 208
Top = 8
Width = 185
Height = 233
TabOrder = 0
end
object btTesta: TButton
Left = 8
Top = 272
Width = 113
Height = 25
Caption = 'Testar por Loop'
TabOrder = 1
OnClick = btTestaClick
end
object edServer: TEdit
Left = 8
Top = 24
Width = 121
Height = 21
TabOrder = 2
Text = '10.1.13.129'
end
object btChega: TButton
Left = 280
Top = 272
Width = 113
Height = 25
Caption = 'Chega!'
TabOrder = 3
OnClick = btChegaClick
end
object btTimer: TButton
Left = 144
Top = 272
Width = 113
Height = 25
Caption = 'Testar por Timer'
TabOrder = 4
OnClick = btTimerClick
end
object edPortaIni: TEdit
Left = 8
Top = 72
Width = 49
Height = 21
TabOrder = 5
Text = '80'
end
object edPortaFin: TEdit
Left = 80
Top = 72
Width = 49
Height = 21
TabOrder = 6
Text = '4000'
end
object cbSincron: TCheckBox
Left = 8
Top = 152
Width = 97
Height = 17
Caption = 'Sincronizado'
TabOrder = 7
end
object cbRefresh: TCheckBox
Left = 8
Top = 176
Width = 97
Height = 17
Caption = 'Refresh no loop'
Checked = True
State = cbChecked
TabOrder = 8
OnClick = cbRefreshClick
end
object edPMT: TSpinEdit
Left = 8
Top = 120
Width = 121
Height = 22
MaxValue = 250
MinValue = 1
TabOrder = 9
Value = 1
end
object cbProcess: TCheckBox
Left = 24
Top = 200
Width = 121
Height = 17
Caption = 'Processa Mensagens'
Checked = True
State = cbChecked
TabOrder = 10
end
object cbRepaint: TCheckBox
Left = 24
Top = 224
Width = 97
Height = 17
Caption = 'Repaint'
TabOrder = 11
end
object Timer1: TTimer
Enabled = False
Interval = 1
OnTimer = Timer1Timer
Left = 256
Top = 48
end
end
Temos o campo FAbort para saber se vamos sair antes de acabar o loop. Para isso, usamos uma variável booleana, e para isso que usamos também o processmessages, senão você não pode nem clicar no botão. E o FPortaTimer: Integer é o contador do timer. Se você estiver percorrendo ou varrendo as portas através do timer esta variável é que marcará a porta corrente.
Na próxima versão vamos tirar essa varredura por timer. Ela está aqui só para ilustrar.
As functions abaixo:
- function PORTA_INI : integer;
- function PORTA_FIN : integer;
- function PROCESS_MESSAGES_TIMEOUT : integer;
dispensam comentários. PORTA_INI traz a porta inicial, já convertida para integer, PORTA_FIN é análoga, para a porta final e PROCESS_MESSAGES_TIMEOUT traz o valor do spinedit, para "de tantos em tantos ciclos" dar um processmessages e/ou um refresh na tela.
O timer começa desabilitado e inicia quando eu clico no botão
FAbort := False;
FPortaTimer := PORTA_INI;
Timer1.Enabled := True;
e termina quando FPortaTimer >= PORTA_FIN.
Execute o programa e experimente todas as opções. Brinque com ele. Perceba a diferença entre chamar o método testar das threads com ou sem o Synchronize e veja que sincronizando os objetos TTcpConnection vão se conectar as portas apenas um de cada vez, tornando o sistema muito lento, já que só sairá do método testar depois do timeout do objeto.
Espero que tanto as dicas sobre threads sejam uteis na sua programação do dia-a-dia como o próprio exemplo sirva para você encontrar e proteger portas abertas na sua rede.
Você pode baixar os fontes dessa versão na opção "código fonte" no topo do artigo, compactado em 7Zip.
Espero que tenha sido útil, até a próxima ;)
Artigos relacionados
-
Artigo
-
Artigo
-
Artigo
-
Artigo
-
Artigo