SYN-Scan的简单实现

  06 May 2015


什么是SYN-Scan

如果想要知道一个网段的所有或者部分端口是不是开启的,该怎么做呢? 你可能会说,写一个程序对每个IP的每个端口发起一次TCP请求。 这样的问题在于,当需要扫描的IP地址很多,端口很多时,消耗的时间是非常长的。

那么有什么替代方法呢?
答案是SYN scan,也叫半开(half open)扫描。 TCP连接建立的时候,需要三次握手:

Server |--------------| Client  
1       SYN->  
2            <-ACK+SYN
3       ACK->

如果只是想知道某一个端口是否打开了,只需要第一步和第二步就可以了。 这意味着,我们并不需要调用connect函数去发起连接,只要向对方发送一个设置了SYN标记的TCP包即可!
这样的程序,可以很方便的并发扫描,一个线程不停的发送SYN包,另一个线程不停的接受ACK+SYN即可。

收发SYN包

如何能构造、发送一个SYN包呢?答案是用raw socket,原始套接字。
通过原始套接字可以构造、接收各种不同类型的包,以太帧、IP、ICMP、TCP等等,甚至错误的数据包。
下面是一个接收TCP包的例子:

int sock_raw = socket(PF_INET , SOCK_RAW , IPPROTO_TCP);
data_size = recvfrom(sock_raw , buffer , 65536 , 0 , &saddr , &saddr_size);

通过原始套接字调用recvfrom可以抓取所有的TCP包,达到sniffer的目的。
发送可以使用sendto函数。

注意:
* 原始套接字需要程序具有CAP_NET_RAW的能力,所以通常需要root权限来运行程序。
* 如果使用MacOS或者FreeBSD等其他UNIX操作系统,使用IPPROTO_TCP的socket也是无法抓到包的。 详细请参考Strange RAW Socket on Mac OS X

端口状态的判断

如果服务器收到我们的SYN探测包,那么有几种可能:

  • 返回ACK+SYN 表示该端口是开启的;
  • 返回ACK而没有SYN 基本也可以认为是开启的,参考下面split-handshake一节;
  • 返回ACK+RST 表示该端口是关闭的,但是该IP的主机是开启的;
  • 没收到任何回应 可能是被防火墙过滤了,也可能该IP无法访问或者服务器没有开启。

使用libpcap

想要抓取Unix系统的所有TCP的包,需要在数据链路层操作才可以。但是如果在数据链路层抓包,会抓到几乎所有的网络流量。 这样一来,数据量过大,不利于处理,而且效率低下。为了解决这个问题,我们使用libpcap来收包。

通过libpcap可以通过使用BPF(Berkeley Packet Filter)来抓取想要的包。 libpcap的使用并不难,网上有大量的参考文档,这里不再赘述。简单说来,libpcap封装了操作系统提供的BPF操作。 通过BPF,程序可以在操作系统的链路层抓包。

操作系统的“干扰”

libpcap或者其他raw socket收到包后,操作系统会继续按照正常流程处理改数据包。 因此,当在SYN-Scan的时候,如果构造一个SYN包发给服务器,服务器返回SYN-ACK包后,虽然你什么都没做,你的操作系统仍然会帮你发送一个RST包。
当服务器收到RST包后,就会终止本次TCP连接;如果没有收到,就会一直等待,直到超时。 如果很多客户端一起不停的对一台服务器发送SYN包,并且客户端在收到服务器发来的ACK+SYN包后,不作处理,服务器就可能瘫痪掉。这就是所谓的SYN洪泛攻击。

对于我们的扫描器,因为只会对一个IP发送少量的包,对服务器造成的负担有限,可以考虑不发送RST包。 可以通过iptable来禁止服务器发RST包:

iptables -A OUTPUT -p tcp --tcp-flags RST RST -j DROP

如何识别服务器端响应的TCP包

libpcap该如何识别哪些包是服务器响应的包呢?我们的网卡上随时随地都在不停的收发各种数据包,必须要识别出那些是我们关心的包,然后才能构造BPF表达式。
不放先考虑一个TCP包中有哪些字段能够用来识别。

  • Ether 源、目的MAC地址和协议号。这些字段都无法自己随意设置,否则会发不出去。

  • IP 源、目的IP地址。收到的包的源地址可以用来做识别,但是对不同IP地址发的探测包的响应中源地址都不一样,因此无法构造一个BPF表达式来抓取这些包。

  • TCP 源、目的端口:如果要用源端口来识别,就要求我们每个发出的包使用相同端口,这会导致其他问题,下面会讲到; 序号:如果我们向服务器端发送序号为N的SYN包,服务器需要回复ACK序号为N+1的包,这样我们只要保持每次发送的包的序号一样,就可以通过判断收到的包的ACK号来判断了。

根据上面的分析,构造如下的BPF表达式:

tcp[8:4] == TCP_ISN + 1

TCP_ISN 就是所有发出的SYN包中的序号。
tcp[8:4] 就是tcp头的第8个字节开始的四个字节组成的数字。

SYN探测包的端口设置

如果使用相同的源端口对一个网段的IP地址短时间内发送大量SYN包,经常会收不到正常的回应,原因可能是服务器设置了防火墙,会对短时间内大量相同源端口的TCP SYN包做过滤。 为了避免这种情况发生,每次发的SYN包应该使用随机的源端口。

IP地址的扫描顺序

如果要扫描一个很大的网段,比如 10.10.0.0/16,如果按照IP地址递增的顺序发包,可能会导致一个比较小的网段的网关在极短时间段内连续收到大量的探测包。
因此最好向所有需要扫描的IP地址随机的发包,保证每个小的网段均匀的收到探测包。但是如果真的使用rand()函数产生随机IP,又需要记录每一个单独IP地址是不是已经扫描过,这样就要消耗大量的内存。
解决方法是把被扫描IP地址的主机部分(比如10.10.0.0/16网段的IP地址的后16位)按位倒序排列。
这样以来,对10.10.0.0/16网段的IP扫描顺序就变成了下面的形式:

序号    正常顺序         按位倒序排列后
 0   10.10.0.0          10.10.0.0
 1   10.10.0.1          10.10.128.0
 2   10.10.0.2          10.10.64.0
 3   10.10.0.3          10.10.192.0
 4   10.10.0.4          10.10.32.0

这样就保证了把探测包平均的“撒”在被探测的网段上。

关于split-handshake

所谓Spliting handshake 就是服务器在收到客户端发来的SYN之后,不发送ACK+SYN,而是分成两个包,一个ACK,一个SYN。
点击这里了解更多。
因此,我们收到一个服务器发来的回应后,只要含有ACK,基本就可以认为该端口是开启的了,而不必一定要同时具有ACK和SYN。

流量控制

发的太频繁会导致大量的包无响应,发送速度主要受扫描器的本地网络带宽限制。可以在没发出一个SYN包后,延时一段时间,已达到流控的效果。

tcpscan

tcpscan是我用c语言实现的一个简单的syn-scan扫描器,github地址:https://github.com/ga0/tcpscan

版权声明:原创文档,转载请注明出处