From 44ff080ae6a8bc8f617dd3f37fd1afe6874fa78f Mon Sep 17 00:00:00 2001 From: OpenCode Date: Fri, 11 Sep 2026 14:49:39 +0800 Subject: [PATCH] feat: decode remaining frame fields (claim/AERE/announce/discovery-resp/EUCE/events) - claim 552B: full layout; 240B opaque blocks proven UNCHECKED by Butler (CLAIM_ZERO=1 ablation: zeroed blocks -> AERE still PASS) -> claim can be built from scratch with zeros - AERE 552B: Butler IP/MAC/netmask, fixture UID@0x33, slow counter@0xB1/0xB5, resp@0xC0; only resp+counter vary across captures - announce 644B: session GUID, UTF-16BE show-file path, full timestamp @0x239 (y/mo/d/h/mi/s/ms; live sample decodes to 22:52:50.992 matching SL session), ephemeral port@0x25C, enable flags - discovery-resp 217B: device name, serial blob, network block, config block - EUCE 540B: 'EUCE' magic, version 5, username+computername+GUID, host MAC - event_b: type=4 1084B RDM-uplink frame identified (RDM response @0x3C) - analyze: new 'xor' subcommand with periodicity analysis --- docs/enet_protocol_current.md | 102 +++++++++++++++++++++++++++++----- src/analyze.rs | 23 ++++++++ src/hostdemo.rs | 6 ++ 3 files changed, 116 insertions(+), 15 deletions(-) diff --git a/docs/enet_protocol_current.md b/docs/enet_protocol_current.md index 763bb90..c597a6c 100644 --- a/docs/enet_protocol_current.md +++ b/docs/enet_protocol_current.md @@ -104,11 +104,11 @@ | 0x25-0x28 | 主机 IP(LE 逆序)= 192.168.123.113 | | 0x29-0x2E | 主机 MAC = 30 0e d5 59 bb 36 | -## 六、42B 事件帧(Butler→主机,部分破解) +## 六、42B 事件帧(Butler→主机,结构破解) type=4, 子类型=02, len=42 (0x2a), `"Butl"`@0x1A: -- 0x20-0x23 = 事件码(已见 0x0318 / 0x0062),其余尾字节 01 00 00 00 00 00。 -- 事件码语义未解码(不影响功能)。 +- 0x1E-0x1F = 01 00;0x20-0x21 = 事件码 u16 LE(已见 0x0318=ack_84 / 0x0062=status_84),0x22-0x23 = 00 00;0x24-0x27 = 01 00 00 00;0x28-0x29 = 00 00。 +- 事件码具体语义未解码(不影响功能)。 ## 六b、心跳 36B(主机→Butler,全破解) @@ -121,12 +121,85 @@ type=2, 子类型=03, len@0x14=36 (0x24): type=1, len=180 (0xb4):0x1A 起为主机名 ASCII(如 "tony"),其余字段未解码。 +## 六d、发现应答 217B(Butler→主机,结构破解) + +type=1, 子类型=0f, len=217 (0xd9): +- 0x1A-0x22 = 设备名 ASCII `"Butler S2"` +- 0x29-0x37 = 设备序列号/GUID 样字节(15B,常量) +- 0x3A-0x3C = 00 15 00(设备类型/能力?,与 AERE 同位置同值) +- 0x40-0x4E = 网络块:`01 | Butler IP(LE) | 掩码(00ffffff=255.255.255.0) | Butler MAC | 00` +- 0x59-0x5C = 1a 00 00 00 (26);0x5D-0x5F = 01 01 fa;0x60-0x63 = 0c 00 02 00 +- 0xAC-0xC8 = Butler 配置块(与 AERE 共享,见七b) +- 两份抓包逐字节一致 → 除配置块慢变计数器外全静态。 + +## 六e、EUCE 广播 540B(主机→广播,结构破解) + +magic `"EUCE"`(非 ENet 信封!),0x0C = 05 00 00 00(版本5?): +- 0x11C 起 = ASCII 串:`<用户名><计算机名>-<机器GUID>`(如 `tonycaoDESKTOP-PED2LVD-f7f4a043-17ec-4156-812b-909bdc99f2ff`) +- 0x216-0x21B = 主机 MAC +- 其余全 0。即 e:cue 主机身份广播,暖会话下非必需。 + +## 六f、1084B RDM 上行帧(Butler→主机,type=4,结构识别) + +len=1084, 子类型=01, 0x20=02, 0x2A=CC(与 RDM 请求同布局),0x3C 起为标准 RDM 应答消息 +(SubSC=01, msgLen@0x3D, dest=ff×6, src=灯具 UID…)。即 Butler 以 1084 信封转发 RDM 应答的另一形态 +(与 69B 小帧 rdm_resp 并存)。 + --- -## 七、AECH 认领(已破解) +## 七、AECH 认领质询帧 552B(主机→Butler,功能全破解) -`G(x) = x ^ TABLE[(x%32 + 1 + (x*13)%30) % 32]`;nonce/B12 主机自选随机;AERE 应答 @0xC0 == G(nonce)。 -真机固件过滤:仅应答源 IP=192.168.123.113 的 AECH。 +type=2, 子类型=05, len=552 (0x228): + +| 偏移 | 含义 | 备注 | +|---|---|---| +| 0x1A-0x1D | 主机 IP(LE 逆序) | | +| 0x1E-0x23 | 主机 MAC | | +| 0x24-0x27 | 00 00 00 00 | | +| 0x28-0x2B | `"AECH"` | | +| 0x2C-0x87 | **不透明区 A(92B)** | 每轮高熵不同;XOR 无周期 → 真随机或密文 | +| 0x88-0x8B | nonce u32 LE | 主机自选,每轮不同 | +| 0x8C-0x8F | B12 u32 LE | 主机自选,会话内恒定(DMX 混淆 rot 基值) | +| 0x90-0x123 | **不透明区 B(148B)** | 同 A | +| 0x124-0x227 | 全 0 填充 | | + +**★消融实证(CLAIM_ZERO=1)**:不透明区 A+B 全清零后认领照样 AERE PASS → +**Butler 只读 nonce/B12/头部,240B 不透明区不校验**(疑为 SL 软件侧证书/许可 blob,与 Butler 无关)。 +→ 认领帧可零模板纯自构造(不透明区填 0 即可)。 + +## 七b、AERE 认领应答 552B(Butler→主机,结构破解) + +type=2, 子类型=06, len=552,同会话多轮/跨时段 diff 仅 0xC0-0xC3 + 配置块计数器变化: + +| 偏移 | 含义 | +|---|---| +| 0x1A-0x1D | Butler IP(LE 逆序)= 192.168.123.1 | +| 0x1E-0x23 | Butler MAC = 00 16 1c f1 2e 39 | +| 0x24-0x27 | 00 71 7b a8(主机 IP 前 3 字节回显?待跨主机验证) | +| 0x28-0x2B | `"AERE"` | +| 0x2C-0x2E | 59 bb 36 = 主机 MAC 低 3 字节(回显?) | +| 0x2F-0x32 | 7f ff ff fd/ff = 端口/状态位掩码(跨时段微变) | +| 0x33-0x38 | **灯具 UID**(首个应答者;跨时段见 010734880900 / 010734670125) | +| 0x39-0x3C | 00 15 00 0a(与发现应答同位置同值) | +| 0x40-0x4E | 网络块:01 \| Butler IP \| 掩码 \| Butler MAC \| 00 | +| 0x59-0x63 | 1a 00 00 00 01 01 fa 0c 00 02 00 | +| 0xAC-0xC8 | Butler 配置块:`00 01 00 00 00 / 00 00 00 00 00 00 00 1c 00 1c 00 1d 00 1d / 00 1c 00 1c 00 f0 0a f0 0a`(ctr=慢变计数器 0x23→0x24;0x1c=28/0x1d=29 疑固件版本;0x0af0=2800 ×2) | +| 0xC0-0xC3 | **resp = G(nonce)**(认领验证值) | +| 0xC4-0xC8 | 00 f0 0a f0 0a | +| 其余 | 全 0 | + +## 七c、announce 644B(主机→广播,结构破解) + +type=1, 子类型=15, len=644 (0x284): +- 0x3A-0x3D = 1a 00 00 00 (26) +- 0x40-0x4E = 主机网络块:01 | 主机IP(LE) | 掩码 | 主机MAC | 00 +- 0x162-0x175 = 会话 GUID 样随机 20B(announce_live 为全 0) +- 0x176-0x17A = 日期头:年 u16LE(0x07ea=2026) + 月 + 日(部分) +- 0x17D-0x1F7 = **UTF-16BE 字符串:SL show 文件路径**(`C:\Users\\AppData\Local\ecue\SYMPHOLIGHT 5.5\CurrentShow`,announce_live 为全 0) +- 0x211-0x215 / 0x219-0x21C / 0x221-0x225 = 3 组 5B 会话随机 +- 0x239-0x241 = **完整时间戳**:年 u16LE, 月, 日, 时, 分, 秒, 毫秒 u16LE(announce_live 解码得 22:52:50.992 与 SL 会话时间吻合 ✓) +- 0x25C-0x25D = 临时端口 u16LE(每会话不同:7200 / 2200) +- 0x268 / 0x274 / 0x27C / 0x280 = 4 个使能 flag(01 00 00 00) --- @@ -144,15 +217,13 @@ type=1, len=180 (0xb4):0x1A 起为主机名 ASCII(如 "tony"),其余字 | 项 | 状态 | |---|---| -| 1947 端口 40B 许可令牌 | 暖会话下**非必需**(当前 show 不发也工作);Butler 冷启动/重启后是否必需未验证 | -| EUCE 6s 广播 | 暖会话下**非必需**(同上);540B 结构未解码 | -| bind52 / ident102 | 暖会话下**非必需**(最小认领 announce144→AECH 即可);bind flag ffff/0000 语义未知 | -| 217B 发现应答 | 仅知含 "Butler S2" 名 + MAC,全字段映射未解码 | -| 644B announce | 除 timestamp@0x235 / 会话ID@0x21D / 主机IP@0x41 外大量字段未解码 | -| 552B AECH 认领 | 仅 IP@0x1A / nonce@0x88 / B12@0x8C 已知,其余未解码 | -| 552B AERE 应答 | resp@0xC0=G(nonce) 已验证;0xB0-0xBF 重复值对、0xC5 起 0x0af0 重复,语义未知 | -| 42B 事件码 0x0318/0x0062 语义 | 未解码 | -| Butler 冷启动是否必须完整前奏(45s 广播+bind+ident+EUCE+1947) | 未验证 | +| 1947 端口 40B 许可令牌 | 固定 ASCII 常量 `BlZRQphAGxUbi7at8KEfmdZ7TdSB03b+qoC1RIA`,SL 每 ~34s 广播 21 帧双目标;语义未知(常量本身排除与狗绑定);暖会话**非必需**;冷启动必需性未验证 | +| 42B 事件码 0x0318(ack)/0x0062(status) 语义 | 结构已解,语义未解码 | +| bind52 flag ffff/0000 语义 | 结构已解(主机+设备 IP/MAC, 0x64/0x01f4 疑速率参数);flag 疑绑定/解绑;暖会话**非必需** | +| AERE 0x24-0x27 / 0x2C-0x2E 回显字段 | 疑主机 IP/MAC 部分回显,需跨主机抓包证实 | +| AERE/发现应答配置块细节(0x1c/0x1d/0x0af0) | 疑固件版本/端口配置,语义未坐实 | +| claim 不透明区 240B 真实内容 | Butler 不校验(已消融证实);疑 SL 软件侧证书/许可 blob,与协议功能无关 | +| Butler 冷启动是否必须完整前奏(45s 广播+bind+ident+EUCE+1947) | 未验证(最有价值的遗留实验:Butler 断电重启后跑 show) | | NFM680S 通道数漂移(曾见 2ch/1ch) | 近期稳定 1ch,疑早期解析误判;未复现深究 | | announce 时间戳/会话 ID 是否参与校验 | 按实测填充新鲜值即可;严格性未消融 | @@ -160,6 +231,7 @@ type=1, len=180 (0xb4):0x1A 起为主机名 ASCII(如 "tony"),其余字 - ~~RDM 变长起始~~ → 固定 0x3C - ~~会话级帧校验~~ → 不存在 - ~~0x2E 严格校验~~ → 不校验(1/2 均接受) +- ~~claim 帧 240B 高熵区被 Butler 校验~~ → 清零照样 PASS --- diff --git a/src/analyze.rs b/src/analyze.rs index 60c12ad..2489ca3 100644 --- a/src/analyze.rs +++ b/src/analyze.rs @@ -238,6 +238,29 @@ pub fn run(args: &[String]) { } if diffs.is_empty() { println!("两版逐字节一致"); } } + _ if args[0] == "xor" => { + // analyze xor [start] [end] — 逐字节 XOR + 周期性分析 + let a = load_hex(&args[1]); + let b = load_hex(&args[2]); + let pf = |s: &String| usize::from_str_radix(s.trim_start_matches("0x"), 16).unwrap(); + let start = args.get(3).map(|s| pf(s)).unwrap_or(0); + let end = args.get(4).map(|s| pf(s)).unwrap_or(usize::MAX); + let n = a.len().min(b.len()).min(end); + let x: Vec = (start..n).map(|i| a[i] ^ b[i]).collect(); + println!("XOR {} ^ {} [0x{:x}..0x{:x}):", args[1], args[2], start, n); + let mut r = 0; + while r < x.len() { + let e = (r + 16).min(x.len()); + println!(" 0x{:03x}: {}", start + r, hx(&x[r..e])); + r = e; + } + println!("周期性 (x[i]==x[i+p] 比例, 随机基线~0.4%):"); + for p in [1usize, 2, 4, 8, 16, 20, 24, 28, 32, 40, 44, 48, 64] { + if x.len() <= p { continue; } + let m = (0..x.len() - p).filter(|&i| x[i] == x[i + p]).count(); + println!(" p={}: {}/{} ({:.1}%)", p, m, x.len() - p, 100.0 * m as f64 / (x.len() - p) as f64); + } + } _ => println!("未知 analyze 子命令 {}", args[0]), } } diff --git a/src/hostdemo.rs b/src/hostdemo.rs index fa451b4..61cba8d 100644 --- a/src/hostdemo.rs +++ b/src/hostdemo.rs @@ -1115,6 +1115,12 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) { ch[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]); ch[0x88..0x8C].copy_from_slice(&nonce.to_le_bytes()); ch[0x8C..0x90].copy_from_slice(&b12c.to_le_bytes()); + // CLAIM_ZERO=1: 清零不透明区 (0x2C-0x87, 0x90-0x123), 消融验证 Butler 是否校验 + if std::env::var("CLAIM_ZERO").unwrap_or_default() == "1" { + for i in 0x2C..0x88 { ch[i] = 0; } + for i in 0x90..0x124 { ch[i] = 0; } + println!("CLAIM_ZERO: 不透明区已清零 (240B)"); + } sock.send_to(&ch, &dst).unwrap(); let w2 = Instant::now(); while w2.elapsed() < Duration::from_millis(700) {