[toc]
nmap中有不少扫描算法,包括-sS(TCP SYN scan)/-sT(TCP connect scan)/-sU(UDP scans)。
TCP基础知识
在详细分析nmap扫描算法之前,先回顾一下TCP建立连接的三次握手 ,三次握手过程如下图所示:

三次握手建立连接的数据包(使用命令为 ssh)如下所示:

在第一次握手的时候,标志位会设成SYN,同时会发送一个Seq Number,在这个数据包中 Seq Number = 2186387149

第二次握手的时候,标志位会被设置为SYN和ACK,同时 ACK Number = Seq Number + 1, Seq Number = 2071329386

第三次握手的时候,标志位会被设置为ACK,同时 ACK Number = Seq Number + 1
以上是正常建立三次握手的过程,当我们想要请求连接一个不存在的端口时,会发送什么样的数据报呢?
根据《TCP/IP详解:卷1》18.7.1可知,当连接到不存在端口的时候,该端口如果没有进程正在听,对于UDP,当一个数据报到达目的端口时,该端口没在使用,它将产生一个ICMP端口不可达的信息。而TCP则使用复位。
nmap扫描算法
TCP SYN scan
#nmap命令 -sS
#open端口流量分析

上述数据报所使用的命令为 nmap -sS IP,扫描结果443是开放的
我们以443端口扫描为例,分析324,334,335数据报。
1
2
3
324: 37796->443 SYN
334: 443->37796 SYN,ACK
335: 37796->443 RST
通过数据报可以看到,当使用TCP SYN scan进行扫描的时候且端口开放时,会涉及到三个数据报:
nmap发送SYN数据报 给指定端口 —> nmap发送数据报- 收到返回
SYN ACK数据报 —>目标IP发回数据报 nmap发送RST数据报到目标端口 —> nmap发送数据报
以上两步和建立TCP连接的前两次握手是相同的
#filtered端口数据报分析

我们以3306端口为例,扫描结果显示该端口为filtered状态,分析344,397数据报
1
2
344: 48501--->3306 SYN
397: 48502--->3306 SYN
通过数据报可以看到,当nmap进行端口扫描时,都会先发送一个SYN数据包进行探测,当一段时间后nmap没有收到返回数据报,会再次发送一个SYN探测包, 若依然没有响应,则判断该端口为filterd状态。重传的时间间隔为多长,可以自己设定。分析到这里其实有个问题,filtered端口的存在对于端口扫描是否有实际意义?
#close端口数据报分析
目前没有抓到关闭端口的数据报,无法分析。nmap官方文档显示,如果目标端口返回的是RST数据报,则端口为关闭状态。
TCP Connect Scan (-sT)
#nmap命令 -sT
#open端口数据报分析

以443 端口为例,分析31,32,33,34数据报。
1
2
3
4
31: 52610-->443 SYN
32: 443--->52610 ACK, SYN
33: 52610--->443 ACK
52610--->443 RST, ACK
31-33数据报完成的TCP三次握手,建立了TCP连接。34数据报则是nmap发送RST数据报了结束TCP连接。所以相比起TCP Connect Scan,TCP SYN Scan更加隐秘。
#close端口数据报分析

