水星 Mercury MIPC251C-4 网络摄像头 ONVIF 与 PTZ 云台控制

概况

最近在 什么值得买 上发现一款水星的网络摄像头, 除了支持云台/夜视功能之外, 还标明支持 onvif 协议. 所以想着买来接入到 HomeAssistat 作为监控使用.
可到手之后发现事情并没有那么简单, 记录如下.

接入 HomeAssistant

按照 HA 的文档 ONVIF Camera 接入无非就是配置文件里写两行配置的事, 但无论是使用默认配置还是尝试后台页面里的几个端口都是添加失败.
最后使用 DSM 的 Surveillance Station 找到了正确的端口, 事后发现其实使用 iSpy 也可以找到正确的端口, 端口是2020.

HA 配置如下:

1
2
3
4
5
6
7
8
ffmpeg:

camera:
- platform: onvif
host: 192.168.2.2
port: 2020
username: admin
password: password

需要注意的是使用 onvif camera 需要配置 ffmpeg, 另外可能根据环境不同需要 pip install onvif-zeep-async

ONVIF 与 RTSP 参数

onvif
http://192.168.2.x:2020/onvif/device_service
rtsp 1920x1080
rtsp://admin:pass@192.168.2.x:554/stream1
rtsp 640x480
rtsp://admin:pass@192.168.2.x:554/stream2

PTZ / 云台控制

接入 HA 和 DSM Surveillance Station 之后发现云台不能控制, 看来说的支持 ONVIF 只是可以录制.
剩下的大部分记录如何控制云台.

抓包

Charles 抓包的结果是失败, 应用没有走设置的代理.

反编译 Android App

在尝试了 Charles http 抓包无果之后试着反编译 水星安防App.
然后发现大段 com.tplink.ipc 的代码, 看来水星真的是 TP 的弟弟.
应用开了混淆, 挺好…

  • 添加本地局域网设备 Fragment
    com.tplink.ipc.ui.device.add.DeviceAddByDeviceDetailInputFragment
  • 登录
    com.tplink.ipc.ui.device.add.DeviceAddByDeviceDetailInputFragment#x
    com.tplink.ipc.core.IPCAppContext#devReqAddDevice(java.lang.String, int, java.lang.String, java.lang.String, int, int)
    而 devReqAddDevice 执行的 native 方法, 真正的实现在 libIPCAppContextJNI.so
    看来网络请求是在 JNI 里做的, Charles 抓不到包. 反编译 so 库吃力不讨好, 换方向.

WireShark iOS 抓包

rvictl 可以将 iOS 设备虚拟成一个网络端口, 然后就可以 WireShark 抓包. 好处是这样可以减少一些局域网的干扰.
使用 rvictl 还可以抓 3G/4G 的包, 可以用来抓一些只能在 4G环境下才能复现的问题的包. 同样, 对于一些不支持设置代理的设备, 也可以通过把 iPhone 设置为热点来抓包.
rvictl -s 0000xxxx-00xxxxxxxxxxxxxx
抓包发现 App登录 控制云台的几个步骤的网络请求如下: (1,2两步是根据网页端登录分析出来的)

  1. 获取 RSA 公钥和 nonce
  2. 使用公钥加密密码, 登录, 获取 stok
  3. 发送控制指令

登录

获取 stok

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 获取 RSA 公钥
---->
POST / HTTP/1.1
Host: 192.168.2.2:80
User-Agent: MERCURY_APP
Content-Type: application/json; charset=UTF-8
{"method":"do","login":{}}
<----
{ "error_code": -40401,
"data": {
"code": -60502,
"encrypt_type": ["1", "2"],
"key": "MIGfMA0GCSqGSIb3DQ省略200个字符uJ7N8wIDAQAB",
"nonce": "4vqqXXXX"
}
}

# 登录 获取 stok
---->
POST /
{ "method":"do",
"login": {
"username":"admin",
"encrypt_type":"2",
"password":"ERcuPXuaiwNQcrOX8woIgUAN省略150%2fIHaA%3d"
}
}
<----
{ "error_code": 0,
"stok": "7exxxxxxxxxxxxxxxxxxxxxxxxxxxx59",
"user_group": "root"
}

控制

使用获取的 stok post 指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 获取信息
POST /stok=7exxxxxxxxxxxxxxxxxxxxxxxxxxxx59/ds
{"method":"get","device_info":{"name":["basic_info"]}}

