使用Perl进行端口扫描,第二部分

在本文的第一部分中,我展示了如何使用Perl开发一个基本的分叉端口扫描器。在这篇文章中,我将添加一些增强功能,使其成为一个真正有用的工具。

扫描端口范围

我想添加的第一个功能是能够扫描用户定义的端口范围,而不是默认的命名端口列表。因为我正在使用Getopt::Long来解析命令行参数,所以我可以将range添加到参数选项中

GetOptions (
  'ip=s'        => \ my $target_ip,
  'range=s'     => \ my $port_range,
  'h|help|?'    => sub { pod2usage(2) },
);

端口处理代码变为

# use named ports if no range was provided
my @ports = shuffle do {
  unless ($port_range)
  {
    map { $port_directory{$_}->{port} }
      grep { $port_directory{$_}->{name} !~ /^unknown$/
             && $port_directory{$_}->{proto} eq $protocol } keys %port_directory;
  }
  else
  {
    my ($min, $max) = $port_range =~ /([0-9]+)-([0-9]+)/
      or die "port-range must be formatted like this: 100-1000\n";
    $min..$max;
  }
};

我检查$port_range变量是否存在,如果存在,我尝试使用正则表达式捕获来解析最小和最大端口。我喜欢这种代码模式

my ($min, $max) = $port_range =~ /([0-9]+)-([0-9]+)/
      or die "port-range must be formatted like this: 100-1000\n";

因为端口范围要么会被成功解析到$min$max中,要么会抛出异常。通过传递以换行符结尾的字符串到die,它不会打印出行引用,这使得“用法”风格的输出更加简洁。

调整进程和频率

简单的端口扫描器启动50个进程,将待扫描的端口平均分配给所有进程,每个进程每秒发送一个请求。这里有几个问题。首先,如果用户想要扫描所有65,535个端口,程序将至少运行20分钟,这相当慢。其次,一些主机有动态防火墙,如果检测到端口扫描,将会开始丢弃数据包,所以用户可能希望隐藏并降低扫描速度。理想情况下,我们应该允许用户定义要运行的进程数以及每个发送数据包之间的延迟量。

为了捕获这些参数,我可以在GetOptions中添加procsdelay

GetOptions (
  'delay=f'     => \(my $delay = 1),
  'ip=s'        => \ my $target_ip,
  'range=s'     => \ my $port_range,
  'procs=i'     => \(my $procs = 50),
  'h|help|?'    => sub { pod2usage(2) },
);

此代码做了几件漂亮的事情:通过使用=i定义,GetOptions将对处理器数量执行整数类型检查。同样,=f将强制执行浮点数类型。此代码的另一点是,在GetOptions函数中声明并设置变量的默认值。

为了支持浮点秒的sleep,我需要导入Time::HiRes模块(Perl核心的一部分)

use Time::HiRes 'sleep';

现在,分叉代码可以变为

for (1..$procs)
{
  my @ports_to_scan = splice @ports, 0, $batch_size;
  my $parent = fork;
  die "unable to fork!\n" unless defined ($parent);

  if ($parent)
  {
    push(@child_pids, $parent);
    next;
  }

  # child waits until the parent signals to continue
  my $continue = 0;
  local $SIG{CONT} = sub { $continue = 1};
  until ($continue) {}

  for my $target_port (@ports_to_scan)
  {
    sleep($delay);
    send_packet($protocol, $target_port, $flags);
  }
  exit 0; # exit child
}

现在,扫描器将生成$procs个进程,并在每个发送数据包之间睡眠$delay秒。这应该使用户能够调整发送数据包的频率和扫描的运行时间。

报告

简单的扫描器会打印出每个已扫描端口的端口状态。这可能会太多信息——在大多数情况下,用户对开放的漏洞端口感兴趣,而不关心过滤或关闭的端口。另一方面,输出中缺少了安全审计所需的关键信息:执行时间、程序版本、使用的参数、总体运行时间等。因此,我需要将这些信息添加到输出中。

为了计算程序运行时长并打印开始时间,我可以使用Time::Piece模块。该模块是Perl的核心部分,因此无需安装,而且你可以用它做几乎所有的事情。

use Time::Piece;

my $start_time = localtime;

...

