Nmap OS识别核心模块深度解析:osscan2.cc源码剖析
本文深入解析Nmap操作系统指纹识别的核心模块------osscan2.cc,从架构设计到实现细节,全面揭示Nmap如何通过TCP/IP协议栈特征精准识别目标操作系统。
目录
前言
Nmap(Network Mapper)作为全球最流行的网络扫描工具,其操作系统识别功能(OS Detection)一直是安全领域的标杆技术。通过向目标主机发送精心构造的探测包,分析TCP/IP协议栈的响应特征,Nmap能够精准识别出目标主机的操作系统类型、版本甚至设备类型。
本文将深入剖析Nmap OS识别的核心实现模块------osscan2.cc,从代码层面揭示其工作原理,帮助读者理解:
- Nmap如何设计OS扫描的整体架构
- 如何通过TCP序列号、窗口大小等特征识别OS
- 如何实现高效的批量扫描和性能优化
- 如何处理网络异常和超时重传
一、OS扫描架构总览
1.1 整体设计理念
Nmap的OS扫描采用分层架构设计,将复杂的扫描任务分解为多个职责明确的模块:
┌─────────────────────────────────────────────────────────┐
│ OSScan 类 │
│ (OS扫描总调度器) │
│ - os_scan(): 统一入口,IPv4/IPv6分类分发 │
│ - os_scan_ipv4(): IPv4扫描逻辑 │
│ - os_scan_ipv6(): IPv6扫描逻辑 │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ OsScanInfo 类 │
│ (扫描批次管理器) │
│ - 管理一批待扫描主机的列表 │
│ - 提供主机遍历、查找、清理接口 │
│ - 维护扫描进度和状态 │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ HostOsScan 类 │
│ (单主机扫描执行引擎) │
│ - 构建探测包列表 │
│ - 发送探测包并处理响应 │
│ - 生成OS指纹 │
│ - 控制发送速率和超时 │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ HostOsScanStats 结构体 │
│ (单轮扫描状态存储) │
│ - 待发送探测列表 │
│ - 活跃探测状态 │
│ - 响应特征数据 │
│ - 超时和计时参数 │
└─────────────────────────────────────────────────────────┘
1.2 核心入口:os_scan方法
os_scan是OS扫描的顶层入口,负责将批量目标按地址族分类后分发到对应的扫描逻辑。
方法签名与核心职责
cpp
int os_scan(std::vector<Target *> &Targets);
核心职责:
- 参数校验:确保目标列表非空
- 协议分类:将目标按IPv4/IPv6分组
- 任务分发:调用对应的扫描方法
- 结果汇总:统一返回扫描结果
执行流程图
否
是
AF_INET6
其他
是
否
是
否
是
否
开始: os_scan
目标列表非空?
返回 OP_FAILURE
遍历目标列表
地址族类型?
加入IPv6目标集合
加入IPv4目标集合
IPv4集合非空?
调用 os_scan_ipv4
IPv6集合非空?
调用 os_scan_ipv6
两组都成功?
返回 OP_SUCCESS
返回 OP_FAILURE
代码实现详解
cpp
// 步骤1:参数合法性校验
if (Targets.size() <= 0)
return OP_FAILURE;
// 步骤2:按地址族分类目标
std::vector<Target *> ip4_targets, ip6_targets;
for (size_t i = 0; i < Targets.size(); i++) {
if (Targets[i]->af() == AF_INET6)
ip6_targets.push_back(Targets[i]);
else
ip4_targets.push_back(Targets[i]);
}
// 步骤3:分协议执行OS扫描
int res4 = OP_SUCCESS, res6 = OP_SUCCESS;
if (ip4_targets.size() > 0)
res4 = this->os_scan_ipv4(ip4_targets);
if (ip6_targets.size() > 0)
res6 = this->os_scan_ipv6(ip6_targets);
// 步骤4:汇总扫描结果
if (res4 == OP_SUCCESS && res6 == OP_SUCCESS)
return OP_SUCCESS;
else
return OP_FAILURE;
设计亮点:
-
协议解耦:IPv4和IPv6的OS检测原理完全不同(IPv4依赖TCP/UDP指纹,IPv6依赖ICMPv6/IPv6扩展头指纹),分开处理便于维护和扩展。
-
批量处理 :接收
std::vector<Target*>批量目标,符合Nmap"分组扫描"的核心设计,提升扫描效率。 -
容错兜底:非IPv6目标默认归为IPv4,避免地址族识别异常导致的目标丢失。
二、核心类与数据结构
2.1 OsScanInfo类:扫描批次管理器
OsScanInfo类负责管理一批待扫描主机的OS扫描任务,维护未完成主机的列表,提供遍历、查找、清理等接口。
类定义与核心成员
cpp
class OsScanInfo {
public:
// 构造函数:传入待扫描目标列表
OsScanInfo(std::vector<Target *> &Targets);
// 析构函数:释放资源
~OsScanInfo();
// 扫描开始时间
float starttime;
// 未完成OS扫描的主机链表(核心容器)
std::list<HostOsScanInfo *> incompleteHosts;
// 获取未完成主机数量
int numIncompleteHosts() const;
// 按地址查找未完成主机
HostOsScanInfo *findIncompleteHost(const struct sockaddr_storage *ss);
// 获取下一个未完成主机(循环遍历)
HostOsScanInfo *nextIncompleteHost();
// 重置主机迭代器
void resetHostIterator();
// 移除已完成扫描的主机
int removeCompletedHosts();
private:
// 初始目标主机数量
unsigned int numInitialTargets;
// 循环迭代器(私有,保证安全)
std::list<HostOsScanInfo *>::iterator nextI;
};
核心功能详解
1. 循环迭代器机制
OsScanInfo实现了循环缓冲区 式的遍历逻辑,通过私有迭代器nextI实现:
cpp
HostOsScanInfo *OsScanInfo::nextIncompleteHost() {
if (incompleteHosts.empty())
return NULL;
if (nextI == incompleteHosts.end())
nextI = incompleteHosts.begin();
return *nextI++;
}
使用场景: 扫描逻辑会反复调用nextIncompleteHost()获取"下一个要扫描的主机",实现轮询式扫描。
2. 迭代器安全机制
⚠️ 重要注意事项:
cpp
// ❌ 错误做法:删除元素后不重置迭代器
incompleteHosts.erase(hostI);
nextIncompleteHost(); // 可能崩溃!迭代器已失效
// ✅ 正确做法:删除后立即重置
incompleteHosts.erase(hostI);
resetHostIterator(); // 重置到链表开头
nextIncompleteHost(); // 安全
原因: C++的std::list迭代器在元素被删除后会失效,继续使用会导致内存错误。
3. 主机查找功能
cpp
HostOsScanInfo *OsScanInfo::findIncompleteHost(
const struct sockaddr_storage *ss) {
for (std::list<HostOsScanInfo *>::iterator i = incompleteHosts.begin();
i != incompleteHosts.end(); i++) {
if (sockaddr_storage_cmp(&(*i)->target->TargetSockAddr(), ss) == 0)
return *i;
}
return NULL;
}
作用: 根据网络地址快速定位特定主机的扫描状态,用于响应包匹配。
2.2 HostOsScan类:单主机扫描执行引擎
HostOsScan是单主机OS扫描的执行引擎,负责所有具体的扫描操作:构建探测包、发送探测、处理响应、生成指纹、控制速率等。
类架构设计
HostOsScan 类
├── 公有接口(对外控制指令)
│ ├── 初始化:reInitScanSystem()
│ ├── 探测构建:buildSeqProbeList(), buildTUIProbeList()
│ ├── 状态管理:updateActiveSeqProbes(), updateActiveTUIProbes()
│ ├── 探测发送:sendNextProbe()
│ ├── 响应处理:processResp()
│ ├── 指纹生成:makeFP()
│ ├── 速率控制:hostSendOK(), hostSeqSendOK()
│ ├── 超时管理:timeProbeTimeout(), nextTimeout()
│ └── 动态调优:adjust_times()
│
└── 私有成员(内部实现细节)
├── 探测发送:sendTSeqProbe(), sendT1_7Probe(), ...
├── 响应处理:processTSeqResp(), processTUdpResp(), ...
├── 通用发送:send_tcp_probe(), send_icmp_echo_probe(), ...
├── 指纹辅助:makeTSeqFP(), get_tcpopt_string(), ...
└── 私有参数:rawsd, tcpSeqBase, icmpEchoId, ...
核心设计原则
1. 执行与状态分离
cpp
// ❌ 不好的设计:状态存在类里
class HostOsScan {
std::vector<Probe> probes; // 状态耦合在类中
void sendProbe();
};
// ✅ 好的设计:状态通过参数传递
class HostOsScan {
void sendProbe(HostOsScanStats *hss); // 状态独立存储
};
优势:
- 多轮扫描只需重置
hss,无需重建HostOsScan对象 - 同一个
HostOsScan可以处理多台主机(通过切换hss) - 状态和执行逻辑解耦,便于测试和维护
2. 接口与实现分离
cpp
// 公有接口:只暴露"要做什么"
bool HostOsScan::sendNextProbe(HostOsScanStats *hss) {
// 调用私有实现
return sendTSeqProbe(hss) || sendT1_7Probe(hss);
}
// 私有实现:隐藏"怎么做"
bool HostOsScan::sendTSeqProbe(HostOsScanStats *hss) {
// 具体的TCP序列探测包发送逻辑
}
3. 通用函数减少冗余
cpp
// 所有TCP探测的底层发送函数
int HostOsScan::send_tcp_probe(
HostOsScanStats *hss,
u8 proto, // 协议类型
u16 sport, // 源端口
u16 dport, // 目的端口
u32 seq, // 序列号
u32 ack, // 确认号
u8 flags, // TCP标志位
u16 window, // 窗口大小
u8 *options, // TCP选项
int optlen, // 选项长度
u16 ipid, // IP ID
u8 ttl, // TTL
bool df, // DF标志
const u8 *ipopt, // IP选项
int ipoptlen // IP选项长度
);
优势: TSeq、T1-T7等所有TCP探测都调用这个函数,避免重复代码。
2.3 HostOsScanStats结构体:单轮扫描状态
HostOsScanStats存储单台主机单轮扫描的所有临时状态,是HostOsScan操作的"工作台"。
核心成员
cpp
struct HostOsScanStats {
// 待发送的探测列表
std::vector<Probe *> seqProbes; // TSeq探测列表
std::vector<Probe *> TUIProbes; // TUI探测列表
// 活跃探测状态(已发送,等待响应)
std::list<Probe *> activeSeqProbes;
std::list<Probe *> activeTUIProbes;
// 响应特征数据
FingerPrint FP; // 当前轮次的指纹
std::vector<FingerPrint *> FPs; // 所有轮次的指纹数组
// 计时参数
struct timeval lastprobe; // 上次发送探测的时间
struct timeval lastrecv; // 上次收到响应的时间
unsigned long rtt; // 往返时间(微秒)
// 统计信息
int num_probes_sent; // 已发送探测数
int num_probes_received; // 已接收响应数
// 初始化函数
void initScanStats();
};
状态流转图
构建探测列表
sendNextProbe()
收到有效响应
收到无效响应
超时未收到响应
提取特征存入FP
丢弃
重传(未达最大次数)
放弃(已达最大次数)
Pending
Active
Completed_Valid
Completed_Invalid
Timeout_Failed
三、扫描执行流程详解
3.1 扫描轮次管理
Nmap的OS扫描采用多轮重试机制,每轮扫描前需要清理旧数据、重置状态。
startRound函数:轮次初始化
cpp
static void startRound(OsScanInfo *OSI, HostOsScan *HOS, int roundNum) {
std::list<HostOsScanInfo *>::iterator hostI;
HostOsScanInfo *hsi = NULL;
// 1. 重置执行层状态
HOS->reInitScanSystem();
// 2. 遍历所有未完成主机
for (hostI = OSI->incompleteHosts.begin();
hostI != OSI->incompleteHosts.end(); hostI++) {
hsi = *hostI;
// 3. 清理本轮旧指纹数据
if (hsi->FPs[roundNum]) {
delete hsi->FPs[roundNum];
hsi->FPs[roundNum] = NULL;
}
// 4. 重置单轮扫描状态
hsi->hss->initScanStats();
}
}
执行流程图:
渲染错误: Mermaid 渲染失败: Parse error on line 4: ...sts] C --> D{FPs[roundNum]存在?} D ----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'SQS'
设计目的:
- 避免旧数据干扰:清理上一轮的指纹数据,确保本轮生成全新指纹
- 内存安全:主动释放旧数据,避免内存泄漏
- 状态一致性:所有主机从相同起点开始,保证扫描结果准确
3.2 轮次间等待策略
Nmap采用渐进式等待策略,根据扫描轮次动态调整等待时间,兼顾快网络效率和慢网络兼容性。
cpp
// 轮次间的等待:第1轮不等待,后续轮次等1秒;第4轮额外多等1.5秒
if (itry > 0)
sleep(1); // 非第1轮,先等1秒
if (itry == 3)
usleep(1500000); // 第4轮额外等1.5秒
等待时间表:
| 扫描轮次 | itry值 | 执行逻辑 | 总等待时间 | 说明 |
|---|---|---|---|---|
| 第1轮 | 0 | 不等待 | 0秒 | 直接发探测包 |
| 第2轮 | 1 | sleep(1) | 1秒 | 基础等待 |
| 第3轮 | 2 | sleep(1) | 1秒 | 基础等待 |
| 第4轮 | 3 | sleep(1) + usleep(1500000) | 2.5秒 | 慢网络兼容 |
| 第5轮+ | ≥4 | sleep(1) | 1秒 | 回到基础等待 |
设计思路:
- 第1轮不等待:尽快发起探测,提升快网络下的扫描效率
- 后续轮次等1秒:避免发包太密集,减少丢包和被目标限速
- 第4轮额外多等1.5秒:专门适配慢网络/高延迟目标,避免因超时漏收响应
3.3 完整扫描流程
否
是
是
否
是
否
否
是
否
是
否
是
开始OS扫描
创建OsScanInfo对象
初始化抓包器begin_sniffer
设置电源管理SetThreadExecutionState
扫描轮次循环
startRound: 初始化轮次
构建探测列表buildSeqProbeList+buildTUIProbeList
发送探测循环
主机可发送?
等待到下次可发送时间
sendNextProbe发送探测
收到响应?
processResp处理响应
超时?
标记探测失败
adjust_times调整参数
updateActiveProbes更新状态
所有探测完成?
makeFP生成指纹
所有主机完成?
达到最大轮次?
匹配指纹库
输出OS识别结果
结束
四、探测包机制深度剖析
4.1 TSeq探测包:TCP序列号探测
TSeq(TCP Sequence)探测是Nmap OS扫描的核心探测类型,用于提取目标主机的TCP初始序列号(ISN)生成规律。
核心原理
不同操作系统的TCP/IP协议栈实现完全不同,其ISN生成算法是OS的"独有特征":
| 操作系统 | ISN生成算法 | 特征 |
|---|---|---|
| 早期Linux | 线性递增 | ISN = 基准值 + 计数器 |
| Windows | 基于时间戳的随机 | ISN = 时间戳 + 随机数 |
| 嵌入式设备 | 固定步长/简单随机 | ISN = 固定值或简单随机 |
TSeq探测包特点
- 自定义TCP头部:构造不同标志位(SYN、SYN+ACK、ACK)、不同SEQ/ACK值的TCP包
- 多探测包组合:一组探测包序列,从多个维度验证ISN生成规律
- 针对开放/关闭端口:覆盖两种场景,提取更全面的特征
- 依赖原始套接字:需要root/管理员权限
TSeq探测流程
目标主机 HostOsScanStats HostOsScan 目标主机 HostOsScanStats HostOsScan 构建TSeq探测列表 提取ISN特征 loop [每个TSeq探测] 生成TSeq指纹片段 buildSeqProbeList() sendNextProbe() sendTSeqProbe(自定义TCP包) TCP响应包 processTSeqResp() makeTSeqFP()
代码示例
cpp
// 构建TSeq探测包列表
void HostOsScan::buildSeqProbeList(HostOsScanStats *hss) {
// 为每个开放端口和关闭端口生成TSeq探测
for (int i = 0; i < 6; i++) {
Probe *p = new Probe();
p->type = PS_SEQ;
p->dport = open_port; // 开放端口
p->seq = tcpSeqBase + i * 1000; // 不同SEQ值
p->flags = TH_SYN; // SYN标志
hss->seqProbes.push_back(p);
}
}
// 发送TSeq探测包
bool HostOsScan::sendTSeqProbe(HostOsScanStats *hss, Probe *p) {
return send_tcp_probe(
hss,
IPPROTO_TCP,
tcpPortBase++, // 源端口
p->dport, // 目的端口
p->seq, // 序列号
0, // ACK
p->flags, // TCP标志位
65535, // 窗口大小
NULL, 0, // TCP选项
get_random_u16(), // IP ID
64, // TTL
true, // DF标志
NULL, 0 // IP选项
);
}
// 处理TSeq响应
bool HostOsScan::processTSeqResp(HostOsScanStats *hss,
const u8 *ip, unsigned int ip_len,
const u8 *tcp, unsigned int tcp_len) {
// 提取TCP序列号
u32 seq = ntohl(((struct TCP_Header *)tcp)->seq);
// 存储ISN特征
hss->FP->seq.responses[hss->FP->seq.num_responses++] = seq;
return true;
}
4.2 TUI探测:TCP/IP栈基础特征探测
TUI是T1~T7 + UDP + ICMP的统称,用于提取目标主机的TCP/IP栈基础特征。
TUI探测组成
| 探测类型 | 探测包 | 核心提取特征 |
|---|---|---|
| T1 | SYN包 | TTL、窗口大小、TCP选项 |
| T2 | SYN包(不同窗口) | TTL、窗口大小、TCP选项 |
| T3 | FIN | URG |
| T4 | ACK包 | TTL、窗口大小、TCP选项 |
| T5 | SYN包(小窗口) | TTL、窗口大小、TCP选项 |
| T6 | ACK包(小窗口) | TTL、窗口大小、TCP选项 |
| T7 | FIN | URG |
| UDP | UDP包(闭合端口) | ICMP错误码、TTL、IP选项 |
| ICMP | ICMP Echo请求 | TTL、ID/SEQ处理方式 |
TUI探测状态管理
TUI探测在扫描过程中有明确的生命周期状态:
cpp
enum ProbeState {
PENDING, // 待发送
ACTIVE, // 已发送,等待响应
COMPLETED_VALID, // 已完成,有效响应
COMPLETED_INVALID, // 已完成,无效响应
TIMEOUT_FAILED // 超时失败
};
状态管理函数:
cpp
// 更新活跃TUI探测状态
void HostOsScan::updateActiveTUIProbes(HostOsScanStats *hss) {
std::list<Probe *>::iterator i;
for (i = hss->activeTUIProbes.begin();
i != hss->activeTUIProbes.end(); ) {
Probe *p = *i;
// 检查是否超时
if (timeval_elapsed(&p->sent) > timeProbeTimeout(hss)) {
// 标记为超时失败
p->state = TIMEOUT_FAILED;
i = hss->activeTUIProbes.erase(i);
} else {
i++;
}
}
}
TUI探测流程
是
是
否
否
是
否
否
是
buildTUIProbeList
生成T1-T7探测
生成UDP探测
生成ICMP探测
加入待发送列表
sendNextProbe
发送探测包
标记为ACTIVE
收到响应?
processResp
响应有效?
标记为COMPLETED_VALID
标记为COMPLETED_INVALID
超时?
标记为TIMEOUT_FAILED
提取特征存入FP
触发重传
updateActiveTUIProbes
所有探测完成?
makeFP生成指纹
4.3 指纹生成机制
所有探测完成后,HostOsScan::makeFP函数汇总所有响应特征,生成完整的OS指纹。
指纹结构
cpp
struct FingerPrint {
// TSeq指纹
struct {
int num_responses;
u32 responses[6];
u32 class_num;
u32 index;
u32 tcp_options;
} seq;
// T1-T7指纹
struct {
u16 port;
u8 ttl;
u16 window;
u8 flags;
char tcp_options[256];
} responses[7];
// UDP指纹
struct {
u16 port;
u8 ttl;
u8 type; // ICMP类型
u8 code; // ICMP代码
} udp;
// ICMP指纹
struct {
u16 id;
u16 seq;
u8 ttl;
} icmp;
};
指纹生成流程
cpp
void HostOsScan::makeFP(HostOsScanStats *hss) {
FingerPrint *FP = &hss->FP;
// 1. 生成TSeq指纹
makeTSeqFP(hss);
// 2. 生成T1-T7指纹
for (int i = 0; i < 7; i++) {
makeT1_7FP(hss, i);
}
// 3. 生成UDP指纹
makeTUdpFP(hss);
// 4. 生成ICMP指纹
makeTIcmpFP(hss);
// 5. 格式化TCP选项
for (int i = 0; i < 7; i++) {
get_tcpopt_string(hss, i);
}
}
五、网络抓包与过滤
5.1 begin_sniffer函数:抓包器初始化
begin_sniffer是OS扫描的抓包器初始化入口,负责打开pcap抓包句柄、构建BPF过滤规则。
函数签名
cpp
static void begin_sniffer(HostOsScan *HOS,
std::vector<Target *> &Targets);
执行流程
是
否
是
否
begin_sniffer
初始化缓冲区
目标数量≤20?
构建精准源IP过滤
使用简化过滤
拼接src host IP1 or IP2...
仅过滤协议
打开pcap抓包句柄
IPv4?
构建BPF过滤器
跳过IPv6暂未实现
编译并应用过滤器
调试模式打印过滤器
结束
代码实现
cpp
static void begin_sniffer(HostOsScan *HOS,
std::vector<Target *> &Targets) {
char pcap_filter[2048];
char dst_hosts[1200];
int filterlen = 0;
int len;
unsigned int targetno;
bool doIndividual = Targets.size() <= 20;
pcap_filter[0] = '\0';
// 步骤1:构建精准源IP过滤(目标≤20个时)
if (doIndividual) {
for (targetno = 0; targetno < Targets.size(); targetno++) {
len = Snprintf(dst_hosts + filterlen,
sizeof(dst_hosts) - filterlen,
"%ssrc host %s",
(targetno == 0)? "" : " or ",
Targets[targetno]->targetipstr());
if (len < 0 || len + filterlen >= (int) sizeof(dst_hosts))
fatal("ran out of space in dst_hosts");
filterlen += len;
}
len = Snprintf(dst_hosts + filterlen,
sizeof(dst_hosts) - filterlen, ")))");
if (len < 0 || len + filterlen >= (int) sizeof(dst_hosts))
fatal("ran out of space in dst_hosts");
}
// 步骤2:打开pcap抓包句柄
HOS->pd = my_pcap_open_live(Targets[0]->deviceName(), 8192,
o.spoofsource ? 1 : 0,
pcap_selectable_fd_valid() ? 200 : 2);
if (HOS->pd == NULL)
fatal("%s", PCAP_OPEN_ERRMSG);
// 步骤3:构建并设置BPF过滤器(仅IPv4)
struct sockaddr_storage ss = Targets[0]->source();
if (ss.ss_family == AF_INET) {
if (doIndividual)
len = Snprintf(pcap_filter, sizeof(pcap_filter),
"dst host %s and (icmp or (tcp and (%s",
inet_ntoa(((struct sockaddr_in *)&ss)->sin_addr),
dst_hosts);
else
len = Snprintf(pcap_filter, sizeof(pcap_filter),
"dst host %s and (icmp or tcp)",
inet_ntoa(((struct sockaddr_in *)&ss)->sin_addr));
if (len < 0 || len >= (int) sizeof(pcap_filter))
fatal("ran out of space in pcap filter");
if (o.debugging)
log_write(LOG_PLAIN, "Packet capture filter (device %s): %s\n",
Targets[0]->deviceFullName(), pcap_filter);
set_pcap_filter(Targets[0]->deviceFullName(), HOS->pd, pcap_filter);
}
}
BPF过滤器示例
精准过滤(目标≤20个):
dst host 192.168.1.100 and (icmp or (tcp and (src host 192.168.1.1 or src host 192.168.1.2)))
简化过滤(目标>20个):
dst host 192.168.1.100 and (icmp or tcp)
过滤规则说明:
dst host 本机IP:只捕获发往本机的包icmp or tcp:只捕获ICMP和TCP协议的包src host 目标IP:只捕获来自指定目标的包(精准过滤时)
设计亮点
- 动态过滤策略:根据目标数量切换"精准过滤/简化过滤",平衡精准度和可用性
- 内核级过滤:BPF在内核层过滤,极大减少CPU/内存开销
- 缓冲区安全:全程做越界检查,避免缓冲区溢出
5.2 响应包处理流程
指纹生成 HostOsScanStats HostOsScan pcap抓包器 指纹生成 HostOsScanStats HostOsScan pcap抓包器 提取TTL、ID、SEQ 提取ISN 提取TTL、窗口、选项 alt [TSeq响应] [T1-T7响应] 提取ICMP错误码 alt [ICMP包] [TCP包] [UDP响应] 调整RTT、超时时间 捕获到响应包 解析IP头 判断协议类型 processTIcmpResp() 判断TCP标志位 processTSeqResp() processT1_7Resp() processTUdpResp() 更新探测状态 adjust_times()
六、性能优化与容错机制
6.1 拥塞控制机制
Nmap通过scan_performance_vars结构体实现精细的拥塞控制,平衡扫描速度和网络稳定性。
拥塞控制参数
cpp
struct scan_performance_vars {
int low_cwnd; // 最低拥塞窗口
int host_initial_cwnd; // 单主机初始cwnd
int group_initial_cwnd; // 组初始cwnd
int max_cwnd; // 最大cwnd
int slow_incr; // 慢启动增量
int ca_incr; // 拥塞避免增量
int cc_scale_max; // 增量缩放上限
int initial_ssthresh; // 慢启动阈值
double group_drop_cwnd_divisor; // 丢包时cwnd降速除数
double group_drop_ssthresh_divisor; // 丢包时ssthresh降速除数
double host_drop_ssthresh_divisor; // 单主机ssthresh降速除数
void init(); // 初始化函数
};
拥塞控制流程
初始状态
每收到响应,cwnd += slow_incr
cwnd >= ssthresh
每RTT,cwnd += ca_incr
检测到丢包
cwnd >= max_cwnd
慢启动
拥塞避免
参数初始化
cpp
void scan_performance_vars::init() {
// 最低拥塞窗口:优先使用用户配置,否则默认1
low_cwnd = o.min_parallelism ? o.min_parallelism : 1;
// 单主机初始cwnd
host_initial_cwnd = 10;
// 组初始cwnd
group_initial_cwnd = 30;
// 最大cwnd
max_cwnd = 100;
// 慢启动增量
slow_incr = 1;
// 拥塞避免增量
ca_incr = 1;
// 慢启动阈值
initial_ssthresh = 50;
// 丢包降速除数
group_drop_cwnd_divisor = 2.0;
group_drop_ssthresh_divisor = 2.0;
host_drop_ssthresh_divisor = 2.0;
}
6.2 速率控制机制
hostSendOK函数:发送速率控制
cpp
bool HostOsScan::hostSendOK(HostOsScanStats *hss,
struct timeval *when) {
struct timeval now;
gettimeofday(&now, NULL);
// 计算距离上次发送的时间间隔
unsigned long elapsed = TIMEVAL_SUBTRACT(now, hss->lastprobe);
// 检查是否达到最小发送间隔
if (elapsed < hss->min_send_interval) {
// 未达到,计算下次可发送时间
TIMEVAL_ADD(hss->lastprobe, hss->min_send_interval, *when);
return false;
}
// 可以发送
*when = now;
return true;
}
动态调优机制
cpp
void HostOsScan::adjust_times(HostOsScanStats *hss,
Probe *probe,
struct timeval *rcvdtime) {
// 计算往返时间(RTT)
unsigned long rtt = TIMEVAL_SUBTRACT(*rcvdtime, probe->sent);
// 更新平均RTT(指数加权移动平均)
if (hss->rtt == 0)
hss->rtt = rtt;
else
hss->rtt = (hss->rtt * 7 + rtt) / 8;
// 根据RTT调整发送间隔
if (rtt < 100000) // RTT < 100ms
hss->min_send_interval = 50000; // 50ms
else if (rtt < 500000) // RTT < 500ms
hss->min_send_interval = 100000; // 100ms
else
hss->min_send_interval = 200000; // 200ms
// 调整超时时间(RTT的3倍)
hss->timeout = hss->rtt * 3;
}
6.3 超时管理机制
timeProbeTimeout函数:计算超时时间
cpp
unsigned long HostOsScan::timeProbeTimeout(HostOsScanStats *hss) const {
// 优先使用目标专属超时值
if (hss->timeout > 0)
return hss->timeout;
// 否则使用全局超时值
return o.scan_delay ? o.scan_delay : 1000000; // 默认1秒
}
nextTimeout函数:查找下一个超时事件
cpp
bool HostOsScan::nextTimeout(HostOsScanStats *hss,
struct timeval *when) {
struct timeval earliest;
bool found = false;
// 遍历所有活跃探测
for (std::list<Probe *>::iterator i = hss->activeSeqProbes.begin();
i != hss->activeSeqProbes.end(); i++) {
Probe *p = *i;
// 计算超时时间
struct timeval timeout;
TIMEVAL_ADD(p->sent, timeProbeTimeout(hss), timeout);
// 找出最早的超时时间
if (!found || TIMEVAL_COMPARE(timeout, earliest) < 0) {
earliest = timeout;
found = true;
}
}
if (found) {
*when = earliest;
return true;
}
return false;
}
6.4 Windows电源管理
Nmap在Windows平台上使用SetThreadExecutionStateAPI防止系统在长时间扫描时进入睡眠状态。
函数原型
cpp
EXECUTION_STATE SetThreadExecutionState(
[in] EXECUTION_STATE esFlags
);
常用标志
| 标志 | 含义 | 使用场景 |
|---|---|---|
ES_CONTINUOUS |
持续生效 | 所有场景必加 |
ES_SYSTEM_REQUIRED |
阻止系统睡眠 | 后台扫描 |
ES_DISPLAY_REQUIRED |
阻止屏幕关闭 | 实时监控界面 |
使用示例
cpp
// 扫描开始时:阻止系统睡眠
SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED);
// 执行扫描...
// ...
// 扫描结束时:恢复默认电源管理
SetThreadExecutionState(ES_CONTINUOUS);
6.5 详细输出控制
Nmap通过verbose变量控制输出详细程度,满足不同用户的需求。
cpp
u8 verbose; // 冗长输出级别(0-3)
| verbose值 | 含义 | 输出内容 |
|---|---|---|
| 0(默认) | 静默 | 只输出最终结果 |
| 1 | 详细 | 输出关键步骤 |
| 2 | 更详细 | 输出次要细节 |
| 3+ | 调试 | 输出底层细节 |
使用示例
cpp
// 扫描完成后,输出核心结果(无论verbose是多少都输出)
printf("扫描完成:发现1台存活主机\n");
// 如果verbose≥1(-v),输出详细进度
if (verbose >= 1) {
printf("详细信息:发送了10个探测包,收到8个响应\n");
}
// 如果verbose≥2(-vv),输出调试级信息
if (verbose >= 2) {
printf("调试信息:探测包SEQ=12345,目标IP=192.168.1.1\n");
}
// 如果verbose≥3(-vvv),输出底层抓包细节
if (verbose >= 3) {
printf("底层细节:收到ICMP响应,TTL=64,窗口大小=1460\n");
}
七、总结与展望
7.1 核心技术总结
本文深入剖析了Nmap OS识别核心模块osscan2.cc的实现细节,主要涵盖:
-
分层架构设计 :通过
OSScan、OsScanInfo、HostOsScan等类实现职责分离,代码结构清晰,易于维护和扩展。 -
探测包机制:
- TSeq探测:提取TCP ISN生成规律
- TUI探测:提取TCP/IP栈基础特征
- 多探测组合:从多个维度验证OS特征
-
性能优化:
- 拥塞控制:动态调整发送速率
- BPF过滤:内核级包过滤,减少CPU开销
- 批量处理:提升扫描效率
-
容错机制:
- 多轮重试:提高扫描成功率
- 超时管理:避免无限等待
- 动态调优:根据网络状况自适应
7.2 设计亮点
-
执行与状态分离 :
HostOsScan只负责执行,状态存储在HostOsScanStats中,便于复用和测试。 -
接口与实现分离:公有接口暴露"要做什么",私有成员隐藏"怎么做",代码模块化程度高。
-
动态策略:根据目标数量、网络状况动态调整扫描策略,兼顾效率和兼容性。
-
安全设计:全程做缓冲区检查、内存管理,避免内存泄漏和缓冲区溢出。
7.3 技术展望
随着网络技术的发展,Nmap的OS识别技术也在不断演进:
-
IPv6支持:当前实现主要针对IPv4,IPv6的OS识别需要进一步完善。
-
机器学习:利用机器学习算法分析探测响应,提高识别准确率。
-
云环境适配:针对云环境(如容器、虚拟机)的特殊网络栈进行优化。
-
实时指纹更新:建立指纹库自动更新机制,及时识别新版本的操作系统。
7.4 学习建议
对于想要深入学习Nmap源码的读者,建议按以下顺序学习:
- 基础概念:先理解TCP/IP协议栈、OS指纹识别的基本原理
- 数据结构 :重点学习
HostOsScanStats、FingerPrint等核心数据结构 - 执行流程 :从
os_scan入口开始,跟踪完整的扫描流程 - 探测机制:深入理解TSeq、TUI等探测包的构造和响应处理
- 性能优化:学习拥塞控制、速率控制等优化机制
- 实践调试 :使用
-vvv参数运行Nmap,观察实际的探测包和响应
参考资源
作者注: 本文基于Nmap 7.98版本源码分析,部分实现细节可能随版本更新而变化。如有疑问或建议,欢迎交流讨论。
本文完,感谢阅读!