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
This commit is contained in:
OpenCode 2026-09-11 14:49:39 +08:00
parent f6b1f104ef
commit 44ff080ae6
3 changed files with 116 additions and 15 deletions

View File

@ -104,11 +104,11 @@
| 0x25-0x28 | 主机 IP(LE 逆序)= 192.168.123.113 | | 0x25-0x28 | 主机 IP(LE 逆序)= 192.168.123.113 |
| 0x29-0x2E | 主机 MAC = 30 0e d5 59 bb 36 | | 0x29-0x2E | 主机 MAC = 30 0e d5 59 bb 36 |
## 六、42B 事件帧(Butler→主机,部分破解) ## 六、42B 事件帧(Butler→主机,结构破解)
type=4, 子类型=02, len=42 (0x2a), `"Butl"`@0x1A: 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,全破解) ## 六b、心跳 36B(主机→Butler,全破解)
@ -121,12 +121,85 @@ type=2, 子类型=03, len@0x14=36 (0x24):
type=1, len=180 (0xb4):0x1A 起为主机名 ASCII(如 "tony"),其余字段未解码。 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)。 type=2, 子类型=05, len=552 (0x228):
真机固件过滤:仅应答源 IP=192.168.123.113 的 AECH。
| 偏移 | 含义 | 备注 |
|---|---|---|
| 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 <ctr> 01 00 00 00 / 00 <ctr> 00 00 00 <ctr> 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\<user>\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 冷启动/重启后是否必需未验证 | | 1947 端口 40B 许可令牌 | 固定 ASCII 常量 `BlZRQphAGxUbi7at8KEfmdZ7TdSB03b+qoC1RIA`,SL 每 ~34s 广播 21 帧双目标;语义未知(常量本身排除与狗绑定);暖会话**非必需**;冷启动必需性未验证 |
| EUCE 6s 广播 | 暖会话下**非必需**(同上);540B 结构未解码 | | 42B 事件码 0x0318(ack)/0x0062(status) 语义 | 结构已解,语义未解码 |
| bind52 / ident102 | 暖会话下**非必需**(最小认领 announce144→AECH 即可);bind flag ffff/0000 语义未知 | | bind52 flag ffff/0000 语义 | 结构已解(主机+设备 IP/MAC, 0x64/0x01f4 疑速率参数);flag 疑绑定/解绑;暖会话**非必需** |
| 217B 发现应答 | 仅知含 "Butler S2" 名 + MAC,全字段映射未解码 | | AERE 0x24-0x27 / 0x2C-0x2E 回显字段 | 疑主机 IP/MAC 部分回显,需跨主机抓包证实 |
| 644B announce | 除 timestamp@0x235 / 会话ID@0x21D / 主机IP@0x41 外大量字段未解码 | | AERE/发现应答配置块细节(0x1c/0x1d/0x0af0) | 疑固件版本/端口配置,语义未坐实 |
| 552B AECH 认领 | 仅 IP@0x1A / nonce@0x88 / B12@0x8C 已知,其余未解码 | | claim 不透明区 240B 真实内容 | Butler 不校验(已消融证实);疑 SL 软件侧证书/许可 blob,与协议功能无关 |
| 552B AERE 应答 | resp@0xC0=G(nonce) 已验证;0xB0-0xBF 重复值对、0xC5 起 0x0af0 重复,语义未知 | | Butler 冷启动是否必须完整前奏(45s 广播+bind+ident+EUCE+1947) | 未验证(最有价值的遗留实验:Butler 断电重启后跑 show) |
| 42B 事件码 0x0318/0x0062 语义 | 未解码 |
| Butler 冷启动是否必须完整前奏(45s 广播+bind+ident+EUCE+1947) | 未验证 |
| NFM680S 通道数漂移(曾见 2ch/1ch) | 近期稳定 1ch,疑早期解析误判;未复现深究 | | NFM680S 通道数漂移(曾见 2ch/1ch) | 近期稳定 1ch,疑早期解析误判;未复现深究 |
| announce 时间戳/会话 ID 是否参与校验 | 按实测填充新鲜值即可;严格性未消融 | | announce 时间戳/会话 ID 是否参与校验 | 按实测填充新鲜值即可;严格性未消融 |
@ -160,6 +231,7 @@ type=1, len=180 (0xb4):0x1A 起为主机名 ASCII(如 "tony"),其余字
- ~~RDM 变长起始~~ → 固定 0x3C - ~~RDM 变长起始~~ → 固定 0x3C
- ~~会话级帧校验~~ → 不存在 - ~~会话级帧校验~~ → 不存在
- ~~0x2E 严格校验~~ → 不校验(1/2 均接受) - ~~0x2E 严格校验~~ → 不校验(1/2 均接受)
- ~~claim 帧 240B 高熵区被 Butler 校验~~ → 清零照样 PASS
--- ---

View File

@ -238,6 +238,29 @@ pub fn run(args: &[String]) {
} }
if diffs.is_empty() { println!("两版逐字节一致"); } if diffs.is_empty() { println!("两版逐字节一致"); }
} }
_ if args[0] == "xor" => {
// analyze xor <a> <b> [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<u8> = (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]), _ => println!("未知 analyze 子命令 {}", args[0]),
} }
} }

View File

@ -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[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]);
ch[0x88..0x8C].copy_from_slice(&nonce.to_le_bytes()); ch[0x88..0x8C].copy_from_slice(&nonce.to_le_bytes());
ch[0x8C..0x90].copy_from_slice(&b12c.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(); sock.send_to(&ch, &dst).unwrap();
let w2 = Instant::now(); let w2 = Instant::now();
while w2.elapsed() < Duration::from_millis(700) { while w2.elapsed() < Duration::from_millis(700) {