maxim-smirnov/hik-qr-export
GitHub: maxim-smirnov/hik-qr-export
海康威视设备导出二维码解码器,利用应用内硬编码的静态 AES 密钥解密并提取设备凭据,即使不知道用户设置的导出密码也能恢复摄像头登录信息。
Stars: 21 | Forks: 7
# HikVision 二维码导出解码器

## 用法
```
$ git clone git@github.com:maxim-smirnov/hik-qr-export.git
$ cd hik-qr-export
$ python3 -m venv venv # Create new virtual environment
$ source venv/bin/activate # Activate venv
$ pip install -r requirements.txt # Install requirements
$ python hik_qr_export.py --help
Usage: hik_qr_export.py [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
decode Decode QR code data, extract metadata and stored devices.
renew Renew QR code.
```
### 附加
```
$ pip install Pillow zxing-cpp # Only if you want to use a QR image from clipboard
```
# 是什么与为什么?
## 简介
我忘记了 HikVision 摄像头的密码。但是这个摄像头之前已经添加到了一个名为 **Hik-Connect** 的应用程序中,我仍然可以通过这个应用访问我的摄像头。但是设备的密码在应用中被隐藏了。
所以我开始寻找提取摄像头密码的方法。
在 **Hik-Connect** 应用程序中有一个导出本地设备的选项。应用会要求你设置一个端到端的加密密码,以保护你导出的设备。好吧,它确实使用了加密,但密钥是硬编码在应用程序中的静态密钥。
因此,即使没有你设置的导出密码,也可以解码和解密导出的数据!如果你忘记了自己的导出密码,甚至还能恢复它!(你的密码甚至没有被哈希处理,它也是用那个静态密钥加密的)。
## 将本地设备添加到应用程序
只需点击右上角的 **+** 并选择 **“手动添加设备”**。

## 导出本地设备
切换到 **Me**(我的)选项卡,向下滚动直到看到 **“Export Local Devices”**(导出本地设备)选项。选择你要导出的设备,然后点击底部的 **“Generate QR Code”**(生成二维码)。系统会要求你输入二维码的密码。当你在另一部手机上使用 **Hik-Connect** 应用扫描此二维码时,将会要求输入此密码。此外,二维码具有过期时间。