my $end_time = localtime;
my $duration = $end_time - $start_time;

当你导入Time::Piece时,它会覆盖内置的localtime和gmtime函数来构造Time::Piece对象。计算开始和结束时间之差会返回一个Time::Seconds对象,这就是我们的运行时长。这两种对象类型打印时格式良好,所以我们只需要做这些。很简单!

我将在GetOptions中添加一个verbose选项。如果存在此选项,我们将打印出所有端口结果,否则仅打印打开的结果。

GetOptions (
  'delay=f'     => \(my $delay = 1),
  'ip=s'        => \ my $target_ip,
  'range=s'     => \ my $port_range,
  'procs=i'     => \(my $procs = 50),
  'verbose'     => \ my $verbose,
  'h|help|?'    => sub { pod2usage(2) },
);

注意,对于布尔参数,在GetOptions中没有给出类型声明(例如,没有=i)。这意味着在命令行中,用户只需输入--verbose-v,则$verbose将被赋予true值。

我将不再在read_packet()子程序中打印端口结果,而是将端口号和状态返回给调用代码,并将打印推迟到稍后。这个简单的更改有两个好处:它更灵活;我可以在不添加多个打印语句的情况下向read_packet()添加更多的数据包解析例程;我可以在打印之前对端口扫描结果进行排序。程序可以以随机顺序扫描端口,但输出应该是有序的!

for (sort { $a <=> $b } keys %port_scan_results)
{
  printf " %5u %-15s %-40s\n", $_, $port_scan_results{$_}, ($port_directory{"$_/$protocol"}->{name} || '')
    if $port_scan_results{$_} =~ /open/ || $verbose;
}

这种方法的一个缺点是结果将在所有响应收到或数据包捕获超时之前不打印到终端。真正好的做法是在收到排序结果时打印它们。例如,如果我们正在扫描1到100的端口,并且收到了1到10的响应,那么打印这些结果,然后等待收到11号端口的响应。这个改进留给读者作为练习(欢迎提交拉取请求!)。

支持不同类型的扫描

简单的扫描器执行TCP "SYN"扫描。这是一个好的默认值,但我们可以进行许多不同的端口扫描,以获得针对不同系统的更好结果。例如,在我的测试中,我发现TCP SYN扫描对Chromebook和移动设备几乎没有作用。

与其他更新一样,我将向GetOptions函数添加新参数。我想捕获要使用的协议(例如,TCP、UDP、ICMP)以及应添加到发送数据包的任何标志。这两个变量应该给我们足够的灵活性来支持各种扫描。

GetOptions (
  'delay=f'     => \(my $delay = 1),
  'ip=s'        => \ my $target_ip,
  'range=s'     => \ my $port_range,
  'procs=i'     => \(my $procs = 50),
  'type=s'      => \(my $protocol = 'tcp'),
  'flag=s'      => \ my @flags,
  'verbose'     => \ my $verbose,
  'h|help|?'    => sub { pod2usage(2) },
);

你可能想知道如何将flag字符串参数读入@flags数组。在这种情况下,我想能够接受一个或多个标志参数,因此用户可以将它们传递给端口扫描器,如下所示:

$ ./port_scanner -flag fin -flag psh -flag urg

或者更简洁地

$ ./port_scanner -f fin -f psh -f urg

这些值将被捕获到@flags中。顺便说一句,这三个标志是TCP端口扫描技术“圣诞树”的一部分。为了处理标志,我将使用以下代码

die "flags are for tcp only!\n" if $protocol ne 'tcp' && @flags;
$flags[0] = 'syn' unless @flags || $protocol eq 'udp';
my $flags = { map { $_ => 1 } @flags };
$flags = {} if exists $flags->{null};

标志只能用于TCP扫描,所以我首先检查是否收到了任何标志,并且请求的协议不是TCP,这将引发异常。然后代码将@flags读入一个散列表,如果协议是TCP且未传递任何标志,则默认为SYN。我们还支持一种特殊的扫描类型“null”扫描,不传递任何标志。

现在,send_packet子程序可以更新以处理不同的协议和扫描。

