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_size | TCP 窗口大小 | WIN(W1=...) 行 |
tcp_options | TCP 选项 | OPS(O1=...) 行 |
ttl | 初始 TTL | T1 行中的 T= 参数 |
df | Don’t Fragment | T1 行中的 DF= 参数 |
seq_index | 序列号增长模式 | SEQ(OS=...) 行 |
os_class | OS 分类 | 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 Size | 25 分 | 精确匹配满分,比例匹配按比例 |
| TCP Options | 25 分 | Jaccard 相似度,完全包含奖励 |
| TTL | 15 分 | 8 以内差距满分,16 以内高分 |
| DF 标志 | 10 分 | 匹配加分,不匹配低惩罚 |
| MSS/WScale | 10 分 | 特征匹配 |
| Timestamp | 10 分 | 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 地址识别面临两个挑战:
- Raw socket 在部分系统上需要特殊权限;
- IPv6 响应特征可能与 IPv4 不同。
当前实现策略是:
- 优先尝试 Raw socket 探测;
- 探测失败时自动降级到 nmap;
- 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_id | TEXT | 任务 ID(主键) |
name | TEXT | 任务名称 |
status | TEXT | 状态(queued/running/completed/failed) |
targets_json | TEXT | 目标 IP 列表 |
total | INTEGER | 目标总数 |
processed | INTEGER | 已处理数 |
completed | INTEGER | 完成数 |
os_distribution_json | TEXT | OS 分布统计 |
created_at / updated_at | TEXT | 时间戳 |
os_results(识别结果)
| 字段 | 类型 | 说明 |
|---|---|---|
id | INTEGER | 主键,自增 |
run_id | TEXT | 任务 ID |
ip | TEXT | IP 地址 |
best_os | TEXT | 最佳匹配操作系统 |
confidence | REAL | 置信度 (0-100) |
os_candidates_json | TEXT | 候选 OS 列表 |
target_features_json | TEXT | 目标网络特征 |
error | TEXT | 错误信息 |
created_at | TEXT | 创建时间 |
后台 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 识别不是万能的,几个需要注意的点:
- 依赖指纹库覆盖。如果目标 OS 不在 nmap-os-db 中,识别结果可能不准确。
- 依赖网络路径。TTL 会被中间设备修改,NAT 和防火墙也可能改变 TCP 选项。
- 需要权限。Raw socket 探测通常需要 root 或
CAP_NET_RAW。 - IPv6 支持有限。部分系统对 IPv6 raw socket 支持不佳,只能降级到 nmap。
小结
os_identify 子系统的价值在于,它把”操作系统识别”从零散探针变成了一个可复用、可观测、可报告的独立能力:
| 能力 | 实现 |
|---|---|
| 探测 | 手工构造 TCP SYN,无状态收发 |
| 匹配 | 七维打分 + nmap 5652 条指纹库 |
| 降级 | 自定义引擎失败自动转 nmap |
| 批量 | 后台 worker + SQLite 任务表 |
| 报告 | 统计、分布、特征、建议六合一 |
| 集成 | 7 个 Agent 工具 + Web API |
下一篇,我们进入 Agent 编排层,看看自然语言是怎么被翻译成这一系列工具调用的。
订阅本站
通过 RSS 或邮箱,第一时间收到新文章。