极域 V6.0 连续投屏与远程操控协议逆向
# 从黑盒到可交互画面:极域 V6.0 连续投屏与远程操控协议逆向> 本文记录 `2026.07.31 研究预览版`中连续屏幕观看与远程操控链路的逆向过程。结论来自 IDA Pro 静态分析、官方教师端/学生端真实流量对照、逐字节重放和运行日志验证。本文只讨论隔离实验环境中的协议研究,不应被用于未经授权的监控或控制。
## 摘要
极域 V6.0 的“观看学生屏幕”和“远程控制学生”不是一个简单的 UDP 命令,也不是把截图循环发送回来。完整链路至少跨越三类通道:
1. 教师端先经 `5512/UDP` 的 `MESS feature=8` 通知学生端启动桌面发送功能;
2. 教师端连接学生端 `4806/TCP`,发送 `SHCO` 选择 UMSP channel 10,再从 `TKPC` 中重组 `HHRF`/`HJRF` 桌面记录;
3. 进入控制模式时,教师端再用 `COMD` 事务承载 `MCMD`,让学生端在指定 UDP 端口启动模拟输入接收器;鼠标和键盘事件通过固定 28 字节输入包发送。
截至当前版本,连续观看、H.264/JPEG 解封装、色度修正、`SPUC` 光标解析以及远程鼠标移动/点击已经在测试环境验证。键盘只验证到教师端捕获和 `kind=16` 发包,学生端是否最终完成 `SendInput` 仍不稳定,因此仍是实验性功能。
| 能力 | 当前证据 |
|---|---|
| 4806/TCP 建连与 SHCO 选路 | 已验证 |
| TKPC 分片重组 | 已验证 |
| HHRF/H.264、HJRF/JPEG 解析 | 已验证 |
| U/V 色度平面修正 | 已验证 |
| SPUC 远端光标解析 | 已验证 |
| MCMD mode=2 建立控制 | 已验证 |
| 28 字节鼠标包 | 移动与点击已验证 |
| kind=16 键盘包 | 捕获与发包已验证,学生端执行未稳定验证 |
---
## 一、先建立正确的系统模型
最初最容易犯的错误,是把“投屏”理解成缩略图协议 `LANT/TNAL` 的高频版本。抓包很快就能否定这个假设:点击官方教师端的“监看”后,学生端除了继续保活,还会开放或使用 `4806/TCP`;真正的连续画面主要出现在这条 TCP 流中。
远程控制同样不是直接向 `4705/UDP` 塞一个 `WM_MOUSEMOVE`。官方流程会先复用已经建立的桌面观看会话,然后通过通用命令事务启动一个独立的模拟输入通道。
```mermaid
sequenceDiagram
participant T as 第三方教师端
participant S as 极域学生端
T->>S: TCP connect probe :4806(只探测)
T->>S: MESS feature=8 / start(5512/UDP)
T->>S: TCP connect :4806
T->>S: SHCO / channel 10
S-->>T: TKPC / channel 10 / 桌面分片
S-->>T: HHRF(H.264) 或 HJRF(JPEG)
T->>S: COMD transaction → MCMD mode=2
Note over S: BeginSimulate,监听指定 UDP 端口
T->>S: 28-byte mouse input
S-->>T: SPUC cursor record
T->>S: 28-byte kind=16 keyboard input(实验性)
```
这个模型解释了几个看似无关的现象:
- 只发送 feature 8,不连接 TCP,不会得到可显示的连续画面;
- 直接发输入包时,学生端没有对应 UDP 接收器,数据只会被操作系统丢弃;
- 从 `view` 切换到 `control` 时重启 H.264 解码器,容易永久黑屏,因为新解码器没有收到此前的 SPS/PPS/IDR;
- 本地鼠标与学生端鼠标短暂错位并非只有坐标算法问题,还包含编码、网络、显示和 `SPUC` 回传延迟。
---
## 二、在 IDA 中如何找到入口
### 2.1 不要只搜索可见字符串
这类老式 C/C++ 网络库常把 FourCC 写成 32 位立即数。文件里的字节顺序是人眼可读的 `SHCO`,反汇编中却可能出现:
```text
wire bytes: 53 48 43 4F-> "SHCO"
IDA imm32 : 0x4F434853
wire bytes: 54 4B 50 43-> "TKPC"
IDA imm32 : 0x43504B54
```
原因是 x86 使用小端序。仅在 Strings 窗口里搜索 `SHCO` 很可能什么也找不到,应该同时搜索对应的立即数,然后查看交叉引用。
本次逆向中最有效的一组锚点是:
| 线索 | 作用 |
|---|---|
| `0x4F434853` | 定位 UMSP channel 选择包 `SHCO` |
| `0x43504B54` | 定位 UMSP 数据包 `TKPC` |
| `0x46524848` | 定位 `HHRF` H.264 桌面记录 |
| `0x46524A48` | 定位 `HJRF` JPEG 桌面记录 |
| `MCMD` | 定位监看/控制模式切换结构 |
| `SPUC` | 定位光标状态记录 |
| `0x2321347C` | 定位模拟输入包分发器 |
| 常量 `4806`、channel `10` | 从通用传输库缩小到桌面通道 |
### 2.2 从网络 API 反推对象边界
打开 `LibBaseTrans.dll` 后,先建立导入函数关系:`connect`、`send`、`recv`、`WSARecv`、线程创建、临界区和缓冲区复制。不要急着给每个函数命名,先回答三个问题:
1. 哪个对象持有 socket?
2. 哪个循环读取 12 字节头并按长度字段切包?
3. 哪个分支在 payload 起始位置读取 channel id?
从 `0x43504B54` 的交叉引用向上回溯,可以看到一个典型的“版本 + magic + payload length”检查;继续向下跟踪,payload 的首个 `u32` 被当作 channel。channel 10 随后进入桌面数据处理对象,而其他 channel 被分派给不同业务。
在 IDA 中可以先做以下重命名,之后伪代码会清晰很多:
```text
sub_xxx_connect -> umsp_connect
sub_xxx_read_loop -> umsp_recv_loop
sub_xxx_dispatch -> dispatch_umsp_channel
sub_xxx_channel10 -> handle_desktop_fragment
sub_xxx_record -> handle_desktop_record
```
名字不必一开始就完全正确。关键是让“传输层、分片层、记录层、编解码层”不再混在同一张调用图里。
### 2.3 从 feature 分发器找到 617 字节参数块
在学生端的 MESS 处理路径中,feature id `8` 会进入远程观看分支。这个分支不是只读一个布尔值,而是复制一个较大的参数对象。把官方教师端报文和伪代码中的复制长度对照后,可以确定:
- 内层 feature payload 总长为 617 字节;
- 前 12 字节是 `length / feature / flags`;
- 中间 604 字节是远程观看参数;
- 最后还有 1 字节尾部填充。
不要把参数块里的端口直接当成学生端 `4806/TCP`。其中两个端口属于教师会话/桌面监控端点;学生端的 TCP 服务端口是另一层连接目标。这两个概念混用,会构造出长度正确但状态错误的请求。
### 2.4 从 MCMD 跟到模拟输入
`MCMD` 位于通用 `COMD` 事务内部。沿 `MCMD` 分支继续跟踪,可以看到 mode 分派以及模拟输入初始化路径。当前观察到的行为是:
- `mode=0`:Share,双方可操作;
- `mode=2`:Control Student,启动模拟输入并额外进入学生端本地输入锁定路径;
- `mode=1`:本项目用于停止当前模拟输入会话。
再从输入接收线程向下跟踪,固定 magic `0x2321347C` 是很强的定位点。分发器读取紧随其后的 `kind`,然后按鼠标或键盘结构解释剩余 20 字节。
逆向时必须继续跟到系统调用边界。看到 UDP `recvfrom` 并不表示控制已经生效,真正需要确认的是后续是否构造 `INPUT`、是否调用 `SendInput`、调用发生在哪个 Windows Session/desktop,以及本地 Hook 是否会再次拦截注入事件。
---
## 三、用抓包给反汇编“定类型”
IDA 告诉我们代码可能怎样解释内存,抓包则告诉我们线上实际发送了什么。两者必须互相约束。
建议只在隔离测试网中采集,并把一次操作拆成单变量实验:
1. 只打开“观看屏幕”,不移动鼠标;
2. 只切换到“远程控制”,不输入;
3. 只移动一次鼠标;
4. 只按一次左键;
5. 只按一个普通字母键;
6. 分别停止控制和停止观看。
Wireshark 初始过滤器:
```text
udp.port == 4705 || udp.port == 5512 || tcp.port == 4806
```
找到对应 TCP stream 后,再单独查看:
```text
tcp.stream eq <n>
```
抓包分析时不要依赖 TCP segment 边界。一次 `recv` 可能拿到半个 UMSP 包,也可能一次拿到多个包。正确做法是维护流缓冲区,只有当缓冲区至少包含 12 字节头,并且达到 `12 + payload_len` 后才取出一包。
### 3.1 差分比对比猜字段更快
对 MCMD 和输入包尤其适合做差分:固定所有条件,只改变一个操作。例如把鼠标从画面左上移动到右下,比较两包中变化的 4 字节字段;连续点击左键,观察 `0x0201` 与 `0x0202` 的成对出现;滚轮则会让 data 字段变化。
字段命名按证据逐步升级:
```text
unknown_08
-> changes_with_x
-> normalized_x
-> normalized_x_u16_range_in_u32
```
不要因为某次值恰好等于屏幕像素,就立即命名为 `pixel_x`。本协议鼠标坐标实际使用 `0..65535` 归一化范围,窗口尺寸和远端编码尺寸只是映射输入。
---
## 四、观看请求:MESS feature 8
当前实现构造的 feature payload 如下。所有多字节整数均为小端;IP 地址保持网络序原始字节。
### 4.1 feature 8 内层结构
| 偏移 | 大小 | 当前解释 | 观察值 |
|---:|---:|---|---:|
| `0x000` | 4 | payload length | `617` |
| `0x004` | 4 | feature id | `8` |
| `0x008` | 4 | start flags | `0x80000000` |
| `0x00C` | 604 | remote-view params | 见下表 |
| `0x268` | 1 | tail padding | `0` |
604 字节参数块中,已经通过官方流量和代码路径确认或稳定观察到的字段:
| 参数内偏移 | 大小 | 含义 | 当前值 |
|---:|---:|---|---:|
| `+0x00` | 4 | RemoteWithVoice | `0` |
| `+0x04` | 4 | 教师 IP | network order |
| `+0x08` | 2 | 教师会话端口 | 当前频道端口 |
| `+0x0A` | 4 | 教师 IP(第二端点) | network order |
| `+0x0E` | 2 | 桌面监控端口 | 当前频道端口 |
| `+0x24` | 4 | NetworkType | `1` |
| `+0x28` | 4 | ShowMonitorControlMessage | `1` |
| `+0x2C` | 4 | MaxSendSpeed | `20480` |
| `+0x30` | 4 | RepairMode | `1` |
| `+0x34` | 4 | MaxPacketSize | `1440` |
| `+0x38` | 4 | FrameLimit | `25` |
| `+0x3C` | 4 | CaptureQuality | `75` |
| `+0x40` | 4 | TcpCommMode | `1` |
| `+0x44` | 4 | 学生 IP | network order |
| `+0x48` | 12 | 未完全命名的三元组 | `5, 12, 16` |
最后一组三元组虽然已经稳定复现,但在没有进一步交叉引用证据前不应强行解释其业务名称。保留“已知偏移 + 已知值 + 未知语义”比写一个听起来合理的名字更有研究价值。
停止观看使用另一种短结构:
```text
u32 length= 13
u32 feature = 0
u32 flags = 0
u8tail = 0
```
### 4.2 正确启动时序
当前稳定时序是:
```text
TCP connect probe 4806(立即关闭,不发 SHCO)
-> MESS feature 8 start
-> 单次连接 4806/TCP
-> 立即发送 SHCO channel 10
-> 从第一条 H.264 记录开始喂给同一个解码器
```
探测步骤只检查 TCP 是否监听,不启动编码器,也不发送 `SHCO`。正式启动后不能在失败时高频重试:学生端编码器和内部线程存在状态,重复 feature 请求或并发 TCP 会话可能导致学生端异常退出。当前实现限制每个 IP 同时只有一个观看会话,并在失败后冷却 60 秒。
---
## 五、UMSP:SHCO、TKPC 与 channel 10
### 5.1 SHCO 选择包
连接 `4806/TCP` 后,教师端发送 20 字节选择包:
| 偏移 | 类型 | 值 |
|---:|---|---:|
| `0x00` | `u32` | version `0x00010000` |
| `0x04` | `u32` | `0x4F434853`,线上为 `SHCO` |
| `0x08` | `u32` | body length `8` |
| `0x0C` | `u32` | selector/op,观察值 `1` |
| `0x10` | `u32` | desktop channel `10` |
这里的 channel 10 是业务复用编号,不是 TCP/UDP 端口。
### 5.2 TKPC 数据包
学生端返回的 UMSP 包使用 12 字节公共头:
| 偏移 | 类型 | 说明 |
|---:|---|---|
| `0x00` | `u32` | version,预期 `0x00010000` |
| `0x04` | `u32` | `0x43504B54`,线上为 `TKPC` |
| `0x08` | `u32` | payload length |
| `0x0C` | `u32` | channel id,桌面为 `10` |
| `0x10` | bytes | channel payload |
代码或符号中可能把它写作 `TCPK`,但线上字节是 `TKPC`。逆向协议时应始终区分“源码/反编译变量名”和“wire bytes”,否则 Wireshark 搜索与代码常量会互相对不上。
---
## 六、桌面分片重组
channel 10 的 payload 不是直接的 H.264 NAL,而是先经过一层 12 字节桌面分片:
```text
+0x00u32 frame_seq
+0x04u32 fragment_offset
+0x08u32 record_total_size
+0x0Cbytes fragment_data
```
重组器按 `frame_seq` 保存状态,以 `record_total_size` 分配缓冲区,然后把每个 fragment 写入指定 offset。实现时至少要处理:
- TCP 读取边界与 UMSP 包边界不一致;
- 同一分片重复到达;
- 分片乱序;
- 新 frame_seq 到来时旧帧仍未完成;
- offset 或 length 越界;
- 恶意或损坏的 total size 导致大内存分配。
当前实现给每帧维护一份 `seen` 位图,只把第一次出现的字节计入 `received`,避免重复分片使计数提前达到 total。最多同时保留四个未完成帧,并将桌面记录大小限制在 `0x241800`。
一个重要踩坑是:并非所有完整记录都大于 64 字节。`SPUC` 光标记录只有 16 字节。如果在分片层直接用“total < 64”判定非法,就会把光标状态误报为坏帧。正确做法是先完成通用重组,再在记录层按 magic 和长度分流。
---
## 七、HHRF/HJRF 桌面记录
视频类记录包含一个 64 字节头:
| 偏移 | 大小 | 说明 |
|---:|---:|---|
| `0x00` | 4 | magic:`HHRF` / `HJRF` / `HMRF` |
| `0x04` | 4 | declared record size |
| `0x08` | 4 | timestamp |
| `0x0C` | 16 | encoded rect:left/top/right/bottom |
| `0x1C` | 16 | visible rect:left/top/right/bottom |
| `0x2C` | 4 | frame type |
| `0x30` | 16 | 尚未完全命名的字段 |
| `0x40` | variable | 编码 payload |
magic 对应关系:
```text
0x46524848 -> wire "HHRF" -> H.264 Annex-B
0x46524A48 -> wire "HJRF" -> JPEG
0x46524D48 -> wire "HMRF" -> legacy/兼容记录
```
日志中的一个真实样本为:
```text
H264 frame=1 seq=11 record=623 declared=623
encoded=(0,0,1728,928) visible=(0,0,1718,918)
```
这说明编码尺寸和实际可见尺寸并不总相同。直接按 encoded rect 展示会在边缘出现填充区域,所以解码后还需要按 visible rect 裁剪。
### 7.1 为什么请求真实 H.264 时容易“黑屏”
H.264 是有状态码流。解码器通常需要先看到 SPS、PPS 和 IDR,才能解释后续 P/B 帧。下面几种做法都会造成只有数据、没有画面:
- TCP 建连太晚,错过码流初始化;
- 先用一个播放器观看,切换 control 时杀掉播放器再启动另一个;
- 把多次观看留下的半段 `.h264` 直接拼在一起;
- 将 UMSP/桌面记录头误写进 H.264 payload;
- 按 TCP recv 块直接喂解码器,破坏了上层记录边界。
当前实现从第一帧开始固定使用 bundled FFmpeg,`view -> control` 只改变窗口交互状态,不重启解码进程。可选裸流也按会话时间戳独立保存,避免把上一次的不完整码流接到新会话前面。
### 7.2 为什么画面颜色会错
拿到画面后,最明显的问题曾是整体色相不对:本应为蓝色的区域显示成另一种颜色。几何内容正确、亮度基本正确而色相系统性错误,通常不是 RGB/BGR 顺序问题,而是 YUV 色度平面顺序问题。
对同一帧分别尝试常见转换后,可以确认极域这条 H.264 链路的 U/V 平面需要交换。FFmpeg 过滤器为:
```text
shuffleplanes=0:2:1:3
```
然后再按可见尺寸裁剪:
```text
shuffleplanes=0:2:1:3,crop=<visible_w>:<visible_h>:0:0
```
这里选择在显示管线修正,而不改写保存的 H.264 裸流。这样原始证据仍可用于后续协议分析,同时实时窗口显示正确颜色。
---
## 八、SPUC:远端光标不是画面的一部分
光标状态使用独立的 16 字节记录:
| 偏移 | 类型 | 说明 |
|---:|---|---|
| `0x00` | 4 bytes | `SPUC` |
| `0x04` | `u32` | record size,预期 `16` |
| `0x08` | `u32` | timestamp |
| `0x0C` | `u32` | packed position |
位置打包方式:
```text
x = packed_position & 0xFFFF
y = packed_position >> 16
```
实际控制窗口中有三个“鼠标位置”:
1. 本机系统指针;
2. 刚刚发送给学生端的预测位置;
3. 学生端通过 `SPUC` 回传的确认位置。
如果三者同时显示,用户会看到两个指针互相追赶。因此投屏区域隐藏本机系统指针;发送鼠标事件后的短时间内显示本地预测位置,约 180 ms 后切回 `SPUC` 确认位置。这个策略不能消除网络和编码延迟,但能减少操作感上的跳变。
画面显示可能经过缩放,坐标必须基于实际图像区域,而不是整个窗口或屏幕:
```text
normalized_x = round(local_x * 65535 / (display_width- 1))
normalized_y = round(local_y * 65535 / (display_height - 1))
```
预测光标绘制回源画面时,再从 `0..65535` 映射到 encoded width/height。忽略这两次映射,会在窗口缩放后产生越来越明显的位置偏差。
---
## 九、远控建立:COMD 事务中的 MCMD
`MCMD` 不能作为裸 UDP 报文发送,它是通用 `COMD` transaction 的 payload。COMD 外层负责版本、事务 GUID、超时、发送者和 payload 长度;学生端先进入事务分发器,再把 MCMD 交给监看/控制模块。
当前 MCMD 内层固定为 53 字节:
| 偏移 | 大小 | 说明 |
|---:|---:|---|
| `0x00` | 4 | inner length `53` |
| `0x04` | 4 | feature/category `8` |
| `0x08` | 4 | flags `0` |
| `0x0C` | 4 | magic `MCMD` |
| `0x10` | 4 | mode:`0` / `1` / `2` |
| `0x14` | 4 | 学生 IP,网络序字节 |
| `0x18` | 2 | 学生端输入监听 UDP 端口 |
| `0x1A` | 27 | 保留/零填充 |
项目默认从 `49152..65535` 随机选择一个高位端口。使用固定端口虽然方便抓包,但学生端异常退出后旧线程或状态可能仍占用它;随机端口更适合反复调试。
MCMD 发出后不能立即发送第一条输入。学生端需要创建接收线程并完成 `BeginSimulate` 初始化。当前实现保守等待约 350 ms;这不是协议字段,而是从实际时序中得到的工程性保护。
为了避免学生端进入“有控制状态、没有画面会话”的不完整状态,`control` 只在 4806/TCP 已经连接后发送 MCMD。停止时发送 `mode=1` 并关闭本地 UDP sender。
---
## 十、28 字节模拟输入包
所有输入事件使用相同外壳:
```text
+0x00u32 magic = 0x2321347C
+0x04u32 kind
+0x08u8payload
total = 28 bytes
```
线上 magic 的小端字节是:
```text
7C 34 21 23
```
固定总长非常重要。多一个字节或少一个字节都可能让学生端接收线程丢包,或者让后续字段错位。
### 10.1 鼠标:kind=1
鼠标 payload 是五个 `u32`:
| payload 偏移 | 字段 | 说明 |
|---:|---|---|
| `+0x00` | message | Windows 鼠标消息 |
| `+0x04` | x | `0..65535` 归一化坐标 |
| `+0x08` | y | `0..65535` 归一化坐标 |
| `+0x0C` | data | 滚轮等附加数据 |
| `+0x10` | swapped | 键位交换/保留状态 |
已验证的 message:
| 动作 | 值 |
|---|---:|
| move | `0x0200` |
| left down / up | `0x0201` / `0x0202` |
| right down / up | `0x0204` / `0x0205` |
| middle down / up | `0x0207` / `0x0208` |
| wheel | `0x020A` |
鼠标移动和点击在当前学生端环境已经观察到实际执行结果,因此证据链不仅停留在“教师端 sendto 成功”。
### 10.2 键盘:kind=16,仍是实验性
当前根据反汇编字段访问和教师端行为构造的 20 字节 payload:
| payload 偏移 | 类型 | 说明 |
|---:|---|---|
| `+0x00` | `u16` | scan code |
| `+0x02` | `u16` | virtual key |
| `+0x04` | `u32` | flags |
| `+0x08` | 12 bytes | zero/reserved |
当前 flags:
```text
bit 0 = extended key
bit 7 = key up
```
普通按下为 `flags=0`,释放为 `flags=0x80`;扩展键按下/释放再或上 `0x01`。
Tk 的键盘事件会受输入法和焦点影响,尤其在中文输入法状态下,部分按键不会按预期到达 Tk。因此控制窗口额外使用 `WH_KEYBOARD_LL` 获取 Windows 提供的 VK、扫描码、扩展键标志以及 key down/up,并拦截 Alt、Windows 键,防止这些键先作用于教师机本地桌面。
但是,“Hook 日志出现”和“学生端收到有效键盘输入”之间仍有多层边界。当前只能确认:
```text
本地物理按键
-> WH_KEYBOARD_LL 捕获成功
-> kind=16 的 28 字节包构造成功
-> UDP sendto 成功
-x-> 学生端解析/SendInput/目标应用收到字符:尚未稳定证明
```
可能影响学生端最终执行的因素包括:学生端所在 Windows Session、当前 input desktop、UAC 安全桌面、接收线程权限、学生端本地 Hook、扫描码/VK 组合、扩展键标志以及注入事件过滤。没有接收端 ACK 时,教师端日志不能跨越这些边界作结论。
---
## 十一、如何判断“真的成功”,而不是日志看起来成功
逆向远控最危险的误区是把 `sendto()` 返回成功当成远端执行成功。建议把证据拆成六级:
| 等级 | 证据 | 鼠标 | 键盘 |
|---:|---|---|---|
| A | 教师端 UI/Hook 捕获事件 | 已验证 | 已验证 |
| B | 教师端构造并发送正确长度报文 | 已验证 | 已验证 |
| C | 抓包看到报文到达学生端 | 已验证 | 需持续复核 |
| D | 学生端接收器进入对应 kind 分支 | 已验证 | 尚缺稳定动态证据 |
| E | 学生端系统注入 API 成功 | 已验证实际效果 | 未稳定验证 |
| F | 目标桌面/应用收到输入 | 已验证 | 未稳定验证 |
如果要继续研究键盘,下一步不应继续盲改教师端字段,而应在授权测试机上对学生端接收分支做动态观测:
1. 在 `recvfrom` 返回后记录 28 字节缓冲区;
2. 确认 magic、kind 和 payload 分支;
3. 在 `SendInput` 前检查构造出的 `INPUT`;
4. 记录 `SendInput` 返回值和 `GetLastError`;
5. 确认线程所属 Session 与当前 input desktop;
6. 最后再观察目标窗口是否收到键盘消息。
只有走完这条链,才能判断问题究竟是协议字段错误,还是 Windows 输入隔离与 Hook 策略导致。
---
## 十二、几个真正耗时的坑
### 12.1 把 4806 与 feature 参数端口混为一谈
feature 参数里的教师端点不等于学生端 TCP 监听端口。包长和 IP 都正确时,这种错误特别难发现,因为学生端可能进入部分状态,却不产生正确桌面流。
### 12.2 按 recv 边界解析 TCP
本地网络里一次 recv 经常“刚好”是一包,导致错误解析器看似可用。一旦包被拆分或合并,就出现随机 magic、超大 length 和画面断流。TCP 必须按字节流处理。
### 12.3 把 16 字节 SPUC 当坏视频帧
早期解析器假设桌面记录至少 64 字节,日志中不断出现 `total=16` 的“非法帧”。保留这批原始数据并查看 magic 后,才发现它是独立光标记录。
### 12.4 在码流中途更换播放器
`view` 能显示、`control` 一切换就卡在“等待画面”,根因不是 MCMD,而是切换时重启了解码器。H.264 参数集已经在流开头发过,新解码器只能等待下一次可恢复点。
### 12.5 把颜色问题误判为 RGB/BGR
如果交换 R/B 后肤色和亮度仍不合理,应检查 YUV 平面。当前链路需要交换 U/V,而不是在最终 RGB 图像上交换红蓝通道。
### 12.6 本地指针、预测指针和远端指针混在一起
鼠标“不同步”不一定是包没生效。若本机系统指针可见,同时又绘制预测位置和 `SPUC` 位置,视觉上必然出现错位。隐藏本地指针并定义明确的预测回退窗口后,问题才可测量。
### 12.7 用错误的 COMD 边界调 MCMD
MCMD 是通用事务的 payload,不是 COMD 应用命令里的任意 cmdId。外层长度、GUID、事务字段或 payload offset 错一处,学生端通用分发器就可能走入完全不同的命令分支。调试时若出现与鼠标无关的窗口或系统提示,应先停止发送,重新核对事务边界,而不是继续猜键码。
---
## 十三、实现中的防崩溃与可观测性设计
协议能跑通不等于实现足够稳。当前版本加入了以下约束:
- `view_probe` 只做 TCP connect 探测,不发送 feature 8 或 SHCO;
- 每个学生 IP 同时只允许一个 RemoteViewSession;
- 正式观看只进行一次 TCP 连接,不循环轰炸学生端;
- 连接/接收失败后发送停止 feature,并进入 60 秒冷却;
- UMSP payload 和桌面 record 都有长度上限与越界检查;
- 未完成分片数量受限,重复字节不重复计数;
- H.264 原始流按会话独立保存;
- `control` 必须等待 TCP 会话 connected event;
- MCMD 后等待接收线程初始化,再发送首个输入包;
- 远控可用 `TEACHER_ENABLE_REMOTE_CONTROL=0` 紧急禁用;
- 日志区分“捕获”“发包”“远端 SPUC 反馈”,避免把不同证据混成一句成功。
建议保留的关键日志字段:
```text
feature start / stop
TCP connected, SHCO channel
frame_seq / offset / total
record magic / declared size / rect / frame type
MCMD mode / UDP endpoint
input count / kind / exact 28-byte hex
SPUC x / y / timestamp
```
日志必须限频。鼠标 move 可能达到每秒上百次,全量 INFO 不仅难读,还会反过来增加延迟;当前实现只详细记录前几包、关键按键和周期性样本。
---
## 十四、当前结论与下一步
连续投屏链路已经形成闭环:
```text
IDA 定位 feature 8 和 UMSP
-> 抓包确认 SHCO/TKPC/channel 10
-> 重组桌面分片
-> 识别 HHRF/HJRF/SPUC
-> FFmpeg 解码、U/V 修正、visible rect 裁剪
-> Pillow/Tk 内嵌交互窗口
```
远程鼠标也形成了从 MCMD 建立接收器到 28 字节输入包、学生端实际动作、SPUC 位置反馈的闭环。
键盘尚未闭环。当前研究最重要的结论不是“再换一个键码试试”,而是已经明确缺失的证据位于学生端接收分支到 Windows 输入桌面之间。下一阶段应优先补动态观测和返回值证据,再决定是否修改 kind=16 字段。
逆向协议真正有价值的部分,不是记住几个 magic 和 offset,而是把每个结论绑定到可重复的证据:反汇编交叉引用说明代码如何读,抓包说明线上如何写,日志说明我们的实现走到了哪一层,远端可见行为才说明功能最终生效。
这也是本项目在 README 中坚持区分“已验证”“可用”“部分验证”和“实验性”的原因。
---
## 附录 A:协议层级速查
```text
MESS feature 8 (5512/UDP)
└── 617-byte remote-view payload
4806/TCP
└── UMSP
├── SHCO: select channel 10
└── TKPC
└── channel 10
└── desktop fragment: seq + offset + total
├── HHRF: H.264 Annex-B
├── HJRF: JPEG
├── HMRF: legacy record
└── SPUC: cursor position
COMD transaction (4705/UDP)
└── MCMD mode 0/1/2 + student input UDP port
└── 28-byte input packet
├── kind=1: mouse
└── kind=16: keyboard (experimental)
```
## 附录 B:运行命令
```text
view_probe <ip> # 只探测 4806/TCP
view <ip> # 启动连续观看
control <ip> # 在观看会话上启用远控
control_stop <ip> # 停止输入模拟
view_stop <ip> # 停止桌面流
```
请先执行 `view_probe`,并只在自己拥有管理权限或已获得明确授权的设备上测试。 为啥这个图识别不出来sequenceDiagram
participant T as 第三方教师端
participant S as 极域学生端
T->>S: TCP connect probe :4806(只探测)
T->>S: MESS feature=8 / start(5512/UDP)
T->>S: TCP connect :4806
T->>S: SHCO / channel 10
S-->>T: TKPC / channel 10 / 桌面分片
S-->>T: HHRF(H.264) 或 HJRF(JPEG)
T->>S: COMD transaction → MCMD mode=2
Note over S: BeginSimulate,监听指定 UDP 端口
T->>S: 28-byte mouse input
S-->>T: SPUC cursor record
T->>S: 28-byte kind=16 keyboard input(实验性),补一张截图 之前我一直想研究来着但是无奈当时太小 最多在github找到指令执行的包 感谢大佬分享这个 很好的思路,谢谢分享 感觉 分享、很实用。学习下。 虽然看不懂,但是很高级 学习了,感谢 学习了,感谢楼主 向大佬学习了 厉害啊,要是直接做一个远程操控的软件就更好了
页:
[1]
2