欢迎光临
我们一直在努力

【nmap】Nmap 扫描引擎核心函数深度解析:从批次管理到主机调度

Nmap 扫描引擎核心函数深度解析:从批次管理到主机调度

前言

在网络扫描工具的设计中,如何高效地管理待扫描主机、合理分配扫描批次、平衡扫描效率与用户体验,是工程师们需要精心设计的核心问题。本文将深入剖析 Nmap 扫描引擎中的三个关键函数——determineScanGroupSize、nexthost 和 refresh_hostbatch,揭示它们如何协同工作,构建出一个高效、灵活、可控的扫描调度系统。


第一部分:智能批次大小计算器——determineScanGroupSize

函数概述

determineScanGroupSize 是网络扫描工具中动态计算单次扫描主机组大小的核心函数。它的核心作用是根据扫描类型(UDP/SCTP/TCP)、已扫描主机数、端口数量、扫描时序等级等参数,自适应调整每批扫描的主机数量(分组大小)。这个函数的设计体现了扫描工具在效率、用户体验和配置约束之间的精妙平衡。

设计理念

在实际的网络扫描场景中,不同的扫描协议有着截然不同的特性:

  • UDP 扫描:UDP 是无连接协议,扫描单个端口的耗时相对较短,因此可以批量扫描更多主机以提升整体效率
  • SCTP 扫描:与 UDP 类似,SCTP 的批量处理效率更高,无需动态调整
  • TCP 扫描:TCP 是面向连接的协议,需要建立三次握手,扫描耗时与端口数量密切相关。端口越多,单主机扫描耗时越长,因此需要动态调整分组大小

完整代码实现

/**
* @brief 动态计算单次扫描的主机组大小(每批扫描的主机数量)
* @details 根据扫描类型(UDP/SCTP/TCP)、已扫描主机数、端口数量、时序等级等参数自适应调整分组大小,
* 优先保证TCP扫描的首批快速结果,同时遵守用户配置的上下限和最大扫描总数限制
*
* @param hosts_scanned_so_far 已扫描的主机数量(用于首批/次批分组大小的特殊调整)
* @param ports 扫描端口列表结构体(包含TCP端口数量等关键信息)
* @return int 最终确定的每批扫描主机数量(分组大小)
* @note 1. 不同协议的默认分组大小不同(UDP/SCTP=128,TCP动态计算);
* 2. TCP扫描会优先让首批分组返回快速结果,提升用户体验;
* 3. 最终分组大小会被限制在用户配置的min/max之间,且不超过剩余待扫描主机数
*/

int determineScanGroupSize(int hosts_scanned_so_far,
const struct scan_lists *ports) {
// 初始化默认分组大小为16(基础值,不同协议会覆盖)
int groupsize = 16;

// 根据扫描协议类型调整分组大小
if (o.UDPScan()) {
// UDP扫描:分组大小固定为128(UDP无连接,可批量扫描更多主机)
groupsize = 128;
} else if (o.SCTPScan()) {
// SCTP扫描:分组大小固定为128(类似UDP,批量扫描效率更高)
groupsize = 128;
} else if (o.TCPScan()) {
/* TCP扫描:动态计算分组大小
核心逻辑:1024 / TCP端口数量(至少为1),且不小于64
解释:端口越多,单主机扫描耗时越长,分组大小越小(避免单批耗时过久);
端口越少,分组大小越大(提升批量扫描效率),最低保障64 */

groupsize = MAX(1024 / (ports->tcp_count ? ports->tcp_count : 1), 64);

/* 特殊优化:TCP端口数>1000且时序等级≤4时,优先返回首批扫描结果 */
if (ports->tcp_count > 1000 && o.timing_level <= 4) {
// 快速分组大小:时序等级4时为8,其他≤4时为4
int quickgroupsz = 4;
if (o.timing_level == 4)
quickgroupsz = 8;

// 首次扫描(已扫描数=0):强制使用快速分组大小,优先返回首批结果
if (hosts_scanned_so_far == 0)
groupsize = quickgroupsz;
// 次批扫描(已扫描数=快速分组大小)且当前分组大小>2倍快速值:
// 调整分组大小,对齐网络边界(如/24网段),抵消首批快速扫描的影响
else if (hosts_scanned_so_far == quickgroupsz &&
groupsize > quickgroupsz * 2)
groupsize -= quickgroupsz;
}
}

/* 将分组大小限制在用户配置的[minHostGroupSz, maxHostGroupSz]范围内
box函数作用:若groupsize < min则返回min,>max则返回max,否则返回原值 */

groupsize = box(o.minHostGroupSz(), o.maxHostGroupSz(), groupsize);

/* 最终校验:若剩余待扫描主机数 < 当前分组大小,调整为剩余数量
避免扫描超过用户指定的最大IP数(尤其是随机生成IP的场景) */

if (o.max_ips_to_scan && (o.max_ips_to_scan hosts_scanned_so_far) < (unsigned int)groupsize)
groupsize = o.max_ips_to_scan hosts_scanned_so_far;

// 返回最终确定的分组大小
return groupsize;
}

核心逻辑深度解析

1. 不同协议的分组大小设计策略
扫描协议分组大小规则设计原因
UDP 固定128 UDP是无连接协议,扫描单端口耗时短,批量扫描更多主机可提升效率
SCTP 固定128 类似UDP,SCTP扫描的批量处理效率更高,无需动态调整
TCP 动态计算:MAX(1024/端口数, 64) TCP是面向连接协议,端口越多→单主机扫描耗时越长→分组大小越小(避免单批卡住);端口越少→分组越大(提升效率),最低保障64

