# Draft: 天璇 v6 — 基于真实TLS特征的恶意流量分析 ## Intent - **intent**: CLEAR - **review_required**: true - **status**: awaiting-approval ## 自我审查发现的问题 1. ❌ Phase 0 依赖用户给真实数据——应基于 TLS 1.3 协议推导 2. ❌ 21x2 hex 列我不知道是哪个,回避了问题 3. ❌ "TlsS"/"TlsC" 列我不知道是哪个,回避了问题 4. ❌ 归一化步骤没说明如何集成到现有 data_loader 中 5. ❌ 特征间有冗余(port_category vs is_standard_port) 6. ❌ 时域特征 `inter_arrival_time_std` 在单流级别不可计算 7. ❌ 缺少 JA3 指纹近似、SNI 长度熵等关键特征 8. ❌ lat/lon 覆盖方案没说具体怎么做 9. ❌ 空白语义只说了 4 列,其他几十列没说 10. ❌ 聚类流程图漏了"归一化"步骤 --- ## Phase 0: 基于 TLS 1.3 协议的列类型推导(不需要用户数据) ### 基础事实 - 数据来源:TLS 1.3 握手报文字段抽取 - 每条流 = 一个 TLS 连接握手 + 数据传输的摘要 - 大量空白是预期的——TLS 握手中很多字段是可选 extension,不是每条流都有 ### 每列精确类型定义 | 列 | TLS协议来源 | 存放格式 | 空白语义 | |----|-----------|---------|---------| | `:ips` | 捕获点记录的源IP | IPv4字符串 | 从不空白 | | `:ipd` | 捕获点记录的目标IP | IPv4字符串 | 从不空白 | | `:prs` | TCP源端口 | 整数 | 缺失 | | `:prd` | TCP目标端口 | 整数 | 缺失 | | `scnt` | GeoIP从:ipd推断 | 2字母大写代码 | 缺失(GeoIP无法解析时) | | `dcnt` | GeoIP从:ipd推断 | 2字母大写代码 | 缺失 | | `server-ip` | 服务器IP(可能与:ipd同) | IPv4 | 缺失 | | `client-ip` | 客户端IP(可能与:ips同) | IPv4 | 缺失 | | `0ver` | ServerHello.protocol_version | 2x2 hex: `03 04` | TLS连接都必有,极少空白 | | `snam` | ClientHello.extensions.server_name | UTF8域名 | 缺失(ClientHello中无SNI扩展 → 可疑) | | `cnam` | Certificate.certificate.subject.CN | UTF8字符串 | 缺失(无证书或未知证书) | | `4dur` | 流持续时间(纳秒/毫秒?) | 整数 | 常见空白(40%+) | | `8ses` | 会话起始偏移 | 整数 | 常见空白 | | `2tmo` | 超时偏移 | 整数 | 常见空白 | | `4ksz` | 证书公钥大小 | 整数(256/384/521/1024/2048) | 空白(无证书) | | `cnrs` | SessionTicket.can_resume | `+`/空白 | **空白≠缺失** `+`=可恢复 `""`=不可恢复 | | `isrs` | SessionTicket.was_resumed | `+`/空白 | **空白≠缺失** `+`=已恢复 `""`=未恢复 | | `8ack` | 反向方向字节数 | 整数 | 缺失 | | `8ppk` | 有效载荷包数 | 整数 | 缺失 | | `8dbd` | 数据库日志记录时间 | 时间戳字符串 | 缺失 | | `1ipp` | IP协议号 | 整数(6/17) | 缺失 | | `4dbn` | 数据库编号 | 整数 | 缺失 | | `tabl` | 数据库表名 | 字符串 | 缺失 | | `name` | 捕获链路名称 | 字符串 | 缺失 | | `source-node` | 捕获节点名 | 字符串 | 缺失 | | `cipher-suite` | 密码套件名(IANA格式) | 大小写混合字符串 | 缺失(无密码协商时) | | `ecdhe-named-curve` | 椭圆曲线名 | 大小写混合字符串 | 缺失(非ECDHE密钥交换时) | | `0cph` | ServerHello.cipher_suite | 2x2 hex: `13 01` | 缺失 | | `0crv` | ServerHello.KeyShare | 2x2 hex: `00 1d` | 缺失(非ECDHE时) | | `0rnd` | ServerHello/ClientHello.random | 32x2 hex(也可能是21x2) | TLS必有,但可能截断存储 | | `0rnt` | Random.gmt_unix_time | 整数或4x2 hex | 常见空白(现代TLS用随机值替代时间) | | `row` | 行号 | 整数 | 从不空白 | | `time` | 时间 | 字符串 | 缺失 | | `timestamp` | 时间戳 | 字符串 | 从不空白 | | `:ips.latd/lond` | GeoIP推断,过时 | 浮点数 | 不可靠,覆盖 | | `:ipd.latd/lond` | GeoIP推断,过时 | 浮点数 | 不可靠,覆盖 | | `:ips.ispn/.orgn/.city` | GeoIP推断 | 字符串 | 大部分空白 | | `:ipd.ispn/.orgn/.city` | GeoIP推断 | 字符串 | 大部分空白 | ### 待识别项目 - **21x2 hex 列**: 不在我已知的任何标准 TLS 结构中。0rnd(32x2)、0cph(2x2)、0crv(2x2)、0ver(2x2)。21x2 可能是一个额外的截断/编码字段。将用 column_survey 自动发现 `hex_pairs_count==21` 的列。 - **TlsS/TlsC**: 可能是 `tabl` 或 `source-node` 列的两个值。含义接近 "TLS Server" / "TLS Client"。待 column_survey 确认。 --- ## Phase 0.5: 值归一化模块 `analysis/value_normalizer.py` ### 架构 新增独立的归一化模块,在 `data_loader.py` 的 `load_csv_directory()` 中作为**最后一个清理步骤**执行(类型分类之后、返回 LazyFrame 之前)。 ### 归一化方式 每列生成 `{col}_norm` 保留原始列,供下游特征工程使用。 ### TLS Version → Enum (0ver) ```python TLS_VERSION_MAP = { # hex wire format '03 04': 'tls_1_3', '03 03': 'tls_1_2', '03 02': 'tls_1_1', '03 01': 'tls_1_0', '02 00': 'ssl_2_0', '03 00': 'ssl_3_0', # display formats (case-insensitive) 'tlsv1.3': 'tls_1_3', 'tls 1.3': 'tls_1_3', '1.3': 'tls_1_3', 'tlsv1.2': 'tls_1_2', 'tls 1.2': 'tls_1_2', '1.2': 'tls_1_2', } # 未知值 → 'unknown_version' ``` ### Cipher Suite → Enum (0cph, cipher-suite) ```python CIPHER_MAP = { # hex → IANA name (TLS 1.3 + common TLS 1.2) '13 01': 'tls_aes_128_gcm_sha256', '13 02': 'tls_aes_256_gcm_sha384', '13 03': 'tls_chacha20_poly1305_sha256', 'c0 2b': 'tls_ecdhe_ecdsa_aes_128_gcm_sha256', 'c0 2f': 'tls_ecdhe_rsa_aes_128_gcm_sha256', 'c0 30': 'tls_ecdhe_rsa_aes_256_gcm_sha384', 'cc a9': 'tls_ecdhe_ecdsa_chacha20_poly1305_sha256', 'cc ac': 'tls_ecdhe_rsa_chacha20_poly1305_sha256', '00 9c': 'tls_rsa_aes_128_gcm_sha256', '00 9d': 'tls_rsa_aes_256_gcm_sha384', 'c0 09': 'tls_ecdhe_ecdsa_aes_128_cbc_sha256', 'c0 13': 'tls_ecdhe_rsa_aes_128_cbc_sha256', # display → IANA (normalize: lowercase, ' '→'_', '-'→'_') 'tls_aes_128_gcm_sha256': 'tls_aes_128_gcm_sha256', 'aes 128 gcm': 'tls_aes_128_gcm_sha256', 'aes-128-gcm': 'tls_aes_128_gcm_sha256', 'tls 1.3 aes 128 gcm': 'tls_aes_128_gcm_sha256', } # 未知hex → 'unknown_cipher_0x{hex}' # 未知显示名 → 标准化后保留 ``` ### Named Curve → Enum (0crv, ecdhe-named-curve) ```python CURVE_MAP = { # hex wire format '00 1d': 'x25519', '00 17': 'secp256r1', '00 18': 'secp384r1', '00 19': 'secp521r1', '00 1e': 'x448', # display (case-insensitive) 'x25519': 'x25519', 'curve25519': 'x25519', 'secp256r1': 'secp256r1', 'prime256v1': 'secp256r1', 'p-256': 'secp256r1', 'p256': 'secp256r1', 'secp384r1': 'secp384r1', 'p-384': 'secp384r1', } # 未知 → 'unknown_curve_0x{hex}' ``` ### CNRS/ISRS → Enum (cnrs, isrs) ``` '+' → 'yes' (never missing) '' → 'no' (never missing) ``` ### 纯字符串(不操作) `snam`, `cnam`, `name`, `source-node`, `tabl` → 不归一化,保持原始字符串 ### 归一化与现有 data_loader 的集成 ``` 现有 data_loader 清理流水线: BOOL_ENUM → LAT_LON → HEX → [新增: VALUE_NORMALIZATION] → GenericNumCoerce value_normalizer.normalize(lf, type_map) → pl.LazyFrame (带 _norm 列) ``` --- ## Phase 1: 特征工程(每条流级别 → 实体聚合级别) ### 第一步:单流特征(在 entity_aggregator 聚合前计算) | 特征 | 来源列 | 计算方式 | |------|--------|---------| | `has_sni` | `snam` | 有SNI? 1/0 | | `sni_length` | `snam` | SNI域名长度(长域名可疑) | | `sni_entropy` | `snam` | 域名Shannon熵(高熵域名=算法生成=DGA) | | `sni_vs_cnam` | `snam`, `cnam` | SNI与CN一致? 1/0 | | `tls_version_risk` | `0ver_norm` | `1024`=2 | | `bytes_per_packet` | `8ack`, `8ppk` | avg bytes/packet | | `is_recoverable` | `cnrs_norm` | `yes`=1, `no`=0 | | `was_resumed` | `isrs_norm` | `yes`=1, `no`=0 | | `has_cert_key` | `4ksz` | 有密钥大小? 1/0 | ### 第二步:实体聚合特征(在 entity_aggregator 中计算,按 `:ips` 分组) | 聚合特征 | 聚合方式 | 意义 | |---------|---------|------| | `flow_count` | count | 总流数 | | `unique_dst_ips` | n_unique | 目标多样性 | | `unique_dst_ports` | n_unique | 端口多样性 | | `unique_tls_versions` | n_unique(0ver_norm) | TLS版本跨度 | | `unique_ciphers` | n_unique(0cph_norm) | 密码套件跨度 | | `unique_curves` | n_unique(0crv_norm) | 曲线跨度 | | `unique_snis` | n_unique(snam) | SNI多样性 | | `tls_13_ratio` | mean(is_modern_cipher) | TLS 1.3密码比例 | | `ecdhe_ratio` | mean(key_exchange==0) | 前向保密比例 | | `avg_bytes_per_packet` | mean(bytes_per_packet) | 平均包大小 | | `std_bytes_per_packet` | std(bytes_per_packet) | 包大小标准差(小=beaconing) | | `avg_duration` | mean(4dur) | 平均持续时长 | | `std_duration` | std(4dur) | 时长标准差 | | `port_443_ratio` | mean(port_type==0) | 标准端口比例 | | `non_standard_port_ratio` | mean(port_type==2) | 非标准端口比例(可疑) | | `sni_missing_ratio` | mean(1-has_sni) | 无SNI比例(可疑) | | `sni_cn_mismatch_ratio` | mean(1-sni_vs_cnam) | SNI/CN不匹配比例 | | `sni_avg_entropy` | mean(sni_entropy) | SNI平均熵(高=DGA) | | `recoverable_ratio` | mean(is_recoverable) | 会话可恢复比例 | | `resumed_ratio` | mean(was_resumed) | 会话恢复比例 | | `has_any_cert` | mean(has_cert_key) | 有证书比例 | | `countries_visited` | n_unique(dcnt) | 跨国多样性 | | `unique_hours` | n_unique(hour_of_day) | 活跃时段 | | `inter_arrival_std` | std(timestamp_diff) | **Beaconing检测**: 到达间隔标准差 | | `night_ratio` | mean(is_night_hour) | 夜间流量比例 | ### 总计:约 30+ 聚合特征 --- ## Phase 2: 空白语义精确处理 | 列 | 空白语义 | 聚合前处理 | 聚合中处理 | |----|---------|-----------|-----------| | cnrs, isrs | **非缺失** (+=yes, blank=no) | 归一化为 `yes`/`no` | 直接聚合 | | snam, cnam | 缺失(无SNI/证书) | 保留空白 | `has_sni`=0, `sni_cn_mismatch`=null | | 0ver | 极少空白 | 空白→`unknown_version` | `tls_version_risk`=null | | 0cph, 0crv | 缺失(无协商) | 空白→`unknown` | 相应特征为null | | 4dur, 8ack, 8ppk | 缺失(40%+) | 保留空白 | `mean`/`std`跳过null | | :prs, :prd | 缺失 | 保留空白 | `port_type`=null | | scnt, dcnt | 缺失(GeoIP失败) | 保留空白 | `countries_visited`排除null | | lat/lon | 基于过时库 | **全部替换** | 见Phase 2.5 | | ispn/orgn/city | 大部分空白 | **不参与分析** | 排除 | --- ## Phase 2.5: 经纬度覆盖方案 lat/lon 基于过时 IP 地理库,不可靠。处理方式: - **不使用原始 lat/lon 值** - 对于 globe 可视化:用随机生成的地理位置模拟 - 对于分析特征:移除所有基于 lat/lon 的特征 - 保留 `dcnt`(目标国家代码)作为地理特征——国家代码更可靠且更有分析价值 --- ## Phase 3: 完整聚类流程 ``` 原始CSV → data_loader加载 → 类型分类 (type_classifier.py) → BOOL_ENUM清洗 → HEX预处理 (hex_pairs_count) → 值归一化 (value_normalizer.py) ← Phase 0.5 新增 → 通用数值清洗 → 单流特征计算 (data_loader.py 扩展) ← Phase 1 新增 → 实体聚合 (entity_aggregator.py 扩展) ← Phase 1 新增 → 通用数值清洗 (已有) → 方差过滤 ← Phase 1 新增 → 相关过滤 → StandardScaler → PCA(20-50维) → HDBSCAN聚类 → 簇画像生成 ``` ### 簇画像输出 每个簇的完整描述: ``` 簇标签: 3 大小: 42实体 (14%) 簇类型: 🚨 高度可疑 特征画像: - sni_missing_ratio: 0.85 (+2.1σ 高于均值) - non_standard_port_ratio: 0.72 (+1.8σ) - tls_13_ratio: 0.15 (-2.3σ) - avg_bytes_per_packet: 45 (-1.9σ, 小包=beaconing) - inter_arrival_std: 2.3s (-2.1σ, 固定间隔) - countries_visited: 12 (+1.5σ, 跨国) 判定: 疑似代理/隧道流量 建议操作: 深度包检测, IP列入观察名单 ``` --- ## Phase 4: 测试数据生成 (gen_test_data.py) 测试数据也使用归一化格式输出,确保端到端可验证。 ### 3 类流量画像 #### 1. 正常浏览 (60%示例数据) - TLS 1.3 (03 04) + AEAD cipher - Curve: x25519 (00 1d) - SNI: `www.google.com`, `api.github.com` 等合法域名 - 端口 443 - 多样的目标IP - 短连接 (0.5-5s), 中等包大小 - cnrs/isrs: 随机 yes/no #### 2. 代理/VPN 隧道 (20%) - TLS 1.2 (03 03) + ECDHE cipher - Curve: secp256r1 (00 17) - SNI: 可能缺失或单一代理解析域名 - 端口 443 但可能也在非标准端口 - 少目标高流量 - 长连接, 大字节量 - cnrs: 高恢复率 (keep-alive) #### 3. 恶意/可疑 (20%) - TLS 1.0/1.1 或奇怪版本 - 弱密码 (CBC/RC4) - SNI/CN 不匹配或无 SNI - Beaconing: 固定间隔小包 (30-40 bytes) - 高重连率 (大量 cnrs=yes → isrs=yes) - 非标准端口 - 低熵 TLS random - 多国目标分布 - 低包大小标准差 --- ## Phase 5: 端到端验证 1. 生成3类测试数据 2. 跑完整管道 3. 验证归一化正确性(每列hex→enum映射) 4. 验证聚类结果的自然分簇(正常/代理/恶意) 5. 验证簇画像的可解读性 6. 单元测试: 120+ passed --- ## 前置依赖 - Phase 0-5 都不需要用户真实数据,全部基于 TLS 1.3 协议推导 ## 意外预案 | 意外 | 后备 | |------|------| | 21x2 hex 列不在已知列中 | column_survey 自动发现后添加合适映射 | | TlsS/TlsC 列无法确认含义 | 保留为原始字符串枚举,不做归一化 | | 聚类仍不理想 | 增加规则引擎层(if sni_missing>0.5 → flag) | | 特征数量不足 | 从 hex 列提取字节分布统计作为补充特征 | | 归一化映射表不全 | 未知hex→`unknown_{type}_0x{hex}` 兜底 |