GravityAgent(四):操作系统识别子系统与双引擎指纹匹配

GravityAgent(四):操作系统识别子系统与双引擎指纹匹配


IPv6 网络测绘 GravityAgent OS识别 指纹
本文属于系列

操作系统识别子系统与双引擎指纹匹配

在 IPv6 测绘中,“这台主机活着”只是第一步。真正有价值的情报是:它跑的是什么操作系统、什么服务。这一篇我们来看 GravityAgent 的 os_identify 子系统。

为什么 OS 识别要单独做成子系统

最早 OS 识别只是 Agent 内部的几个探针函数。但随着需求增长,它逐渐演变成一个独立模块,因为它需要:

  • 独立的原始套接字探测逻辑;
  • 独立的指纹库解析与匹配算法;
  • 独立的任务队列和结果库;
  • 独立的报告生成能力;
  • 可被 CLI、Agent、Web 三种入口复用。

现在 os_identify/ 有自己的 SQLite 结果库、独立的 CLI 函数、指纹库解析器和报告生成器,可以单独演进、测试和复用。

双引擎识别架构

目标 IP (IPv4/IPv6)
      │
      ▼
┌─────────────────┐
│  引擎选择策略   │
└─────────────────┘
      │
      ├──────────────┬──────────────────────┐
      ▼              ▼                      ▼
┌────────────┐  ┌──────────────┐  ┌──────────────┐
│ Raw Socket │  │ 自定义指纹    │  │ nmap OS DB   │
│   探测     │─▶│   匹配       │─▶│   (5652条)   │
└────────────┘  └──────────────┘  └──────────────┘
      │              │                   │
      └──────────────┴───────────────────┘
                     │
                     ▼
          ┌─────────────────┐
          │  置信度打分     │
          │   (0-100分)     │
          └─────────────────┘
                     │
                     ▼
          ┌─────────────────┐
          │  SQLite 结果库   │
          │ os_identify.db  │
          └─────────────────┘
                     │
                     ▼
          ┌─────────────────┐
          │ 可信度分析报告   │
          │ JSON / Markdown │
          └─────────────────┘

核心思路是:优先用自己构造的 TCP SYN 探测获取特征,与 nmap 指纹库做相似度匹配;当自定义引擎拿不到特征时,自动降级到 nmap 子进程。

原始 TCP SYN 探测

手工构造 TCP 包

pipeline.py 里有两个函数分别构造 IPv4 和 IPv6 的 TCP SYN 包:

  • _build_tcp_ipv4_packet
  • _build_tcp_ipv6_packet

探测包会带上模拟 Linux 特征的 TCP 选项:MSS=1460、WScale=7、Timestamp、SACK。这样做是为了让目标正常响应,同时避免因为选项过于异常而被防火墙丢弃。

发送与接收

_send_tcp_probe 使用 SOCK_RAW + IP_HDRINCL 发送并接收响应:

# IPv4:原始套接字 + IP_HDRINCL,手工填写 IP 头
# IPv6:AF_INET6 裸套接字

探测端口列表

PROBE_PORTS = [21, 22, 23, 25, 53, 80, 110, 111, 135, 139, 143, 443,
               445, 993, 995, 1723, 3306, 3389, 5900, 8080]

这些端口覆盖了常见的服务,只要目标有一个端口开放并能返回 SYN-ACK,就能提取到 TCP/IP 特征。

识别主流程

def identify_os(ip, ports=PROBE_PORTS, timeout=3.0, top_k=5, use_nmap_fallback=True):
    # 1. 逐端口探测,提取特征
    # 2. find_best_os_match 打分
    # 3. 无特征或指纹库缺失时降级 _identify_with_nmap

nmap 的调用方式是:

nmap -O -T4 --max-os-tries=1 <ip>

它只作为 fallback 使用,因为子进程开销远大于自己发包。

nmap 指纹库解析

nmap_fingerprints.py 负责解析 nmap-os-db(约 5 MB,5652 条指纹)。它提取的关键特征字段包括:

