mahumadad/ubox-p4p
GitHub: mahumadad/ubox-p4p
该项目完整逆向了 UBox 智能摄像头的 P4P 协议,并提供了一个无需厂商应用即可直连摄像头的独立 Python 客户端。
Stars: 0 | Forks: 0
# UBox P4P 协议 — 完整的逆向工程与 NAT 穿透分析




对 UBox P4P 协议的完整逆向工程 —— 包含自定义分组密码、基于 KCP 且带有 RDT 成帧的可靠传输,以及将客户端绑定到摄像头的 ICE 风格 P2P NAT 穿透。本仓库提供了一个**独立的 Python 客户端**,无需厂商的手机应用即可直接与摄像头通信。

## 目录
1. [概述](#1-overview)
2. [密码学 — 已破解](#2-cryptography--cracked)
3. [握手与 P2P 穿透](#3-handshake--p2p-traversal)
4. [视频流处理 — KCP + RDT](#4-video-pipeline--kcp--rdt)
5. [NAT 墙与中继行为](#5-the-nat-wall--relay-behavior)
6. [可行的解决方案](#6-working-solutions)
7. [安装与使用](#7-setup--usage)
8. [项目结构](#8-project-structure)
## 1. 概述
```
┌─────────────┐
│ Masters │ UDP :10240 ← queryreq / queryrsp (device discovery)
└──────┬──────┘
│
┌──────▼──────┐
┌─────────────│ Relays │──────────────┐
│ │ UDP :20001 │ │ ← relay_login / rlywakeupreq
│ └──────┬──────┘ │ rlystreamrsp (ICE candidates)
│ 0x0021xxxx │ 0x0042xxxx │ bootstrap KCP forwarding
│ (client →) │ (← relay) │
▼ ▼ ▼
Client (VPS / phone / peer) ◄──────────► Camera (behind residential NAT)
0x0012xxxx (direct)
```
两端通过 UBox 的基础设施进行通信:**masters** 负责处理设备发现,**relays** 负责协调 NAT 穿透,而一旦打洞成功,实际的视频流理想情况下将直接通过点对点 (P2P) 传输。链路上传输的所有数据都被封装在 16 字节的 P4P 头部中并进行加密。
## 2. 密码学 — 已破解
一种**自定义的 16 字节分组密码**(非 AES/ChaCha),使用了嵌入在 `.rodata` 中的固定密钥。这不是为了保密性 —— 而是为了进行传输层混淆。
- **算法(针对每个 16 字节的数据块):** `ROTR(i+1) → XOR(key) → Swap16(σ) → ROTR(i+3)`。
- **密钥**(32 字节;每个数据块使用前 16 字节):`"I believe 1 ^ill win the battle!"`。
- **排列** σ = `[11,9,8,15,13,10,12,14,2,1,5,0,6,4,7,3]` — **对合**
(σ = σ⁻¹,8 个不相交对换的乘积)。
- **尾部**(< 16 字节):仅进行密钥 XOR(同样是自反的)。
- **状态:** 已实现,并针对网络流量完成了往返验证 (`p4p_crypto.py`)。
完整的数学推导,包括关于
$D \circ E = \mathrm{id}$ 的逐步证明,详见 **[docs/CRYPTO.md](docs/CRYPTO.md)**。
## 3. 握手与 P2P 穿透
由中继服务器 (relays) 协调的多阶段类 ICE 流程。每个消息类型都有一个整型
`msg_type`,其*高字节*编码了角色(`0x21`=客户端→,`0x12`=摄像头直连←,
`0x42`=通过中继←),其*低半部分*用于选择处理程序。
```
Client Camera
│ queryreq 0x00281051 → masters (:10240) │
│ │
│ relay_login 0x00241205 → relays (:20001) │
│ rlywakeupreq 0x00241201 → relays (bursts, ~60 in 5s) │
│ │
│ ← 0x0012130e camera knock ──────│
│ 0x0021130b knock_response │
│ ← 0x0012130c device_info ───────│
│ 0x00211405 p2p_bind (@ ~1 s heartbeat) │
│ 0x0021130d knock_r │
│ 0x00211409 ioctrl KCP burst (start-stream commands) │
│ │
▼ → 0x0012140a KCP data (video) ────▼
RDT framing → H.265
```
关于字节级别的所有内容(偏移量、msg_type 表、接收端分发、摄像头
knock/knock_r/p2p_bind 结构、ioctrl 突发发送)均在
**[docs/PROTOCOL.md](docs/PROTOCOL.md)** 和 **[docs/PACKET_CAPTURES.md](docs/PACKET_CAPTURES.md)** 中。
## 4. 视频流处理 — KCP + RDT
视频并**不**只是作为标准的 KCP 传输。UBox 在 KCP 之上封装了一层自定义的 **RDT
(Reliable Data Transport) 成帧层**:
- **KCP 头部:** 标准 24 字节的 `ikcp` (`conv | cmd | frg | wnd | ts | sn | una | len`)。
- **RDT 头部 (32 字节):** 偏移量 0 处为单元类型,偏移量 8 处为 payload 长度 + 16。
- `0x11` = HEVC 视频,`0x13` = 元数据,`0x04` = 控制。
- **Payload:** 原始 H.265 NAL (VPS/SPS/PPS + IDR + P/B 帧)。
`kcp_demux.py` 中的解复用器会重组 KCP,剥离 RDT,并输出可播放的 `.h265`
文件,该文件可由 `ffplay` 解码。已与基准真实抓包进行了逐字节的精确验证。
详见 **[docs/KCP_DEMUX.md](docs/KCP_DEMUX.md)**。
## 5. NAT 墙与中继行为
这项工作最重要的发现 —— 建立在**确凿的证据之上,而非猜测**:包含了 Hook、反汇编以及可复现的实验。
### 从公网 IP VPS 进行直接 P2P:失败
带有公网 IP 的 VPS **无法完成与**位于对称 NAT(典型的家用 ISP)后方的摄像头之间的**同时打开**。我们尝试了逐字节完美的数据包、向 relays 通告的每个反射地址进行 ICE 风格的候选者竞速、模仿手机应用进行主动打洞,以及双重发送。摄像头的 NAT 在所有配置下都会静默丢弃我们的数据包。**位于移动运营商 NAT 下的手机确实能够成功**(观察到在与 VPS 尝试失败的同时,同一摄像头上通过用户手机持续获取了超过 123 秒的直接视频)。
### 中继的幻象:不是 TURN,而是引导阶梯
UBox relays **并不是持久化的 TURN 服务器**。可复现的行为如下:
1. 客户端发送 `rlystreamreq` → 中继提交 session 状态,返回 tags/conv。
2. 中继转发摄像头的 KCP 窗口(约 1 个 GOP,在约 3 秒内约 1000 个 KCP 数据包) — 这足以
解码出一个关键帧及约 60 帧的 HEVC 数据。
3. **中继不会将客户端的 KCP ACK 向上游转发给摄像头。** 摄像头
永远收不到 ACK,会在循环中不断重传其窗口,并在约 3 秒后停止发送。
4. **没有任何客户端侧的标志、数据包或 keepalive** 能够改变这一点。我们尝试了:
主动的 `rlywakeupreq` 突发发送(176 次敲门,而手机约为 60 次),与手机完全一致的 1 秒
`p2p_bind`/`relay_bind` 心跳,每个周期全新的 sockets,
干净的 `logoutreq`,忽略重传的尽力而为的 KCP 解码,以及来自
`.so` 的完整 `send_avctrl`/`send_alive` 库存。每一种配置最终都
收敛在同一个结果上:**每个 session 最多只能持续约 2 秒 / 约 60 帧**。
**有趣的附带发现。** 我们的 `parse_rlystreamrsp` 匹配到了 `0x00421406`,
而接收端分发表 (`p4p_client_receiver`) 实际上将其映射到了 `handle_alive`,而不是
`handle_rlystreamrsp`(后者是 `0x00421206`)。在实践中,中继**从不向 VPS 发送
0x00421206**;只发送 `0x00421202`(唤醒响应)和 `0x00421406`(alive) —
而将 `alive` 视为开启数据流的触发器,正是真正能够开启 KCP 数据流的方法。
这个问题已被记录在文档中,而不是作为 Bug 修复,因为它反映了已部署中继服务器的真实行为。
完整的证据、数据包抓包、分发表以及死胡同(不可行方案)均记录在
**[docs/FINDINGS.md](docs/FINDINGS.md)** 中。
## 6. 可行的解决方案
### A. 从 VPS 按需获取片段 — 已实现 ✅
由于来自中继服务器的约 2 秒引导窗口包含一个完整的 GOP(关键帧 + P/B
帧),它可以被顺利解复用为有效的 HEVC/MP4 文件。这可以在任何主机上运行
(无论是在 NAT 后还是公网 VPS 上),并且不依赖于手机。
```
python3 grab_clip.py # → clip_.mp4
```
非常适合太阳能或运动触发触发的摄像头:当你需要时拉取一张快照,
而无需保持手机应用在线作为桥梁。围绕此功能的一个最小化本地控制面板位于
`dashboard.py` 中。
### B. 从家庭网络端获取持续的 24/7 视频流 — 路径已验证 ✅
直接 P2P **在客户端位于家庭 NAT 之后时确实有效**(与手机相同的网络环境)。
实测:在一个移动运营商 NAT 下,用户的手机在同一测试中
与摄像头维持了超过 123 秒的连续直接视频。在你家里的局域网内的
树莓派或旧电脑上运行 `p4p_stream.py`,可以通过直连路径提供流畅连续的
HEVC 视频。代码在同一个文件中,无需 `--relay` 标志:
```
python3 p4p_stream.py -o live.h265
```
然后通过 WireGuard/SSH 隧道将解复用后的 HEVC 数据流转发到 VPS,用于 HLS 或 Web
控制面板。这就是任何主电源供电设备的预期架构。
### C. 音频 — Frida 辅助,已工作 ✅ (非独立运行)
音频使用 **G.726**(位于 `libG726Android.so` 中,这是另一个我们*没有*重新分发的厂商库),
且并不标准,因此如果不进行大量的逆向工程,纯 Python 客户端无法对其进行解码。
我们验证过的实用方案:
- 正在运行的 UBox 应用内部的一个 Frida hook 拦截了 `G726Codec.decode` 并捕获
已经解码的 **PCM 16-bit / 16 kHz 单声道** 音频样本。
- 该 hook 通过 TCP 将这些 PCM 样本推送到 `127.0.0.1:8556`(通过 `adb reverse`)。
- 接收主机上的 `audio_receiver.py` 将它们馈送给 `ffplay`,并带有可选的
降噪过滤器。仅能收听,但可以正常工作。
这意味着音频需要手机(带有被 Hook 的应用)保持在线。它不像视频那样可以独立运行 —
因为该编解码器位于 KCP 数据流和我们可以播放的格式之间,而我们选择不
从头重新实现 G.726。独立音频仍然是一个待解决的问题。
### D. PTZ + 变焦 + 捕获 — 已实现 (Frida 辅助) ✅
该 SDK 是 **TUTK IOTC** 的换壳版本,但该特定 OEM 的共享用户变体具有
比通用 TUTK 更长的 PTZ 结构:
- **PTZ** (`ioctrl 0x1001`, 12 字节): `[0, 0, speed, 0, 0, code, 0, 0, 0, 0, 0, 0]` —
**速度在 byte[2],方向代码在 byte[5]**。代码已经过实证确认
(2026-07-07 在 N°3 摄像头上): `1=UP, 2=DOWN, 3=RIGHT, 6=LEFT, 241=RESET/HOME, 0=STOP`。
速度 1–8(6 是应用的默认值)。该摄像头**仅支持四向** — 对角线代码
(4,5,7,8) 返回 OK,但电机从不移动。
- **变焦** (`ioctrl 0x2120`, 20 字节): `[level, 0×19]`。Level 0x23 (35) = 放大,
0x0a (10) = 缩小。Level 越高 = 缩放越深。
我们的共享用户应用 UI 隐藏了这些控制,但 Frida hook `hooks/hook_video_ptz.js`
依然将 ioctrls 注入到正在运行的 SDK session 中 — 具有与任何
实时预览请求相同的授权 — 并且摄像头会移动。
`ptz_control.py` 提供了一个本地 Web 控制面板(长按移动的四向十字键 + 速度滑块 + 变焦
按钮 + 重置)。它将命令写入手机上的 `/tmp/ptz_cmd.txt`;该 hook
监视该文件并触发 ioctrl。同一个页面还**捕获实时视频流**:
它作为 hook 视频 (:8555) 和音频 (:8556) 的接收端,因此📷 按钮可保存
一帧 JPEG 关键帧,而 ⏺ 按钮可录制包含音视频的 `.mp4` — 两者均直接保存到你的电脑上。
```
adb shell 'touch /tmp/ptz_cmd.txt' # ensure file exists (first time)
python3 ptz_control.py # → http://127.0.0.1:8081
python3 ptz_control.py --outdir ~/Videos/ubox # captures dir (default ~/Desktop/ubox-captures)
# 先打开 UBox live view(已打补丁的 app,保持在 FOREGROUND),然后使用 pad
```
因为它绑定了 :8555/:8556,请在运行它时**替换掉** `stream_receiver.py` /
`audio_receiver.py`(只有一个进程可以消费各自的流;该 hook 会自动
重连)。在录制时,请将 UBox 应用保持在实时预览的**前台** —
将其切换到后台会暂停视频流,导致录制提前停止。
这不是独立运行的 — 就像音频一样,它需要被修改过的手机作为认证桥梁。从裸
VPS 进行独立的 PTZ 控制需要重新实现该应用执行的授权 session,
这是未来的工作(参见 docs/DEVICE_FIRMWARE.md)。
## 快速开始 — 仅获取一段视频 (60秒)
如果你只想要从你自己的其中一个摄像头获取一段 `.mp4` 视频,无论从哪台主机
(笔记本电脑、VPS、在 NAT 后 —— 均可运行,因为使用的是中继路径):
```
git clone https://github.com/mahumadad/ubox-p4p && cd ubox-p4p
pip install -r requirements.txt # one binding: `kcp`
export UBOX_UID=XXXXXXXXXXXXXXXXXXXX # 20-char UID from the UBox app
export UBOX_PASSWORD=xxxxxxxxxxxxxxxx # device password
python3 grab_clip.py -o clip.mp4 # → ~2 s HEVC/MP4 clip
```
这就是用于按需获取快照的整个循环。对于连续视频、
多摄像头控制面板、实时 PTZ 或音频,请参见下文第 6 节。
## 7. 安装与使用
```
python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txt # the `kcp` binding
export UBOX_UID=XXXXXXXXXXXXXXXXXXXX # 20-char camera UID (from the UBox app)
export UBOX_PASSWORD=xxxxxxxxxxxxxxxx # device password
```
握手完整性检查(验证针对你摄像头的密码学和认证):
```
python3 p4p_client.py
```
单段视频(在任何地方均可运行,包括 VPS):
```
python3 grab_clip.py -o clip.mp4
```
完整的中继流输出为原始 HEVC:
```
python3 p4p_stream.py --relay -o video.h265
ffplay video.h265 # or: ffmpeg -i video.h265 -c copy video.mp4
```
直连模式(来自其 NAT 可以穿透到摄像头的对等端 —— 同一局域网或
家庭网络连接):
```
python3 p4p_stream.py -o video.h265
```
本地控制面板(自动刷新的快照 + 片段下载):
```
python3 dashboard.py # → http://127.0.0.1:8080
```
实时音频(需要手机应用通过拦截
`G726Codec.decode` 的 Frida hook 进行修改 — 详见 `hooks/`):
```
adb reverse tcp:8556 tcp:8556
python3 audio_receiver.py # ffplay opens automatically on first frame
```
## 8. 项目结构
```
p4p_crypto.py Block cipher + every packet builder / parser used
p4p_client.py Handshake-only client (proves reachability + auth)
p4p_stream.py Video client — direct mode and --relay mode
grab_clip.py One-shot clip capture via relay → MP4
dashboard.py Local web dashboard (snapshot + clip button)
kcp_demux.py KCP reassembler + RDT frame parser → HEVC
audio_receiver.py Play PCM streamed by the Frida audio hook (listen-only)
docs/
CRYPTO.md Formal spec of the cipher, correctness proof (D∘E = id)
PROTOCOL.md Byte-level protocol; msg_type table; handshake states
FINDINGS.md NAT/relay analysis; every dead-end catalogued with evidence
PACKET_CAPTURES.md Annotated real captures (handshake, keepalive, video)
KCP_DEMUX.md KCP + RDT reassembly details
ENDPOINTS.md Cloud REST endpoints found in the app (portal.ubianet.com)
IOCTRLS.md ioctl reference (PTZ, zoom, recording, session, all iotypes)
DEVICE_FIRMWARE.md How to extract & analyse the camera-side firmware
SESSION_LOG.md Chronological engineering log — how we got where we are
hooks/ Frida instrumentation used to reverse the protocol
(plaintext dump, dest, punch choreography, ikcp_input, relay)
tools/EXTRACT_SO.md How to extract libUBICAPIs29.so from the UBox APK yourself
```
## 原生库(特意未包含在仓库中)
`libUBICAPIs29.so` 是 UBox 受版权保护的 ARM64 二进制文件。分发它会招致 DMCA
下架,而且使用本客户端**完全不需要它** — 从逆向工程中获得的所有信息
(算法、偏移量、消息格式、分发表)都在 `docs/` 中。要复现这些
静态分析,请自行从 UBox APK 中提取 `.so` — 参见
[tools/EXTRACT_SO.md](tools/EXTRACT_SO.md)。
## 超越本仓库 — 更进一步
这里对 P4P 协议进行的所有逆向工程都局限于**客户端**。如果你有
权限访问摄像头硬件或其局域网,你可以提取固件并反汇编
**设备端** — 这将消除目前存在的限制(中继的 1 个 GOP 上限、独立音频、
真实的 PTZ 结构,可能还包含一个隐藏的 RTSP endpoint)。
请参阅 [docs/DEVICE_FIRMWARE.md](docs/DEVICE_FIRMWARE.md) 获取分层指南:首先是局域网侦察
(投资回报率最高,无需物理访问)、基于 Web 的固件分析,以及设备本身的 UART / SPI 提取。
## 路线图 — 值得攻克的开放性问题
欢迎贡献。具体的待解决项目,按影响力排序:
- **独立音频 (无需应用的 G.726)。** 目前音频需要一个拦截厂商应用内
`G726Codec.decode` 的 Frida hook。彻底的修复方法是
直接从 KCP+RDT 流中解码 G.726(音频 NAL 已经在
解复用的输出中了 — 详见 [docs/KCP_DEMUX.md](docs/KCP_DEMUX.md) 的帧类型
`0x12`)。ffmpeg 拥有 G.726 解码器;将重组后的音频单元通过它进行处理,
将消除音频对手机的依赖。
- **独立的 PTZ / 授权 session。** PTZ 和变焦目前依赖于
被 Frida hook 的实时预览 session(与任何应用命令具有相同的认证)。一个纯 Python 的
客户端需要重现 `knock_r` 之后的认证握手 —
knock 之后的 device_info 交换已记录在
[docs/PROTOCOL.md](docs/PROTOCOL.md) 中;在此之上连接 `send_ioctrl` 将能实现
从无头 VPS 控制 PTZ。
- **打破中继的 1 个 GOP 上限。** 目前的中继路径只能提供约 1 个 GOP
(约 2 秒)的数据,然后就会停滞,因为中继服务器从不向上游转发 ACK(参见
[docs/FINDINGS.md](docs/FINDINGS.md))。我们尝试过的任何客户端标志都未能改变
这一点;修复方案很可能存在于设备端。固件转储将能够证实这一点,
并可能暴露一种客户端可见的替代方案
([docs/DEVICE_FIRMWARE.md](docs/DEVICE_FIRMWARE.md))。
- **固件转储。** 该摄像头是 TUTK IOTC OEM 产品(已确认)。一层局域网侦察
适用于连接 WiFi 的摄像头;但此次测试中的设备使用的是蜂窝网络 (5G 调制解调器,无
局域网),这迫使我们采用二层方案(OEM 固件门户)或三层方案 (UART/SPI)。一个
成功的转储将解锁上述大多数问题。
- **运营商 NAT 之外的直连模式 NAT 穿透。** 直接 P2P 在
移动运营商 NAT 下可以工作(已验证),但在家庭网络/VPS NAT 下会失败(已记录,
可复现)。其机制(STUN 行为差异)在
大体上已被理解,但尚未被利用 — 一种“伪装成手机”的模式可能会起作用。
- **同时进行多摄像头实时预览。** 厂商 SDK 在打开新的实时预览时会关闭前一个,
因此单个被 hook 的应用一次只能喂给一个摄像头
的数据。多实例(应用克隆)或多设备扇出将会起作用 —
`ptz_control.py` 中的解复用代码在编写时已经考虑到了这一点,可以接收
带有标签的数据流。
- **获取固件后的 RTSP/ONVIF 探测。** 基于 TUTK 的固件
通常包含一个未经验证的 RTSP 守护进程;但在蜂窝网络摄像头上,并没有
局域网可供探测,不过固件镜像可以显示该守护进程是否存在以及存在于哪个路径。
对其中任何一个问题有修复方案或部分答案吗?欢迎提交 PR 和 issues。
## 许可证
[MIT](LICENSE)。按“原样”提供,用于研究和互操作性。所有商标均属于
其各自的所有者;本项目与 UBox / UBIA 无任何关联。
标签:P2P, Python, 云资产清单, 内核驱动, 密码学, 密码管理, 手动系统调用, 无后门, 物联网, 网络协议, 视频流, 逆向工具, 逆向工程