利用python重构NMAP的服务版本扫描模块
2023-02-02 15:58:36 # Python

1. NMAP 扫描原理

1
2
3
4
5
6
7
8
9
10
NMAP 服务与版本扫描命令:
-sV: 指定让Nmap进行版本侦测

–version-intensity<level>: 指定版本侦测强度(0-9),默认为7。数值越高,探测出的服务越准确,但是运行时间会比较长。

–version-light: 指定使用轻量侦测方式 (intensity 2)

–version-all: 尝试使用所有的probes进行侦测 (intensity 9)

–version-trace: 显示出详细的版本侦测过程信息。

扫描原理是服务指纹(或称签名)对比匹配。Nmap内部包含了几千种常见服务指纹的数据库(nmap-service-probes),对目标端口进行连接通信,产生当前端口的服务指纹,再与指纹数据库对比,寻找出匹配的服务类型。

服务与版本侦测主要分为以下几个步骤:

  1. 首先检查open与open|filtered状态的端口是否在排除端口列表内。如果在排除列表,将该端口剔除。

  2. 如果是TCP端口,尝试建立TCP连接。尝试等待片刻(通常6秒或更多,具体时间可以查询文件nmap-services-probes中ProbeTCP NULL q||对应的totalwaitms)。通常在等待时间内,会接收到目标机发送的“Welcome Banner”信息。nmap将接收到的Banner与nmap-services-probes中NULLprobe中的签名进行对比。查找对应应用程序的名字与版本信息。

  3. 如果通过“Welcome Banner”无法确定应用程序版本,那么nmap再尝试发送其他的探测包(即从nmap-services-probes中挑选合适的probe),将probe得到回复包与数据库中的签名进行对比。如果反复探测都无法得出具体应用,那么打印出应用返回报文,让用户自行进一步判定。

  4. 如果是UDP端口,那么直接使用nmap-services-probes中探测包进行探测匹配。根据结果对比分析出UDP应用服务类型。

  5. 如果探测到应用程序是SSL,那么调用openSSL进一步的侦查运行在SSL之上的具体的应用类型。

  6. 如果探测到应用程序是SunRPC,那么调用brute-forceRPC grinder进一步探测具体服务。

2. NMAP 实现框架

2.1. 文件组织

文件 功能
service_scan.cc/service_scan.h 服务扫描的核心功能都在此两个文件中实现,文件结构清晰简洁,代码行数总共3000余行。与端口扫描部分类似,服务扫描也定义了不少类与接口函数。
nmap-service-probes 此文件是nmap服务扫描所需的数据库文件,包括定制的探测包及预期的回复包,及识别服务类型的具体匹配方式。
nsock library 服务与版本扫描部分用到nsock库,该库设计用于并发处理网络事件。在Nmap源码树下,有单独的nsock目录来管理nsock库。此处我们仅需要关注nsock.h文件中提供的API函数即可。

下面摘取 nmap-service-probes 的片段,简单了解其结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Probe TCPNULL q||

# Wait forat least 6 seconds for data. It used tobe 5, but some

# smtpservices have lately been instituting an artificial pause (see

#FEATURE('greet_pause') in Sendmail, for example)

totalwaitms6000

