14 KiB
14 KiB
Draft: 天璇 v6 — 基于真实TLS特征的恶意流量分析
Intent
- intent: CLEAR
- review_required: true
- status: awaiting-approval
自我审查发现的问题
- ❌ Phase 0 依赖用户给真实数据——应基于 TLS 1.3 协议推导
- ❌ 21x2 hex 列我不知道是哪个,回避了问题
- ❌ "TlsS"/"TlsC" 列我不知道是哪个,回避了问题
- ❌ 归一化步骤没说明如何集成到现有 data_loader 中
- ❌ 特征间有冗余(port_category vs is_standard_port)
- ❌ 时域特征
inter_arrival_time_std在单流级别不可计算 - ❌ 缺少 JA3 指纹近似、SNI 长度熵等关键特征
- ❌ lat/lon 覆盖方案没说具体怎么做
- ❌ 空白语义只说了 4 列,其他几十列没说
- ❌ 聚类流程图漏了"归一化"步骤
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)
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)
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)
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: 端到端验证
- 生成3类测试数据
- 跑完整管道
- 验证归一化正确性(每列hex→enum映射)
- 验证聚类结果的自然分簇(正常/代理/恶意)
- 验证簇画像的可解读性
- 单元测试: 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} 兜底 |