# 获取信息2
POST /stok=7exxxxxxxxxxxxxxxxxxxxxxxxxxxx59/ds
{"method":"get","function":{"name":["module_spec"]}}

# 获取预设位置
POST /stok=7e82c0908c8b141c8e9bb8353b54e259/ds
{"method":"get","preset":{"name":["preset"]}}

# 控制云台转到预设位置
POST /stok=7exxxxxxxxxxxxxxxxxxxxxxxxxxxx59/ds
{"method":"do","preset":{"goto_preset":{"id":"1"}}}

# 云台水平/垂直移动
POST /stok=7exxxxxxxxxxxxxxxxxxxxxxxxxxxx59/ds
{"method":"do","motor":{"move":{"x_coord":"10","y_coord":"0"}}}

# 云台步进 direction 0 / 90
POST /stok=7exxxxxxxxxxxxxxxxxxxxxxxxxxxx59/ds
{"method":"do","motor":{"movestep":{"direction":"0"}}}

# 云台停止
POST /stok=7exxxxxxxxxxxxxxxxxxxxxxxxxxxx59/ds
{"method":"do","motor":{"stop":"null"}}

# 增加预置点
POST /stok=7exxxxxxxxxxxxxxxxxxxxxxxxxxxx59/ds
{"method":"do","preset":{"set_preset":{"name":"name","save_ptz":"1"}}}

# 镜头遮蔽
POST /stok=7exxxxxxxxxxxxxxxxxxxxxxxxxxxx59/ds
{"method":"set","lens_mask":{"lens_mask_info":{"enabled":"on"}}}

小结

目前看来想要控制云台/摄像头的关键是获取 stok, 可以通过抓包可以获得, 但是通过观察当设备重启或着过一段时间(?)就就会失效.

获取 stok

这款摄像头虽然有 web 端但是不支持在 web 端控制云台, 所以开始的抓包没有考虑 web 端. 对 web 端分析发现登录验证使用的是同样的逻辑.
步骤如下

  1. 获取 RSA 公钥 和 nonce (公钥和 nonce 每次都会变化)
  2. 使用 tp-link 的通用加密方式将 密码 password 加密 为 tpPassword
  3. tpPassword 追加 :nonce 为 tpPassword:nonce
  4. tpPassword:nonce 使用 公钥加密为 rsaPassword
  5. rsaPassword 作为密码发送给摄像头验证

对于步骤 2 3 记录如下

tp-link 加密密码为 tpPassword


搜索 RDpbLfCPsJZ7fiv 可以找到这种加密的各种实现.

RSA 加密 tpPassword 为 rsaPassword


其中 a = a.concat(":", $.authRltObj.nonce) 即为追加 nonce
c.setPublicKey($.authRltObj.key) 设置公钥
后面的 sendAjaxReq 就是发送请求

控制脚本

根据上面的分析写的脚本
https://github.com/likaci/mercury-ipc-control

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
usage
need to install packages rsa and requests
pip install requests
pip install rsa

python mipcc.py admin password url data
python mipcc.py admin password http://192.168.2.89:80 '{"method":"do","preset":{"goto_preset": {"id": "1"}}}'

data example:
PTZ to preset position {"method":"do","preset":{"goto_preset": {"id": "1"}}}
PTZ by coord {"method":"do","motor":{"move":{"x_coord":"10","y_coord":"0"}}}
PTZ horizontal by step {"method":"do","motor":{"movestep":{"direction":"0"}}}
PTZ vertical by step {"method":"do","motor":{"movestep":{"direction":"90"}}}
stop PTZ {"method":"do","motor":{"stop":"null"}}
add PTZ preset position {"method":"do","preset":{"set_preset":{"name":"name","save_ptz":"1"}}}
lens mask {"method":"set","lens_mask":{"lens_mask_info":{"enabled":"on"}}}

目录

  1. 1. 概况
  2. 2. 接入 HomeAssistant
  3. 3. ONVIF 与 RTSP 参数
  4. 4. PTZ / 云台控制
    1. 4.1. 抓包
    2. 4.2. 反编译 Android App
    3. 4.3. WireShark iOS 抓包
    4. 4.4. 登录
    5. 4.5. 控制
    6. 4.6. 小结
  5. 5. 获取 stok
    1. 5.1. tp-link 加密密码为 tpPassword
    2. 5.2. RSA 加密 tpPassword 为 rsaPassword
  6. 6. 控制脚本