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

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