流传的NDEF信息
目前网上流传最多的小米一碰传NFC NDEF信息大多为:
text
d42a4d636f6d2e7869616f6d692e6d695f636f6e6e6563745f736572766963653a65787465726e616c747970650a4b0801100d2201032a094d492d4e4643544147380f4a31271700000008000e5441475f444953434f564552454465064d4952524f52011141383a36443a41413a31373a44373a35316a02fa7f
其中:
00000008000
被认为是设备类型:8
为平板,3
为电脑41383a36443a41413a31373a44373a3531
被认为是蓝牙Mac地址的字符串
诚然这样的修改确实起效,但是这个NDEF信息中的其他部分是什么含义依然并不清晰。
因此必须逆向APP才能得到的准确含义,以下为解析过程和实际含义解释。
解析真实NDEF含义
NDEF解析
首先,NDEF信息中包含了NFC TAG需要的配置信息,这些信息都是可以通过NDEF协议进行解析的。
使用Python的ndef
库,将以上HEX
字符串转为bytes
后可以得到如下信息:
text
Record 0
flags:
message_begin: True
message_end: True
chunked: False
short: True
id: False
tnf: 4 [EXTERNAL]
type_len: 42
type: b'com.xiaomi.mi_connect_service:externaltype'
id_len: 0
id: None
payload_len: 77
payload: 0a4b0801100d2201032a094d492d4e4643544147380f4a31271700000008000e5441475f444953434f564552454465064d4952524f52011141383a36443a41413a31373a44373a35316a02fa7f
一条NDEF数据被认为是一条Message
,一个Message
中包含了多个Record
。
经过解析,这条NDEF数据中只包含了一个Record
,其载荷为外置(EXTERNAL
)类型的com.xiaomi.mi_connect_service:externaltype
。
因此下一步需要找到安卓系统如何处理这样的NDEF内容。
处理NDEF信息的APP
根据安卓官方文档,要处理这种类型的数据,就需要APP在Manifest
的activity
中声明以下内容:
xml
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="vnd.android.nfc" android:host="ext" android:pathPrefix="/com.xiaomi.mi_connect_service:externaltype"/>
</intent-filter>
也就是说要处理Uri为vnd.android.nfc://ext/com.xiaomi.mi_connect_service:externaltype
的NDEF内容。
使用ADB命令在手机上搜索可以得到com.xiaomi.mi_connect_service/.nfc.NfcFieldOnPublisher
:
shell
adb shell cmd package query-activities --components -d "vnd.android.nfc://ext/com.xiaomi.mi_connect_service:externaltype"
找到com.xiaomi.mi_connect_service
包名的App,名称为小米互联通信服务
。
查看其中的.nfc.NfcFieldOnPublisher
组件,发现使用了com.xiaomi.mi_connect.nfc.proto.v1.NfcTagAdvDataCoder
类对数据进行解析。
不难发现,这个NDEF TAG使用了ProtoBuf的数据存储格式,在APP的资源文件中进行对比可以找到AttrProto.proto
文件,以下为部分截取的片段。
protobuf
syntax = "proto3";
package mi_connect_service;
message AttrAdvData {
int32 versionMajor = 1;
int32 versionMinor = 2;
bytes apps = 3;//已过时。请使用appIds替代。
bytes flags = 4;
string name = 5;
bytes idHash = 6;
int32 deviceType = 7;
int32 securityMode = 8;
repeated bytes appsData = 9;
repeated bytes supportSetting = 10;
repeated bytes currentSetting = 11;
string wifiMac = 12;
repeated int32 appIds = 13;//引入appIds之前使用的是apps
int32 commData = 14;
bool ziped = 15;
string wiredMac = 16;//有线网卡mac
string btMac = 17; //Bluetooth Device MAC
}
message AttrOps {
AttrAdvData advData = 1;
int32 sequenceId = 14;
}
使用Python将载荷中的数据使用ProtoBuf数据格式解析,可以得到以下JSON:
json
{
"advData": {
"appIds": [
16378
],
"appsData": [
"JxcAAAAIAA5UQUdfRElTQ09WRVJFRGUGTUlSUk9SARFBODo2RDpBQToxNzpENzo1MQ=="
],
"deviceType": 15,
"flags": "Aw==",
"name": "MI-NFCTAG",
"versionMajor": 1,
"versionMinor": 13
}
}
注:flags
和appsData
下的数据使用base64编码,实际为bytes
text
flags: b'03'
app data: [b'27:17:00:00:00:08:00:0e:54:41:47:5f:44:49:53:43:4f:56:45:52:45:44:65:06:4d:49:52:52:4f:52:01:11:41:38:3a:36:44:3a:41:41:3a:31:37:3a:44:37:3a:35:31']
AttrAdvData字段含义
进一步解析APP相关代码,可以得到如下表格:
appId
数字 | 类型 |
---|---|
1 | MI_SHARE |
2 | MI_PLAY |
62 | MI_ACCOUNT |
63 | MI_MOVER |
16382 | MI_TEST |
3 | MI_MIRROR |
16381 | MI_REMOTE_CONTROLLER |
16379 | MI_VOIP |
16377 | MI_VIDEO_RELAY |
16378 | MI_TAP |
16376 | MI_WATCH_CAMERA |
flags
数字 | 操作 |
---|---|
1 | 发送 com.xiaomi.nfc.action.TAG_DISCOVERED 广播,使用ProtoBuf中的wifiMac 和btMac 数据 |
3 | 发送 com.xiaomi.nfc.action.[ACTION] 自定义ACTION广播,使用ProtoBuf中的appsData 数据 |
Others | 发送 com.xiaomi.aiot.nfc.message 本地广播,转发NDEF的Tag 和Record 数据 |
注:接收 com.xiaomi.nfc.action.TAG_DISCOVERED
广播或者 com.xiaomi.nfc.action.[ACTION]
自定义ACTION广播,需要对应的BroadcastReceiver拥有 com.xiaomi.mi_connect_service.permission.RECEIVE_ENDPOINT
权限。
目前可用Action只有TAG_DISCOVERED
shell
adb shell "dumpsys package resolvers receiver | grep com.xiaomi.nfc.action."
appsData
数据含义
数据:27:17:00:00:00:08:00:0e:54:41:47:5f:44:49:53:43:4f:56:45:52:45:44:65:06:4d:49:52:52:4f:52:01:11:41:38:3a:36:44:3a:41:41:3a:31:37:3a:44:37:3a:35:31
数据 | 位置 | 含义 |
---|---|---|
27:17 |
0 ~ 1 | 版本号,十进制为39 23,直接通过是否相等判断 |
00:00:00:08 |
2 ~ 5 | 设备类型,十进制为8,int32 |
00 |
6 | 数字键值对数据长度,此处为0表示无数据 |
数字键值对数据
由于没有真实数据所以只说明数据格式
位置 | 含义 |
---|---|
0 | 键,整数 |
1 | 值长度 |
2 ~ 2+X-1 |
值,bytes |
尾部
数据 | 位置 | 含义 |
---|---|---|
0e |
7 | 自定义ACTION广播的名称的长度,0x0e=14 |
54: ... :44 |
8 ~ 8+14-1 |
自定义ACTION广播的名称,此处含义TAG_DISCOVERED |
65: ... :31 |
之后全部 | 发送的bytes 载荷 |
自定义ACTION广播
由于这个NDEF只涉及flag=3
的情况,所以本文只分析这个广播的参数构建
- Action:
com.xiaomi.nfc.action.[ACTION]
- Permission:
com.xiaomi.mi_connect_service.permission.RECEIVE_ENDPOINT
解析appsData
可得com.xiaomi.nfc.action.TAG_DISCOVERED
- Flags: 添加了多组flags
16777216
安卓FlagIntent.FLAG_ACTIVITY_PREVIOUS_IS_TOP
32
安卓FlagIntent.FLAG_INCLUDE_STOPPED_PACKAGES
268435456
小米自定义FlagStaticConfigServiceConnector.BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE
- Extra:
protocol_value_key
内容为JSON
protocol_value_key
JSON构建
键 | 值 |
---|---|
device_type_key |
设备类型,将00:00:00:08 转为int32,即8 |
attribute_value_key |
数字键值对数据转JSON后,UTF-8编码为Base64的字符串 |
protocol_payload_key |
发送的bytes 载荷,转为的Base64的字符串 |
注:不存在某种数据就不存在这个JSON字段。
数字键值对数据转JSON:键数字转为字符串,目前值直接交给org.json.JSONObject
处理(这样写会出BUG,不知道啥时候修复,可能用不到就不修复)。
处理NFC TAG信息的APP
对于以上Intent,使用ADB命令在手机上搜索可以得到com.milink.service/com.miui.circulate.nfc.relay.NfcTransportReceiver
:
shell
adb shell cmd package query-receivers --components -a "com.xiaomi.nfc.action.TAG_DISCOVERED"
找到 com.milink.service
包名的App,名称为投屏
或者互联互通服务
。
查看其中的 com.miui.circulate.nfc.relay.NfcTransportReceiver
组件,发现使用了 com.xiaomi.mi_connect.nfc.proto.v1.NfcTagAdvDataCoder
类对数据进行解析。
device_type_key
数据
可以在这个APP中找到更多的设备类型定义:
数值 | 类型 |
---|---|
2 | TV |
3 | PC |
5 | CAR |
8 | PAD |
protocol_payload_key
数据
数据:65:06:4d:49:52:52:4f:52:01:11:41:38:3a:36:44:3a:41:41:3a:31:37:3a:44:37:3a:35:31
数据 | 位置 | 含义 |
---|---|---|
65 |
0 | 键,整数,101 |
06 |
1 | 值长度,0x16=6 |
4d:49:52:52:4f:52 |
2 - 2+6-1 |
值,UTF-8字符串,MIRROR |
01 |
9 | 键,整数,1 |
11 |
10 | 值长度,0x11=17 |
41: ... : 31 |
11 - 11+17-1 |
值,UTF-8字符串,A8:6D:AA:17:D7:51 |
键数值含义
数值 | 字段名 | 数据类型 | 解释 |
---|---|---|---|
101 | actionSuffix | String | Action后缀 |
1 | btMac | String | 蓝牙Mac地址 |
2 | wifiMac | String | WIFI Mac地址 |
3 | wiredMac | String | 有线Mac地址 |
121 | extAbility | bytes | 额外能力 |
actionSuffix
目前已知的actionSuffix
为:
MIRROR
TVCAST
shell
adb shell "dumpsys package resolvers receiver | grep com.miui.onehop.action."
根据actionSuffix
发送自定义Action的广播
发送的是有序广播:OrderedBroadcast
- Action:
com.miui.onehop.action.[ACTION]
解析protocol_payload_key
可得com.miui.onehop.action.MIRROR
- Permission:
com.miui.onehop.permission.MIRROR
- Flags: 添加了多组flags
32
安卓FlagIntent.FLAG_INCLUDE_STOPPED_PACKAGES
16777216
小米自定义FlagRemoteCallAdapterKt.FLAG_RECEIVER_INCLUDE_BACKGROUND
- Extra:
_device_type
int,设备类型,device_type_key
_bt_mac
String,蓝牙Mac地址,btMac
_wifi_mac
String,WIFI Mac地址,wifiMac
_wired_mac
String,有线Mac地址,wiredMac
_ability_lyra
Boolean,(extAbility[0] & 1) > 0
这个广播目前只由Appcom.milink.service
自己接收并处理。
至此NFC Tag中的NDEF数据就已经解析完毕,再向下分析已经没有必要了。
NDEF解析总结
其实使用流传的NDEF修改方法并无问题,只是在修改设备类型时存在不影响结果的错误。
至于其他的字段含义,对于NFC快速流转并无影响,了解数据结构可能对未来协议功能升级后的破解会更加方便。
因此简单来说,可以将NDEF进行如下修改即可:
text
d42a4d636f6d2e7869616f6d692e6d695f636f6e6e6563745f736572766963653a65787465726e616c747970650a4b0801100d2201032a094d492d4e4643544147380f4a3127170000000[deviceType]000e5441475f444953434f564552454465064d4952524f520111[btMac]6a02fa7f
deviceType: 3 or 8
btMac: 4138 3a 3644 3a 4141 3a 3137 3a 4437 3a 3531
触发小米一碰传的其他方法
如果想要代码触发小米一碰传功能有两种方法:
- 发送虚拟的NFC Tag的NDEF信息
- 直接向
投屏
或者互联互通服务
发送广播
发送虚拟的NFC Tag的NDEF信息
安卓中NDEF广播要求必须有android.nfc.extra.TAG
的Tag
数据,但是这个数据只有真实的NFC硬件才能生成。
因此一个安全的解决方案是,对于已有但不能修改的NFC Tag,可以使用代码获取到这个NFC Tag的硬件Tag
数据,并对其中的内容修改后转发。
kotlin
// 从Intent中获取已有的NDEF数据
val tag: Tag? = IntentCompat.getParcelableExtra(intent, NfcAdapter.EXTRA_TAG, Tag::class.java)
val id: ByteArray = intent.getByteArrayExtra(NfcAdapter.EXTRA_ID)
val btMac = "00:00:00:00:00:00"
val hexPart1 = "0a4b0801100d2201032a094d492d4e4643544147380f4a3127170000000"
val hexPart2 = "000e5441475f444953434f564552454465064d4952524f520111"
val hexPart3 = "6a02fa7f"
// PC
val hex = hexPart1 + "3" + hexPart2 + btMac.toByteArray().toHexString() + hexPart3
val payload = hex.decodeHex()
val externalType = "com.xiaomi.mi_connect_service:externaltype"
val record = NdefRecord(NdefRecord.TNF_EXTERNAL_TYPE, externalType.toByteArray(), null, payload)
Intent(NfcAdapter.ACTION_NDEF_DISCOVERED).apply {
data = Uri.parse("vnd.android.nfc://ext/$externalType")
putExtra(NfcAdapter.EXTRA_TAG, tag)
putExtra(NfcAdapter.EXTRA_NDEF_MESSAGES, arrayOf(NdefMessage(record)))
putExtra(NfcAdapter.EXTRA_ID, id)
}.also {
ContextCompat.startActivity(this, it, null)
}
另一方面,com.xiaomi.mi_connect_service
(小米互联通信服务
)在处理收到的数据时,只有flags != 1
且flags != 3
时才会获取并处理Tag
。
因此想要模拟这个广播,另一个不安全但是能用的方案就是不传递这个Tag
数据,这样在当前使用场景下不会报错。
kotlin
// 直接设置为空即可
val tag: Tag? = null
// 一个空的NFC Tag ID
val id: ByteArray = "000000000000".decodeHex()
// 类似的代码 ...
关于如何接收指定的NDEF数据,可以查看官方文档。
向投屏
或者互联互通服务
发送广播
模拟NDEF数据总可能会出现一些问题,因此另一个方案就是直接发送投屏的广播。
根据广播权限的定义,应用程序需要拥有com.xiaomi.mi_connect_service.permission.RECEIVE_ENDPOINT
权限,才能向同样要求这个权限的广播接收器发送广播。
值得庆幸的是,这个权限定义在com.xiaomi.mi_connect_service
(小米互联通信服务
)中并且没有任何的签名或者系统限制,所以可以直接在测试APP的Manifest中申明这个权限。
kotlin
val btMac = "00:00:00:00:00:00"
val hex = "65064d4952524f520111" + btMac.toByteArray().toHexString()
val payload = Base64.encodeToString(hex.decodeHex(), Base64.DEFAULT)
val json = JSONObject()
json.put("device_type_key", 3) // PC
json.put("protocol_payload_key", payload)
val extra = json.toString()
sendBroadcast(Intent("com.xiaomi.nfc.action.TAG_DISCOVERED").apply {
addFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP)
addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES)
addFlags(268435456)
putExtra("protocol_value_key", extra)
}, "com.xiaomi.mi_connect_service.permission.RECEIVE_ENDPOINT")
一碰传文件
触发一碰传文件的要求是文件/相册APP在前台,并且选中了要传输的文件。
如果想要触发,那么发送Intent的APP就必须在后台,或者不显示任何界面的发送完广播就退出。
Activity不显示界面可以使用以下Theme:
xml
<activity android:theme="@android:style/Theme.NoDisplay" />
分析的版本
以上所有的分析过程建立在对HyperOS中提取出的APP上,并在小米电脑管家1.0.0.489
上通过测试。
名称 | 包名 | 版本 |
---|---|---|
小米互联通信服务 | com.xiaomi.mi_connect_service |
3.1.450.10 |
投屏 | com.milink.service |
15.0.4.1.e4a233b.2894359 |
NFC服务 | com.android.nfc |
14 |