342 lines
14 KiB
Markdown
342 lines
14 KiB
Markdown
# 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` | `<tls_1_2`=3, `tls_1_2`=1, `tls_1_3`=0 |
|
||
| `cipher_suite_category` | `0cph_norm` | `aead`=0, `cbc`=1, `rc4`=2, `null`=3 |
|
||
| `key_exchange` | `0cph_norm` | `ecdhe`=0, `dhe`=1, `static`=2 |
|
||
| `is_modern_cipher` | `0cph_norm` | tls 1.3 only cipher? 1/0 |
|
||
| `curve_type` | `0crv_norm` | `x25519`=0, `secp256r1`=1, other=2 |
|
||
| `curve_bit_strength` | `0crv_norm`/`4ksz` | 128/192/256 bits |
|
||
| `port_type` | `:prd` | `443`=0, `80-1023`=1, `>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}` 兜底 |
|