mahumadad/ubox-p4p

GitHub: mahumadad/ubox-p4p

该项目完整逆向了 UBox 智能摄像头的 P4P 协议,并提供了一个无需厂商应用即可直连摄像头的独立 Python 客户端。

Stars: 0 | Forks: 0

# UBox P4P 协议 — 完整的逆向工程与 NAT 穿透分析 ![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-3776ab?logo=python&logoColor=white) ![Frida](https://img.shields.io/badge/Frida-16-fda638?logo=frida&logoColor=black) ![License: MIT](https://img.shields.io/badge/license-MIT-blue) ![Platform: macOS · Linux](https://img.shields.io/badge/platform-macOS%20%C2%B7%20Linux-8b949e) 对 UBox P4P 协议的完整逆向工程 —— 包含自定义分组密码、基于 KCP 且带有 RDT 成帧的可靠传输,以及将客户端绑定到摄像头的 ICE 风格 P2P NAT 穿透。本仓库提供了一个**独立的 Python 客户端**,无需厂商的手机应用即可直接与摄像头通信。 ![实时预览 + 本地 PTZ 控制面板](https://static.pigsec.cn/wp-content/uploads/repos/cas/89/89d1377aeb47ed486e26be4060a0a129d914d79908e50863837e3a73e15ecc68.png) ## 目录 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, 云资产清单, 内核驱动, 密码学, 密码管理, 手动系统调用, 无后门, 物联网, 网络协议, 视频流, 逆向工具, 逆向工程