特征字段说明解析来源
window_sizeTCP 窗口大小WIN(W1=...) 行
tcp_optionsTCP 选项OPS(O1=...) 行
ttl初始 TTLT1 行中的 T= 参数
dfDon’t FragmentT1 行中的 DF= 参数
seq_index序列号增长模式SEQ(OS=...) 行
os_classOS 分类Class 行

特征提取

from os_identify.nmap_fingerprints import parse_tcp_packet

features = parse_tcp_packet(raw_packet)
print(f"TTL: {features.ip_ttl}")
print(f"Window: {features.tcp_window_size}")
print(f"Options: {features.tcp_options}")

TCPResponseFeatures 包含:TTL、IP ID、DF、窗口、SYN 标志、MSS、WScale、Timestamp、SACK、NOP、TCP 选项序列和 banner。

七维相似度打分算法

这是整个子系统的核心。对每个指纹计算 0–100 的置信度分数,共七个维度:

维度权重打分规则
Window Size25 分精确匹配满分,比例匹配按比例
TCP Options25 分Jaccard 相似度,完全包含奖励
TTL15 分8 以内差距满分,16 以内高分
DF 标志10 分匹配加分,不匹配低惩罚
MSS/WScale10 分特征匹配
Timestamp10 分TS 选项支持
OS 分类5 分特征推断与分类匹配
from os_identify.nmap_fingerprints import score_fingerprint, TCPResponseFeatures

target = TCPResponseFeatures(
    ip_ttl=64,
    ip_flags_df=True,
    tcp_window_size=3661,
    tcp_options=['S', 'W0', 'T', 'N'],
    tcp_mss=1460,
    tcp_wscale=0,
)

score = score_fingerprint(target, fingerprint)
print(f"匹配分数: {score.total_score:.1f}")
print(f"匹配原因: {score.reason}")

为什么 TTL 用”差距分级”而不是精确匹配?因为 TTL 会经过路由器递减,64 出发的包到达时可能是 63、62 甚至更小。给 8 以内的差距满分,能容忍常见的路径跳数。

为什么 TCP Options 用 Jaccard 相似度?因为不同实现会额外插入 NOP 填充,选项序列不会完全一致,但核心选项集合的相似度很能说明问题。

find_best_os_match 返回 Top-K 的 MatchScore,当指纹库匹配失败时,_infer_os_from_features 会做启发式兜底(比如 TTL=64 且窗口较小 → 倾向 Unix/Linux)。

指纹库有缓存机制:get_os_fingerprints / get_service_probes / invalidate_cache。

IPv6 支持策略

IPv6 地址识别面临两个挑战:

  1. Raw socket 在部分系统上需要特殊权限;
  2. IPv6 响应特征可能与 IPv4 不同。

当前实现策略是:

  1. 优先尝试 Raw socket 探测;
  2. 探测失败时自动降级到 nmap;
  3. nmap 使用 -6 参数指定 IPv6 模式。
# IPv6 识别
result = identify_os("2001:da8:7005:320::10")
print(f"OS: {result['best_os']}")
print(f"置信度: {result['confidence']:.1f}")
print(f"来源: {result.get('source', '自定义')}")

批量任务管理

OsIdentifyManager 负责批量识别。它维护两张表:

os_runs(任务记录)

字段类型说明
run_idTEXT任务 ID(主键)
nameTEXT任务名称
statusTEXT状态(queued/running/completed/failed)
targets_jsonTEXT目标 IP 列表
totalINTEGER目标总数
processedINTEGER已处理数
completedINTEGER完成数
os_distribution_jsonTEXTOS 分布统计
created_at / updated_atTEXT时间戳

os_results(识别结果)

字段类型说明
idINTEGER主键,自增
run_idTEXT任务 ID
ipTEXTIP 地址
best_osTEXT最佳匹配操作系统
confidenceREAL置信度 (0-100)
os_candidates_jsonTEXT候选 OS 列表
target_features_jsonTEXT目标网络特征
errorTEXT错误信息
created_atTEXT创建时间

后台 worker 模式

manager = get_os_identify_manager()
run = manager.start_os_identify(
    targets=["202.194.68.10", "2001:da8:7005:320::10"],
    name="test_os_identify",
)
print(f"任务已启动: {run['run_id']}")