sub send_packet
{
  my ($protocol, $target_port, $flags) = @_;

  Net::RawIP->new({ ip => {
                      saddr => $local_ip,
                      daddr => $target_ip,
                    },
                    $protocol => {
                      source => $local_port,
                      dest   => $target_port,
                      %$flags,
                    },
                  })->send;
}

更新后的子例程透明地将接收到的参数传递给Net::RawIP,该模块处理具体细节。剩余的ip和port变量在此代码点已经定义为全局变量。

还需要更新read_packet子例程,以便解析不同的数据包类型。

sub read_packet
{
  my $raw_data = shift;
  my $ip_data = NetPacket::Ethernet::strip($raw_data);
  my $ip_packet = NetPacket::IP->decode($ip_data);

  if ($ip_packet->{proto} == 6)
  {
    my $tcp = NetPacket::TCP->decode(NetPacket::IP::strip($ip_data));
    my $port = $tcp->{src_port};

    if ($tcp->{flags} & SYN)
    {
      return ($port, 'open');
    }
    elsif ($tcp->{flags} & RST)
    {
      return ($port, 'closed');
    }
    return ($port, 'unknown');
  }
  elsif ($ip_packet->{proto} == 17)
  {
    my $udp = NetPacket::UDP->decode(NetPacket::IP::strip($ip_data));
    my $port = $udp->{src_port};
    return ($port, 'open');
  }
  else
  {
    warn "Received unknown packet protocol: $ip_packet->{proto}\n";
  }
}

如果我们收到TCP数据包,代码将检查数据包标志以确定端口的状况。如果收到ACK/SYN响应,则认为端口是开放的,可以通过检查SYN标志的存在来测试。如果存在RST标志,则表示端口已关闭。请注意,为了测试标志的存在,我们使用位运算&与由NetPacket::TCP导出的标志常量进行测试。

UDP更简单,因为它不支持标志。如果我们收到UDP数据报,我们将端口视为开放的。

ICMP

尽管我们没有发送ICMP消息,但我们可能从目标主机接收到它们。有时主机返回类型为“目标端口不可达”的ICMP消息,而不是回复TCP/UDP数据包。ICMP消息将包含发送者原始消息的IP头,但由于IP头不包含目标端口,我们如何从ICMP响应中确定目标端口?一种方法是在IP数据包的数据部分包含目标端口。一旦我们收到ICMP响应,我们就解析出IP头,并从消息的数据组件中提取目标端口。

这还不是我们可以做的所有事情。ICMP响应还可以指示动态防火墙已经开始丢弃我们的数据包,因为我们已经超过了速率限制。如果在收到ICMP消息后,端口扫描器能够自动增加发送消息之间的延迟,那将非常好。为了将此更新传达给子进程,我们可以安装一个信号处理器。为了“看到”ICMP消息响应,pcap过滤器需要更新以删除端口子句。这引入了新的问题:我们可能从目标主机收到与我们扫描无关的消息。到目前为止,我已避免处理ICMP。

运行新的端口扫描器

就是这样!完整代码可以在这里找到。现在让我们看看如何运行此代码的示例。

# tcp syn scan of common ports, 100 processes sending packets every 0.25 sec:
$ sudo $(which perl) -i 192.168.1.5 -p 100 -d 0.25

# same as before but print all closed and filtered ports too
$ sudo $(which perl) -i 192.168.1.5 -p 100 -d 0.25 -v

# udp scan
$ sudo $(which perl) -i 192.168.1.5 -t udp

# tcp fin scan
$ sudo $(which perl) -i 192.168.1.5 -f fin

# tcp null scan
$ sudo $(which perl) -i 192.168.1.5 -f null

# tcp xmas scan
$ sudo $(which perl) -i 192.168.1.5- f fin -f psh -f urg

结论

我们已经创建了一个开始类似于专业工具的东西:一个可定制的、高性能的TCP/UDP端口扫描器,具有有用的报告功能。通过开发我们自己的解决方案,而不是依赖nmap等工具,我们可以更深入地了解网络的工作原理以及扫描主机的所需技能。


这篇文章最初发布在PerlTricks.com

标签

David Farrell

David是一位专业的程序员,他经常推文博客关于代码和编程艺术。

浏览他们的文章

反馈

这篇文章有什么问题吗?请通过在GitHub上打开问题或拉取请求来帮助我们。