Nmap源码深度解析系列 (一):深入解构 nmap_main 执行流与核心架构
系列导读
本系列将从源码层面深度拆解 Nmap 7.98 (DEV) 的底层实现逻辑。作为网络安全领域无可替代的"瑞士军刀",Nmap 代码库历经 20 余年迭代打磨,沉淀了海量网络编程与系统工程的顶尖实践。
作为系列开篇,本文聚焦 Nmap 的核心入口 ------ nmap_main 函数。我们将跳出常规使用教程的视角,深入 C++ 源码底层,逐行拆解 Nmap 从启动到退出的全生命周期指令流,剖析其模块化设计、扫描循环控制及资源管理的底层逻辑。
特别说明 :本文是《Nmap 设备类型识别》系列的前置核心内容,对
nmap_main执行流的完整解析,将为后续理解 OS 指纹识别、服务版本探测等核心模块的调用时机与交互逻辑奠定关键基础。
1. nmap_main 函数执行总览
nmap_main 是 Nmap 程序的实际执行入口(程序原生 main 函数仅负责环境封装与初始化,随后即调用该函数)。其核心职责覆盖参数解析、环境适配、扫描任务调度、结果输出及全局资源回收的全生命周期管理,是整个 Nmap 架构的"中枢神经"。
以下思维导图完整呈现了 nmap_main 的核心执行脉络,清晰覆盖从初始化到最终退出的全流程逻辑:
渲染错误: Mermaid 渲染失败: Parse error on line 29: ... LoopStart{{核心扫描循环\n(while numhosts_scan -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'
2. 源码逐行深度解析
以下解析基于 Nmap 核心源码文件 nmap.cc 中的 nmap_main 函数,为保证解析精度,我们按"功能模块"对代码进行分块拆解与解读。
2.1 变量声明与基础类型定义
该阶段核心是在栈区分配核心变量,为后续全流程逻辑执行完成基础数据准备。Nmap 延续了经典 C 语言编码风格,在函数起始位置集中声明所有核心变量。
cpp
/* 代码行号对应 main.cc/nmap.cc 核心逻辑段 */
int i;
// 核心作用:通用循环计数器
// 上下文关联:适配参数遍历、端口列表迭代、文件描述符检查等多类临时循环场景。
// 设计考量:单字母变量名是老牌 C/C++ 项目的典型风格,适用于生命周期短、作用域有限的迭代场景,兼顾代码简洁性与执行效率。
std::vector<Target *> Targets;
// 核心作用:定义扫描目标核心容器
// 上下文关联:Nmap 最核心的数据结构之一,存储当前扫描批次中所有活跃主机的指针。
// 设计考量:采用指针而非对象本身存储于 vector,既规避对象拷贝的性能损耗,也提升指针传递的效率(尽管 Target 类本身无多态设计,但指针传递仍具备轻量化优势)。
time_t now;
// 核心作用:存储程序启动时的时间戳
// 上下文关联:用于全局时间初始化、扫描耗时计算、报告时间戳生成等场景。
time_t timep;
// 核心作用:时间处理临时变量
// 上下文关联:作为 XML 报告 `start` 属性、日志时间字符串生成的中间载体。
char mytime[128];
// 核心作用:存储格式化后的时间字符串
// 上下文关联:适配日志头部、XML 报告 `startstr` 属性的输出需求。
// 设计考量:128 字节定长数组可容纳所有标准时间格式,避免动态内存分配的额外开销。
struct addrset *exclude_group;
// 核心作用:指向排除地址集合的指针
// 上下文关联:存储用户通过 `--exclude`/`--excludefile` 指定的免扫描 IP/网段。
// 设计考量:基于 `addrset` 结构体(底层为红黑树/位图实现)实现 IP 排除状态的高效查询。
#ifndef NOLUA
/* Pre-Scan and Post-Scan script results datastructure */
ScriptResults *script_scan_results = NULL;
#endif
// 核心作用:脚本扫描结果存储指针
// 上下文关联:仅在启用 Lua 支持(未定义 NOLUA 宏)时生效,存储 `--script` 触发的预扫描/后扫描结果。
// 设计考量:通过条件编译宏 `#ifndef NOLUA` 实现功能裁剪,适配嵌入式/受限环境下的轻量编译需求。
unsigned int ideal_scan_group_sz = 0;
// 核心作用:定义理想扫描分组大小
// 上下文关联:Nmap 采用分组扫描机制,该变量基于网络速率、系统负载动态计算当前批次并行扫描的主机数。
// 设计考量:动态调整分组大小是 Nmap 高性能的核心设计,可避免并发过高导致的网络拥塞或数据包丢失。
Target *currenths;
// 核心作用:指向当前处理的主机对象
// 上下文关联:遍历 `Targets` 向量时,始终指向当前被操作的主机实例。
char myname[FQDN_LEN + 1];
// 核心作用:存储本地主机名
// 上下文关联:原始套接字扫描中,用于填充 IP 包源数据、反向解析验证等场景。
// 设计考量:`FQDN_LEN` 宏定义全限定域名最大长度,`+1` 预留字符串结束符 `\0` 存储空间。
int sourceaddrwarning = 0;
// 核心作用:源地址警告标志位
// 上下文关联:当 Nmap 无法自动确定最优源 IP 而需猜测时置位,避免重复输出同类警告信息。
unsigned int targetno;
// 核心作用:目标索引计数器
// 上下文关联:通过下标访问 `Targets` 向量中的指定主机实例。
char hostname[FQDN_LEN + 1] = "";
// 核心作用:目标主机名缓冲区
// 上下文关联:适配目标 IP 域名解析、用户输入目标名称存储等场景。
struct sockaddr_storage ss;
// 核心作用:通用套接字地址结构
// 上下文关联:`sockaddr_storage` 具备足够存储空间,兼容 IPv4 (`sockaddr_in`) 与 IPv6 (`sockaddr_in6`),体现 Nmap 对双栈网络的完整支持。
size_t sslen;
// 核心作用:存储套接字地址结构的实际长度
// 上下文关联:配合 `ss` 使用,标识当前地址类型(IPv4/IPv6)对应的结构长度。
int err;
// 核心作用:系统调用/内部函数错误码存储
// 上下文关联:接收 `socket`/`bind` 等系统调用、`n_localtime` 等内部函数的返回值,用于执行状态判断。
2.2 平台检测与时间初始化
该段代码集中体现 Nmap 对跨平台兼容性的极致优化,尤其针对 Windows Subsystem for Linux (WSL) 环境的特殊适配。
cpp
#ifdef LINUX
/* Check for WSL and warn that things may not go well. */
struct utsname uts;
// 核心作用:定义系统信息存储结构体
// 上下文关联:用于获取内核版本、系统标识等核心环境信息。
if (!uname(&uts)) {
// 核心作用:调用 uname 系统调用获取系统信息
// 设计考量:uname 返回 0 表示调用成功,非 0 则跳过 WSL 检测逻辑。
if (strstr(uts.release, "Microsoft") != NULL) {
error("Warning: %s may not work correctly on Windows Subsystem for Linux.\n"
"For best performance and accuracy, use the native Windows build from %s/download.html#windows.",
NMAP_NAME, NMAP_URL);
}
// 核心作用:WSL 环境特征识别与提示
// 上下文关联:WSL1 对原始套接字支持不完善,易导致 OS 探测、SYN 扫描等核心功能受限。
// 设计考量:通过内核发行版字符串中的 "Microsoft" 特征识别 WSL 环境,向用户给出原生 Windows 版本的使用建议。
}
#endif
cpp
tzset();
// 核心作用:初始化时区转换信息
// 上下文关联:读取环境变量 `TZ`,确保 `localtime` 等时间函数能正确处理时区偏移。
now = time(NULL);
// 核心作用:获取当前Epoch时间戳
// 上下文关联:标记 Nmap 启动的基准时间点。
err = n_localtime(&now, &local_time);
// 核心作用:安全的时间戳转本地时间结构
// 上下文关联:`n_localtime` 是 Nmap 封装的跨平台时间转换函数,解决了原生 `localtime` 的线程安全与平台兼容问题。
if (err) {
fatal("n_localtime failed: %s", strerror(err));
}
// 核心作用:致命错误处理
// 设计考量:时间初始化是基础依赖,若失败则判定系统环境异常,直接终止程序并输出错误信息。
2.3 参数预检与内存预留
cpp
if (argc < 2){
printusage();
exit(-1);
}
// 核心作用:命令行参数数量校验
// 上下文关联:`argc` 最小值为 1(仅包含程序名),小于 2 表示用户未传入任何扫描参数。
// 设计考量:直接调用 `printusage()` 输出帮助信息,提升用户交互体验。
Targets.reserve(100);
// 核心作用:为目标容器预分配内存
// 上下文关联:`std::vector` 扩容时会触发内存重分配与数据拷贝,预分配可规避该损耗。
// 设计考量:`reserve(100)` 确保前 100 个目标添加时无内存重分配,是 C++ 容器性能优化的经典手段。
#ifdef WIN32
win_pre_init();
#endif
// 核心作用:Windows 平台前置初始化
// 上下文关联:完成 Winsock 库初始化(WSAStartup)、控制台编码配置等 Windows 特有环境准备。
// 设计考量:通过平台宏隔离跨平台代码,保证非 Windows 编译时无冗余逻辑。
2.4 核心参数解析与日志初始化
参数解析是 Nmap 启动流程中最复杂的阶段,需处理数百个配置选项的解析与全局配置填充。
cpp
parse_options(argc, argv);
// 核心作用:全量命令行参数解析
// 上下文关联:该函数遍历 `argv` 数组,将 `-sS`/`-p`/`-O` 等选项解析后填充至全局配置单例 `NmapOps o`。
// 设计考量:`NmapOps` 作为全局配置中心,贯穿程序全生命周期,保证配置的一致性与可访问性。
if (o.debugging)
nbase_set_log(fatal, error);
else
nbase_set_log(fatal, NULL);
// 核心作用:日志回调函数配置
// 上下文关联:`nbase` 是 Nmap 基础工具库,此处根据调试模式(`-d`)决定是否输出普通错误信息。
// 设计考量:通过函数指针解耦日志输出逻辑,便于后续日志模块的扩展与定制。
tty_init(); // Put the keyboard in raw mode
// 核心作用:终端交互模式初始化
// 上下文关联:将终端置为 Raw 模式,支持实时捕获按键操作(如 'v' 增加详细度、'space' 查看扫描进度)。
// 设计考量:实现扫描过程中的交互式干预,是 Nmap 易用性的核心设计之一。
#ifdef WIN32
// Must come after parse_options because of --unprivileged
// Must come before apply_delayed_options because it sets o.isr00t
win_init();
#endif
// 核心作用:Windows 平台深度初始化
// 上下文关联:完成 Npcap 驱动加载、权限校验等核心操作。
// 设计细节:严格的调用顺序依赖 ------ 需在参数解析后(识别 `--unprivileged` 选项)、延迟选项应用前(确定 root 权限状态)执行。
apply_delayed_options();
// 核心作用:延迟配置项生效
// 上下文关联:部分依赖网卡、权限等环境的配置项,需在基础环境初始化完成后才能安全生效。
2.5 辅助功能分支:路由与接口列表
Nmap 兼具扫描器与网络调试工具属性,以下代码处理 --route-dst/--iflist 等辅助参数的核心逻辑。
cpp
for (unsigned int i = 0; i < route_dst_hosts.size(); i++) {
// 循环遍历:route_dst_hosts 存储用户指定的路由查询目标
const char *dst;
struct sockaddr_storage ss;
struct route_nfo rnfo;
// 变量定义:`rnfo` 存储路由查询结果(出口网卡、网关、源地址等)。
dst = route_dst_hosts[i].c_str();
rc = resolve(dst, 0, &ss, &sslen, o.af());
// 核心作用:目标地址 DNS 解析
// 上下文关联:将域名转换为 IP 地址,`o.af()` 指定地址族(IPv4/IPv6)。
if (rc != 0)
fatal("Can't resolve %s: %s.", dst, gai_strerror(rc));
// 错误处理:解析失败则判定为致命错误,终止程序。
printf("%s\n", inet_ntop_ez(&ss, sslen));
// 输出:打印解析后的目标 IP 地址。
if (!route_dst(&ss, &rnfo, o.device, o.SourceSockAddr())) {
printf("Can't route %s (%s).", dst, inet_ntop_ez(&ss, sslen));
} else {
// 核心输出:打印路由详情
printf("%s %s", rnfo.ii.devname, rnfo.ii.devfullname);
printf(" srcaddr %s", inet_ntop_ez(&rnfo.srcaddr, sizeof(rnfo.srcaddr)));
if (rnfo.direct_connect)
printf(" direct");
else
printf(" nexthop %s", inet_ntop_ez(&rnfo.nexthop, sizeof(rnfo.nexthop)));
}
printf("\n");
}
route_dst_hosts.clear();
// 清理:完成路由查询后清空目标列表,释放临时内存。
cpp
if (delayed_options.iflist) {
print_iflist();
exit(0);
}
// 核心作用:处理 `--iflist` 参数
// 上下文关联:若用户仅需查看网络接口列表,打印完成后直接退出,不执行后续扫描逻辑。
2.6 扫描前置准备:FTP 弹跳与 XML 初始化
cpp
/* If he wants to bounce off of an FTP site, that site better damn well be reachable! */
if (o.bouncescan) {
// 核心作用:FTP 弹跳扫描前置校验
// 上下文关联:FTP Bounce Scan 是经典的隐蔽扫描技术,需先验证代理 FTP 服务器的可达性。
int rc = resolve(ftp.server_name, 0, &ss, &sslen, AF_INET);
// ... 省略:FTP 服务器地址解析与可达性验证逻辑 ...
}
cpp
timep = time(NULL);
err = n_ctime(mytime, sizeof(mytime), &timep);
// 核心作用:生成可读时间字符串
// 上下文关联:为 XML 报告、日志文件提供标准化时间戳。
if (!o.resuming) {
// 核心作用:XML 报告头部生成
// 上下文关联:非断点续扫模式下,需从头构建 XML 报告结构。
xml_start_document("nmaprun");
// 生成 <nmaprun> 根标签
// ... 省略部分 XML 属性填充代码 ...
xml_attribute("start", "%lu", (unsigned long) timep);
xml_attribute("version", "%s", NMAP_VERSION);
// 核心设计:记录扫描启动时间戳与 Nmap 版本,为报告解析、兼容性适配提供依据。
output_xml_scaninfo_records(&ports);
// 将扫描配置(端口范围、扫描类型等)写入 XML 报告。
} else {
xml_start_tag("nmaprun", false);
// 断点续扫模式:恢复原有 XML 标签上下文,保证报告完整性。
}
2.7 端口列表与排除组初始化
该阶段决定扫描的"目标范围"与"豁免范围",是核心扫描逻辑的前置基础。
cpp
/* Before we randomize the ports scanned, we must initialize PortList class. */
if (o.ipprotscan)
PortList::initializePortMap(IPPROTO_IP, ports.prots, ports.prot_count);
// 核心作用:初始化 IP 协议扫描列表
// 上下文关联:适配 `-sO` 扫描类型,加载 /etc/protocols 协议号映射关系。
if (o.TCPScan())
PortList::initializePortMap(IPPROTO_TCP, ports.tcp_ports, ports.tcp_count);
// 核心作用:初始化 TCP 端口扫描列表
// 上下文关联:适配 `-sS`/`-sT`/`-sA` 等 TCP 扫描类型,根据 `-p` 指定的端口范围构建扫描列表。
// 设计考量:PortList 类封装端口号与服务名的映射关系(如 80 -> http),是后续版本探测的核心依赖。
if (o.UDPScan())
PortList::initializePortMap(IPPROTO_UDP, ports.udp_ports, ports.udp_count);
// 核心作用:初始化 UDP 端口扫描列表
// 上下文关联:适配 `-sU` 扫描类型。
if (o.SCTPScan())
PortList::initializePortMap(IPPROTO_SCTP, ports.sctp_ports, ports.sctp_count);
// 核心作用:初始化 SCTP 端口扫描列表
// 上下文关联:适配 `-sY`/`-sZ` 扫描类型。
cpp
if (o.randomize_ports) {
if (ports.tcp_count) {
shortfry(ports.tcp_ports, ports.tcp_count);
// 核心作用:TCP 端口扫描顺序随机化
// 上下文关联:默认启用端口随机化,通过 `shortfry` 对端口数组原地洗牌。
// 设计考量:打乱扫描顺序可规避基于序列检测的 IDS,同时避免单服务的瞬时并发压力。
// move a few more common ports closer to the beginning to speed scan
random_port_cheat(ports.tcp_ports, ports.tcp_count);
// 核心作用:常用端口前置优化
// 设计细节:即使开启随机化,仍将 80/443/22 等高频端口移至列表前端。
// 价值:绝大多数目标仅开放常用端口,前置扫描可快速返回核心结果,大幅提升用户体验。
}
if (ports.udp_count)
shortfry(ports.udp_ports, ports.udp_count);
// 核心作用:UDP 端口随机化
if (ports.sctp_count)
shortfry(ports.sctp_ports, ports.sctp_count);
// 核心作用:SCTP 端口随机化
if (ports.prot_count)
shortfry(ports.prots, ports.prot_count);
// 核心作用:IP 协议号随机化
}
cpp
exclude_group = addrset_new();
// 核心作用:初始化排除地址集合
// 上下文关联:创建空的地址集合,用于存储用户指定的免扫描 IP/网段。
/* lets load our exclude list */
if (o.excludefd != NULL) {
load_exclude_file(exclude_group, o.excludefd);
fclose(o.excludefd);
// 核心作用:从文件加载排除列表
// 上下文关联:适配 `--excludefile` 参数,解析文件中的 CIDR、IP 范围、单 IP 等格式。
}
if (o.exclude_spec != NULL) {
load_exclude_string(exclude_group, o.exclude_spec);
// 核心作用:从命令行加载排除列表
// 上下文关联:适配 `--exclude` 参数(如 --exclude 192.168.1.1)。
}
if (o.debugging > 3)
dumpExclude(exclude_group);
// 核心作用:调试级排除列表输出
// 上下文关联:高调试级别下打印所有排除的 IP 范围,便于排查"扫描遗漏"类问题。
2.8 脚本引擎(NSE)初始化与预扫描
cpp
#ifndef NOLUA
if (o.scriptupdatedb) {
o.max_ips_to_scan = o.numhosts_scanned; // 等效设置扫描 IP 数为 0
}
if (o.servicescan)
o.scriptversion = true; // 服务探测隐含启用脚本版本探测
if (o.scriptversion || o.script || o.scriptupdatedb)
open_nse();
// 核心作用:Lua 虚拟机初始化
// 上下文关联:加载 scripts/script.db 脚本库,编译 Lua 标准库,完成 NSE 引擎初始化。
/* Run the script pre-scanning phase */
if (o.script) {
script_scan_results = get_script_scan_results_obj();
script_scan(Targets, SCRIPT_PRE_SCAN);
// 核心作用:执行 Pre-Scan 阶段脚本
// 上下文关联:Pre-Scan 脚本不针对特定主机,用于网络环境探测(如广播查询、DHCP 发现)。
// 注意事项:此时 Targets 向量为空,传入仅为兼容函数签名。
printscriptresults(script_scan_results, SCRIPT_PRE_SCAN);
// 输出:打印预扫描脚本执行结果
script_scan_results->clear();
}
#endif
2.9 核心扫描循环(The Main Loop)
一、核心架构与整体流程
代码以 do-while 循环为核心骨架,终止条件为「无更多目标」或「达到最大扫描数(max_ips_to_scan)」,整体流程如下:
无
有
继续循环
计算理想扫描分组大小
填充目标列表(凑够分组)
是否有目标分组?
退出循环
执行各类扫描(端口/OS/路由/脚本)
输出扫描结果(XML+日志)
释放当前分组资源
更新已扫描计数
二、关键模块详细解析
1. 目标分组与填充(核心预处理)
目标:按「最优分组大小」凑集待扫描主机,同时处理特殊场景(源地址伪造、主机存活状态等)。
-
步骤1:动态计算分组大小
调用
determineScanGroupSize函数,根据「已扫描主机数」和「端口配置」动态调整分组大小(平衡扫描效率与资源占用)。 -
步骤2:多场景目标处理(填充逻辑)
循环从目标队列获取主机(
nexthost),按以下场景分支处理:场景类型 触发条件 核心操作 仅主机发现/列表扫描 无端口扫描+无路由追踪+无脚本(或列表扫描模式) 直接输出主机基础信息(IP、主机名、MAC、时间),释放资源,不执行后续扫描 源地址伪造 启用 --spoof-source参数绑定配置的源地址到当前主机,隐藏真实扫描源 主机未存活 主机标记无 HOST_UP状态详细模式下输出基础信息,释放资源;非详细模式直接跳过 原始扫描(如SYN扫描) 启用 RawScan(底层数据包扫描,非全连接)1. 自动补全源地址(优先配置→本地主机名解析);2. 检查分组同构性(同网卡/源地址),不同构则放入下一组;3. 配置诱饵地址( decoys)隐藏真实扫描源最终,符合条件的主机被加入
Targets列表(待扫描分组)。
2. 扫描执行模块(核心功能实现)
当目标分组就绪后,按配置执行各类扫描(顺序:端口扫描→OS检测→路由追踪→脚本扫描):
(1)端口扫描(核心扫描类型)
支持多种端口扫描策略,分为「批量扫描」和「单目标扫描」两类:
- 批量高速扫描(
ultra_scan) :适用于大部分扫描类型,效率高,支持:- TCP类:SYN扫描(半开放)、ACK扫描(检测防火墙)、FIN/XMAS/NULL扫描(隐蔽扫描)、Maimon扫描
- UDP扫描(
UDP_SCAN) - TCP全连接扫描(
CONNECT_SCAN) - SCTP协议扫描(
SCTP_INIT_SCAN/SCTP_COOKIE_ECHO_SCAN) - IP协议扫描(
IPPROT_SCAN)
- 单目标扫描 :需逐个处理,支持:
- 空闲扫描(
idle_scan):利用僵尸机扫描,隐藏真实源 - FTP弹跳扫描(
bounce_scan):通过FTP服务器代理扫描,绕过防火墙 - 服务版本扫描(
service_scan):检测开放端口上的服务名称及版本(如nginx 1.21.0)
- 空闲扫描(
(2)辅助扫描功能
- OS检测(
osscan):创建OSScan引擎,通过指纹识别目标主机操作系统(如Windows 10、Ubuntu 20.04) - 路由追踪(
traceroute):追踪数据包从扫描端到目标主机的路径(跳数、中间节点IP) - 脚本扫描(
script_scan):启用Lua脚本扩展(如漏洞检测、服务指纹细化),需编译时开启NOLUA宏
3. 结果输出模块(多格式适配)
输出目标:同时支持「机器可读(XML)」和「人类可读(日志)」格式,确保兼容性。
-
输出触发条件:
- 超时主机:强制输出(标记
timedout="true") - 非超时主机:默认输出全部;若启用
--open参数,仅输出有开放端口的主机
- 超时主机:强制输出(标记
-
输出内容(按顺序):
输出项 说明 主机基础信息 IP地址、主机名、扫描起止时间戳 端口扫描结果 开放/关闭/过滤状态、端口号、协议(TCP/UDP)、服务名称 硬件信息 MAC地址(局域网内有效) OS检测结果 操作系统名称、版本、可信度 服务版本信息 服务名称、版本号、额外信息(如 Apache httpd 2.4.49 (Unix))脚本扫描结果 Lua脚本执行结果(如漏洞检测报告、配置信息泄露) 路由追踪结果 跳数、每个节点的IP及延迟 扫描时间统计 总耗时、各阶段耗时 -
日志输出:
LOG_PLAIN:人类可读文本(如"Skipping host 192.168.1.1 due to host timeout")LOG_MACHINE:机器可解析格式(如"Host: 192.168.1.1 () Status: Timeout")
4. 资源管理与安全保障
- 内存泄漏防护 :所有主机对象(
currenths)在使用后通过delete释放,分组处理完后清空Targets列表 - 日志刷新 :每次输出后调用
log_flush_all(),确保日志实时写入(避免缓存丢失) - 异常处理 :
- 原始扫描时无法获取源地址/网卡:直接终止程序(
fatal)并提示解决方案(-S指定IP/-e指定网卡) - 主机超时:标记超时状态,跳过后续扫描,仅输出基础信息
- 原始扫描时无法获取源地址/网卡:直接终止程序(
三、核心配置参数与功能映射
代码中o为配置对象(扫描参数集合),关键参数与功能对应:
| 参数/标记 | 功能描述 |
|---|---|
noportscan |
禁用端口扫描(仅执行主机发现) |
traceroute |
启用路由追踪功能 |
script/scriptversion |
启用Lua脚本扫描/脚本版本检测 |
osscan |
启用OS指纹识别 |
synscan/ackscan等 |
启用对应类型的端口扫描 |
spoofsource |
启用源地址伪造(隐藏真实扫描IP) |
openOnly() |
仅输出有开放端口的主机(--open参数) |
verbose |
详细模式(输出更多信息,如未存活主机的基础信息) |
max_ips_to_scan |
最大扫描主机数(限制扫描范围) |
四、核心特点总结
- 高效分组扫描:动态调整分组大小,平衡扫描速度与系统资源占用;原始扫描时保证分组同构性(避免网卡/源地址冲突)。
- 功能全面:覆盖主机发现、端口扫描、OS检测、路由追踪、脚本扩展等网络扫描核心场景。
- 灵活适配:支持源地址伪造、诱饵扫描、FTP弹跳等隐蔽扫描方式,适配不同网络环境。
- 安全可靠:严格的内存释放逻辑(无泄漏)、日志实时刷新、异常场景容错(如超时主机处理)。
- 多格式输出:XML(机器解析)+ 双类型日志(人类/机器可读),适配自动化分析与人工查看。
以下按 代码执行流程+功能模块 分段解析,每段聚焦核心逻辑、关键函数、参数含义及设计目的,覆盖代码全量细节:
2.9.1 核心循环框架与分组大小计算
c
// ========== 核心扫描循环 ==========
// 循环逻辑:凑够一组目标→执行扫描→输出结果→释放资源,直至无目标或达到最大扫描数
do {
// 动态计算当前最优的扫描分组大小(根据已扫描数、端口配置)
ideal_scan_group_sz = determineScanGroupSize(o.numhosts_scanned, &ports);
// 填充目标列表:直至达到理想分组大小
while (Targets.size() < ideal_scan_group_sz) {
o.current_scantype = HOST_DISCOVERY; // 标记当前阶段为「主机发现」
// 从目标队列获取下一个待扫描主机
currenths = nexthost(&hstate, exclude_group, &ports, o.pingtype);
if (!currenths) // 无更多目标:退出填充循环
break;
// 统计存活主机数(仅主机存活且非列表扫描时)
if (currenths->flags & HOST_UP && !o.listscan)
o.numhosts_up++;
核心说明:
- 循环本质:外层
do-while是核心闭环,内层while负责「凑齐一组目标」,避免单目标扫描的低效(分组扫描平衡效率与资源)。 - 关键函数/参数:
determineScanGroupSize:动态调整分组大小(例如已扫描数少则分组小,避免初期资源过载;端口多则分组小,避免扫描超时)。nexthost:从目标队列提取下一个主机,入参包含「扫描状态(hstate)、排除组(exclude_group)、端口配置(ports)、探测类型(pingtype)」。HOST_UP:主机存活标记(位运算判断,高效);o.listscan:列表扫描模式(仅罗列主机,不扫描)。
- 核心目的:初始化扫描分组,获取目标主机并统计存活数,为后续扫描做准备。
2.9.2 目标填充场景1------仅主机发现/列表扫描(直接输出释放)
c
// 场景1:仅主机发现(无端口扫描/路由追踪/脚本)或列表扫描 → 直接输出主机信息
if ((o.noportscan && !o.traceroute
#ifndef NOLUA
&& !o.script
#endif
) || o.listscan) {
/* 无需扫描端口,直接输出主机信息 */
// 输出条件:主机存活 或 详细模式且非仅显示开放端口
if (currenths->flags & HOST_UP || (o.verbose && !o.openOnly())) {
xml_start_tag("host"); // 打开XML <host>标签
write_host_header(currenths); // 输出主机基础信息(IP、主机名)
printmacinfo(currenths); // 输出MAC地址信息
printtimes(currenths); // 输出扫描时间信息
xml_end_tag(); // 关闭</host>标签
xml_newline(); // 换行
log_flush_all(); // 刷新所有日志输出
}
delete currenths; // 释放主机对象内存(避免泄漏)
o.numhosts_scanned++; // 已扫描主机数+1
// 未达到最大扫描数:继续获取下一个目标
if (!o.max_ips_to_scan || o.max_ips_to_scan > o.numhosts_scanned + Targets.size())
continue;
else
break; // 达到最大扫描数:退出循环
}
核心说明:
- 触发条件(二选一):
- 仅主机发现:禁用端口扫描(
o.noportscan)、路由追踪(!o.traceroute)、脚本(!o.script,需启用Lua); - 列表扫描(
o.listscan):仅罗列主机,不执行任何扫描。
- 仅主机发现:禁用端口扫描(
- 输出逻辑:
- 必须满足「主机存活」或「详细模式(
o.verbose)且不限制仅显示开放端口(!o.openOnly())」; - 输出格式:XML结构化标签(便于机器解析)+ 日志实时刷新(
log_flush_all()避免缓存丢失)。
- 必须满足「主机存活」或「详细模式(
- 资源与计数:直接释放主机对象(
delete currenths),更新已扫描数,判断是否达到最大扫描数(o.max_ips_to_scan)。
2.9.3 目标填充场景2-3------源地址伪造与未存活主机处理
c
// 场景2:源地址伪造(--spoof-source)→ 配置主机源地址
if (o.spoofsource) {
o.SourceSockAddr(&ss, &sslen); // 获取配置的源地址
currenths->setSourceSockAddr(&ss, sslen); // 绑定到当前主机
}
/* 注:原逻辑曾检查weird_responses,现移除(部分异常IP仍可扫描) */
// 场景3:主机未存活 → 输出基础信息后释放
if (!(currenths->flags & HOST_UP)) {
// 输出条件:详细模式 且(非仅显示开放端口 或 主机有开放端口)
if (o.verbose && (!o.openOnly() || currenths->ports.hasOpenPorts())) {
xml_start_tag("host");
write_host_header(currenths);
xml_end_tag();
xml_newline();
}
delete currenths; // 释放内存
o.numhosts_scanned++; // 已扫描数+1
// 未达最大扫描数:继续
if (!o.max_ips_to_scan || o.max_ips_to_scan > o.numhosts_scanned + Targets.size())
continue;
else
break;
}
核心说明:
- 场景2(源地址伪造):
- 用途:隐藏真实扫描IP(规避防火墙拦截或溯源);
- 关键操作:通过
o.SourceSockAddr获取配置的伪造源地址,绑定到当前主机(setSourceSockAddr)。
- 场景3(未存活主机):
- 输出条件:仅详细模式下,且「不限制仅显示开放端口」或「主机虽未存活但有开放端口」(极端场景);
- 简化输出:仅输出主机基础信息(IP/主机名),不涉及端口/OS等细节;
- 同样执行内存释放和计数更新,避免泄漏。
- 注释要点:移除
weird_responses检查,允许扫描异常IP(提升兼容性)。
2.9.4 目标填充场景4------原始扫描(RawScan)的特殊配置
c
// 场景4:原始扫描(RawScan,如SYN扫描)→ 配置源地址/网卡
if (o.RawScan()) {
// 主机未配置源地址:自动补全
if (currenths->SourceSockAddr(NULL, NULL) != 0) {
// 尝试从配置获取源地址
if (o.SourceSockAddr(&ss, &sslen) == 0) {
currenths->setSourceSockAddr(&ss, sslen);
}
else {
// 从本地主机名解析源地址,失败则终止程序
if (gethostname(myname, FQDN_LEN) ||
resolve(myname, 0, &ss, &sslen, o.af()) != 0)
fatal("Cannot get hostname! Try using -S <my_IP_address> or -e <interface to scan through>\n");
// 配置源地址并绑定到主机
o.setSourceSockAddr(&ss, sslen);
currenths->setSourceSockAddr(&ss, sslen);
// 输出源地址猜测警告(仅一次)
if (!sourceaddrwarning) {
error("WARNING: We could not determine for sure which interface to use, so we are guessing %s . If this is wrong, use -S <my_IP_address>.",
inet_socktop(&ss));
sourceaddrwarning = 1;
}
}
}
// 主机无网卡名称:终止程序(原始扫描需指定网卡)
if (!currenths->deviceName())
fatal("Do not have appropriate device name for target");
/* 分组内主机需同构(如同一网卡/源地址):不同构则放入下一组 */
if (Targets.size() && target_needs_new_hostgroup(&Targets[0], Targets.size(), currenths)) {
returnhost(&hstate); // 将当前主机放回待处理队列
o.numhosts_up--; // 存活主机数-1
break; // 退出填充循环,处理下一组
}
// 配置诱饵(decoy)源地址:用于隐藏真实扫描源
o.decoys[o.decoyturn] = currenths->source();
}
Targets.push_back(currenths); // 将当前主机加入待扫描列表
}
核心说明:
- 原始扫描定义:底层数据包扫描(如SYN半开放扫描、FIN扫描),不依赖TCP全连接,需手动配置源地址/网卡。
- 关键配置流程:
- 源地址补全:优先使用用户配置→其次解析本地主机名→失败则终止(
fatal),并提示用-S指定IP或-e指定网卡; - 网卡检查:原始扫描必须绑定网卡(
deviceName()非空),否则终止; - 分组同构性:同一分组主机需相同网卡/源地址(
target_needs_new_hostgroup判断),不同构则放回队列(returnhost),避免扫描冲突; - 诱饵地址:将当前主机源地址加入
decoys数组,用于混淆目标主机的流量溯源。
- 源地址补全:优先使用用户配置→其次解析本地主机名→失败则终止(
- 最终操作:符合条件的主机加入
Targets列表(待扫描分组)。
2.9.5 扫描执行------端口扫描(批量+单目标)
c
if (Targets.size() == 0)
break; /* 无更多目标:退出核心循环 */
// 设置当前扫描的主机数(用于状态打印)
o.numhosts_scanning = Targets.size();
// 重置诱饵源地址(避免nexthost修改导致失效)
if (o.RawScan())
o.decoys[o.decoyturn] = Targets[0]->source();
/* 目标分组已就绪,开始执行端口扫描 */
if (!o.noportscan) { // 未禁用端口扫描
// Ultra_scan:批量高速扫描(支持大部分扫描类型)
if (o.synscan)
ultra_scan(Targets, &ports, SYN_SCAN); // SYN扫描(半开放)
if (o.ackscan)
ultra_scan(Targets, &ports, ACK_SCAN); // ACK扫描(检测防火墙)
if (o.windowscan)
ultra_scan(Targets, &ports, WINDOW_SCAN); // 窗口扫描
if (o.finscan)
ultra_scan(Targets, &ports, FIN_SCAN); // FIN扫描
if (o.xmasscan)
ultra_scan(Targets, &ports, XMAS_SCAN); // XMAS扫描
if (o.nullscan)
ultra_scan(Targets, &ports, NULL_SCAN); // NULL扫描
if (o.maimonscan)
ultra_scan(Targets, &ports, MAIMON_SCAN); // Maimon扫描
if (o.udpscan)
ultra_scan(Targets, &ports, UDP_SCAN); // UDP扫描
if (o.connectscan)
ultra_scan(Targets, &ports, CONNECT_SCAN); // TCP全连接扫描
if (o.sctpinitscan)
ultra_scan(Targets, &ports, SCTP_INIT_SCAN);// SCTP INIT扫描
if (o.sctpcookieechoscan)
ultra_scan(Targets, &ports, SCTP_COOKIE_ECHO_SCAN); // SCTP Cookie Echo扫描
if (o.ipprotscan)
ultra_scan(Targets, &ports, IPPROT_SCAN); // IP协议扫描
/* 特殊扫描:仅支持单目标处理,需循环遍历 */
if (o.idlescan) { // 空闲扫描(僵尸机扫描)
for (targetno = 0; targetno < Targets.size(); targetno++) {
o.current_scantype = IDLE_SCAN;
keyWasPressed(); // 检查是否需打印扫描状态
idle_scan(Targets[targetno], ports.tcp_ports,
ports.tcp_count, o.idleProxy, &ports);
}
}
if (o.bouncescan) { // FTP弹跳扫描
for (targetno = 0; targetno < Targets.size(); targetno++) {
o.current_scantype = BOUNCE_SCAN;
keyWasPressed(); // 检查是否需打印扫描状态
if (ftp.sd <= 0)
ftp_anon_connect(&ftp); // 匿名连接FTP服务器
if (ftp.sd > 0)
// 执行FTP弹跳扫描
bounce_scan(Targets[targetno], ports.tcp_ports, ports.tcp_count, &ftp);
}
}
if (o.servicescan) { // 服务版本扫描
o.current_scantype = SERVICE_SCAN;
service_scan(Targets); // 检测端口上的服务版本
}
}
核心说明:
- 前置检查:若
Targets为空,直接退出核心循环;原始扫描需重置诱饵地址(避免被nexthost篡改)。 - 端口扫描分类:
- 批量高速扫描(
ultra_scan):支持12种扫描类型,效率高(批量处理分组目标),涵盖TCP/UDP/SCTP/IP协议; - 单目标扫描(循环遍历):
- 空闲扫描(
idle_scan):依赖僵尸机,需逐个目标绑定代理(o.idleProxy); - FTP弹跳扫描(
bounce_scan):先匿名连接FTP服务器(ftp_anon_connect),通过服务器代理扫描; - 服务版本扫描(
service_scan):检测开放端口的服务名称+版本(如Apache 2.4.49)。
- 空闲扫描(
- 批量高速扫描(
- 状态打印:
keyWasPressed()检查用户是否按下按键(如Ctrl+C),需实时打印扫描状态。
2.9.6 扫描执行------辅助扫描功能(OS/路由/脚本)
c
if (o.osscan) { // 启用OS检测
OSScan os_engine; // 创建OS检测引擎对象
os_engine.os_scan(Targets); // 执行OS指纹识别
}
if (o.traceroute) // 启用路由追踪
traceroute(Targets); // 执行路由追踪
#ifndef NOLUA
if (o.script || o.scriptversion) { // 启用脚本扫描/版本检测
script_scan(Targets, SCRIPT_SCAN); // 执行脚本扫描
}
#endif
核心说明:
- OS检测:创建
OSScan引擎实例,通过数据包指纹(如TTL、TCP窗口大小)识别目标OS(如Windows 10、Ubuntu 20.04)。 - 路由追踪:
traceroute(Targets)追踪数据包从扫描端到目标主机的路径(跳数、中间节点IP、延迟)。 - 脚本扫描:仅启用Lua(
#ifndef NOLUA)时生效,支持漏洞检测、服务配置提取等扩展功能(script_scan)。
2.9.7 扫描结果输出(超时+正常主机)
c
// ========== 扫描结果输出 ==========
// 遍历当前分组的所有主机,输出扫描结果
for (targetno = 0; targetno < Targets.size(); targetno++) {
currenths = Targets[targetno];
/* 输出单个主机的扫描结果 */
if (currenths->timedOut(NULL)) { // 主机扫描超时
xml_open_start_tag("host"); // 打开<host>标签(带属性)
xml_attribute("starttime", "%lu", (unsigned long)currenths->StartTime()); // 扫描开始时间戳
xml_attribute("endtime", "%lu", (unsigned long)currenths->EndTime()); // 扫描结束时间戳
xml_attribute("timedout", "true"); // 标记超时
xml_close_start_tag();
write_host_header(currenths); // 输出主机基础信息
printtimes(currenths); // 输出扫描时间
xml_end_tag(); /* 关闭</host>标签 */
xml_newline();
// 输出超时日志(人类可读+机器可读)
log_write(LOG_PLAIN, "Skipping host %s due to host timeout\n",
currenths->NameIP(hostname, sizeof(hostname)));
log_write(LOG_MACHINE, "Host: %s (%s)\tStatus: Timeout\n",
currenths->targetipstr(), currenths->HostName());
}
else {
/* --open参数:仅输出有开放端口的主机 */
if (o.openOnly() && !currenths->ports.hasOpenPorts())
continue; // 无开放端口:跳过
// 输出完整的主机扫描结果
xml_open_start_tag("host");
xml_attribute("starttime", "%lu", (unsigned long)currenths->StartTime());
xml_attribute("endtime", "%lu", (unsigned long)currenths->EndTime());
xml_close_start_tag();
write_host_header(currenths); // 主机基础信息
printportoutput(currenths, ¤ths->ports); // 端口扫描结果
printmacinfo(currenths); // MAC地址
printosscanoutput(currenths); // OS检测结果
printserviceinfooutput(currenths); // 服务版本信息
#ifndef NOLUA
printhostscriptresults(currenths); // 脚本扫描结果
#endif
if (o.traceroute) printtraceroute(currenths); // 路由追踪结果
printtimes(currenths); // 扫描时间
log_write(LOG_PLAIN | LOG_MACHINE, "\n");
xml_end_tag(); /* 关闭</host> */
xml_newline();
}
}
核心说明:
- 输出分类:
- 超时主机:标记
timedout="true",仅输出基础信息(IP/主机名、时间戳),日志分「人类可读(LOG_PLAIN)」和「机器可读(LOG_MACHINE)」; - 正常主机:若启用
--open(o.openOnly()),无开放端口则跳过;否则输出完整信息(基础信息→端口→MAC→OS→服务→脚本→路由→时间)。
- 超时主机:标记
- 输出格式:XML结构化标签(便于自动化解析)+ 双类型日志(适配人工查看和程序处理)。
2.9.8 资源释放与循环终止
c
log_flush_all(); // 刷新所有日志输出
o.numhosts_scanned += Targets.size(); // 更新已扫描主机总数
/* 释放当前分组的所有主机对象,避免内存泄漏 */
while (!Targets.empty()) {
currenths = Targets.back(); // 获取最后一个主机对象
delete currenths; // 释放内存
Targets.pop_back(); // 从向量中移除
}
o.numhosts_scanning = 0; // 重置当前扫描主机数
} while (!o.max_ips_to_scan || o.max_ips_to_scan > o.numhosts_scanned); // 循环终止条件:无最大限制 或 未达上限
核心说明:
- 资源释放:通过
while循环清空Targets列表,逐个delete主机对象(避免内存泄漏),并从列表中移除(pop_back())。 - 计数更新:
o.numhosts_scanned:累加当前分组大小,更新总扫描数;o.numhosts_scanning:重置为0(当前分组扫描完成)。
- 循环终止条件:
- 无最大扫描数限制(
!o.max_ips_to_scan); - 已扫描数未达到最大限制(
o.max_ips_to_scan > o.numhosts_scanned);
满足任一条件则继续循环,否则退出。
- 无最大扫描数限制(
2.10 收尾阶段
cpp
addrset_free(exclude_group);
// 释放:排除地址集合内存
if (o.inputfd != NULL)
fclose(o.inputfd);
// 关闭:扫描目标输入文件
printfinaloutput();
// 核心作用:打印扫描最终统计信息
// 上下文关联:如 "Nmap done: 1 IP address (1 host up) scanned in 0.52 seconds" 类输出。
free_scan_lists(&ports);
// 释放:端口扫描列表内存
if (o.release_memory) {
nmap_free_mem();
}
// 核心作用:全局内存统一释放
return 0;
// 程序正常退出
3. 总结与下篇预告
通过对 nmap_main 函数超 600 行核心代码的逐行拆解,我们清晰梳理出 Nmap 的宏观架构逻辑:
- 标准化准备流程:通过精细化的参数解析、跨平台适配、资源预分配,为扫描全流程构建稳定的基础环境;
- 动态分组调度 :基于
determineScanGroupSize实现扫描批次大小的动态调整,在扫描性能与系统资源消耗间实现最优平衡; - 模块化引擎设计 :端口扫描(
ultra_scan)、服务探测(service_scan)、OS 探测(os_scan)等核心能力被设计为独立模块,由主循环统一调度,兼具扩展性与维护性; - 轻量化资源管理:"扫描一批、输出一批、释放一批"的设计,使 Nmap 可高效支撑互联网级别的超大规模扫描任务。
nmap_main 是 Nmap 的"骨架",而各核心扫描引擎的实现则是其"血肉"。下篇预告 :我们将聚焦 Nmap 最具代表性的核心模块 ------ OS 指纹识别(os_scan),深度拆解其指纹库设计、协议栈特征提取与匹配逻辑,揭示 Nmap 精准识别操作系统的底层原理。