使用TCP Connect Scan扫描时,若端口关闭,则会收到ICMP不可达的数据包。对于已经关闭的端口并不会做再次发送SYN数据报的操作,所以相比起SYN Scan,这种扫描方式不会造成网络拥堵。
UDP Scan (-sU)
NMAP端口扫描代码分析
本着既然研究一个东西,就好好研究的观点,我还是决定看看NMAP的源码。但在源码分析这一部分,只浅析端口扫描部分的代码。
入口函数
端口扫描的主要代码分布在scan_engine.cc,scan_engine_connect.cc和scan_engine_raw.cc中。入口函数为 ultra_scan,函数声明如下:
1
void ultra_scan(std::vector<Target *> &Targets, struct scan_lists *ports, stype scantype, struct timeout_info *to)
参数介绍
-
std::vector<Target *> &Targets=====> 需要扫描的目标 -
struct scan_lists *ports======> TCP或者UDP的端口和一些协议1 2 3 4 5 6 7 8
struct scan_lists { unsigned short *tcp_ports; int tcp_count; unsigned short *udp_ports; int udp_count; unsigned short *prots; int prot_count; };
-
stype scantype======> nmap扫描的方式1 2 3 4 5 6 7 8 9 10 11
typedef enum { STYPE_UNKNOWN, HOST_DISCOVERY, ACK_SCAN, SYN_SCAN, FIN_SCAN, .... SCRIPT_POST_SCAN, TRACEROUTE, PING_SCAN_ND } stype;
-
timeout_info=====> 存放超时信息
数据结构
-
UltraScanInfo每一次扫描都会创建一个
UltraScanInfo实例。1
scan_engine.cc:2749 UltraScanInfo USI(Targets, ports, scantype);
进入类中,其中定义了一些布尔变量,用来标志扫描类型。其中还包含
GroupScanStats类,该类用于记录整个扫描过程的相关数据。除此之外,还包含一些没有完成扫描的主机,每一个该类型的主机会抽象为一个HostScanStats类来表示。 -
UltraProbeUltraProbe表示一种可以被用于扫描端口的探针。在HostScanStats中包含着一系列的探针,可以将它们设置为IP,connect或者ARP探针,其使用的方法为setIP(),setConnect(),setARP()。这三个函数的实现在scan_engine_raw.cc中,等后面可以仔细分析一下。1 2 3 4 5 6 7
void setIP(u8 *ippacket, u32 iplen, const probespec *pspec); /* Sets this UltraProbe as type UP_CONNECT, preparing to connect to given port number*/ void setConnect(u16 portno); /* Pass an arp packet, including ethernet header. Must be 42bytes */ void setARP(u8 *arppkt, u32 arplen); void setND(u8 *ndpkt, u32 ndlen);
-
probespec设置
IP探针的时候,会使用到这个结构体。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
typedef struct probespec { /* To save space, I changed this from private enum (took 4 bytes) to u8 that uses #defines above */ u8 type; u8 proto; /* If not PS_ARP -- Protocol number ... eg IPPROTO_TCP, etc. */ union { struct probespec_tcpdata tcp; /* If type is PS_TCP or PS_CONNECTTCP. */ struct probespec_udpdata udp; /* PS_UDP */ struct probespec_sctpdata sctp; /* PS_SCTP */ struct probespec_icmpdata icmp; /* PS_ICMP */ struct probespec_icmpv6data icmpv6; /* PS_ICMPV6 */ /* Nothing needed for PS_ARP, since src mac and target IP are avail from target structure anyway */ } pd; } probespec;
ultra_scan()函数流程
-
随机生成一个用于扫描的端口
- 如果扫描目标为空,则返回
-
创建
UltraScanInfo对象 -
如果为
raw scan,则调用begin_sniffer()。这个函数调用了netutil.h中的my_pcap_open_live(),其封装了libpcap中的pcap_open_live()。- 除了
TCP connect scan,其他扫描方式都为raw scan。
- 除了
-
扫描主循环
-
doAnyPings(): 探测主机是否存活; -
doAnyOutstandingRetransmits(): 遍历每个主机的ProbeQueue, 如果发现有超时的probe,则重新发送 doAnyRetryStackRetrasmits()/doAnyNewProbes(): 这两个函数本质上的区别是,前者是针对已经发送过的probes进行再次发送操作,而后者是对新的端口进行发送probe操作。- 帮助函数—->
gettimeofday(),printAnyStats() waitForResponses(): 获取并解析begin_sniffer()截获的数据包,其中包括标记主机是否开启,端口是否开启。主要处理逻辑在ultrascan_port_probe_update()中processData(): 标记已经完成扫描的主机,设置超时probe。如果probe没有回复,则设置主机和端口的状态,主要是scantype_no_response_means()处理。
-
NMAP绕过IDS
其他扫描方式
socket扫描
#代码
1
2
3
4
5
6
7
8
9
10
async def Scan(ip, port):
try:
reader, writer = await asyncio.open_connection(host=ip, port=port)
writer.close()
await writer.wait_closed()
except Exception as e:
return False
else:
result_port.append(port)
return result_port
#数据报分析

这一系列的数据报为通过socket方式扫描目的IP的80端口,返回的数据报。其中584,587,588三个数据报为TCP建立连接的三次握手包。
参考文章:
- 《TCP/IP详解:卷1》
- https://nmap.org/book/man-port-scanning-techniques.html
- https://www.bamsoftware.com/wiki/Nmap/ScanEngine