任务启动后,后台 daemon worker 轮询 queued/running 状态的 run 并逐个处理。Web 端发起的长任务不会阻塞 HTTP 请求线程。

还支持从扫描批次启动识别:

manager.start_os_identify_from_batch(...)

它会从 scanner 批次的存活地址中抽样(random.seed(42) 保证可复现)。

查询与过滤

# 获取所有结果
results = manager.get_results(run_id, limit=100)

# 只看高置信度
high_conf = manager.get_results(run_id, min_confidence=80.0)

# 按 OS 类型过滤
linux_results = manager.get_results(run_id, os_filter="Linux")

可信度分析报告

这是子系统里最有”情报价值”的部分。报告包含六块内容:

1. 总体统计

{
  "total_ips": 100,
  "identified": 85,
  "no_response": 15,
  "identification_rate": 85.0
}

2. 置信度分布

{
  "average": 72.5,
  "high_confidence": {"count": 50, "percentage": 58.8},
  "medium_confidence": {"count": 25, "percentage": 29.4},
  "low_confidence": {"count": 10, "percentage": 11.8}
}

置信度等级划分:

  • 高置信度:≥ 70%
  • 中置信度:40–70%
  • 低置信度:< 40%

3. OS 分布分析

{
  "Linux 2.6.x - 3.x": {
    "count": 60,
    "percentage": 70.6,
    "avg_confidence": 85.2,
    "sample_ips": ["202.194.68.10", "2001:da8:..."]
  }
}

4. 网络特征分析

{
  "ttl_distribution": {
    "33-64 (常见 Unix/Linux)": 55,
    "65-128 (常见 Windows)": 25
  },
  "df_flag_usage": {"enabled": 80, "percentage": 94.1},
  "tcp_options": {"S": 80, "W": 75, "T": 70}
}

5. 低置信度 IP 列表

列出识别困难的 IP(前 20 个),方便人工复核。

6. 自动改进建议

_generate_recommendations 会根据识别结果自动生成建议:

[
  {
    "level": "warning",
    "category": "coverage",
    "message": "识别率较低 (45.0%)",
    "suggestion": "尝试增加超时时间或更换探测端口"
  }
]

建议的触发条件包括:识别率过低、单一 OS 占比过高、无响应比例过高等。

报告导出

# 生成 JSON 报告
report = manager.generate_confidence_report(run_id)

# 导出 Markdown 报告
result = manager.export_report(run_id, format="markdown")
print(f"报告已保存: {result['path']}")

报告会写入用户隔离目录 download/<user>/os_identify/,这样 Web 端才能正确归档给对应用户。

与 Agent 的集成

在 agent/agent.py 中注册了 7 个 OS 识别相关工具:

工具说明
identify_os_single单 IP 识别
identify_os_batch批量识别
identify_os_from_scan_batch从扫描批次启动识别
list_os_identify_runs列出任务
get_os_identify_status查询状态
get_os_identify_results获取结果
generate_os_confidence_report生成报告

系统提示词中明确规定:用户要求”操作系统识别 / OS 指纹”时,调用 identify_os_single 或 identify_os_batch;要求”生成可信度报告”时,调用 generate_os_confidence_report。

已知限制

OS 识别不是万能的,几个需要注意的点:

  1. 依赖指纹库覆盖。如果目标 OS 不在 nmap-os-db 中,识别结果可能不准确。
  2. 依赖网络路径。TTL 会被中间设备修改,NAT 和防火墙也可能改变 TCP 选项。
  3. 需要权限。Raw socket 探测通常需要 root 或 CAP_NET_RAW。
  4. IPv6 支持有限。部分系统对 IPv6 raw socket 支持不佳,只能降级到 nmap。

小结

os_identify 子系统的价值在于,它把”操作系统识别”从零散探针变成了一个可复用、可观测、可报告的独立能力:

能力实现
探测手工构造 TCP SYN,无状态收发
匹配七维打分 + nmap 5652 条指纹库
降级自定义引擎失败自动转 nmap
批量后台 worker + SQLite 任务表
报告统计、分布、特征、建议六合一
集成7 个 Agent 工具 + Web API

下一篇,我们进入 Agent 编排层,看看自然语言是怎么被翻译成这一系列工具调用的。