konst3658-crypto/divoom-times-frame-research
GitHub: konst3658-crypto/divoom-times-frame-research
Divoom Times Frame 智能相框云端及局域网 API 的逆向研究项目,记录了已跑通的登录、上传等接口和尚未突破的照片绑定环节。
Stars: 2 | Forks: 0
# Divoom Times Frame — 云端 API 逆向研究(约 80%)
本文档记录了在某天晚上尝试将自定义仪表盘图片推送到 Divoom **Times Frame**(10.1″ Wi-Fi 相框,2024–25 款)上的过程。
**状态:** 大部分云端 API 已经跑通——包括登录、账户信息、相册列表、向 CDN 上传文件。但最后一步——也就是告诉相框“现在显示这个刚上传的文件”——在公共 HTTP API 中被封锁了,几乎可以肯定它是通过官方手机应用一直保持开启的 **RongCloud MQTT** 通道进行传输的。如果不拦截应用的流量,我们就无法看到这些消息。
如果你补全了缺失的部分,请提交一个 PR——这正是那些手握 mitmproxy 和 iPhone 的人能在一小时内闭环搞定的地方。
## 已跑通的功能 ✅
| 步骤 | Endpoint | 结果 |
|---|---|---|
| 登录 | `POST appin.divoom-gz.com/UserLogin`,参数为 `{Email, Password: md5(pwd)}` | 返回 `Token` + `UserId` + `UserToken`(后者包含一个 RongCloud 导航 URL —— 见“为什么我们卡住了”) |
| 账户信息 / DeviceId | `POST appin.divoom-gz.com/User/GetUserInfo` | 确认你的 `DeviceId` |
| 相框上的照片 | `POST appin.divoom-gz.com/PhotoFrame/GetList` | 返回 `{PhotoName, BigImageId, SmallImageId, PixelStartX, PixelStartY}` 的数组 |
| 读取任意文件 | `POST appin.divoom-gz.com/Cloud/GetFileData` | 通过 `FileId`(`group1/M00/...`)下载 |
| **上传任意图片到 Divoom CDN** | `POST f.divoom-gz.com/upload.php`(multipart,字段名为 `upFile`,无需认证) | 返回一个真实的 `FileId`,格式与相框使用的 `group1/M00/...` 相同 |
CDN 上传是让我最惊讶的发现——它完全无需认证,并且会返回一个官方应用用于大/小图引用的真实 `FileId`。所以你现在已经可以把文件存放在 Divoom 的 CDN 上了。
## 未能破解的部分 ❌
也就是“将此 `FileId` 绑定到我的相框播放列表”的调用。我们尝试了 `PhotoFrame/*`、`Device/*`、`Channel/*`、`Cloud/*`、`Photo/*` 下的大约 160 个方法名,穷举了所有看似合理的动词(`Add`, `Save`, `Set`, `Push`, `Insert`, `Sync`, `Update`, …)。每次未命中都会返回 `ReturnCode: 10 "Command is not match"`。唯一有暗示性的命中是:
```
POST /PhotoFrame/UploadFile
→ "Call to protected method app\\Controllers\\ApiBase::UploadFile()
from context 'Server\\CoreBase\\Http'"
```
也就是说,该 endpoint 在它们的 PHP 路由中确实存在,但被设为 `protected`(受保护)——无法从外部调用。
## 为什么我们卡住了(关于 RongCloud 的假设)
登录响应中包含 `UserToken: "...@ynfz.cn.rongnav.com;ynfz.cn.rongcfg.com"`。RongCloud 是中国的企业级 MQTT/IM SDK。Divoom 官方应用似乎保持着一个 MQTT 长连接,并直接通过该通道向设备下发指令——因此,“显示这张照片”实际上是一条发送给设备 user-token 的 RongCloud 消息,而不是 HTTP 调用。
如果真是这样,那么公共 HTTP API 在设计上就*永远不会*暴露绑定照片的方法。接下来的突破口在于:
1. 使用 **mitmproxy** 抓包官方 Android/iOS 应用,精确捕获从相册上传照片时发生了什么,并复制那个流程。
2. 如果 SSL pinning 挡住了第 1 步:在已 root 的 Android 模拟器上使用 Frida 绕过,或者反编译 APK。
## :9000 端口上的本地 HTTP API(部分)
**发现于 2026-05,于 2026-06 再次确认。** 该相框在局域网内暴露了一个本地 HTTP 指令 API。感谢 **Vasily Simanin**(`vsimanin`,issue #1),他的 Home Assistant 集成最先使用了它。由于该 endpoint 使用的是不同寻常的 **GET 请求携带 JSON body** 发往 `:9000/divoom_api`(而不是 Pixoo 的 `:80/post`),所以最初的四月份文章错误地将该设备称为“纯云端”设备,导致简单的端口扫描会将其漏掉。
- **传输方式:** `http://:9000/divoom_api`,JSON body 为 `{"Command": "...", ...}`。与 Pixoo 属于同一个 Divoom JSON-RPC 协议族,但端口和路径不同。
- **无需认证,仅限局域网。** 相框会回复自身的 `DeviceId` 和 `DeviceType: "Frame"`。
- **未实现指令的基线响应:** `{"ReturnCode": 1, "ReturnMessage": "Only accept JSON parameters"}`。经验证这是一个兜底响应——无论是发送乱码指令、空对象,还是没有 `Command` 字段的请求,都会返回它。因此,这个回复的意思是“未被固件路由”,**而**不是“需要更多参数”。
### 已在本地实现(返回 `ReturnCode: 0`)
| Command | Payload | 备注 |
|---|---|---|
| `Channel/GetConfig` | — | `RotationFlag, ClockTime, GalleryTime, SingleGalleyTime, ChannelIndex, StartUpClockId, GalleryShowTimeFlag` |
| `Channel/GetClockInfo` | — | `Brightness, ClockId` |
| `Channel/GetOnOffScreen` | — | `OnOff` |
| `Channel/GetAmbientLight` | — | `Brightness, Color, ColorCycle, EqOnOff, SelectEffect` |
| `Channel/GetEqPosition` | — | `EqPosition` |
| `Device/GetWeatherInfo` | — | `Weather, CurTemp, MinTemp, MaxTemp, Pressure, …` |
| `Channel/SetClockSelectId` | `{ClockId}` | 切换内置表盘(写入) |
| `Channel/SetBrightness` | `{Brightness 0..100}` | 写入 |
| `Channel/OnOffScreen` | `{OnOff 0\|1}` | 屏幕开启/关闭(写入) |
| `Device/SysReboot` | — | 重启(写入) |
| `Danmaku/SendText` | `{DeviceId, Text, TextColor, UserId}` | 需用 **POST**,而非 GET |
| `Device/EnterCustomControlMode` / `ExitCustomControlMode` | — | 请求被接受;效果不明,屏幕无变化 |
| `Draw/ResetHttpGifId`, `Draw/SendHttpGif`, `Draw/GetHttpGifId` | Pixoo 像素缓冲格式 | **请求被接受**(`ReturnCode 0`)但不可见:目标是绘制频道,而频道切换(`Channel/SetIndex`)在本地*并未*实现,因此你无法通过 API 将其显示在屏幕上。退一步讲,即使能显示,它也只是个约 64px 的像素缓冲区,而不是全屏照片。 |
### 本地未实现
照片/播放列表控制功能缺失。我们带上真实的 `FileId` 对 `Photo/*`, `PhotoFrame/*`, `Picture/*`, `Image/*`, `File/*`, `Cloud/*`, `Slideshow/*`, `Gallery/*`, `Channel/*` 交叉 `{Add, Set, Push, Insert, Bind, Sync, Update, Play, Show, …}` 进行了约 920 次探测,全部命中了兜底的基线响应。频道切换(`Channel/SetIndex` / `GetIndex`)同样缺失。
**结论:** 本地 API 可以控制亮度 / 屏幕 / 内置表盘 / 弹幕 / 自定义控制模式——但**不能**控制将 FileId 绑定到播放列表的步骤。这依然存在于云端 / RongCloud 通道中;本地 API 并没有缩短缺失的拼图。下文的 mitmproxy 路径依然是完成闭环的方法。
## 为什么是这款设备,而不是 Pixoo
Pixoo 系列(Pixoo 16/64)在 80 端口上有一个完全开放的本地 HTTP API——拥有大量的第三方库、Home Assistant 集成等一整套生态。Times Frame 则暴露了一个**范围更窄**的本地 API(如上所述):支持屏幕/亮度/表盘,但不支持照片控制。其照片体验是刻意被云端锁定的——Times Frame 定位为一款面向非技术用户、无需订阅的高级相框,将相册功能限制在它们自家的应用背后有助于保护其商业捆绑价值。开源社区之所以还没完全破解它,是因为既会黑客技术又买了这款设备的受众群体实在太小了。
## 使用工具包
环境要求:Python 3.10+,`requests`,`pillow`。
```
# 1. 登录(从 env 获取密码可避免其留在 shell 历史记录中)
DIVOOM_PASSWORD='your-password' python divoom_cloud.py login --email you@example.com
# 2. 检查 DeviceId 是否已成功传递
python divoom_cloud.py info
# 3. 列出 frame 上今天的所有内容
python divoom_cloud.py list
# 4. 上传任意 JPG/PNG 到 CDN,获取一个 FileId
python divoom_cloud.py upload my_dashboard.jpg
# → FileId: group1/M00/17/E6/...jpg
# 5. (尚不可用)尝试使用该 FileId 测试每一个合理的 attach endpoint
python divoom_cloud.py probe-attach group1/M00/17/E6/...jpg
```
Token 会被缓存在 `.divoom_token` 中(已被 gitignore 忽略)。邮箱存在于 `DIVOOM_EMAIL` 环境变量或缓存的 token 中。密码**绝对不会**被写入磁盘。
## 要完成该项目你需要什么
* 装有 Divoom 应用并已与你的 Times Frame 配对的 iPhone 或 Android 手机
* 在同一网络下的笔记本电脑上运行 mitmproxy(设置约需 10 分钟)
* 从应用中捕获一次照片上传过程——缺失的 API 调用就在其中
* 与本仓库的发现进行交叉比对;上传步骤我们已经搞定,你只需要找到第二个调用
## 文件结构图
* `divoom_cloud.py` — 云端 CLI:登录、信息、列表、上传、探测绑定
* `divoom_local.py` — 本地 `:9000/divoom_api` CLI:`info`, `call`, `probe`
* `divoom_timesframe.py`, `divoom_dashboard.py`, `divoom-dashboard-prompt.md` — 仪表盘图像生成实验
* `probe_test.jpg` — 用于 CDN 上传的 800×1280 测试图像
* `HANDOFF.md` — 完整的会话日志(2026-05 实体相框测试环节 + 合作笔记)
* `.gitignore` — 排除 `.divoom_token`
* `README.md` — 本文件
## 速查结果表
| Host | Endpoint | Method | Auth | 状态 |
|---|---|---|---|---|
| `appin.divoom-gz.com` | `/UserLogin` | POST | 无 | ✅ `{Email, Password: md5(pwd)}` |
| `appin.divoom-gz.com` | `/User/GetUserInfo` | POST | Token+UserId | ✅ |
| `appin.divoom-gz.com` | `/PhotoFrame/GetList` | POST | Token+UserId+DeviceId | ✅ |
| `appin.divoom-gz.com` | `/Cloud/GetFileData` | POST | Token+UserId+FileId | ✅ |
| `appin.divoom-gz.com` | `/PhotoFrame/UploadFile` | POST | — | ⚠️ 200 OK 但显示 `protected method`(仅限内部) |
| `appin.divoom-gz.com` | `/PhotoFrame/{Add,Save,Set,…}Photo` | POST | — | ❌ `Command is not match`(尝试了约 160 种变体) |
| `f.divoom-gz.com` | `/upload.php` | POST multipart, 字段 `upFile` | **无** | ✅ 返回 `FileId` |
| `:9000` | `/divoom_api` (`Channel/Get*`, `OnOffScreen`, `SetBrightness`, `Draw/*`, `Danmaku/SendText`) | GET/POST + JSON body | **无 (局域网)** | ✅ 本地控制子集(屏幕/亮度/表盘/弹幕) |
| `:9000` | `/divoom_api` 照片/播放列表绑定 (`Photo/*`, `Cloud/*`, `Channel/SetIndex`, …) | GET/POST + JSON body | — | ❌ 命中兜底基线响应(尝试了约 920 种变体)— 本地不支持 |
标签:API逆向, IoT, Python, Python脚本, 无后门, 智能硬件, 网络协议分析, 逆向工具