match 1c-server m|^S\xf5\xc6\x1a{|p/1C:Enterprise business management server/

match4d-server m|^\0\0\0H\0\0\0\x02.[^\0]*\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0$|sp/4th Dimension database server/

match acapm|^\* ACAP IMPLEMENTATION"CommuniGateProACAP(\d[?.\w]+)"IMPLEMENTATION"CommuniGateProACAP(\d[?.\w]+)" |p/CommuniGate Pro ACAP server/ v/$1/ i/for mail client preference sharing/

match acmpm|^ACMP Server Version ([\w._-]+)\r\n| p/Aagon ACMP Inventory/ v/$1/

matchactivemq m|^\0\0\0.\x01ActiveMQ\0\0\0|s p/Apache ActiveMQ/

第一行Probe关键字表示定义一个探测包,其类型为TCP,名字为NULL,探测字符串为空(q||)。以#开头为注释行,为读者提供参考信息。随后定义默认服务等待时间totalwaims 6000,即6秒钟。后面的match行,定义服务匹配的情况,即在此条件下认为此端口是运行的具体服务类型。match行的格式如下: match []

service为服务名称,pattern为匹配的模式(正则表达式),versioninfo为该服务对应的版本信息。这里以 match 1c-server m|^S\xf5\xc6\x1a{| p/1C:Enterprise business management server/为例,当使用NULL探测包获取的返回包中包含:m|^S\xf5\xc6\x1a{|模式(该正则表达式含义:以字符S开头,紧随其后三个字符\xf5\xc6\x1a)时,并且从提取出厂商产品名字与1C:Enterprise businessmanagement server相符,那么判断该服务为1c-server

2.2. 核心类分析

服务扫描过程中,主要构建了5个类,分别描述不同层次的数据类型。下面我们将以宏观到微观的思路,依次查看每个类的结构与用法。

结构与用法
ServiceGroup 从整理的角度管理服务扫描过程
ServiceNFO 具体的负责管理每一种服务扫描过程
AllProbes 负责管理所有用于服务扫描的探测包
ServiceProbe 描述每一个进行服务探测的探测包细节(对应nmap-service-probes中描述的探测包)
ServiceProbeMatch 描述探测包的匹配类型(每一个ServiceProbe可能包含多种匹配类型)

2.2.1. ServiceGroup

ServiceGroup用于管理一组目标机进行服务扫描的信息。这个类非常重要,负责统一管理其他具体的信息:如单个服务扫描信息(ServiceNFO)、全部探测包信息(AllProbes)、服务探测包信息(ServiceProbe)等等。

该类主要包含以下具体内容:

  1. 扫描完成的服务列表services_finished,记录目前已经扫描完毕的服务。

  2. 正在扫描的服务列表services_in_progress。多个服务可能在同时并发地被探测,所以此处将当前正在扫描的服务全部记录在该列表中。

  3. 剩余服务列表services_remaining,当前还没有开始探测的服务被放置在该列表中。在服务扫描初始化时,所有的服务都被放置在列表中。

  4. 最大的并发探测包ideal_parallelism,用于确定同时发送服务探测包的并发数量,此值取决于用户配置的时序参数和具体网卡的支持能力等因素。若配置时序为-T4,那么会将ideal_parallelism设置40。

  5. 扫描进度测量器ScanProgressMeter,用于记录服务扫描的进度情况,以便能够实时地反馈给用户。在控制台界面按下普通按键(如按下空格键,不包括 “vVdDp?” 字符,这几个字符有特殊含义),Nmap会打印出当前的扫描进度。

  6. 超时主机的数量,记录当前扫描超时的主机数量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// This holds theservice information for a group of Targets being service scanned.

class ServiceGroup {

public:

ServiceGroup(vector<Target *>&Targets, AllProbes *AP);

~ServiceGroup();

list<ServiceNFO *> services_finished;// Services finished (discovered or not)

list<ServiceNFO *>services_in_progress; // Services currently being probed

list<ServiceNFO *> services_remaining;// Probes not started yet

unsigned int ideal_parallelism; // Max (anddesired) number of probes out at once.

ScanProgressMeter *SPM;

int num_hosts_timedout; // # of hosts timedout during (or before) scan

};

2.2.2. ServiceNFO

ServiceNFO负责管理特定的服务的探测细节。上述的ServiceGroup中就是管理ServiceNFO对象组成的列表。

ServiceNFO类包含以下信息:

  1. 服务指纹的管理(提供添加与获取等操作)

  2. 服务扫描对应的主机(Target *target)

  3. 服务探测匹配的信息(是否匹配、是否softmatch、ssl配置、产品、版本、CPE等信息)

  4. 管理探测包(服务扫描过程可能需要发送多个探测包,在此对当前探测包、下一个探测包
    进行管理)

  5. 管理回复包(提供添加与获取等操作)。

  6. 服务扫描所需的全部探测包 AllProbes *AP;

2.2.3. AllProbes

AllProbes负责管理全部的服务探测包(Probes)。该类的对象从nmap-service-probes数据库文件中解析出探测包及匹配方式等信息,将之管理起来。在后续服务扫描时,在此对象中来按需取出探测包发送即可。

AllProbes负责管理全部的服务探测包(Probes)。该类的对象从nmap-service-probes数据库文件中解析出探测包及匹配方式等信息,将之管理起来。在后续服务扫描时,在此对象中来按需取出探测包发送即可。

  1. 探测包管理(探测包向量std::vectorprobes、NULL探测包等)

  2. 编制回退数组(compileFallbacks),当回复包无法匹配当前字符串时,允许回退到上一次匹配字符串。

  3. 管理排除端口列表。在nmap-service-probes中指定需排除的服务扫描,默认排除TCP的9100-9107端口,此类打印机服务会返回大量的无用信息。

  4. 服务初始化接口与释放接口。

2.2.4. ServiceProbe

ServiceProbe负责管理单个的服务探测包的详细信息。服务探测包具体的信息来自nmap-service-probes数据库文件,当AllProbes类在初始化时会读取该文件,并依据其每个探测信息创建ServiceProbe对象,放置在AllProbes内部的向量std::vectorprobes中。

该类主要包含以下内容:

  1. 探测包名字,比如探测包名字叫NULL或GenericLines等等。

  2. 探测包字符串及字符串长度。非NULL探测包都包含探测需要字符串,所以此处对该信息进行管理。例如,对于探测包:Probe TCP GenericLinesq|\r\n\r\n|,其探测字符串为\r\n\r\n。

  3. 允许的端口及SSL端口。除NULL外,探测包通常只会针对特定的端口扫描才有效,所以此处即管理该探测包允许的扫描的端口。

  4. 探测包的协议类型probeprotocol,只能是TCP或UDP。

  5. 可被探测的服务类型detectedServices。与允许端口类似,探测包可能只能用于某些特定的服务的探测,所以此处统一管理能被探测的服务类型。

  6. 服务探测包匹配管理。该类中使用向量std::vector matches来管理此服务探测包可能会匹配的情况,匹配情况对应到nmap-service-probes中的match与softmatch行。

  7. 探测回退数组(fallback array)的管理,此对应到AllProbes中compileFallbacks()函数,此处管理具体的服务探测包进行回退的数组。数组结构:ServiceProbe*fallbacks[MAXFALLBACKS+1];

  8. 测试是否匹配,此接口函数用于测试某个回复包是否与预期结果匹配。

  9. 其他接口函数,管理其他普通信息。

2.2.5. ServiceProbeMatch

ServiceProbeMatch用于管理特定的服务探测包的匹配信息(match)。nmap-service-probes文件中每一个match和softmatch行都对应到该类的对象。

该类信息比较丰富,以下仅简要描述:

  1. 探测包匹配详细信息(版本、产品、CPE等等)

  2. 探测匹配情况(匹配类型、匹配字符串、正则表达式等等)

  3. 测试是否匹配接口函数。若匹配成功,返回详细的服务与版本信息。

2.3. 代码流程

在nmap.cc文件的nmap_main()函数中,如果配置了服务扫描,那么调用service_scan()函数(位于service_scan.cc文件中)。服务扫描的内容主要在service_scan()函数中完成。

service_scan()函数比较简洁,只有120多行代码。因为服务扫描涉及到具体详细的操作都封装到类或其他的静态非成员函数中了,而并发处理网络事件部分调用nsock库来处理。

NMAP 服务与版本扫描详细过程:

流程解析:

  1. 首先在nmap_main()中将扫描目标机传入service_scan()函数中,以便根据目标机端口状态来筛选需要扫描的服务。

  2. 然后,在AllProbes:: service_scan_init()读取nmap-service-probes文件,解析出被排除的端口、扫描过程需要的探测包、探测包匹配等详细信息。将信息存放在AllProbes对象内。

  3. 随后,根据Targets和AllProbes创建服务组对象(ServiceGroup),从Targets中解析出开放的端口与处于open|filtered状态的端口,创建对应的ServiceNFO对象,该服务等待被扫描。并创建扫描进度测量器,以便后续打印出扫描进度;确定最佳的扫描并发度ideal_parallelism。

  4. 然后,确定排除端口。默认情况下,排除nmap-service-probes中指定的端口Exclude T:9100-9107;而如果用户命令行指定–all-ports,那么不排除Exclude指定的端口。

  5. 为每个目标机设置超时时钟,获取当前时间。

  6. 然后开始进入关键环节,创建nsock pool,即nsock处理并发探测包的事件池。在创建nsock pool后,服务扫描才能使用nsock建立连接并注册事件。

  7. 根据用户需求,设置服务扫描的trace信息。

  8. 若配置了openssl时,将其速度设置为最大。因为对于服务扫描,仅关心端口的服务类型,不必在安全性花费过多时间。

  9. 然后开始启动少量的服务探测包(launchSomeServiceProbes)。根据前述步骤得出的服务探测包,创建nsock niod(io描述符,类似于文件描述符,管理输入输出),完成地址等信息配置,然后建立TCP连接或UDP连接,在建立连接后向nsock pool注册事件。此后,该连接的事件将交给nsock loop来统一处理。

  10. 创建nsock主循环(nsock_loop),在此循环中来接收网络事件(例如接收到回复包),调用相应的处理函数对事件响应(函数servicescan_read_handler()、servicescan_write_handler()、servicescan_connect_handler())。在处理函数中,扫描完成了某些服务后,会再调用launchSomeServiceProbes()函数加载剩余的服务进来扫描,以此整个服务扫描过程就被有序地连接起来了。

  11. 当nsock循环退出,检查是否有错,并删除nsock pool对象。

  12. 打印出调试信息,处理最终扫描结果。

源码注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
/* Execute a service fingerprinting scan against all open ports of the
Targets specified. */
///针对指定目标机的开放的端口进行服务指纹扫描,
///此处会用到Nmap的nsock库(并发的Socket Event处理库)
int service_scan(vector<Target *> &Targets) {
// int service_scan(Target *targets[], int num_targets)
AllProbes *AP;
ServiceGroup *SG;
nsock_pool nsp;
struct timeval now;
int timeout;
enum nsock_loopstatus looprc;
struct timeval starttv;

if (Targets.size() == 0)
return 1;

AP = AllProbes::service_scan_init();///获取AllProbes对象,AllProbes仅维护一个Static对象
///在service_scan_init()中将读取nmap-service-probes文件,解析出需要的探测包,并存放在
///AllProbes中std::vector<ServiceProbe *> probes向量中。


// Now I convert the targets into a new ServiceGroup
///使用Targets向量与AllProbes创建服务组ServiceGroup,从Targets中提取open端口及
///open|filtered端口,放入services_remaining等待进行服务扫描。
///在创建服务组时,确定出服务扫描的最佳并发度ideal_parallelism
SG = new ServiceGroup(Targets, AP);

if (o.override_excludeports) {
///覆盖被排除端口,当命令行中指定--all-ports时会走到此分支。
///被排除的端口是指在nmap-service-probes文件用Exclude指令定义的端口。
if (o.debugging || o.verbose) log_write(LOG_PLAIN, "Overriding exclude ports option! Some undesirable ports may be version scanned!\n");
} else {
///从ServiceGroup中移除被排除的端口,Nmap默认会排出掉9100-9107与打印机相关的服务,
///因为此类服务只是简单返回Nmap发送过去的探测包,会产生大量的垃圾的流量。
///默认情况下在nmap-service-probes文件头部定义:Exclude T:9100-9107
remove_excluded_ports(AP, SG);
}
///为所有需要进行服务扫描的主机设置超时值
startTimeOutClocks(SG);

if (SG->services_remaining.size() == 0) {
delete SG;
return 1;
}

gettimeofday(&starttv, NULL);
if (o.verbose) {
char targetstr[128];
bool plural = (Targets.size() != 1);
if (!plural) {
(*(Targets.begin()))->NameIP(targetstr, sizeof(targetstr));
} else Snprintf(targetstr, sizeof(targetstr), "%u hosts", (unsigned) Targets.size());

log_write(LOG_STDOUT, "Scanning %u %s on %s\n",
(unsigned) SG->services_remaining.size(),
(SG->services_remaining.size() == 1)? "service" : "services",
targetstr);
}

// Lets create a nsock pool for managing all the concurrent probes
// Store the servicegroup in there for availability in callbacks
///创建nsock pool,以使用nsock并发控制探测包
if ((nsp = nsp_new(SG)) == NULL) {
fatal("%s() failed to create new nsock pool.", __func__);
}

///根据用户指定的packettrace配置,设置nsock的trace级别
if (o.versionTrace()) {
nsp_settrace(nsp, NULL, NSOCK_TRACE_LEVEL, o.getStartTime());
}

#if HAVE_OPENSSL
/* We don't care about connection security in version detection. */
///配置SSL时,关注传输速度,而不关注安全性本身,以加速服务扫描过程。
nsp_ssl_init_max_speed(nsp);
#endif

///从service_remaining列表中找出满足条件的等待探测服务,对之进行配置,
///创建nsock文件描述符(niod),并通过nsock建立连接(如nsock_connect_tcp()),
///并将此探测服务移动到services_in_progress列表中。
launchSomeServiceProbes(nsp, SG);

// How long do we have before timing out?
gettimeofday(&now, NULL);
timeout = -1;

// OK! Lets start our main loop!
///nsock主循环,在此循环内处理各种探测包的事件(nsock event)
///在上述的launchSomeServiceProbes操作中,调用到nsock_connect_tcp/udp/sctp等,
///最终执行nsp_add_event函数向nsock pool添加等待处理的事件。
looprc = nsock_loop(nsp, timeout);
if (looprc == NSOCK_LOOP_ERROR) {
int err = nsp_geterrorcode(nsp);
fatal("Unexpected nsock_loop error. Error code %d (%s)", err, strerror(err));
}
///退出主循环后,删除nsock pool
nsp_delete(nsp);

if (o.verbose) {
char additional_info[128];
if (SG->num_hosts_timedout == 0)
Snprintf(additional_info, sizeof(additional_info), "%u %s on %u %s",
(unsigned) SG->services_finished.size(),
(SG->services_finished.size() == 1)? "service" : "services",
(unsigned) Targets.size(), (Targets.size() == 1)? "host" : "hosts");
else Snprintf(additional_info, sizeof(additional_info), "%u %s timed out",
SG->num_hosts_timedout,
(SG->num_hosts_timedout == 1)? "host" : "hosts");
SG->SPM->endTask(NULL, additional_info);
}

// Yeah - done with the service scan. Now I go through the results
// discovered, store the important info away, and free up everything
// else.
///对服务扫描结果的处理
processResults(SG);

delete SG;

return 0;
}

3. python重构思路

模仿nmap的识别思路,流程如下:

  1. 为了利用 nmap 指纹库方便,先将 nmap-service-probes 转化为一个 json 文件

  2. 只与端口建立 TCP 连接,不发信息,利用 TCP NULL 探针,根据目标端口返回的 Banner 信息,利用 nmap 指纹库中的正则去匹配返回版本

  3. 若第一步失败,利用TCP的其他探针,首先判断目标端口是否存在于指纹库一个探针的 ports 列表里,若存在,给端口发送消息( probestring 中的内容),根据返回回来的消息,用此探针下的各种 matches 正则去匹配,匹配成功,返回该条 match 下的 name ,若都匹配失败,就更换TCP探针,直至匹配成功或全部匹配失败

  1. 与端口建立 UDP 连接,思路与第二步类似,也是直至其中一个匹配成功或全部匹配失败,在代码实现部分只改变socket连接方式即可

  2. 应用程序是 SSL 的情况包含在了第二步和第三步当中,将 sslports 字段中的内容也添加进端口列表,看被查询端口是否存在于端口列表中,进而进行探针探查

4. python重构遇到的问题

4.1. 已解决

Q: socket 与端口建立 TCP 连接后,会一直等待端口返回的消息,若无返回消息,socket 就会一直等到端口主动断连

A: 使用 socket.settimeout() 函数,等待一段时间后主动断连

Q: socket 向80端口发送消息,端口不予回应

A: 第一步:json文件里的 ports 字段用的是80-85端口,对代码有影响;第二步:网上脚本转换成的json文件里,probestring字段的内容会有转义字符,如 \\r\\n\\r\\n ,这些信息在python里转化为 byte 类型后,也会是\\r\\n\\r\\n,这样80端口无法识别报文体,不满足 HTTP 协议,所以不反回消息,将"probestring": GET / HTTP/1.0\\r\\n\\r\\n 变为 "probestring": GET / HTTP/1.0\r\n\r\n即可,其他相关的probestring类似

Q: 有些 probestring 的内容是十六进制,形如"probestring": "\\x02\\x60",一旦将转义字符删掉,json文件会报错

A: 在 python 文件里利用 repr() 和 eval() 函数,将字符串转化后处理,再恢复原始即可

4.2. 未解决

笔者利用 nmap 的指纹库里的正则去匹配版本号,匹配效果不尽人意,3306 端口会匹配到所有返回消息(包括很多UTF-8无法编码的字符),80 端口会匹配到所有返回报文,笔者只能通过自己写的正则去匹配,但很难做到适用所有情况,只能用如下正则 \d+\.(?:\d+\.)*\d+ 去匹配 XX.XX.XX 样式的版本号,非常简陋,所以笔者认为nmap会有其他的操作去处理返回结果,匹配出完整的版本号

第二步很多端口无法用探针匹配到,比如25,135,139等,所以第二步的扫描结果是比较失败的,最后一步还是要靠调用 python 的 nmap 库才能完成指定端口的扫描

5. 实现结果

https://github.com/Magi2B0y/NMAP-of-Python-refactoring


参考文章:

NMAP原理:

https://www.codenong.com/cs106947616/

https://www.cnblogs.com/liun1994/p/6985796.html

https://blog.csdn.net/wwl012345/article/details/96427974

NMAP 指纹库解析:

https://nmap.org/book/vscan-fileformat.html

https://www.cnblogs.com/liun1994/p/6986544.html

nmap-service-probes 的JSON转化:

https://www.cnblogs.com/zpchcbd/p/15221460.html

https://x.hacking8.com/post-418.html


ENDฅฅ