TCP 动态计算公式的深层含义:

公式 MAX(1024 / 端口数, 64) 的设计非常巧妙:

  • 分子 1024:这是一个经验值,代表"理想情况下单批扫描的总端口数"。无论端口数量多少,都希望单批扫描的总端口数保持在 1024 左右,这样可以保证扫描的稳定性
  • 除以端口数:端口越多,单主机扫描耗时越长,因此需要减少每批的主机数量,避免单批扫描时间过长
  • 下限 64:即使端口数量很少(比如只有 1 个端口),每批至少扫描 64 台主机,保证批量扫描的效率优势
2. TCP 扫描的"快速首批结果"优化(用户体验的核心亮点)

当 TCP 端口数 > 1000(扫描耗时久)且时序等级 ≤ 4(非极速扫描)时,函数会执行一个特殊的优化逻辑:

首次扫描(已扫描数 = 0):

  • 强制使用小分组(时序等级 4 时为 8,其他 ≤ 4 时为 4)
  • 目的:优先返回少量主机的扫描结果,避免用户长时间等待第一批结果
  • 这是一种"快速反馈"的设计理念,让用户尽早看到扫描进展

次批扫描(已扫描数 = 快速分组大小):

  • 如果当前分组大小 > 2 倍快速值,则减去快速分组大小
  • 目的:抵消首批小分组的影响,让后续分组对齐网络边界
  • 例如:正常分组大小是 64,首批用了 4,次批就调整为 60,这样 4 + 60 = 64,正好对齐网络边界(如 /24 网段的 256 台主机)

为什么需要对齐网络边界?

网络扫描中,很多目标是以网段形式指定的(如 192.168.1.0/24)。如果分组大小不能整除网段大小,会导致跨网段的扫描,可能影响扫描的连续性和效率。通过这种调整,可以保证扫描批次更好地对齐网络拓扑结构。

3. 关键辅助函数与变量说明
名称作用
box(min, max, val) 范围限制函数:保证val在[min, max]之间,是扫描工具中常用的参数约束函数
o.timing_level 扫描时序等级(0-5):等级越高扫描越快,等级4时快速分组大小从4提升到8
o.max_ips_to_scan 用户指定的最大扫描IP数:避免超量扫描(尤其是随机生成IP的场景)
ports->tcp_count 待扫描的TCP端口数量:核心影响TCP扫描的分组大小
4. 实际计算示例

让我们通过一个具体的场景来理解这个函数的工作流程:

场景设定:

  • TCP 扫描
  • 端口数 = 2000
  • 时序等级 = 3
  • minHostGroupSz = 4
  • maxHostGroupSz = 1024
  • 已扫描数 = 0
  • max_ips_to_scan = 1000

