本文档提供了截至 2018 年 1 月 oBike 通信协议的分析。
相关成果已在 insomni'hack 安全大会 2019 上展示:
同时也在 AREA41 安全大会 2018 上展示:
oBike 车锁由 TI CC2541 微控制器构成,这是一款用于蓝牙低功耗(BLE)应用的功耗优化型片上系统(SoC)。车锁本身不具备 IP 连接能力;它借助移动设备的 3G/4G 连接与 oBike 后端进行通信。车锁通过 BLE 与移动设备上的 oBike 应用通信。协议消息随后通过 REST API 中继到 oBike 后端。
车锁本身没有 GPS 模块。因此,上报到后端的位置始终是移动设备的位置,而非 oBike 本身的位置。``` GPS | +---------+ +---------+ +----------+ | oBike | | Mobile | | oBike | | Lock | +--- BLE ---> | Device | +--- HTTPS ---> | Backend | +---------+ +---------+ +----------+
## 解锁序列```
oBike Lock (BLE) Mobile Device (HTTPS) oBike Backend
------------+------------------------+---------------------------+--------------
| | |
| [1] hello(lat, lng) | |
| <--------------------- | |
Generate | | |
32bit | [2] keySource | |
Challenge | ---------------------> | [3] unlockPass(keySource) |
| | ========================> | Compute
| | | Response
| [5] | [4] encKey, keys |
| sendKeys(encKey, keys) | <======================== |
!Unlock | <--------------------- | |
Bike! | | |
| | |
Generate | | |
Acknowledge | [6] macKey, index | [7] |
Message | ---------------------> | lockMessage(macKey,index) |
| | ========================> | Register
| | | Ride (start
| | | billing)
步骤:
hello消息,将坐标推送到锁。keySource,这是一个32位值,表示芯片上电以来的毫秒数(小端序)。unlockPass REST调用将keySource发送到oBike后端。encKey(密钥索引)和keys中的128位密钥值。encKey(截断为96位)和index(对应于encKey)。此时,自行车将解锁。macKey和index,表示解锁成功的确认。lockMessage,包含相应的值(macKey和index)。此时,oBike后端将登记行程并开始计费。以下章节描述的BLE协议组件已在Python模块obike.ble_client中实现。此外,用于检测obike BLE广播的扫描器已在obike.ble_scanner.py中实现。
6774 0D 86 59AEB6...3931 FD | | | | | | | | | +-- Check byte | | | +----------------- Payload | | +--------------------- Command type | +------------------------- Length of payload in bytes +------------------------------- Command Signature ('gt')
消息(无论是传入还是传出)始终以签名 `\x67\x74`(ASCII `gt`)开头。
载荷长度是不含头部/尾部(header/trailer)的字节数。
该协议支持由单个字节标识的不同消息类型。最高两位定义消息方向:```
0x86 1000 0101 mobile -> obike
0x46 0100 0101 obike -> mobile
校验字节是通过对命令类型和载荷字节进行异或运算计算得出的:``` check_byte = cmdtype ^ b[0] ^ b[1] ^ ... ^ b[N-1]
最大PDU大小为19字节(头 + 负载 + 尾)。超过此大小的消息将被分片。
### BLE getLockRecord/deleteLockRecord
命令类型:`6`
这些消息用于管理“锁记录”,即芯片持久化的一条数据记录,包含上次骑行信息,例如会员ID、时间戳、oBike标识符、坐标等。
在无负载的情况下调用该命令,用于检索已保存的锁记录:```
00000000 67 74 00 86 86 |gt...|
如果没有可用的锁记录,锁将以空载荷进行响应。``` 00000000 67 74 00 46 46 |gt.FF|
否则,锁的响应包含上次行程中的几个值,采用
以下格式:```
00000000 67 74 46 46 00 00 01 23 45 67 59 9d 72 2a 44 31 |gtFF...#d2Y.r*D1|
00000010 39 33 36 42 33 31 37 2a 72 9d 59 00 34 37 2e 33 |936B317*r.Y.47.3|
00000020 37 32 37 36 30 00 00 00 30 38 2e 35 33 30 38 34 |72760...08.53084|
00000030 32 32 00 00 87 76 f3 7a 8c be 90 f8 4b a4 fa 00 |22...v.z....K...|
00000040 2e ae e3 dc 91 00 00 00 a9 01 8b |...........|
0004 000001234567 member-id (explicitly coded in decimal, value is: 1234567)
000a 599d722a UNIX timestamp, 08/23/2017 @ 12:16pm
000e 443139333642333137 obike identifier (D1936B317) this corresponds to the MAC address without the first 3 hex digits, in this case: D4:3D:19:36:B3:17
0017 2a729d59 same UNIX timestamp, little endian
001b 00 transaction type
001c 34372e333732373630000000 latitude (47.372760)
0028 30382e353330383432320000 longitude (08.5308422)
0034 8776f37a8cbe90f84ba4fa002eaee3dc mackey (128 bits)
0044 91 key index
0045 000000 ? 0048 a901 battery voltag level (little endian) e.g. 4.25V
当与有效负载一起使用时,此命令会删除当前的锁记录:```
00000000 67 74 0d 86 59 d5 ff a4 36 33 39 38 37 37 31 33 |gt..Y...63987713|
00000010 43 14 |C.|
0004 59d5ffa4 timestamp (little endian) 000e 3633393837373133 "63987713C" obike identifier
### BLE 坐标推送 / 获取挑战
命令类型:`1`
这些消息由移动设备用于指示 obike 的当前位置,并将该位置存储起来。```
00000000 67 74 13 81 30 38 2e 35 33 30 38 34 32 32 34 37 |gt..08.530842247|
00000010 2e 33 37 32 37 36 30 b7 |.372760.|
0004 30382e35333038343232 longitude (08.5308422) 000e 34372e333732373630 latitude (47.372763)
该命令也可以在没有 payload 的情况下使用:```
00000000 67 74 00 81 81 |gt...|
作为响应,obike 会发送一个挑战,一个32位整数(小端序) 表示自通电以来经过的毫秒数:``` 00000000 67 74 0b 41 00 11 51 00 06 ef 5f 34 aa 01 00 28 |gt.A..Q..._4...(|
- [HaE](https://github.com/gh0stkey/HaE) - [BurpSuite](https://portswigger.net/burp) 的辅助工具,可以高度自定义地高亮标记有趣的 HTTP 请求/响应。```
Offset Value Description
-------------------------------------------------------------------------
0004 00115100 ? (constant, sometimes also 00115900)
0008 06ef5f34 challenge (aka keysource)
000c aa01 battery voltage level (little endian),
e.g 4.26V
000e 00 ? (constant)
命令类型: 2
这些消息用于将挑战的响应传递给 obike。```
00000000 67 74 18 82 8b 00 00 01 23 45 67 00 2a 72 9d 59 |gt......#d2.*r.Y|
00000010 2f 42 d3 b4 3b 1b 9d 51 e7 67 13 e3 77 |/B..;..Q.g..w|
输入内容为空。请提供需要翻译的文本内容。```
Offset Value Description
-------------------------------------------------------------------------
0004 8b key index
0005 00000123456700 member-id (explicitly coded in decimal,
value is 1234567)
000c 2a729d59 UNIX timestamp, little endian
08/23/2017 @ 12:16pm (UTC)
0010 2f42d3b43b1b9d51e76713e3 enckey (truncated to 96 bits)
obike 响应单个状态字节 (00):``` 00000000 67 74 01 42 00 42 |gt.B.B|
### BLE 重置
命令类型:`9`
重置 CC2541 芯片。```
00000000: 67 74 00 89 89 gt...
不返回任何响应。
一个包含加密层的 obike REST API 客户端实现在 Python 模块 obike.http_client 中。
与 oBike 后端进行所有通信的 REST API 端点为:``` https://mobile.o.bike/api//
根据 REST 调用的不同,当前使用 `v1` 或 `v2`。
oBike 移动应用向 oBike 后端发起的请求使用以下请求头:```
Host: mobile.o.bike
Connection: keep-alive
Accept-Encoding: gzip, deflate
Accept: */*
User-Agent: okhttp/3.4.2
platform: Android
Content-Type: application/json; charset=utf-8
version: 2.5.4
Authorization: Bearer 0123456789abcdef0123456789abcdef
Content-Length: 397
自2017年秋季起,推出了API第2版,该版本使用对称加密方案对大多数 HTTP POST 负载进行加密。加密值通过以下形式的 JSON 对象的负载 进行传递:``` {"value": "b9fb151cd004e1d570201c8ee1a42a6bea3053550c545c446f192f11cfebac8389741 94cff9d3985cebf2bc751acc98359769a9886bc02fe9ab9fc1a5a646c46e1c51e668a717343307fc 5dcef00f8bf92e1badc6d0506f31303e5947ee3453d"}
十六进制值使用 AES-128 CBC 模式按以下方式加密:```
ciphertext = AES(plaintext_with_hash, secret_key, iv)
其中 secret_key 是字符串 oBAddMYFUzLed 后附加移动应用的版本
号。例如,对于版本 2.5.4,密钥为 oBAddMYFUzLed254。
所使用的初始化向量是 1234567890123456。
plaintext_with_hash 由以下内容计算得出:```
plaintext_with_hash = plaintext + '&' + SHA1(hash_key + plaintext + '&')
其中 `hash_key` 是字符串 `oBaddX4buhBMG`。
以下是如何使用 OpenSSL 解密负载:```
$ echo -n "b9fb151cd0...453d" | \
xxd -r -p | \
openssl enc -aes-128-cbc -d -K 6f424164644d5946557a4c6564323534 -iv 313233343536
37383930313233343536
REST API 的认证方案与 OAuth/OpenID 略有相似。用户名 和密码会被提交,作为回报,服务器会为新会话发送访问令牌和刷新令牌。 示例:
HTTP 请求:``` POST /api/v2/member/login HTTP/1.1 platform: Android Connection: close Accept-Language: en version: 2.5.4 Content-Type: application/json; charset=utf-8 Content-Length: 396 Host: mobile.o.bike Accept-Encoding: gzip User-Agent: okhttp/3.4.2
{"value":"e4526007ae3791e4fabdd5f6833563f8499d076fe5a4039e1ac1bcb7788dc7a053e8b4 384faf202828e6587bbc4bf32f505429129871253ecc388b493f32368ac418f627acc7720c1b5e1a 4ecc35fca7e80dd99062c24cea0b920fcc297164f8703511520f05c2f91ada946dbee9320a0d2f24 f1101036133d53425e91f2b52b7abbea95cde3f395ce8f2c586aa1ea9eaa35fecb26214eb498dbd3 5c56d37b88ebdc100180da662cdae6d6aa50c31d2f92063c2acb8ff45f62d12d34005d48a3"}
解密后的载荷:```
{"phone":"791234567","deviceId":"0123456789abc-0123456789abcdef01","password":"s
wordfish","countryCode":"41","dateTime":"1515100814123"}&687fdcb704d0661b67cf1a2
97b5059b1d4d67900
HTTP 响应:``` HTTP/1.1 200 Server: nginx/1.10.3 (Ubuntu) Date: Thu, 04 Jan 2018 21:20:14 GMT Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Connection: close Vary: Accept-Encoding
{"data":{"resourceOwnerId":1234567,"accessToken":"0123456789abcdef0123456789abcd ef","refreshToken":"0123456789abcdef0123456789abcdef","scope":"customer","expire sIn":5184000,"registerCoupon":null},"success":true,"errorCode":100}
已认证的请求必须在 Authorization 标头中包含访问令牌
,例如:```
Authorization: Bearer 0123456789abcdef0123456789abcdef
发送一个挑战并接收用于解锁 oBike 的响应。
HTTP 请求:``` POST /api/v2/bike/unlockPass HTTP/1.1 Host: mobile.o.bike Connection: keep-alive Accept-Encoding: gzip, deflate Accept: / User-Agent: okhttp/3.4.2 platform: Android Content-Type: application/json; charset=utf-8 version: 2.5.4 Authorization: Bearer 0123456789abcdef0123456789abcdef Content-Length: 397
{"value": "697560f47a113c7950836a914d60a342ea3feac166dd74ae67aabfa5c51f79f27d0a0 3335e72d6fcf747681badbd177499deec15a2e545ef1e293431592f2e080ae5fa6e031064369bbfe f7bccf1a626d7c22265d2df4bcc20b514bf8aa0263d808d1de57ab3a8ef71093e0d558f0f6fba15b 59901b01ca6c95878d69f2f85c5dd90d9610222eb5f7ca2cf955991b7701496e701d39ed0b05514b 6f014634554"}
已解密的载荷:```
{"dateTime":1515016440105,"deviceId":"0123456789abc-0123456789abcdef01","bikeId"
:"041001337","keySource":"54835578"}&cd835995e39c8076f15e3d989727106115b02b8b
备注:
bikeId 不得对应于实际被解锁的 oBikekeySource 是 oBike 锁作为对 "BLE Push Coords / Get Challenge" 命令的响应
而生成的 32 位挑战码HTTP 响应:``` HTTP/1.1 200 Server: nginx/1.10.3 (Ubuntu) Date: Fri, 05 Jan 2018 16:03:35 GMT Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Connection: close Vary: Accept-Encoding X-Application-Context: customer:prod:8084
{"data":{"encryptionKey":136,"keys":"ff533ae8ec0b1a1816160e9182af7d74","serverTi me":1515168215665},"success":true,"errorCode":100}
### 锁定消息
将确认码发送到服务器。服务器收到此消息后开始计费。
HTTP 请求:```
POST /api/v2/bike/lockMessage HTTP/1.1
platform: Android
Connection: close
Accept-Language: en
version: 2.5.4
Authorization: Bearer 80828d0bd57c95f6d5012c45e548598e
Content-Type: application/json; charset=utf-8
Content-Length: 652
Host: mobile.o.bike
Accept-Encoding: gzip
User-Agent: okhttp/3.4.2
{"value":"14420a2ef69b61c06667a58bc6842c7b781b3fc650100652982bbe369b5c818df3225c
6cd8bb63e9cbd7c33a21b6d7be5c66b4ff7744ddcc3b5debf1a1fc0a1a8b379278b360edb5ab3ceb
7014954dc084c1f05a76aa311b09aafe5ca84215f8f3e03f4d9187c14bd0215c8f8c1f535cb46f8f
fc3cd8f327a9410b0378a61dddc0ffdbadd084ccae67442da6598e52f69452a5a7c4fc087df6f8f0
dfd3277db90ae6ecbaf4c7763f5c2bb24611fd8b89c00a2f87442b526bce73405b7370b8adca045c
2852068516c3f8a473cb79c8635092531ef9104f361370c2d11f352c8b3ad9c356a8f363e8da7120
0dcf36d264009e0098c208089f7fbc113fdf342b4222b1466e3d40287dd1b024b3c55bfcf4dbce28
e7156ea5ef1883dd2c390bf7f5eef7833e72ff33eb04ed501f91c7915670f4bb24382fe232adbe30
7ebf80acfb"}
解密后的负载:``` {"deviceId":"0123456789abc-0123456789abcdef01","lati":47.3732953,"longi":8.53144 7,"memberId":1234567,"dateTime":"1515159966559","index":"85","timestamp":"151515 9965000","mackey":"6C81C6822A08C30881CFB0D0FAF7D89B","vol":"3.79","transtype":0, "bikeTradeNo":"5A4F819D639BC4AE1"}&4b5d220310f48744cfadcfd0d554384e2df03921
说明:
* `mackey` 对应 BLE Hello 命令接收到的 128 位值,
此时自行车处于解锁状态。
HTTP 响应:```
HTTP/1.1 200
Server: nginx/1.10.3 (Ubuntu)
Date: Fri, 05 Jan 2018 13:46:07 GMT
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Connection: close
Vary: Accept-Encoding
{"data":{"startTime":1515159967958,"message":"Unlocked successfully."},"success"
:true,"errorCode":100}
获取通过此账户预订的行程列表。
HTTP 请求:``` POST /api/v2/order/list HTTP/1.1 platform: Android Connection: close Accept-Language: en version: 2.5.4 Authorization: Bearer 0123456789abcdef0123456789abcdef Content-Type: application/json; charset=utf-8 Content-Length: 268 Host: mobile.o.bike Accept-Encoding: gzip User-Agent: okhttp/3.4.2
{"value":"14420a2ef69b61c06667a58bc6842c7b781b3fc650100652982bbe369b5c818df3225c 6cd8bb63e9cbd7c33a21b6d7be45a70bc261bf64e65f985aa51faefe44de6dcafd6e4037b82f1d05 029b4dcb85caeac6f67df317710c7ccd0b3303c92bb9557e2fde213bc397ede304c0c13a16b0310c 799a569df81f2f07a89c5cca06"}
解密的有效载荷:```
{"deviceId":"0123456789abc-0123456789abcdef01","current":1,"dateTime":"151605127
4357"}&11fae15b700f6f69133bfe167c1f99c503359cde
HTTP 响应(JSON 美化打印):``` HTTP/1.1 200 Server: nginx/1.10.3 (Ubuntu) Date: Mon, 15 Jan 2018 21:21:16 GMT Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Connection: close Vary: Accept-Encoding
{ "data": { "per": 20, "current": 1, "count": 42, "list": [ { "id": 15871962, "orderNo": "1801060003123123", "status": 5, "price": 0, "bookTime": null, "cancelBookTime": null, "startTime": 1515168315000, "endTime": 1515168321000, "userId": 1234567, "bikeId": "041001337", "trackId": 17041234, "distance": 0, "minutes": 0, "freeMinutes": 0, "carbon": 0, "calorie": 0, "grade": 0, "unitPrice": 1.5, "currency": "CHF", "currencySymbol": "CHF", "actualPrice": 0, "actualCurrency": "CHF", "actualCurrencySymbol": "CHF", "activityId": null, "couponId": null, "clubcardId": null, "savedMoney": null, "tempActualPrice": null, "tradeNo": "5A4FA1D763990A444", "tempEndTime": null, "tempMinutes": null, "lockType": 1, "countryId": 167, "cityId": null, "platForm": null, "promotionActivityId": 0, "parkingAreaType": 0 }, ... ], "pages": 3 }, "success": true, "errorCode": 100 }
### 列出自行车
获取指定坐标 (lat, lng) 附近的自行车列表。
HTTP 请求:```
POST /api/v2/bike/list HTTP/1.1
platform: Android
Connection: close
Accept-Language: en
version: 2.5.4
Authorization: Bearer 0123456789abcdef0123456789abcdef
Content-Type: application/json; charset=utf-8
Content-Length: 428
Host: mobile.o.bike
Accept-Encoding: gzip
User-Agent: okhttp/3.4.2
{"value":"1a585fffc27493e530e50c48b834f0df905fa023d59a7db8d64bae39473cd75adc7c33
e8a0e9173845c9478046cff0c85bccbaecc7acd9cfadfc650c30cae4278d7f906da3a710742b6372
79f21f5367f3ea2fd95564ee7077b85af5ef9ebcf7d456c15616ffda4f25524b587936984b62abfe
8a2a94c042d4893c9e74c267a42aed5310acc5fbb4924d57008cb2081e3e3009fdab5cbc7fc640b7
2efc4e2ba2a10af81ac72aee5100e1c706b9eb500810e40aae855134fe9f625bb34a3626e843922c
7c2b222e6f32daf69f130e9350"}
解密后的载荷:``` {"countryCode":41,"latitude":"47.37326917039802","longitude":"8.531275056302547" ,"deviceId":"0123456789abc-0123456789abcdef01","dateTime":"1508318416019"}&21b7e af2631f62fca73faf4c63fd75b1302aed4f
HTTP 响应(JSON 已美化打印并截断):```
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Connection: close
Server: cloudflare-nginx
{
"data": {
"iconUrl": null,
"list": [{
"id": "041003059",
"longitude": 8.527892,
"latitude": 47.369085,
"imei": "6F6E384C30503166",
"countryId": 167,
"helmet": 0
}, {
"id": "041001731",
"longitude": 8.526518,
"latitude": 47.369303,
"imei": "6F6E53696D39706C",
"countryId": 167,
"helmet": 0
},
...
{
"id": "041000911",
"longitude": 8.526561,
"latitude": 47.377715,
"imei": "72616C48496E6974",
"countryId": 167,
"helmet": 0
}]
},
"success": true
}
$ apt update $ apt install git build-essential python python-pip libbluetooth-dev libglib2.0-dev $ git clone https://github.com/antoinet/obike.git $ cd obike $ pip install -r requirements.txt
## 参考资料
* [oBike 锁拆解与重建,无桩共享单车救援](https://www.youtube.com/watch?v=Vl3Gl8w8n-Q)
* [共享单车系统弱点探索](http://www.comp.nus.edu.sg/~hugh/CS3235/CS3235-SemI-2017-18-FinalProjects.pdf), [备用来源](http://web.archive.org/web/20180102175104/http://www.comp.nus.edu.sg/%7Ehugh/CS3235/CS3235-SemI-2017-18-FinalProjects.pdf)
* [FCC 备案 Huangzhou Luoping Electronics - 智能储物柜 HBT203](https://fccid.io/2ALWC-HBT203)
* [苏黎世和伯尔尼的共享单车现已暂时停运,rts.ch](https://www.rts.ch/play/tv/19h30/video/les-velos-en-libre-service-dans-les-villes-de-zurich-et-de-berne-sont-desormais-retires-de-la-circulation-temporairement?id=9797225&station=a9e7621504c6960e35c3ecbe7f6bed0446cdf8da)
* [CVE-2018-16242](https://seclists.org/bugtraq/2018/Sep/30)
## 致谢
其他安全研究人员也与我取得了联系,他们同样在研究 oBike 锁系统。感谢你们的意见和贡献:
* [@vincent_tky](https://twitter.com/vincent_tky)
* mh ([SSD e.V.](https://ssdev.org))
* NUS Computing 的 CS3235 学生