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:

Temos também outros métodos e técnicas, bem como um ou mais overloads de Syncronize e um StaticSynchronize. Esses vão ficar para um próximo artigo.

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:

É 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:

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 ;)

Ebook exclusivo
Dê um upgrade no início da sua jornada. Crie sua conta grátis e baixe o e-book

Artigos relacionados