宝马i3 车载系统API逆向分析报告
我个人真的很享受宝马i3带来的驾驶乐趣。
宝马i3的车载系统可以将运作状态整理成文字发送到微博上、或者将其位置信息上传到我的服务器当中、抑或是在温度太高时允许我以远程方式启动其空调系统—— 总而言之,我们可以利用宝马i3车载系统的车辆数据实现上百种炫酷有趣的应用效果。不过必须承认,虽然宝马官方的应用已经提供了其中部分功能,但其速度极 慢、界面丑陋而且难于使用。
宝马公司曾经对外向技术人员开放了一项API,不过随后又将其关闭了。
目标
我们的最终目标在于利用该API访问车辆自带的全部功能——这样我们就能实现以下预期效果:
设备
为了实现这项看起来并不困难的任务,我们需要准备以下设备……
一辆宝马i3。
宝马ConnectedDrive账户。
宝马I Remote App的Android版本。(我使用的是非美国版。)
一台Android手机或者平板设备。
一套能够抓取加密数据包的工具——我个人推荐由Grey Shirts推出的Packet Capture。
在Packet Capture安装完成之后,关闭其它所有应用,开户登录,而后登录至I Remote应用。点击其中几项功能,接下来切换回Packet Capture应用。
大家会看到其顺利捕捉到了一大堆API调用信息。
点击其中一项,我们就能查看到完整的API调用信息外加JSON响应。我在其中隐去了部分与个人相关的敏感信息。
我们需要的最为重要的实现要素就是Authorization:Bearer令牌。这是一条长度为32个字符的字母加数字字符串。
描述
大家可以点击此处在GitHub上下载宝马I Remote的最新版本。
这些API数据接口在设计思路上用于帮助大家与自己的宝马i3进行交互。而在此次尝试当中,我们将通过官方发布的宝马I Remote Android应用对其进行逆向分析。
当然,如何使用这些API数据接口则完全取决于大家的意愿——包括由此带来的风险。宝马公司对此不负任何责任但也并不禁止我们加以利用。
该软件需要以其“原始形式”加以使用,且不以明示或者默示方式做出任何保证,包括但不限于被用于实现任何与适销性、特定用途以及非侵权性行为相关的目的。在 任何情况下,软件二次编写者或版权所有者须承担一切关于合同、侵权或者其它不当行为所产生的,或者由软件在使用或者其它相关场景当造成的索赔、损害或者其 它连带责任。
服务器
有三个宝马的API服务器地址。
https://b2vapi.bmwgroup.cn:8592 China
https://b2vapi.bmwgroup.us USA
https://b2vapi.bmwgroup.com Europe / Rest of World
授权
为了能够通过该API的授权验证,大家需要在宝马的ConnectedDrive服务上完成注册。
在这里我们需要:
用于注册ConnectedDrive的电子邮箱地址。
用于ConnectedDrive注册的密码内容。
i Remote API密钥。
I Remote API Secret。
大家可以通过对宝马I Remote 安卓版App应用进行反编译或者拦截手机与宝马API服务器间的通信数据来了解i Remote的细节信息。这部分工作就留给各位读者朋友自行探索吧。
首先,我们要使用Basic授权。这意味着我们需要获取该API密钥及Secret,并利用Base64对其进行编码。
这样一来key:secret就变成了2V5OnNlY3JldA==的形式。
我们还需要发送以下参数:
Content-Type: application/x-www-form-urlencoded
grant_type=password
&username=whatever%40example.com
&password=p4ssw0rd
&scope=remote_services+vehicle_data
下面是我们如何利用Curl对其进行处理:
curl \
-H "Authorization: Basic a2V5OnNlY3JldA==" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password&username=whatever%40example.com&password=p4ssw0rd&scope=remote_services+vehicle_data" \
"https://b2vapi.bmwgroup.com/webapi/oauth/token/"
如果一切进展顺利,那么大家应该会收到以下JSON响应:
{
"access_token": "RCQ1hLP4AFaUBW9BjcPUN3i4WgkwF90R",
"token_type": "Bearer",
"expires_in": 28800,
"refresh_token": "7WgKmEJ2kD1ydl9Hefp01eS8qDGzKnzjeORpA6vtsoFIEanz",
"scope": "vehicle_data remote_services"
}
注意:我们必须将
Authorization: Bearer RCQ1hLP4AFaUBW9BjcPUN3i4WgkwF90R
添加到每一条请求头当中。
另外expires_in是以秒来计算的——这意味着我们每八个小时就需要对令牌进行一次更新。
我到现在也没弄明白refresh_token到底有什么用。因为一旦access_toke过期,大家可以直接对其重新授权并获取到一条新令牌。
API
我们必须将
Authorization: Bearer RCQ1hLP4AFaUBW9BjcPUN3i4WgkwF90R
添加到每一条请求头当中。
获取车辆数据
/webapi/v1/user/vehicles/
Remember to include the Authorization: Bearer
在这里,最重要的就是VIN一项——也就是车辆识别号码。大家需要在全部其它API调用以及Authorization Bearer当中使用VIN。
>>>>响应结果
值
{
"vehicleStatus": {
"vin": "WAB1C23456V123456",
"mileage": 1234,
"updateReason": "VEHICLE_SHUTDOWN_SECURED",
"updateTime": "2015-10-30T18:45:04+0100",
"doorDriverFront": "CLOSED",
"doorDriverRear": "CLOSED",
"doorPassengerFront": "CLOSED",
"doorPassengerRear": "CLOSED",
"windowDriverFront": "CLOSED",
"windowDriverRear": "CLOSED",
"windowPassengerFront": "CLOSED ",
"windowPassengerRear": "CLOSED",
"trunk": "CLOSED",
"rearWindow": "INVALID",
"convertibleRoofState": "INVALID",
"hood": "CLOSED",
"doorLockState": "SECURED",
"parkingLight": "OFF",
"positionLight": "OFF",
"remainingFuel": 8.9,
"remainingRangeElectric": 73,
"remainingRangeElectricMls": 45,
"remainingRangeFuel": 126,
"remainingRangeFuelMls": 78,
"maxRangeElectric": 134,
"maxRangeElectricMls": 83,
"fuelPercent": 99,
"maxFuel": 9,
"connectionStatus": "DISCONNECTED",
"chargingStatus": "INVALID",
"chargingLevelHv": 58,
"lastChargingEndReason": "UNKNOWN",
"lastChargingEndResult": "FAILED",
"position": {
"lat": 51.123456,
"lon": -1.2345678,
"heading": 211,
"status": "OK"
},
"internalDataTimeUTC": "2015-10- 30T18:47:44"
}
}
>>>>值说明
Mileage的单位为公里。
remainingFuel的单位为升。
maxRangeElectric的单位为公里。
maxRangeElectricMls的单位为英里。
chargingLevelHv代表电池当前剩余电量的百分比(Hv可能代表的是高压?)。
maxFuel的单位为升。
heading的单位为度(角度)。
>>>>有效的chargingStatuses包括以下几种:
CHARGING
ERROR
FINISHED_FULLY_CHARGED
FINISHED_NOT_FULL
INVALID
NOT_CHARGING
WAITING_FOR_CHARGING
>>>>有效的connectionStatuses包括以下几种:
CHARGING_DONE
CHARGING_INTERRUPED [sic]
CHARGING_PAUSED
CHARGIN_STARTED [sic]
CYCLIC_RECHARGING
DOOR_STATE_CHANGED
NO_CYCLIC_RECHARGING
NO_LSC_TRIGGER
ON_DEMAND
PREDICTION_UPDATE
TEMPORARY_POWER_SUPPLY_FAILURE
UNKNOWN
VEHICLE_MOVING
VEHICLE_SECURED
VEHICLE_SHUTDOWN
VEHICLE_SHUTDOWN_SECURED
VEHICLE_UNSECURED
上一次的车辆行程
这里显示的是最近一次行程的相关细节信息。
/webapi/v1/user/vehicles/:VIN/statistics/lastTrip
其中VIN代表的是我们车辆的VIN。
请记得在报头当中包含Authorization: Bearer。
>>>>响应结果
值
{
"lastTrip":{
"efficiencyValue":0.53,
"totalDistance":141,
"electricDistance":100.1,
"avgElectricConsumption":16.6,
"avgRecuperation":2,
"drivingModeValue":0,
"accelerationValue":0.39,
"anticipationValue ":0.81,
"totalConsumptionValue":0.79,
"auxiliaryConsumptionValue":0.66,
"avgCombinedConsumption":1.9,
"electricDistanceRatio":71,
"savedFuel":0,
"date":"2015-12-01T20:44:00+0100 ",
"duration":124
}
}
>>>>值说明
这里的距离计算单位似乎是公里而非英里,因此请确保对具体值进行相应调整。将公里乘以0.621371就得到了对应的英里数字。
totalDistance的单位为公里。
electricDistance的单位为公里。
avgElectricConsumption的单位为千瓦时/百公里。
avgRecuperation的单位为千瓦时/百公里。
duration的单位为分钟。
大家可以利用以下公式将千瓦时/百公里换算成千瓦时/百英里。
1 / (0.01609344 * avgElectricConsumption)
获取充电时间
显示车辆何时应该进行充电。
/webapi/v1/user/vehicles/:VIN/chargingprofile
其中VIN代表的是我们车辆的VIN。
请记得在报头当中包含Authorization: Bearer。
>>>>响应结果
{
"weeklyPlanner":{
"climatizationEnabled":true,
"chargingMode":"DELAYED_CHARGING",
"chargingPreferences":"CHARGING_WINDOW",
"timer1":{
"departureTime":"07:30",
"timerEnabled":true,
" weekdays":[
"MONDAY"
]
},
"timer2":{
"departureTime":"13:00",
"timerEnabled":false,
"weekdays":[
"SATURDAY"
]
},
"timer3":{
"departureTime" :"08:00",
"timerEnabled":false,
"weekdays":[
]
},
"overrideTimer":{
"departureTime":"07:30",
"timerEnabled":false,
"weekdays":[
"MONDAY"
]
},
"preferredChargingWindow":{
"enabled":true,
"startTime":"05:02",
"endTime":"17:31"
}
}
}
>>>>值说明
departureTime似乎代表的是车辆的的当地时间。
获取车辆目的地信息
显示我们此前发送至车辆的目的地信息。
/webapi/v1/user/vehicles/:VIN/destinations
其中VIN代表的是我们车辆的VIN。
请记得在报头当中包含Authorization: Bearer。
>>>>响应结果
{
"destinations":[
{
"lat":51.53053283691406,
"lon":-0.08362331241369247,
"country":"UNITED KINGDOM",
"city":"LONDON",
"street":"PITFIELD STREET",
"type": "DESTINATION",
"createdAt":"2015-09-25T08:06:11+0200"
}
]
}
>>>>值说明
一组位置数据。
获取全部行程细节信息
显示车辆获取到的全部行程统计信息。
/webapi/v1/user/vehicles/:VIN/statistics/allTrips
其中VIN代表的是我们车辆的VIN。
请记得在报头当中包含Authorization:Bearer。
>>>>响应结果
{
"allTrips": {
"avgElectricConsumption": {
"communityLow": 0,
"communityAverage": 16.33,
"communityHigh": 35.53,
"userAverage": 14.76
},
"avgRecuperation": {
"communityLow": 0,
"communityAverage" : 3.76,
"communityHigh": 14.03,
"userAverage": 2.3
},
"chargecycleRange": {
"communityAverage": 121.58,
"communityHigh": 200,
"userAverage": 72.62,
"userHigh": 135,
"userCurrentChargeCycle": 60
},
"totalElectricDistance": {
"communityLow": 1,
"communityAverage": 12293.65,
"communityHigh": 77533.6,
"userTotal": 3158.66
},
"avgCombinedConsumption": {
"communityLow": 0,
"communityAverage": 1.21,
" communityHigh": 6.2,
"userAverage": 0.36
},
"savedCO2": 87.58,
"savedCO2greenEnergy": 515.177,
"totalSavedFuel": 0,
"resetDate": "1970-01-01T01:00:00+0100"
}
}
>>>>值说明
chargecycleRange的单位为公里。
totalElectricDistance的单位为公里。
我目前还不太确定其它数值的确切含义。
获取范围地图
生成一份折线图,用于显示该车辆的预计行驶范围。
/webapi/v1/user/vehicles/:VIN/rangemap
其中VIN代表的是我们车辆的VIN。
请记得在报头当中包含Authorization: Bearer。
>>>>响应结果
{
"rangemap": {
"center": {
"lat": 51.123456,
"lon": -1.2345678
},
"quality": "AVERAGE",
"rangemaps": [
{
"type": "ECO_PRO_PLUS",
"polyline" : [
{
"lat": 51.6991281509399,
"lon": -2.00423240661621
},
{
"lat": 51.6909098625183,
"lon": -1.91526889801025
},
...
]
},
{
"type": "COMFORT",
"polyline" : [
{
"lat": 51.7212295532227,
"lon": -1.7363977432251
},
{
"lat": 51.6991496086121,
"lon": -1.73077583312988
},
...
]
}
]
}
}
>>>>值说明
ECO_PRO_PLUS代表以经济模式行驶。
COMFORT代表以舒适模式行驶。
向车辆发送信息
向车辆发送信息相对更复杂一些。
我们的应用会与该API进行通信,而API随后再与车辆的3G模块进行通信,接下来我们就可以等待其做出响应了。
如果我们的车辆处于信号覆盖较差的地区,那么大家肯定会感受到相当明显的响应延迟——通常其延迟水平远远高于正常驾驶员能够接受的范围。
从基础层面出发,我们可以只发送一条简单请求——例如锁定车门或者设置非峰值充电。
获取请求状态
以下所示为一条POST请求的具体状态:
/webapi/v1/user/vehicles/:VIN/serviceExecutionStatus?serviceType=:SERVICE
其中VIN代表的是我们车辆的VIN。
请记得在报头当中包含Authorization:Bearer。
>>>>响应结果
{
"executionStatus":{
"serviceType":"DOOR_LOCK",
"status":"EXECUTED",
"eventId":" 123456789012345AB1CD1234@bmw.de"
}
}
>>>>值说明
有效状态包括:
DELIVERED
EXECUTED
INITIATED
NOT_EXECUTED
PENDING
TIMED_OUT
>>>>以下所示为有效的:SERVICE类型,但大家的车辆上不一定支持其中全部项目。
CHARGE_NOW
CHARGING_CONTROL
CLIMATE_CONTROL
CLIMATE_NOW
DOOR_LOCK
DOOR_UNLOCK
GET_ALL_IMAGES
GET_PASSWORD_RESET_INFO
GET_VEHICLES
GET_VEHICLE_IMAGE
GET_VEHICLE_STATUS
HORN_BLOW
LIGHT_FLASH
LOCAL_SEARCH
LOCAL_SEARCH_SUGGESTIONS
LOGIN
LOGOUT
SEND_POI_TO_CAR
VEHICLE_FINDER
POST一条指令
向车辆发送指令以执行相应行动。
/webapi/v1/user/vehicles/:VIN/executeService
其中VIN代表的是我们车辆的VIN。
请记得在报头当中包含Authorization: Bearer。
可用指令
这些指令皆可通过API实现,不过大家的车辆上不一定支持其中全部项目。
这些只是我截至目前发现的可用指令。
数据必须被POSTed到服务器端。
开始充电
如果车辆已经接入充电桩但未开始正确充电(也许是因此进行了非峰值时段设定),那么我们可以通过以下方式要求其立刻进行充电。
serviceType=CLIMATE_NOW
锁定车门
启动中央锁。
serviceType=DOOR_LOCK
解锁车门
这项指令将解开车辆上的所有门锁。
请在发送这条指令之前务必认真判断当前状况。确保车辆已经在自己的视野当中,而且有能力在必要时将其恢复锁定。
serviceType=DOOR_UNLOCK
开启前端照明灯
如果大家找不到自己的车子,或者需要照亮其附近的某些景物,则可以直接激活前端照明灯。
serviceType=LIGHT_FLASH&count=2
我猜其中的count值代表的是车灯亮起的具体秒数——但目前只是猜测。
充电时间表
设置峰值/非峰值充电时间表。
serviceType=CHARGING_CONTROL
我还没鼓捣过这部分功能,不过其返回的错误信息应该能给大家带来一些启示:
{
"error":{
"code":500,
"description":"(SmartPhoneUtil-A-102) Bad value(s) for parameter(s): Invalid chargingProfile, expected weeklyPlanner or twoTimesTimer"
}
}
寻车功能
serviceType=VEHICLE_FINDER
我不太确定这项功能的具体作用。
>>>>响应结果
所有POST指令的示例响应结果:
{
"executionStatus": {
"serviceType": "LIGHT_FLASH",
"status": "INITIATED",
"eventId": " 123456789012345AB1CD1234@bmw.de"
}
}
最后
利用上述指令,大家应该已经能够重现官方应用所提供的各项功能了。
如大家所见,我已经能够让自己的车辆自动发表微博推文了:
如果宝马公司能够开放官方API就太好了,这样用户们就能够拥有更加自由的车辆调整空间。就目前来看,其API安全性尚佳而且不太可能对车辆造成损害。
我已经将全部说明文档添加到了GitHub当中(点击下方的“阅读原文”进行查看),欢迎大家就此提出问题或者发送任何与变更相关的Pull Request。
本文由E安全编译 转载请注明来自 E安全 (www.easyaq.com)
您也可以下载E安全app获取及时最及时的安全资讯、预警信息等等
微信名:E安全
微信ID:EAQapp
❶ 点击往期内容,查看更多内容
❷ 复制网址在浏览器打开
www.easyaq.com
❸ 长按右侧二维码,关注E安全