首次扫描计算过程:

  • 初始 TCP 分组大小计算:

    groupsize = MAX(1024 / 2000, 64) = MAX(0.512, 64) = 64

    因为端口数很多,所以初始分组大小取下限 64

  • 触发快速首批结果优化:

    • 端口数 2000 > 1000 ✓
    • 时序等级 3 ≤ 4 ✓
    • 因此 quickgroupsz = 4
  • 首次扫描特殊处理:

    • 已扫描数 = 0
    • 强制 groupsize = 4(首批快速结果)
  • 用户配置范围限制:

    groupsize = box(4, 1024, 4) = 4

    4 在 [4, 1024] 范围内,保持不变

  • 剩余扫描数校验:

    • 剩余待扫描数 = 1000 – 0 = 1000
    • 1000 > 4,无需调整
    • 最终返回:4
  • 次批扫描计算过程(已扫描数 = 4):

  • 初始 TCP 分组大小计算:

    groupsize = MAX(1024 / 2000, 64) = 64

  • 次批扫描特殊处理:

    • 已扫描数 = 4(等于 quickgroupsz)
    • 当前分组大小 64 > 4 × 2 = 8 ✓
    • 调整:groupsize = 64 – 4 = 60
  • 用户配置范围限制:

    groupsize = box(4, 1024, 60) = 60

  • 剩余扫描数校验:

    • 剩余待扫描数 = 1000 – 4 = 996
    • 996 > 60,无需调整
    • 最终返回:60
  • 通过这个示例,我们可以看到:

    • 首批只扫描 4 台主机,快速返回结果
    • 次批扫描 60 台主机,4 + 60 = 64,正好对齐正常分组大小
    • 后续批次将保持 60 台主机的分组大小
    5. 边界条件处理

    函数在设计时充分考虑了各种边界情况:

    剩余主机数不足:

    • 如果用户指定了 max_ips_to_scan,且剩余待扫描数 < 当前分组大小
    • 直接将分组大小调整为剩余数量
    • 避免扫描超过用户指定的最大 IP 数

    用户配置约束:

    • 所有场景下的分组大小都会被限制在 minHostGroupSz 和 maxHostGroupSz 之间
    • 保证符合用户的配置预期

    端口数为 0 的处理:

    • 使用三元运算符 ports->tcp_count ? ports->tcp_count : 1 避免除以 0
    • 即使没有 TCP 端口,也能正常计算分组大小

    总结

    determineScanGroupSize 函数体现了扫描工具设计的几个核心原则:

  • 核心功能:根据扫描协议、端口数、已扫描数等参数动态计算每批扫描的主机分组大小,兼顾效率与用户体验
  • 设计亮点:
    • TCP 扫描优先返回首批快速结果,提升用户体验
    • UDP/SCTP 固定大分组提升效率
    • 最终分组大小受用户配置和剩余扫描数约束
  • 关键逻辑:
    • TCP 分组大小与端口数成反比(端口越多分组越小)
    • 首批扫描强制小分组,次批调整对齐网络边界
  • 适用场景:网络扫描工具中动态调度扫描批次,平衡扫描效率、用户等待时间和配置约束
  • 简单来说,这个函数是扫描工具的"批次大小智能调节器"——不同协议用不同策略,TCP 扫描优先让用户快速看到首批结果,同时不超配置限制。


    第二部分:主机取数器——nexthost

    函数概述

    nexthost 是网络扫描工具中获取下一个待扫描主机的核心函数。它的核心逻辑是:先检查当前批次的主机是否已全部取完,若取完则刷新批次缓存(加载新一批待扫描主机),再从批次缓存中按顺序取出下一个主机;若批次缓存无可用主机,则返回 NULL。

    这个函数是连接"主机批次管理(HostGroupState)"和"实际扫描逻辑"的关键桥梁,是扫描主循环的核心入口。

    设计理念

    在批量扫描系统中,如何高效地管理待扫描主机是一个重要问题。nexthost 函数采用了"批次缓存"的设计模式:

  • 批量加载:一次性加载一批主机到内存缓存中
  • 顺序取用:从缓存中按顺序取出主机进行扫描
  • 自动刷新:当前批次取完时,自动加载下一批
  • 优雅结束:无可用主机时返回 NULL,通知扫描结束
  • 这种设计的优势:

    • 减少频繁的主机生成和筛选操作
    • 提高内存访问的局部性
    • 简化扫描主循环的逻辑
    • 支持批次的随机化和预处理

    完整代码实现

    /**
    * @brief 获取下一个待扫描的主机(Target实例)
    * @details 从HostGroupState的批次缓存(hostbatch)中按顺序取主机,若当前批次已取完则刷新缓存,
    * 刷新后仍无可用主机则返回NULL,是扫描循环中获取目标主机的核心入口
    *
    * @param hs 主机组状态管理器(包含批次缓存、随机化、预扫描等状态)
    * @param exclude_group 排除的地址组(扫描时需跳过的IP/网段)
    * @param ports 扫描端口列表(用于计算批次大小、端口随机化等)
    * @param pingtype ping扫描类型(如ICMP echo、TCP SYN ping等,用于预扫描筛选存活主机)
    * @return Target* 成功返回下一个待扫描的主机实例;无可用主机返回NULL
    * @note 1. 依赖refresh_hostbatch刷新批次缓存,该函数会加载新一批主机到hs->hostbatch;
    * 2. hs->next_batch_no是当前批次的取数指针,每取一个主机自增1;
    * 3. 两次检查hs->next_batch_no是为了处理refresh_hostbatch刷新失败的情况
    */

    Target *nexthost(HostGroupState *hs, struct addrset *exclude_group,
    const struct scan_lists *ports, int pingtype) {
    // 第一步:检查当前批次是否已取完(取数指针 ≥ 当前批次大小)
    if (hs->next_batch_no >= hs->current_batch_sz) {
    // 批次已空,刷新批次缓存:加载新一批待扫描主机到hs->hostbatch
    // 刷新逻辑包含:计算批次大小、生成/筛选主机、随机化、排除黑名单等
    refresh_hostbatch(hs, exclude_group, ports, pingtype);
    }

    // 第二步:再次检查批次是否为空(处理refresh_hostbatch刷新失败/无新主机的情况)
    if (hs->next_batch_no >= hs->current_batch_sz) {
    // 无可用主机,返回NULL(扫描结束)
    return NULL;
    }

    // 第三步:返回当前批次的下一个主机,并将取数指针自增(指向下一个主机)
    return hs->hostbatch[hs->next_batch_no++];
    }

    核心逻辑深度解析

    1. 核心变量与执行流程

    关键变量说明:

    变量名作用
    hs->next_batch_no 批次取数指针(初始0):标记当前批次中"下一个要取的主机下标",每取一个主机自增1;
    hs->current_batch_sz 当前批次的有效主机数量:refresh_hostbatch 刷新后会更新该值;
    hs->hostbatch 批次缓存数组:存储当前批的待扫描主机指针(Target*);

    执行流程示例:

    假设 current_batch_sz = 4(批次有 4 台主机):

  • 首次调用 nexthost:

    • next_batch_no = 0 < 4 → 不触发刷新
    • 直接返回 hostbatch[0]
    • next_batch_no 变为 1
  • 第二次调用:

    • next_batch_no = 1 < 4 → 不触发刷新
    • 返回 hostbatch[1]
    • next_batch_no 变为 2
  • 第三次调用:

    • next_batch_no = 2 < 4 → 不触发刷新
    • 返回 hostbatch[2]
    • next_batch_no 变为 3
  • 第四次调用:

    • next_batch_no = 3 < 4 → 不触发刷新
    • 返回 hostbatch[3]
    • next_batch_no 变为 4
  • 第五次调用:

    • next_batch_no = 4 ≥ 4 → 触发 refresh_hostbatch 刷新批次
    • 假设新批次 current_batch_sz = 6
    • 刷新后 next_batch_no = 4 < 6 → 返回 hostbatch[4]
    • next_batch_no 变为 5
  • 若 refresh_hostbatch 刷新后 current_batch_sz = 4:

    • 第二次检查 next_batch_no = 4 ≥ 4 → 返回 NULL(扫描结束)
  • 2. 两次检查 next_batch_no 的必要性

    函数中两次检查 hs->next_batch_no >= hs->current_batch_sz 的设计非常巧妙:

    第一次检查:

    • 目的:触发批次刷新
    • 场景:当前批次已取完,需要加载新一批主机
    • 操作:调用 refresh_hostbatch 刷新批次缓存

    第二次检查:

    • 目的:处理"刷新失败"的边界情况
    • 场景:
      • 已无新主机可扫描(所有目标都已扫描完成)
      • 所有主机都被排除(在黑名单中)
      • 网段表达式已耗尽
    • 操作:返回 NULL,通知扫描结束

    为什么需要两次检查?

    因为 refresh_hostbatch 可能会刷新失败(比如没有新主机),此时 current_batch_sz 可能仍然是 0 或小于 next_batch_no。如果没有第二次检查,就会返回 hostbatch 数组的越界元素,导致未定义行为。

    3. refresh_hostbatch 的隐含逻辑(核心依赖)

    nexthost 的核心能力完全依赖 refresh_hostbatch 函数,该函数会完成以下关键操作:

  • 计算批次大小:调用 determineScanGroupSize 计算新批次的大小
  • 生成/解析目标主机:从随机 IP 或用户指定的网段中生成主机
  • 过滤黑名单:排除 exclude_group 中的黑名单主机
  • 随机化:对主机列表执行随机化(如 shortfry)
  • 填充批次缓存:将筛选后的主机填充到 hs->hostbatch
  • 更新批次状态:更新 hs->current_batch_sz
  • 重置取数指针:重置 hs->next_batch_no 为 0(新批次从第一个主机开始取)
  • 预扫描:执行 ARP ping 或 IP 层 ping,检测主机存活
  • DNS 解析:批量反向 DNS 解析(如果未禁用)
  • 4. 典型使用场景(扫描主循环)

    // 扫描主循环示例
    HostGroupState *hs = new HostGroupState(100, 1, false, 0, argc, argv);
    struct addrset *exclude = load_exclude_list("exclude.txt");
    struct scan_lists *ports = load_scan_ports("80,443,22");
    int pingtype = PING_TCP_SYN;

    Target *target;
    // 循环获取下一个主机,直到返回NULL
    while ((target = nexthost(hs, exclude, ports, pingtype)) != NULL) {
    // 扫描该主机的指定端口
    scan_host(target, ports, pingtype);
    // 释放主机资源(若由nexthost分配)
    delete target;
    }

    // 清理资源
    free_exclude_list(exclude);
    free_scan_ports(ports);
    delete hs;

    这个示例展示了 nexthost 在扫描主循环中的典型用法:

    • 初始化 HostGroupState、排除列表、端口列表
    • 使用 while 循环持续获取主机
    • 对每个主机执行扫描
    • 扫描完成后释放资源
    • 最后清理所有资源

    总结

    nexthost 函数体现了扫描工具设计的几个核心原则:

  • 核心功能:从批次缓存中按顺序获取下一个待扫描主机,批次空时自动刷新,无主机时返回 NULL,是扫描循环的核心入口
  • 关键设计:
    • 两次检查批次指针避免越界
    • 依赖 refresh_hostbatch 完成主机加载/筛选/随机化
    • 简洁的接口设计,隐藏复杂的批次管理逻辑
  • 核心依赖:
    • HostGroupState 的批次管理
    • determineScanGroupSize 的批次大小计算
    • refresh_hostbatch 的批次刷新
  • 适用场景:网络扫描工具的主循环中,持续获取待扫描主机,直到所有主机扫描完成
  • 简单来说,这个函数是扫描工具的"主机取数器"——一批一批地拿主机,拿完一批自动补新一批,没新的就结束,是连接主机管理和实际扫描的核心纽带。


    第三部分:主机批次刷新与目标生成——refresh_hostbatch 及相关函数

    整体架构概述

    这组函数构成了网络扫描工具中"主机批次刷新+目标主机生成"的完整链路。核心作用是:当 nexthost 检测到当前批次主机已取完时,refresh_hostbatch 会触发新批次主机的加载——从目标表达式/随机 IP 中生成主机、过滤黑名单、分组校验、随机化、预扫描(ARP/ICMP ping)、DNS 解析,最终填充到 hostbatch 批次缓存中。

    而 next_target/get_next_host/TargetGroup::get_next_host 则是生成单个目标主机的底层支撑,负责从网段表达式中逐个生成 IP、排除黑名单、处理续扫逻辑。

    函数调用链路

    #mermaid-svg-gBIck8gk6OTuyNJQ{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gBIck8gk6OTuyNJQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gBIck8gk6OTuyNJQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gBIck8gk6OTuyNJQ .error-icon{fill:#552222;}#mermaid-svg-gBIck8gk6OTuyNJQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gBIck8gk6OTuyNJQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gBIck8gk6OTuyNJQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gBIck8gk6OTuyNJQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gBIck8gk6OTuyNJQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gBIck8gk6OTuyNJQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gBIck8gk6OTuyNJQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gBIck8gk6OTuyNJQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gBIck8gk6OTuyNJQ .marker.cross{stroke:#333333;}#mermaid-svg-gBIck8gk6OTuyNJQ svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gBIck8gk6OTuyNJQ p{margin:0;}#mermaid-svg-gBIck8gk6OTuyNJQ .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-gBIck8gk6OTuyNJQ .cluster-label text{fill:#333;}#mermaid-svg-gBIck8gk6OTuyNJQ .cluster-label span{color:#333;}#mermaid-svg-gBIck8gk6OTuyNJQ .cluster-label span p{background-color:transparent;}#mermaid-svg-gBIck8gk6OTuyNJQ .label text,#mermaid-svg-gBIck8gk6OTuyNJQ span{fill:#333;color:#333;}#mermaid-svg-gBIck8gk6OTuyNJQ .node rect,#mermaid-svg-gBIck8gk6OTuyNJQ .node circle,#mermaid-svg-gBIck8gk6OTuyNJQ .node ellipse,#mermaid-svg-gBIck8gk6OTuyNJQ .node polygon,#mermaid-svg-gBIck8gk6OTuyNJQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gBIck8gk6OTuyNJQ .rough-node .label text,#mermaid-svg-gBIck8gk6OTuyNJQ .node .label text,#mermaid-svg-gBIck8gk6OTuyNJQ .image-shape .label,#mermaid-svg-gBIck8gk6OTuyNJQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-gBIck8gk6OTuyNJQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gBIck8gk6OTuyNJQ .rough-node .label,#mermaid-svg-gBIck8gk6OTuyNJQ .node .label,#mermaid-svg-gBIck8gk6OTuyNJQ .image-shape .label,#mermaid-svg-gBIck8gk6OTuyNJQ .icon-shape .label{text-align:center;}#mermaid-svg-gBIck8gk6OTuyNJQ .node.clickable{cursor:pointer;}#mermaid-svg-gBIck8gk6OTuyNJQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gBIck8gk6OTuyNJQ .arrowheadPath{fill:#333333;}#mermaid-svg-gBIck8gk6OTuyNJQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gBIck8gk6OTuyNJQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gBIck8gk6OTuyNJQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gBIck8gk6OTuyNJQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gBIck8gk6OTuyNJQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gBIck8gk6OTuyNJQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gBIck8gk6OTuyNJQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gBIck8gk6OTuyNJQ .cluster text{fill:#333;}#mermaid-svg-gBIck8gk6OTuyNJQ .cluster span{color:#333;}#mermaid-svg-gBIck8gk6OTuyNJQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gBIck8gk6OTuyNJQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gBIck8gk6OTuyNJQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-gBIck8gk6OTuyNJQ .icon-shape,#mermaid-svg-gBIck8gk6OTuyNJQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gBIck8gk6OTuyNJQ .icon-shape p,#mermaid-svg-gBIck8gk6OTuyNJQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gBIck8gk6OTuyNJQ .icon-shape rect,#mermaid-svg-gBIck8gk6OTuyNJQ .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gBIck8gk6OTuyNJQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gBIck8gk6OTuyNJQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gBIck8gk6OTuyNJQ :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    nexthost触发refresh_hostbatch

    重置批次状态+处理延迟主机

    调用next_target加载新主机

    优先取undeferred列表的延迟主机

    无延迟主机则调用get_next_host生成新IP

    get_next_host调用TargetGroup::get_next_host从网段生成IP

    过滤黑名单,返回有效IP

    处理续扫逻辑,跳过已扫描IP

    setup_target初始化Target实例

    加入排除列表去重

    校验主机是否需要单独分组(需要则延迟)

    将有效主机加入hostbatch批次缓存

    填满批次后随机化主机顺序

    ARP ping/批量IP ping检测存活

    批量DNS解析

    hostbatch填充完成,返回nexthost

    核心函数详解

    1. refresh_hostbatch – 批次刷新的核心逻辑

    /**
    * @brief 刷新主机批次缓存(核心逻辑)
    * @details 清空旧批次→加载新一批主机→分组校验→随机化→ARP/MassPing预扫描→DNS解析,
    * 最终将有效主机填充到hostbatch数组,是nexthost的核心依赖
    */

    static void refresh_hostbatch(HostGroupState *hs, struct addrset *exclude_group,
    const struct scan_lists *ports, int pingtype) {
    int i;
    bool arpping_done = false; // 标记是否已完成ARP ping
    struct timeval now; // 当前时间(用于超时判断)

    // 1. 重置批次状态:清空当前批次大小和取数指针
    hs->current_batch_sz = hs->next_batch_no = 0;
    // 2. 将延迟处理的主机移到undeferred列表(优先处理)
    hs->undefer();

    // 3. 循环加载主机,直到填满批次缓存(max_batch_sz)
    while (hs->current_batch_sz < hs->max_batch_sz) {
    Target *t = NULL;

    // 3.1 获取下一个目标主机(优先处理undeferred,再生成新主机)
    t = next_target(hs, exclude_group, ports, pingtype);
    if (t == NULL) break; // 无新主机,终止加载

    // 3.2 校验:该主机是否需要单独分组(如跨网段/跨网卡)
    if (target_needs_new_hostgroup(hs->hostbatch, hs->current_batch_sz, t)) {
    // 需要单独分组:放入延迟列表,继续加载下一个
    if (hs->defer(t)) continue;
    // 延迟失败,终止加载
    else break;
    }

    // 3.3 设置诱饵IP(反检测:轮询设置 decoys 诱饵地址)
    o.decoys[o.decoyturn] = t->source();
    // 3.4 将主机加入批次缓存,批次大小+1
    hs->hostbatch[hs->current_batch_sz++] = t;
    }

    // 无主机加载成功,直接返回
    if (hs->current_batch_sz == 0) return;

    // 4. 随机化批次内的主机顺序(若开启随机化)
    if (hs->randomize) {
    hoststructfry(hs->hostbatch, hs->current_batch_sz); // 类似shortfry的主机洗牌函数
    }

    // 5. ARP ping/二层MAC地址预处理(仅以太网网卡且非强制IP发送时)
    gettimeofday(&now, NULL);
    Target *current_target = hs->hostbatch[0];
    if (current_target->ifType() == devt_ethernet && o.sendpref != PACKET_SEND_IP_STRONG) {
    // 打开网卡的以太网缓存(获取数据链路层信息)
    netutil_eth_t *eth = eth_open_cached(current_target->deviceName());
    if (DLT_EN10MB == netutil_eth_datalink(eth)) { // 确认是以太网数据链路层
    // 5.1 对直连主机执行ARP/ND ping(快速检测存活)
    if (current_target->directlyConnected() && o.implicitARPPing) {
    arpping(hs->hostbatch, hs->current_batch_sz);
    arpping_done = true;
    }
    // 5.2 若需要二层发送(ETH),为每个主机设置下一跳MAC地址
    if ((o.sendpref & PACKET_SEND_ETH)) {
    for (i=0; i < hs->current_batch_sz; i++) {
    current_target = hs->hostbatch[i];
    // 跳过已标记为下线/超时的主机
    if (!(current_target->flags & HOST_DOWN) && !current_target->timedOut(&now)) {
    // 获取MAC地址失败:标记为主机下线,原因是无路由
    if (!setTargetNextHopMAC(current_target)) {
    error("%s: Failed to determine dst MAC address for target %s",
    __func__, current_target->NameIP());
    current_target->flags = HOST_DOWN;
    current_target->reason.reason_id = ER_NOROUTE;
    }
    }
    }
    }
    }
    }

    // 6. IP层批量Ping(预扫描:检测主机存活)
    // 场景1:无需ping/已ARP ping,或回环网卡 → 直接标记为上线
    if ((pingtype == PINGTYPE_NONE && !arpping_done) || current_target->ifType() == devt_loopback) {
    for (i=0; i < hs->current_batch_sz; i++) {
    current_target = hs->hostbatch[i];
    if (!(current_target->flags & HOST_DOWN || current_target->timedOut(&now))) {
    initialize_timeout_info(&current_target->to); // 初始化超时信息
    current_target->flags |= HOST_UP; // 标记为主机上线
    // 设置上线原因:用户指定/本地回环
    current_target->reason.reason_id = (pingtype == PINGTYPE_NONE && !arpping_done) ? ER_USER : ER_LOCALHOST;
    }
    }
    }
    // 场景2:未执行ARP ping → 执行IP层批量ping(如ICMP/TCP SYN ping)
    else if (!arpping_done) {
    massping(hs->hostbatch, hs->current_batch_sz, ports);
    }

    // 7. 批量反向DNS解析(若未禁用解析)
    if (!o.noresolve) {
    nmap_mass_rdns(hs->hostbatch, hs->current_batch_sz);
    }
    }

    核心步骤解析:

    步骤 1-2:批次状态重置与延迟主机处理

    • 清空当前批次,准备加载新主机
    • 将上一批次延迟的主机移到 undeferred 列表,优先处理

    步骤 3:循环加载主机

    • 调用 next_target 获取下一个主机
    • 检查是否需要单独分组(如跨网段、跨网卡)
    • 如果需要单独分组,放入延迟列表,继续加载下一个
    • 否则,将主机加入批次缓存

    步骤 4:随机化

    • 如果开启了随机化,对批次内的主机进行洗牌
    • 目的:避免固定的扫描顺序,增加反检测能力

    步骤 5:ARP ping 与 MAC 地址预处理

    • 对以太网直连主机执行 ARP ping,快速检测存活
    • 为需要二层发送的主机设置下一跳 MAC 地址
    • 如果 MAC 地址获取失败,标记主机为下线

    步骤 6:IP 层批量 Ping

    • 如果无需 ping 或已执行 ARP ping,直接标记为上线
    • 否则,执行 IP 层批量 ping(ICMP/TCP SYN ping)

    步骤 7:DNS 解析

    • 如果未禁用 DNS 解析,批量反向解析主机名
    2. next_target – 获取单个目标主机

    /**
    * @brief 获取单个目标主机(优先处理延迟列表,再生成新主机)
    * @details 先处理延迟的主机→再从网段生成新IP→处理续扫逻辑→初始化Target实例→加入黑名单(去重)
    */

    static Target *next_target(HostGroupState *hs, struct addrset *exclude_group,
    const struct scan_lists *ports, int pingtype) {
    struct sockaddr_storage ss; // 存储生成的IP地址
    size_t sslen; // IP地址结构体长度
    Target *t = NULL;

    // 1. 优先处理上一批次延迟的主机(undeferred列表)
    if (!hs->undeferred.empty()) {
    t = hs->undeferred.front();
    hs->undeferred.pop_front();
    return t;
    }

    tryagain: // 循环生成新主机的标签(失败则重试)
    // 2. 从网段表达式/随机IP生成下一个IP(排除黑名单)
    if (!hs->get_next_host(&ss, &sslen, exclude_group)) {
    return NULL; // 无新主机,返回NULL
    }

    // 断言:生成的IP地址族与用户指定的一致(如IPv4/IPv6)
    assert(ss.ss_family == o.af());

    // 3. 处理续扫逻辑:跳过已扫描到resume_ip的主机
    if (o.resume_ip.ss_family != AF_UNSPEC) {
    // 找到resume_ip,后续不再跳过
    if (!sockaddr_storage_cmp(&o.resume_ip, &ss)) {
    o.resume_ip.ss_family = AF_UNSPEC;
    }
    goto tryagain; // 跳过当前IP,继续生成下一个
    }

    // 4. 初始化Target实例(填充IP、端口、网卡等信息)
    t = setup_target(hs, &ss, sslen, pingtype);
    if (t == NULL) goto tryagain; // 初始化失败,重试

    // 5. 去重:将该IP加入排除列表,避免重复扫描
    if (o.unique) {
    addrset_add_spec(exclude_group, t->targetipstr(), o.af(), 0);
    }
    return t;
    }

    核心逻辑解析:

    优先处理延迟主机:

    • 如果 undeferred 列表不为空,优先返回延迟的主机
    • 这样可以保证扫描不遗漏任何主机

    生成新主机:

    • 调用 get_next_host 从网段表达式生成下一个 IP
    • 如果生成失败(网段耗尽),返回 NULL

    续扫逻辑:

    • 如果用户指定了 resume_ip,跳过该 IP 之前的所有主机
    • 找到 resume_ip 后,清除标记,后续不再跳过

    初始化与去重:

    • 调用 setup_target 初始化 Target 实例
    • 如果开启了 unique 模式,将 IP 加入排除列表,避免重复扫描
    3. HostGroupState::get_next_host – 生成下一个 IP 地址

    /**
    * @brief 生成下一个IP地址(控制总数+从网段生成+排除黑名单)
    */

    bool HostGroupState::get_next_host(struct sockaddr_storage *ss, size_t *sslen, struct addrset *exclude_group) {
    // 1. 校验:是否超过用户指定的最大扫描IP数
    unsigned long num_queued = o.numhosts_scanned + current_batch_sz;
    if (o.max_ips_to_scan > 0 && num_queued >= o.max_ips_to_scan) {
    return false;
    }

    // 2. 循环生成IP,直到找到不在黑名单中的IP
    do {
    // 2.1 从当前网段表达式生成IP(无更多则加载下一个表达式)
    while (current_group.get_next_host(ss, sslen) != 0) {
    // 无更多表达式可加载,返回false
    if (!current_group.load_expressions(this, o.af())) {
    return false;
    }
    }
    // 2.2 检查是否在排除列表中:不在则保留该IP,终止循环
    if (!addrset_contains(exclude_group, (const struct sockaddr *) ss)) {
    current_group.reject_last_host(); // 标记该IP为已分配
    break;
    }
    } while (true);

    return true; // 生成成功,返回true
    }

    核心逻辑解析:

    总数控制:

    • 检查是否超过用户指定的最大扫描 IP 数
    • 如果超过,返回 false,停止生成

    循环生成 IP:

    • 从当前网段表达式生成 IP
    • 如果网段耗尽,加载下一个表达式
    • 如果所有表达式都耗尽,返回 false

    黑名单过滤:

    • 检查生成的 IP 是否在排除列表中
    • 如果在黑名单中,继续生成下一个 IP
    • 如果不在黑名单中,标记为已分配,返回成功
    4. TargetGroup::get_next_host – 从网段表达式中生成下一个 IP

    /**
    * @brief 从网段表达式中生成下一个IP(底层核心)
    * @details 遍历网段块(NetBlock),逐个生成IP,网段耗尽则删除并加载下一个
    */

    int TargetGroup::get_next_host(struct sockaddr_storage *ss, size_t *sslen) {
    // 遍历所有网段块(如192.168.1.0/24、8.8.8.0/24)
    while (!netblocks.empty()) {
    NetBlock *nb = netblocks.front(); // 取第一个网段块
    // 从该网段生成下一个IP(成功则返回0,填充ss/sslen)
    if (nb->next(ss, sslen)) {
    return 0;
    }
    // 该网段IP已耗尽:删除网段块,继续下一个
    netblocks.pop_front();
    delete nb;
    }
    // 所有网段都已耗尽,返回-1
    return 1;
    }

    核心逻辑解析:

    遍历网段块:

    • netblocks 是一个队列,存储所有待扫描的网段块
    • 每个网段块可以是一个 IP 地址、一个网段(如 192.168.1.0/24)或一个 IP 范围

    生成 IP:

    • 调用 nb->next() 从当前网段块生成下一个 IP
    • 如果成功,返回 0,填充 ss 和 sslen
    • 如果失败(网段耗尽),删除该网段块,继续下一个

    结束条件:

    • 所有网段块都耗尽,返回 -1

    关键细节深度解析

    1. 主机生成的核心规则

    优先级:

    • 延迟主机(undeferred)> 新生成主机
    • 这样可以保证扫描不遗漏任何主机

    过滤:

    • 生成的 IP 必须不在 exclude_group 黑名单中
    • 黑名单可以包含单个 IP、网段或 IP 范围

    总数控制:

    • 不超过 o.max_ips_to_scan 用户指定的最大扫描数
    • 避免扫描过多主机,浪费时间和资源

    续扫:

    • 跳过 o.resume_ip 之前的所有 IP
    • 从指定 IP 开始扫描,支持中断后继续扫描
    2. 预扫描(Ping)的分层逻辑
    场景预扫描方式目的
    以太网直连主机 ARP ping 二层快速检测存活,无需IP层交互
    回环网卡/无需ping 直接标记上线 本地主机无需检测,提升效率
    其他场景 massping(ICMP/TCP SYN) IP层批量检测存活,覆盖跨网段主机

    为什么需要分层预扫描?

    不同场景下,检测主机存活的最佳方式不同:

    • ARP ping:对于以太网直连主机,ARP ping 是最快的方式,因为它工作在数据链路层,无需 IP 层交互
    • 直接标记上线:对于本地回环网卡或用户明确指定无需 ping 的场景,直接标记为上线,提升效率
    • massping:对于跨网段主机,需要使用 IP 层的 ping(ICMP 或 TCP SYN),覆盖范围更广
    3. 关键边界处理

    网段耗尽:

    • TargetGroup::get_next_host 遍历完一个网段后删除该网段块
    • 自动加载下一个表达式,继续生成 IP

    MAC 地址获取失败:

    • 标记主机为 HOST_DOWN
    • 原因设置为 ER_NOROUTE(无路由)
    • 避免后续扫描尝试连接不可达的主机

    随机化:

    • 批次填充完成后调用 hoststructfry 洗牌
    • 避免固定的扫描顺序,增加反检测能力

    去重:

    • 开启 o.unique 时,扫描过的 IP 加入排除列表
    • 永不重复扫描,避免浪费资源
    4. 延迟主机机制

    为什么需要延迟主机?

    在某些情况下,主机需要单独分组:

    • 跨网段:不同网段的主机可能需要不同的扫描策略
    • 跨网卡:不同网卡的主机可能需要不同的路由
    • 特殊配置:某些主机可能有特殊的扫描需求

    延迟主机的工作流程:

  • refresh_hostbatch 检测到主机需要单独分组
  • 调用 hs->defer(t) 将主机放入延迟列表
  • 继续加载下一个主机,填满当前批次
  • 下一批次刷新时,优先处理延迟主机
  • 总结

    这组函数体现了扫描工具设计的几个核心原则:

  • 核心链路:nexthost → refresh_hostbatch → next_target → get_next_host → TargetGroup::get_next_host,从"批次刷新"到"单个 IP 生成"形成完整的主机加载流程

  • 设计亮点:

    • 优先处理延迟主机,保证扫描不遗漏
    • 分层预扫描(ARP/IP),兼顾效率和兼容性
    • 随机化 + 去重 + 续扫,平衡反检测、准确性和用户需求
  • 核心约束:

    • 严格控制扫描总数
    • 排除黑名单
    • 匹配地址族
    • 避免无效扫描
  • 适用场景:网络扫描工具的核心主机调度逻辑,实现高效、灵活、可控的目标主机加载

  • 简单来说,这组函数是扫描工具的"主机工厂"——从网段表达式中批量"生产"主机,经过过滤、预扫描、随机化等工序,最终打包成"批次"供扫描逻辑使用,兼顾效率、反检测和用户配置。


    整体架构总结

    通过深入分析这三个核心函数,我们可以看到 Nmap 扫描引擎的精妙设计:

    1. 三层架构

    ┌─────────────────────────────────────────┐
    │ nexthost – 主机取数器(用户接口层) │
    ├─────────────────────────────────────────┤
    │ refresh_hostbatch – 批次刷新(管理层) │
    ├─────────────────────────────────────────┤
    │ next_target/get_next_host – 主机生成 │
    │ (底层实现层) │
    └─────────────────────────────────────────┘

    2. 核心设计原则

    • 分层设计:每层负责不同的职责,职责清晰,易于维护
    • 批量处理:通过批次缓存减少频繁的主机生成和筛选操作
    • 智能调度:根据扫描类型、端口数量、时序等级等参数动态调整策略
    • 用户体验:优先返回首批快速结果,减少用户等待时间
    • 反检测:支持随机化、诱饵 IP 等反检测机制
    • 容错性:完善的边界条件处理,避免崩溃和未定义行为

    3. 性能优化

    • ARP ping 优先:对以太网直连主机使用 ARP ping,快速检测存活
    • 批量 DNS 解析:一次性解析批次内所有主机的 DNS,减少网络请求
    • 批次随机化:避免固定的扫描顺序,增加反检测能力
    • 延迟主机机制:优化跨网段、跨网卡主机的扫描效率

    4. 扩展性

    • 插件化设计:通过 pingtype 参数支持不同的 ping 扫描类型
    • 配置灵活:支持用户配置最小/最大分组大小、最大扫描数等参数
    • 协议支持:支持 TCP、UDP、SCTP 等多种扫描协议
    • 地址族支持:同时支持 IPv4 和 IPv6

    结语

    Nmap 的扫描引擎设计充分体现了系统工程的精髓:在效率、用户体验、反检测、配置约束等多个维度之间找到最佳平衡点。通过深入理解这些核心函数,我们不仅可以更好地使用 Nmap,还可以学习到优秀的系统设计思想和编程技巧。

    希望本文能够帮助读者深入理解网络扫描工具的核心原理,为后续的学习和实践打下坚实的基础。


    参考资料:

    • Nmap 源代码:https://github.com/nmap/nmap
    • Nmap 官方文档:https://nmap.org/book/man.html
    • 网络扫描技术原理与实践

    作者注:本文基于 Nmap 7.98 版本的源代码进行分析,不同版本可能存在细微差异。如有疑问或建议,欢迎交流讨论。

    赞(0)
    未经允许不得转载:171主机测评 » 【nmap】Nmap 扫描引擎核心函数深度解析:从批次管理到主机调度
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址