我们稍后再讨论密码和过期时间。
## 二维码结构
让我们扫描我为测试摄像头创建的二维码,看看里面有什么。
`QRC03010003eJwrKnNNzC0vNy/yLogwD041LTUocg13tLW1ijRyK4mK8MpQM1DzDcmu9MlyNfJ3NqkA0rZqFgYGBmpqySWGuSYp5iEVwc5eHkZJHpnhWcFBQK04JVSsjJO8g5IC0gMSU6KcqsxczEuzjfQNA21tAQ4rKR0=`
嗯,还不错。我们可以清楚地看到**头部**是:`QRC03010003`。
这可能是二维码结构的格式,或者是创建此二维码的应用程序的名称。在头部之后,我们可以看到 `base64` 编码的数据:`eJwrKnNNzC0vNy/yLogwD041LTUocg13tLW1ijRyK4mK8MpQM1DzDcmu9MlyNfJ3NqkA0rZqFgYGBmpqySWGuSYp5iEVwc5eHkZJHpnhWcFBQK04JVSsjJO8g5IC0gMSU6KcqsxczEuzjfQNA21tAQ4rKR0=`
让我们解码它并将输出保存到某个文件中:
```
$ echo -n 'eJwrKnNNzC0vNy/yLogwD041LTUocg13tLW1ijRyK4mK8MpQM1DzDcmu9MlyNfJ3NqkA0rZqFgYGBmpqySWGuSYp5iEVwc5eHkZJHpnhWcFBQK04JVSsjJO8g5IC0gMSU6KcqsxczEuzjfQNA21tAQ4rKR0=' | base64 -d > /tmp/b64_decoded
```
那么里面有什么呢...
```
$ cat /tmp/b64_decoded | hexdump -C
00000000 78 9c 2b 2a 73 4d cc 2d 2f 37 2f f2 2e 88 30 0f |x.+*sM.-/7/...0.|
00000010 4e 35 2d 35 28 72 0d 77 b4 b5 b5 8a 34 72 2b 89 |N5-5(r.w....4r+.|
00000020 8a f0 ca 50 33 50 f3 0d c9 ae f4 c9 72 35 f2 77 |...P3P......r5.w|
00000030 36 a9 00 d2 b6 6a 16 06 06 06 6a 6a c9 25 86 b9 |6....j....jj.%..|
00000040 26 29 e6 21 15 c1 ce 5e 1e 46 49 1e 99 e1 59 c1 |&).!...^.FI...Y.|
00000050 41 40 ad 38 25 54 ac 8c 93 bc 83 92 02 d2 03 12 |A@.8%T..........|
00000060 53 a2 9c aa cc 5c cc 4b b3 8d f4 0d 03 6d 6d 01 |S....\.K.....mm.|
00000070 0e 2b 29 1d |.+).|
00000074
$ file /tmp/b64_decoded
/tmp/b64_decoded: zlib compressed data
```
还不错,但这些压缩数据没有文件头,所以 `zcat` 会提示它不是 gzip 格式。
我使用 Python 对数据进行了解压缩,但还有很多其他方法可以做到这一点。
```
>>> import base64
>>> import zlib
>>> zlib.decompress(base64.b64decode('eJwrKnNNzC0vNy/yLogwD041LTUocg13tLW1ijRyK4mK8MpQM1DzDcmu9MlyNfJ3NqkA0rZqFgYGBmpqySWGuSYp5iEVwc5eHkZJHpnhWcFBQK04JVSsjJO8g5IC0gMSU6KcqsxczEuzjfQNA21tAQ4rKR0='))
b'rvEamww7rKpX7Se5u0rEWA==:Y2FtZXJh&0&MTkyLjE2OC4xLjE=&8000&&ct1m4d7TxSCJH2bHiWjSRA==&ct1m4d7TxSCJH2bHiWjSRA==$:3bKRbPgPadZBz6D7uk2/1Q=='
```
我们可以在这里清楚地看到一些结构。让我们尝试解码它:
## 二维码中的摄像头信息
我们已经可以看到摄像头的名称:`camera`,我填写的 IP 地址:`192.168.1.1`,端口:`8000`,以及两个紧挨着的完全相同的数据块。你还记得我在 `username` 和 `password` 这两个字段填的都是 `admin` 吗……
我为同一个摄像头创建了几个使用不同导出密码的二维码,发现解码后的数据中只有部分内容发生了变化:`rvEamww7rKpX7Se5u0rEWA==` 和 `3bKRbPgPadZBz6D7uk2/1Q==`,它们都通过 `:` 与摄像头数据分隔开。
然后我为两个摄像头创建了二维码导出,发现 `$` 符号用于分隔不同摄像头的结构。当我更改摄像头的 `username` 时,**第一个** `ct1m4d7TxSCJH2bHiWjSRA==` 发生了变化。当我更改摄像头的 `password` 时,**第二个** `ct1m4d7TxSCJH2bHiWjSRA==` 发生了变化。
好了,现在我们知道了摄像头的 `username` 和 `password` 存放的位置,也知道了我们设置的导出密码并不会影响这些加密。
之后,我开始深入分析应用程序本身。
## 应用程序的发现
因为我为摄像头创建了许多具有不同长度 `username` 和 `password` 的二维码,我注意到解码数据中的字节数总是相同的:`16`。看起来我们遇到的是块密码!那我们最熟知的块密码是什么呢?**AES!**
## 最终步骤
Android 应用程序加载了一个包含我们要找功能的 native library。我使用 Ghidra 继续探索那个 native library。
在该 library 中,我找到了硬编码的 AES 加密密钥:`dkfj4593@#&*wlfm`,并发现它是密钥长度为 `16` 字节的 AES,且使用了自定义的非标准加密轮数:`4`!
导出密码被加密并存储在 `:` 分隔符之间的最前面位置。但是在最后面,也就是在加密的导出密码和所有摄像头数据之后是什么呢?那是二维码创建时的加密时间戳,过期机制就是这么实现的……
在此之后,这个程序被编写出来了,让我们最终解码我们的二维码数据:
```
$ python hik_qr_export.py decode 'QRC03010003eJwrKnNNzC0vNy/yLogwD041LTUocg13tLW1ijRyK4mK8MpQM1DzDcmu9MlyNfJ3NqkA0rZqFgYGBmpqySWGuSYp5iEVwc5eHkZJHpnhWcFBQK04JVSsjJO8g5IC0gMSU6KcqsxczEuzjfQNA21tAQ4rKR0='
Data header: QRC03010003
Password used: 12Qweq24
QR code generated at: 1740175993 (2025-02-21T23:13:13)
Device Name: camera
IP Address: 192.168.1.1
Port: 8000
Username: admin
Password: admin
```
| 编码后 | 解码后 |
|---|---|
rvEamww7rKpX7Se5u0rEWA== | \xae\xf1\x1a\x9b\x0c;\xac\xaaW\xed'\xb9\xbbJ\xc4X |
: 分隔符 |
|
Y2FtZXJh | camera |
& 分隔符 |
|
0 未编码 | |
& 分隔符 |
|
MTkyLjE2OC4xLjE= | 192.168.1.1 |
& 分隔符 |
|
8000 未编码 | |
| 空字段 | |
& 分隔符 |
|
ct1m4d7TxSCJH2bHiWjSRA== | r\xddf\xe1\xde\xd3\xc5 \x89\x1ff\xc7\x89h\xd2D |
& 分隔符 |
|
ct1m4d7TxSCJH2bHiWjSRA== | r\xddf\xe1\xde\xd3\xc5 \x89\x1ff\xc7\x89h\xd2D |
$ 分隔符 |
|
: 分隔符 |
|
3bKRbPgPadZBz6D7uk2/1Q== | \xdd\xb2\x91l\xf8\x0fi\xd6A\xcf\xa0\xfb\xbaM\xbf\xd5 |
标签:Python, StruQ, 二维码解析, 云资产清单, 数据解密, 无后门, 物联网, 逆向工具, 逆向工程