nmap端口扫描算法详解

顺便看看代码~

Posted by 不知道什么时候能写完的小你同学 on May 31, 2020

[toc]

nmap中有不少扫描算法,包括-sS(TCP SYN scan)/-sT(TCP connect scan)/-sU(UDP scans)

TCP基础知识

在详细分析nmap扫描算法之前,先回顾一下TCP建立连接的三次握手 ,三次握手过程如下图所示:

img

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

tcp第一次握手

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

TCP建立连接第二次握手

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

TCP建立连接第三次握手

​ 第三次握手的时候,标志位会被设置为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进行扫描的时候且端口开放时,会涉及到三个数据报:


  1. nmap发送 SYN 数据报 给指定端口 —> nmap发送数据报
  2. 收到返回SYN ACK数据报 —>目标IP发回数据报
  3. 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 ScanTCP SYN Scan更加隐秘。

#close端口数据报分析

使用TCP Connect Scan扫描时,若端口关闭,则会收到ICMP不可达的数据包。对于已经关闭的端口并不会做再次发送SYN数据报的操作,所以相比起SYN Scan,这种扫描方式不会造成网络拥堵。

UDP Scan (-sU)

NMAP端口扫描代码分析

本着既然研究一个东西,就好好研究的观点,我还是决定看看NMAP的源码。但在源码分析这一部分,只浅析端口扫描部分的代码。

入口函数

端口扫描的主要代码分布在scan_engine.ccscan_engine_connect.ccscan_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类来表示。

  • UltraProbe

    UltraProbe表示一种可以被用于扫描端口的探针。在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
  • 扫描主循环

    1. doAnyPings(): 探测主机是否存活;

    2. doAnyOutstandingRetransmits(): 遍历每个主机的ProbeQueue, 如果发现有超时的probe,则重新发送

    3. doAnyRetryStackRetrasmits()/doAnyNewProbes(): 这两个函数本质上的区别是,前者是针对已经发送过的probes进行再次发送操作,而后者是对新的端口进行发送probe操作。
    4. 帮助函数—->gettimeofday(), printAnyStats()
    5. waitForResponses(): 获取并解析begin_sniffer()截获的数据包,其中包括标记主机是否开启,端口是否开启。主要处理逻辑在ultrascan_port_probe_update()
    6. 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建立连接的三次握手包。


参考文章:

  1. 《TCP/IP详解:卷1》
  2. https://nmap.org/book/man-port-scanning-techniques.html
  3. https://www.bamsoftware.com/wiki/Nmap/ScanEngine