起因

freebuf中有一篇文章,讲述了基本的扫描原理并给出了简易的python代码,几种扫描方式中我发现SYN的扫描准确率高返回的信息明确,而且不会留下握手的痕迹,但是速度有些慢,因此我们可以使用无状态的扫描,提升扫描速度。

scapy

Scapy 是一个python的库,是一个强大的操纵报文的交互程序。它可以伪造或者解析多种协议的报文,还具有发送、捕获、匹配请求和响应这些报文以及更多的功能。所以我们使用scapy编写扫描程序。

有状态的扫描

#! /usr/bin/python import logging
logging.getLogger("scapy.runtime").setLevel(logging.ERROR) from scapy.all import *

dst_ip = "10.0.0.1" src_port = RandShort()
dst_port=80 stealth_scan_resp = sr1(IP(dst=dst_ip)/TCP(sport=src_port,dport=dst_port,flags="S"),timeout=10) if(str(type(stealth_scan_resp))==""):
    print "Filtered" elif(stealth_scan_resp.haslayer(TCP)):
    if(stealth_scan_resp.getlayer(TCP).flags == 0x12):
        send_rst = sr(IP(dst=dst_ip)/TCP(sport=src_port,dport=dst_port,flags="R"),timeout=10)
        print "Open"     elif (stealth_scan_resp.getlayer(TCP).flags == 0x14):
        print "Closed" elif(stealth_scan_resp.haslayer(ICMP)):
    if(int(stealth_scan_resp.getlayer(ICMP).type)==3 and int(stealth_scan_resp.getlayer(ICMP).code) in [1,2,3,9,10,13]):
        print "Filtered" 

这是那篇文章中给出的syn扫描的代码,可以看到对dst_ip的dport端口发送了SYN,然后对返回的数据包进行了详细的处理。

代码中发送数据包的函数均为scapy包中的sr*发包函数,他们会等待服务器的回复,所以要设置timeout参数,当进行大量扫描时,这个等待的时间会成为提高扫描速度的瓶颈,不论timeout -1s还是减了几秒,还是使用多线程也还是很慢。因此考虑用异步的无状态的扫描提升速度。

无状态的扫描

前面我们知道了提升扫描速度的瓶颈在于发包后等待回复的时间,所以去掉这个等待时间我们就可以提升扫描速度,同时因为去掉了等待,我们需要另加一个收包的模块,用来收集扫描后返回的信息。

在无状态扫描中,收发是异步的,发包的模块不关心收包模块会不会收到回复、收包模块也不知道发包模块向谁发送了什么,也就是收发包模块间没有交互,发包的函数只负责发送,收包的模块接收特定tcp flags字段的数据包就好,这样就没有了等待回复的时间。

这也导致了: 

    1. 得到扫描结果的顺序和发包的顺序不一致,因为不同的目标可能有不同的响应时间。

    2. 扫描的速度取决于带宽,我们可以一次发送大量的包出去,所以需要根据你的网络环境,选择合适的发包速度 (寝室的路由器就被搞崩过)

    3. 使用扫描器时本机的网络环境需要很安静,因为收包的模块不知道这个数据包是被探测的服务器返回的,还是本机的程序进行的通信,比如mac会进行各种请求。。kali就是完全安静的。

    4. 由于发包后不需要等待回复,所以可以用scapy包中的send函数,发包模块发包后拍拍屁股就可以走了。

    5. 网络不好的时候,可能出现同一个目标的ip出现多次,所以必要时需要对结果进行去重,并且降低扫描速度。

python实现

发包部分

from scapy.all import * import netaddr

ip = ['1.34.0.0/16', '1.33.0.0/16']
port = [80, 81, 82]

ipArray = []
portArray = [] for i in ip: ipArray.extend([str(i) for i in  netaddr.IPNetwork(i).subnet(24)])
portArray = [port[i:i+3] for i in range(0, len(port), 3)] for i in ipArray:
    for j in portArray:
        send(IP(dst=i)/TCP(dport=j, flags=2), verbose=False)
        print i, j 

发包模块使用了scapy和netaddr包,ip 和port 两个列表定义了要被扫描的部分

netaddr包用于处理ip,由于scapy的send发包函数可以传入一个IP段为目的ip,而且实践证明这样比一个for循环一个一个发快的多的多,测试了几次之后发现一次探测一个c段比较好,能兼顾速度和准确率。所以将字符串的ip段”1.34.0.0/16″初始化一个IPNetwork类,并使用subnet函数分割为c段,返回一个列表,再将这些列表合并,就得到了由c段组成的所有需要扫描的ip地址。

同理端口一次只扫描三个,将端口分为三个一组,存进portArray中。

最后发包过程中,可以选择先遍历ip或先遍历端口,注意send函数verbose参数为False避免输出很多东西,构造的数据包TCP首部flags为2,也就是flags字段只有SYN标志。如下图

0_1304045456tpaK.jpg

收包部分

from scapy.all import *

iface = 'eth0' userIP = '192.168.205.160' def prn(pkt): print pkt.sprintf("%IP.src%:%IP.sport%  %TCP.flags%")

sniff(iface=iface, filter='tcp and dst %s and tcp[13:1] & 18==18'%userIP, prn=prn) 

收包模块部分也需要导入scapy包,定义了用户的网卡名iface和本机ip userIP,传入本机ip的目的是过滤到目标为本机的数据包,在虚拟机上使用时需要格外注意。

sniff函数为scapy包的嗅探函数,用途为将iface网卡上的、符合filter的数据包传给prn回调函数进行处理,首先注意嗅探需要root权限,然后是filter函数,他也可以写成这样

sniff(iface=iface, lfilter=lambda x:x.haslayer(TCP) and x[IP].dst==userIP and x.flags==18, prn=prn)

lfilter参数传入一个函数,返回True则留下这个数据包,反之False则丢弃它, filter 传入的是tcpdump的过滤语法,有更高的效率,所以推荐使用filter

至于tcp[13:1] & 18==18是怎么来的,因为在syn扫描中,我们向目标端口发送SYN,如果它开放的话会回复SYN+ACK,也就是SYN ACK位均为1,在上面tcp首部的图中,ACK为高位,SYN为低位,2(SYN) + 16(ACK) = 18。

此外tcp[13:1]是tcpdump里的一个高级语法,意为取tcp数据包的下标为13的字节(也就是第14个字节)开始的1个字节,也就是上面图中flags所在的字节,这样用其值与18与一下,就过滤掉了别的包。

在回调函数prn中,可以对扫描结果进行处理,可以打印出来,也可以存入文件中。(sprintf是scapy包中的格式化输出函数)

组合起来

上面实际是两个文件,可以用多线程,主线程发包,另开一个线程sniff嗅探达到整合的目的。

当然也可以粗暴的开两个命令行,分别执行两个文件,在收包那里,就可以看到扫描结果很快的出来啦。

*本文原创作者:addadd