端口扫描是Nmap的核心功能,用于确定目标机的端口状态(开放、关闭、过滤等),也为Nmap的服务与版本扫描、OS扫描、脚本扫描提供基本的指引信息。所以,深入理解端口扫描的实现对分析其他的扫描方式很有帮助。
1 简单回顾
首先简要回顾一下端口扫描的原理以及命令行选项。
1.1 扫描原理
Nmap提供的10多种类型的端口扫描方法,如TCP SYN/ACK/FIN/Xmas/NULL/ Windows/Connect,FTP Bounce, Idle scan, UDP port unreachable/ UDP recv_from, IP protocol,SCTPINIT/SCTP COOKIE ECHO等扫描方式。原理是基于网络数据包的特征或者网络编程API来判定端口的状态。例如,以TCP SYN方式(Nmap的默认的TCP扫描方式)为例来简单地回顾端口扫描原理。
TCP SYN探测到端口关闭:
TCP SYN探测到端口开放:
更多扫描原理的细节,请参考电子书:Secrets of NetworkCartography
1.2 端口扫描用法
扫描方式指定选项
[plain] view plaincopy
- -sS/sT/sA/sW/sM:指定使用 TCPSYN/Connect()/ACK/Window/Maimon scans的方式来对目标主机进行扫描。
- -sU: 指定使用UDP扫描方式确定目标主机的UDP端口状况。
- -sN/sF/sX: 指定使用TCP Null, FIN, and Xmas scans秘密扫描方式来协助探测对方的TCP端口状态。
- --scanflags <flags>: 定制TCP包的flags。
- -sI <zombiehost[:probeport]>: 指定使用idle scan方式来扫描目标主机(前提需要找到合适的zombie host)
- -sY/sZ: 使用SCTP INIT/COOKIE-ECHO来扫描SCTP协议端口的开放的情况。
- -sO: 使用IP protocol 扫描确定目标机支持的协议类型。
- -b <FTP relay host>: 使用FTP bounce scan扫描方式
端口指定选项
[plain] view plaincopy
- -p <port ranges>: 扫描指定的端口
- 实例:-p22; -p1-65535; -p U:53,111,137,T:21-25,80,139,8080,S:9(其中T代表TCP协议、U代表UDP协议、S代表SCTP协议)
- -F: Fast mode – 快速模式,仅扫描TOP 100的端口
- -r: 不进行端口随机打乱的操作(如无该参数,nmap会将要扫描的端口以随机顺序方式扫描,以让nmap的扫描不易被对方防火墙检测到)。
- --top-ports <number>:扫描开放概率最高的number个端口(nmap的作者曾经做过大规模地互联网扫描,以此统计出网络上各种端口可能开放的概率。以此排列出最有可能开放端口的列表,具体可以参见文件:nmap-services。默认情况下,nmap会扫描最有可能的1000个TCP端口)
- --port-ratio <ratio>: 扫描指定频率以上的端口。与上述--top-ports类似,这里以概率作为参数,让概率大于--port-ratio的端口才被扫描。显然参数必须在在0到1之间,具体范围概率情况可以查看nmap-services文件。
详细端口扫描用法介绍
官方手册:http://nmap.org/book/man-port-scanning-techniques.html
2 实现框架
下面我们从端口扫描部分的文件组织、核心类介绍与代码流程框架三个角度来整体把握其要点。
2.1 文件组织
scan_engine.cc/scan_engine.h
Nmap的端口扫描功能主要在scan_engine.h/scan_engine.cc中实现,其中scan_engine.cc为C++文件,共有6000多行代码(目前版本Nmap6.0)。实现端口扫描所需的基础类及调用接口。
nmap-services
端口扫描功能会用到nmap-services数据库文件,此文件描述互联网上常见的注册端口对应的服务名称,以及该端口开放的频率和注释信息。端口开放的频率是Nmap项目组对互联网上大量的计算机进行扫描,统计出的每个端口开放的概率值,所以根据此概率可以很方便指定扫描覆盖的范围。例如,使用--top-ports 3000,可以让Nmap扫描概率排名前3000的端口;使用--port-ratio 0.001,可以让Nmap扫描开放概率在0.001以上的所有端口。
2.2 核心类分析
Nmap端口扫描部分涉及到多个Class,这些类将端口扫描过程中涉及到各种数据及接口有效地管理起来。
2.2.1 UltraScanInfo
UltraScanInfo是统一管理端口扫描过程信息的类。该类的对象主要用在ultra_scan()函数中。下面简要列举UltraScanInfo类中的关键点:
- 执行的扫描类型详细信息(如TCP SYN/UDP/ARP扫描)
- massping状态信息(massping用于主机发现阶段对大多数情况的目标PING操作)
- now时间:记录当前时间值
- 主机组扫描状态信息(GroupScanStats *gstats),记录扫描的一组主机的整体扫描状态。
- 性能状态
- 未完成主机列表:放置还没有扫描完成的主机
- 已完成主机列表:放置已经扫描完毕的主机
- 扫描进度与发包频率度量:用于记录扫描进度和发包频率
- 接口函数:对主机列表进行操作;获取扫描状态信息(完成百分数、未完成主机数等)。
上述的未完成主机列表与已完成主机列表是核心部分,因为其他的操作都围绕此两张列表进行操作。
[cpp] view plaincopy
- /* Any function whichmesses with (removes elements from)
- incompleteHosts mayhave to manipulate nextI */
- list<HostScanStats*> incompleteHosts;
- /* Hosts are moved fromincompleteHosts to completedHosts as they are
- completed. We keepthem around because sometimes responses come back very
- late, after weconsider a host completed. */
- list<HostScanStats*> completedHosts;
2.2.2 GroupScanStats
GroupScanStats用于管理端口扫描过程中一组主机的整体统计状态。通常情况下,为了加快执行速度,Nmap是将一组主机(根据扫描类型,可能由64或128等等不同数量主机组成)统一进行端口扫描的。所以,此处就会用到GroupScanStats来对整个主机组进行状态的管理。
下面简单介绍该类的核心作用:
- 记录超时值(timeout):等到何时未完成就放弃此次扫描
- 检查是否允许发送:sendOK(when),判断在when的时刻是否允许发送数据包
- 统计所有主机的活动的探测包:还没有完成的探测过程,例如该探测还没有收到回复包,还在持续等待。
- 时序及超时信息:拥塞窗口、阀值、期待回复包数量等信息。
- 扫描的主机数量与探测包数量:包括完成主机与未完成主机;探测包数量为每个主机扫描预计的探测包数量(每个端口一个探测包,探测包数量也就等于端口数量)。
- 扫描频率控制:send_no_earlier_than与send_no_later_than,控制扫描发送包的频率。对应到用户输入的命令行选项:--max-rate和--min-rate。
- 全局的ping host记录:在主机发现过程进行全局PING时候,对该主机进行PING操作。该主机为最新发现的在线主机。
2.2.3 HostScanStats
HostScanStats类管理单个的目标主机的扫描统计状态。在上述的UltraScanInfo类中的两个列表:未完成列表与已完成列表中存放的都是HostScanStats指针类型数据,列表中每一项都指向HostScanStats对象,因此可从列表快速获取到单个主机的详细信息。
该类中包含单个主机的以下的信息:
- 指向目标主机(Target对象)的指针:用于获取目标主机基本控制信息。
- 剩余端口数量:记录扫描主机还剩余多少主机没有被扫描。
- massping状态:记录发送探测包类型及各种探测方式对应的下一个端口的索引值
- 超时时间和过期时间:探测包多长时间算超时;完全放弃一个探测包的时间。
- 获取下一个最早超时的探测包:该主机可能发送大量的探测包,其中多个探测包都可能超时,在此处可以返回其中最早超时的探测包。
- 销毁未完成的探测包:销毁单个包或销毁全部为完成的探测包(OutstandingProbes)。
- 未完成的探测包列表:list<UltraProbe*> probes_outstanding;该列表记录当前处于活动状态但没有完成的探测包。
- 探测包工作台:vector<probespec>probe_bench;该向量用于存放达到try_no(尝试次数)允许最大的尝试次数并在等待继续尝试的探测包。若允许继续增加尝试次数,那么在探测包工作台中的Probe被移动重试栈区(retry_stack)中。
- 重试栈区:vector<probespec>retry_stack;该向量用于保存从探测包工作台上移动出来的探测包。
- 判断扫描是否结束:接口函数bool completed();
- 允许的尝试次数:allowedTryno()函数返回主机允许最大尝试次数。
- 完成的端口数量:已经完成探测的端口数量
- 扫描延时:包括扫描延时信息、频率限制检测、增加扫描延时等内容。
其中,未完成探测包列表、探测包工作台、重试栈区是该类的核心部分,针对目标主机进行所有的探测包由以上三个列表或向量来统一管理。
2.2.4 UltraProbe
UltraProbe类是用于管理每一个探测包的信息。
该类主要包含以下几类关键信息:
- 设置探测包类型:比如setIP(),setConnect(),setARP()等。
- 源端口与目的端口:根据探测包类型,返回对应端口配置。
- 获取协议相关信息:例如IPID,TCP seqnumber, SCTPvtag等信息。
- 重试次数、超时信息、重传等信息:与探测包进行过程紧密相关的属性。
2.3 代码流程
Nmap的端口扫描主要从nmap_main()函数中的ultra_scan()函数进入,根据配置的不同扫描类型,ultra_scan()中进行不同的处理。两个特殊扫描方式idle scan和FTP bounce scan是单独实现的处理函数,不借用ultra_scan()函数。
这里,我们主要分析ultra_scan()函数方式进行的扫描,因为这是最通用最有代表性的。
2.3.1 代码流程图
2.3.2 流程解析
以TCPSYN为例,在nmap_main()中调用ultra_scan(Targets, &ports,SYN_SCAN),此处传入参数目标主机Targets(是vector容器保存的);ports是struct scan_list类型的指针,指向解析出来的端口列表;SYN_SCAN是预先定义的枚举值,让ultra_scan能够辨别出扫描类型。
进入ultra_scan()后,第一个重要步骤是加载UDP扫描需要的负载,即UDP探测方式需要发送的包的内容(该内容从数据库文件nmap-payloads中读取出来)。此步骤在init_payloads()中完成。
判断是否是在Windows平台扫描环回接口(loopback),若是,则打印出提示信息:Windows平台无法支持扫描自己的环回接口。
创建UltraScanInfo对象,使用Targets,ports,scantype初始化。
开始嗅探(begin_sniffer),启动libpcap库对网络数据包进行嗅探,以便能够接收到目标机的回复包。主要调用libpcap的API:pcap_open_live打开实时嗅探,然后再设置libpcap抓包的过滤器(Filter),最终调用pcap_setfilter()具体设置。
进入端口扫描的主循环,只要UltraScanInfo中的未完成列表不为空,都将继续执行循环结构体。
1) 首先进行PING探测操作,发送必要的探测包到目标机特定端口。
2) 然后重传所有未完成的探测包。处于outstanding状态,并允许重传的探测包将在这里进行重传。
3) 重传retry_stack中探测包。retry_stack中探测包是从探测包工作台(probe bench)中移动出来的,是重新获得重传机会的探测包。所以在此处检查retry_stack中是否有探测包等待重传。
4) 检查是否需要传输新的探测包。Nmap扫描时候,是对一批主机扫描同一个端口,然后推进到下一个端口进行扫描。所以,在此处检查是否有主机需要进行新的端口的探测。
5) 获取时间,并打印出端口扫描状态。
6) 等待接收回复包。根据不同类型的探测方式,等待接收不同类型的回复包。这里是通过libpcap的API:pcap_next()函数来读取到回复包的。
7) 获取时间,并对接收到的数据进行处理。
8) 检测是否按键,若有允许的按键按下(v增加verbose,V减少verbose;d增加debugginglevel,D减少debugginglevel;p是打开packet trace,P是关闭packet trace),则执行相应功能,否则仅仅打印出扫描进度信息。
退出循环后,首先停止发送频率度量,USI->send_rate_meter.stop(&USI->now),因为此时真正的扫描已经结束,所以此处可以停止度量。
保存计算出的超时信息,并将扫描过程的详细信息与调试信息打印出来。
删除UltraScanInfo对象,该对象仅仅用于每一个ultra_scan()函数调用。
3 代码注释
[cpp] view plaincopy
- /* 3rd generation Nmap scanning function. Handles most Nmap port scan types.
- The parameter to gives group timing information, and if it is not NULL,
- changed timing information will be stored in it when the function returns. It
- exists so timing can be shared across invocations of this function. If to is
- NULL (its default value), a default timeout_info will be used. */
- void ultra_scan(vector<Target *> &Targets, struct scan_lists *ports,
- stype scantype, struct timeout_info *to) {
- UltraScanInfo *USI = NULL;///扫描信息控制类
- o.current_scantype = scantype;///标记当前扫描类型,用于输出
- init_payloads(); /* Load up _all_ payloads into a mapped table */
- if (Targets.size() == 0) {
- return;
- }
- #ifdef WIN32
- if (scantype != CONNECT_SCAN && Targets[0]->ifType() == devt_loopback) {
- log_write(LOG_STDOUT, "Skipping %s against %s because Windows does not support scanning your own machine (localhost) this way.\n", scantype2str(scantype), Targets[0]->NameIP());
- return;
- }
- #endif
- // Set the variable for status printing
- o.numhosts_scanning = Targets.size();
- startTimeOutClocks(Targets);
- USI = new UltraScanInfo(Targets, ports, scantype);
- /* Use the requested timeouts. */
- if (to != NULL)
- USI->gstats->to = *to;
- if (o.verbose) {
- char targetstr[128];
- bool plural = (Targets.size() != 1);
- if (!plural) {
- (*(Targets.begin()))->NameIP(targetstr, sizeof(targetstr));
- } else Snprintf(targetstr, sizeof(targetstr), "%d hosts", (int) Targets.size());
- log_write(LOG_STDOUT, "Scanning %s [%d port%s%s]\n", targetstr, USI->gstats->numprobes, (USI->gstats->numprobes != 1)? "s" : "", plural? "/host" : "");
- }
- ///begin_sniffer()开启libpcap并设置pcap filter,以便接收目标主机返回的数据包
- begin_sniffer(USI, Targets);
- while(!USI->incompleteHostsEmpty()) {
- ///向目标机发送探测包(probe)
- doAnyPings(USI);
- ///重传未完成探测过程的数据包
- doAnyOutstandingRetransmits(USI); // Retransmits from probes_outstanding
- /* Retransmits from retry_stack -- goes after OutstandingRetransmits for
- memory consumption reasons */
- ///
- doAnyRetryStackRetransmits(USI);
- ///检查需要进行的新的探测包类型。
- doAnyNewProbes(USI);
- gettimeofday(&USI->now, NULL);
- // printf("TRACE: Finished doAnyNewProbes() at %.4fs\n", o.TimeSinceStartMS(&USI->now) / 1000.0);
- printAnyStats(USI);
- ///在waitForResponses()中接收libpcap中抓取到的数据包
- waitForResponses(USI);
- gettimeofday(&USI->now, NULL);
- // printf("TRACE: Finished waitForResponses() at %.4fs\n", o.TimeSinceStartMS(&USI->now) / 1000.0);
- ///对整个扫描进行统计处理:标记过期的探测包、判断探测是否完毕
- processData(USI);
- ///扫描过程中,若检测到按键,打印出扫描进度信息
- if (keyWasPressed()) {
- // This prints something like
- // SYN Stealth Scan Timing: About 1.14% done; ETC: 15:01 (0:43:23 remaining);
- USI->SPM->printStats(USI->getCompletionFraction(), NULL);
- if (o.debugging) {
- /* Don't update when getting the current rates, otherwise we can get
- anomalies (rates are too low) from having just done a potentially
- long waitForResponses without sending any packets. */
- USI->log_current_rates(LOG_STDOUT, false);
- }
- log_flush(LOG_STDOUT);
- }
- }
- USI->send_rate_meter.stop(&USI->now);
- /* Save the computed timeouts. */
- if (to != NULL)
- *to = USI->gstats->to;
- ///输出详细信息与调试信息
- if (o.verbose) {
- char additional_info[128];
- if (USI->gstats->num_hosts_timedout == 0)
- if (USI->ping_scan) {
- Snprintf(additional_info, sizeof(additional_info), "%lu total hosts",
- (unsigned long) Targets.size());
- } else {
- Snprintf(additional_info, sizeof(additional_info), "%lu total ports",
- (unsigned long) USI->gstats->numprobes * Targets.size());
- }
- else Snprintf(additional_info, sizeof(additional_info), "%d %s timed out",
- USI->gstats->num_hosts_timedout,
- (USI->gstats->num_hosts_timedout == 1)? "host" : "hosts");
- USI->SPM->endTask(NULL, additional_info);
- }
- if (o.debugging)
- USI->log_overall_rates(LOG_STDOUT);
- if (o.debugging > 2 && USI->pd != NULL)
- pcap_print_stats(LOG_PLAIN, USI->pd);
- delete USI;
- USI = NULL;
- }
来源:网络安全技术修炼