安卓手机NFC模拟门禁卡(设置UID)的一种方法

本文通过对Android源码中NFC部分的简单分析,实现了另外一种设置UID的方式,可用于部分场景下的门禁卡模拟。

一、背景本人就读于西南地区某大学,学校于2016年为学生宿舍楼大门安装了NFC门禁系统。这个时候手机的NFC技术已经相当成熟,网上充斥着各种手机模拟门禁、刷公交的帖子,各大手机厂商也与公交公司合作共同推进手机刷公交的进步。于是我也试着看看能不能用手机来刷开宿舍的门禁。我通过Acr122u将校园卡的UID写入一张MIFARE? Classic 1K兼容卡片后,成功刷开了宿舍的大门。
从08年NXP公司的MIFARE? Classic Cards被攻破后,M1卡就不再具有安全性,在如身份识别、电子钱包等需要一定安全性的场景下逐渐被安全性更高CPU卡取代。但是由于CPU卡本生比M1卡成本高,并且某些工程中大量使用的M1卡及相关系统全面更新将会是一大笔支出,加之新系统建设时监管不严,目前仍有部分工程中使用着M1卡。可笑的是16年安装的门禁居然是通过UID来进行身份验证(即使我们校园卡是复旦CPU卡)。安全建设的实施情况可见一斑。既然已经确定了它通过UID进行身份识别,那接下来的工作便是在手机上来模拟这样一张具有固定UID的卡片了。

二、原理分析NFC设备有三种工作模式:Tag Reader/Writer、Peer to Peer、Card Emulation模式,详情可参见NFC Forum的介绍。现在很多安卓手机都具有NFC芯片,安卓系统也从Android 4.4开始原生提供了NFC卡片模拟的实现,即HCE。但是Android系统提供的卡模拟API是工作在国际智能卡标准ISO 7816-4下,同时Android也明确指出了使用ISO/IEC 14443-3协议中用于冲突检测的UID进行身份识别是不安全的,所以Android也没有提供控制UID的相关API,详情可参见这里。因此我们使用Android手机来进行卡模拟时,通过读卡器读到的UID通常是以0x08开头的随即值,这是ISO/IEC 14443-3标准的Anticollision部分要求的。当然这一点,不同的厂家有不同的实现,并且目前流行于Android平台的Broadcom和NXP这两家公司的芯片通常都可以通过修改配置文件的方式来指定UID。
如果在配置文件中没有指定UID,将由NFCC(NFC Controler)产生随机值。基于这点,网上有很多热心网友写了指定UID的教程,可以参见这里和这里。甚至有人写了相应软件来更方便的修改UID。后来有些手机厂商甚至在自家应用中添加了门禁卡模拟的功能,比如(18年初?)更新的小米钱包。有些门禁是要读取卡内的除UID以外的其他信息的,M1卡它可能读取加密或不加密的Sector,而CPU卡你也很难知道它会读取哪个DF里的信息,以及是否需要密钥认证。因此通用的门禁模拟软件还大多停留在UID的模拟上,本文也只讨论如何设置固定的NFCID1。

三、修改配置文件经过前面的分析,我开始在Mi 5s Plus手机上进行尝试。这款手机采用了NXP的 pn551 芯片,在文档AN11690.pdf中介绍了NXP的NFC芯片在Android下的移植过程。从文档中我们得知在Android O平台上的移植需要用到 libnfc-brcm.conf、libnfc-nxp.conf 这两个配置文件,在Android P上则变为了 libnfc-nci.conf 和 libnfc-nxp.conf 这两个配置文件。我在手机上刷入了LineageOS 15.1,在/vendor/etc/路径下可以找到这两个配置文件。通过修改libnfc-brcm.conf中的APPL_TRACE_LEVEL和PROTOCOL_TRACELEVEL日志级别可以在logcat中看到NCI协议栈及NFC HAL层详细的调试信息,libnfc-nxp.conf中修改NXPLOG_LOGLEVEL来更改日志级别。按照前面帖子介绍的方式修改了NFA_DM_START_UP_CFG和NXP_CORE_CONF,杀死*com.android.nfc进程重启NFC服务。
NFC服务有个android:persistent=”true”属性,ActivityManager检测到进程被杀死后会自动重启它。从logcat中可以看到两个配置文件均被加载了,但是读卡器读到的UID仍然是0x08开头的NFCID3。使用小米钱包的门禁模拟功能应该是可以成功的,看网上的介绍说支持Mi 5s Plus,但我不想为了刷个门禁刷回MIUI。于是我开始尝试着用其它的方式来解决问题。

四、安卓系统如何与NFC硬件交流LineageOS源代码clone到本地Lineageos目录下,确保能为Mi 5s Plus设备正常编译。以下实验均在此目录下完成。我们首先通过AN11690.pdf中的一幅图来整体认识一下NFC在Android平台的实现。安卓底层是基于Linux内核的,因此驱动一个硬件设备的Linux设备驱动必不可少。代码位于Lineageos/kernel/xiaomi/msm8996/drivers/nfc,编译后在内核镜像中。

HAL意为硬件抽象层,运行在用户空间,与内核中实现设备基本操作的Linux设备驱动共同组成完整的设备驱动。HAL的最初目的是规避Linux内核GPL协议,现在已发展为规范设备驱动程序编写,便于移植。详情可以参见这里与Android Treble详细分析。Android O开始强制使用HIDL来定义HAL接口,NFC HAL代码位于Lineageos/hardware/interfaces/nfc,编译后生成android.hardware.nfc@1.0.so,android.hardware.nfc@1.0-impl.so,android.hardware.nfc@1.0-service, 启动NFC HAL的脚本android.hardware.nfc@1.0-service.rc。
NCI层实现了NFC协议栈,上层通过它与NFCC进行通信。NCI的实现与蓝牙协议栈在Android的实现类似。代码位于Lineageos/system/nfc,编译后生成libnfc-nci.so以及nfc_nci.msm8996.so。
通过JNI实现Android框架中Java代码与NCI中的代码相互调用。代码位于Lineageos/packages/apps/Nfc/nci/jni,编译后生成libnfc_nci_jni.so。
与蓝牙类似,NFC在Android中也以服务的形式存在,Android Framework通过AIDL与服务通信。NFC Service代码位于Lineageos/packages/apps/Nfc,对应NXP的芯片编译后生成NfcNci.apk,而Broadcom的芯片生成Nfc.apk。
Android APP通过调用Android框架提供的API来使用NFC功能。

五、NFC Enable流程上一节介绍了NFC在Android的总体结构,本节结合具体代码来跟踪一下当我们点击设置菜单里的NFC按钮后NFC Enable的具体流程。
首先找到Preferences中切换NFC这个开关。系统设置是一个软件包,代码位于Lineageos/packages/apps/Settings。从Android项目中文件及目录的命名可以看出Android的命名是相当规范的,因此我们进入到这个目录后应该就能猜出它会通过NfcEnabler.java中的NfcEnabler类的相关方法来启用NFC。当然,我们也可以一步步把它找出来。
在strings.xml找到如下与设置界面一致的字符串:
<string name="connected_devices_dashboard_title" msgid="2355264951438890709">"已连接的设备"</string>

搜索以下看哪些布局用到这个字符串,在connected_devices.xml中找到:
<SwitchPreference android:key="toggle_nfc" android:title="@string/nfc_quick_toggle_title" android:icon="@drawable/ic_nfc" android:summary="@string/nfc_quick_toggle_summary" android:order="-5"/>

以上布局是如何被加载的这里不用关心,知道PreferenceScreen可以通过key找到这个组件就行啦。以toggle_nfc为关键字搜索java代码,可以发现NfcPreferenceController.java用到了它:
public class NfcPreferenceController extends AbstractPreferenceController implements PreferenceControllerMixin, LifecycleObserver, OnResume, OnPause { public static final String KEY_TOGGLE_NFC = "toggle_nfc"; public static final String KEY_ANDROID_BEAM_SETTINGS = "android_beam_settings"; private NfcEnabler mNfcEnabler; private NfcAdapter mNfcAdapter; ...... @Override public void displayPreference(PreferenceScreen screen) { if (!isAvailable()) { removePreference(screen, KEY_TOGGLE_NFC); removePreference(screen, KEY_ANDROID_BEAM_SETTINGS); mNfcEnabler = null; return; } mNfcPreference = (SwitchPreference) screen.findPreference(KEY_TOGGLE_NFC); mBeamPreference = (RestrictedPreference) screen.findPreference( KEY_ANDROID_BEAM_SETTINGS); mNfcEnabler = new NfcEnabler(mContext, mNfcPreference, mBeamPreference); // Manually set dependencies for NFC when not toggleable. if (!isToggleableInAirplaneMode(mContext)) { mAirplaneModeObserver = new AirplaneModeObserver(); updateNfcPreference(); } } ...... }

从上面的代码可以看出显示这个Fragment的时候new了一个NfcEnabler对象,正是通过它来进行NFC的开与关。下面截取NfcEnabler.java部分代码:
/** * NfcEnabler is a helper to manage the Nfc on/off checkbox preference. It is * turns on/off Nfc and ensures the summary of the preference reflects the * current state. */ public class NfcEnabler implements Preference.OnPreferenceChangeListener { private final Context mContext; private final SwitchPreference mSwitch; private final RestrictedPreference mAndroidBeam; private final NfcAdapter mNfcAdapter; private final IntentFilter mIntentFilter; private boolean mBeamDisallowedBySystem; private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (NfcAdapter.ACTION_ADAPTER_STATE_CHANGED.equals(action)) { handleNfcStateChanged(intent.getIntExtra(NfcAdapter.EXTRA_ADAPTER_STATE, NfcAdapter.STATE_OFF)); } } }; ...... public boolean onPreferenceChange(Preference preference, Object value) { // Turn NFC on/off final boolean desiredState = (Boolean) value; mSwitch.setChecked(desiredState); mSwitch.setEnabled(false); if (desiredState) { mNfcAdapter.enable(); } else { mNfcAdapter.disable(); } return false; } ...... }

可以看到在这个Listener中创建了一个Brodcasteceiver,当我们点击NFC设置项那个SwitchPreference(相当于ListView的自定义item)时,它就会收到广播,并通过NfcAdapter来开关NFC。
前面我们知道,通过调用NfcAdapter.enable()方法来进行NFC硬件的开关。它具体又做了些什么事呢?我们来看看Lineageos/frameworks/base/core/java/android/nfc/NfcAdapter.java:
@SystemApi @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean enable() { try { return sService.enable(); } catch (RemoteException e) { attemptDeadServiceRecovery(e); return false; } }

可以看出这是一个系统API,也就是说我们编写的一般应用是不能调用这个API的。sService是一个static INfcAdapter的对象,INfcAdapter是AIDL定义的接口,用于调用NfcService的方法。可以看出它执行了Service的enable()方法。代码位于Lineageos/packages/apps/Nfc/NfcService.java,相关aidl也定义在这个目录。实现如下:
@Override public boolean enable() throws RemoteException { NfcPermissions.enforceAdminPermissions(mContext); saveNfcOnSetting(true); new EnableDisableTask().execute(TASK_ENABLE); return true; }

NfcService作为系统服务,由NfcNci.apk提供,并在开机时启动由NfcApplication启动。下面我们来看看NfcService在这个异步任务里面又做了些什么。
class EnableDisableTask extends AsyncTask<Integer, Void, Void> { @Override protected Void doInBackground(Integer... params) { ...... switch (params[0].intValue()) { case TASK_ENABLE: enableInternal(); break; case TASK_DISABLE: disableInternal(); break; case TASK_BOOT: Log.d(TAG, "checking on firmware download"); if (mPrefs.getBoolean(PREF_NFC_ON, NFC_ON_DEFAULT)) { Log.d(TAG, "NFC is on. Doing normal stuff"); enableInternal(); } else if (!isSecHal()) { Log.d(TAG, "NFC is off. Checking firmware version"); mDeviceHost.checkFirmware(); } ...... } ...... } /** * Enable NFC adapter functions. * Does not toggle preferences. */ boolean enableInternal() { if (mState == NfcAdapter.STATE_ON) { return true; } Log.i(TAG, "Enabling NFC"); updateState(NfcAdapter.STATE_TURNING_ON); WatchDogThread watchDog = new WatchDogThread("enableInternal", INIT_WATCHDOG_MS); watchDog.start(); try { mRoutingWakeLock.acquire(); try { if (!mDeviceHost.initialize()) { Log.w(TAG, "Error enabling NFC"); updateState(NfcAdapter.STATE_OFF); return false; } } finally { mRoutingWakeLock.release(); } } finally { watchDog.cancel(); } if (mIsHceCapable) { // Generate the initial card emulation routing table mCardEmulationManager.onNfcEnabled(); } nci_version = getNciVersion(); Log.d(TAG, "NCI_Version: " + nci_version); synchronized (NfcService.this) { mObjectMap.clear(); mP2pLinkManager.enableDisable(mIsNdefPushEnabled, true); updateState(NfcAdapter.STATE_ON); } initSoundPool(); mScreenState = mScreenStateHelper.checkScreenState(); int screen_state_mask = (mNfcUnlockManager.isLockscreenPollingEnabled()) ? (ScreenStateHelper.SCREEN_POLLING_TAG_MASK | mScreenState) : mScreenState; if(mNfcUnlockManager.isLockscreenPollingEnabled()) applyRouting(false); mDeviceHost.doSetScreenState(screen_state_mask); /* Start polling loop */ applyRouting(true); return true; } ...... }

enableInternal方法里调用了mDeviceHost的initialize()方法。
mDeviceHost = new NativeNfcManager(mContext, this);

在nci和nxp目录下都有相应的NativeNfcManager.java实现了DeviceHost接口。从Android.mk中可以看出他们分属于两个不同的Package:NfcNciNfc。这里有两个包是因为以前Android平台的NFC HAL层没有一个统一的接口,NfcNci对应的是Broadcom公司NFC芯片的实现,而Nfc对应的是NXP公司的芯片。在Linageos 15.1中Mi 5s Plus采用的这款NXP的pn54x芯片,用的是NfcNci的代码实现,说明两家公司NCI的实现终于还是统一了。从手机/system/lib/下的libnfc-nci.so、libnfc_nci_jni.so,以及/system/app/NfcNci.apk都可以看出的确是用的NfcNci这个Package,当然我们也可以从*Lineageos/device/xiaomi/msm8996-common/msm8996.mk得到印证。其中包含的这部分代码:
# NFCPRODUCT_PACKAGES += android.hardware.nfc@1.0-impl android.hardware.nfc@1.0-service com.android.nfc_extras nfc_nci.msm8996 NfcNci Tag

我们来看看NativeNfcManager类的initialize()方法:
private native boolean doInitialize(); private native int getIsoDepMaxTransceiveLength(); @Override public boolean initialize() { boolean ret = doInitialize(); mIsoDepMaxTransceiveLength = getIsoDepMaxTransceiveLength(); return ret; }

它调用了一个名为doInitialize的native方法。这个方法通过jniRegisterNativeMethods注册到了函数nfcManager_doInitialize,其实最终调用的是JNIEnv里面的RegisterNatives函数来完成动态注册,这里Android对它进行了一下封装。下面我们来看看nfcManager_doInitialize这个函数。
static jboolean nfcManager_doInitialize (JNIEnv* e, jobject o){ ALOGV("%s: enter; ver=%s nfa=%s NCI_VERSION=0x%02X", __func__, nfca_version_string, nfa_version_string, NCI_VERSION); tNFA_STATUS stat = NFA_STATUS_OK; PowerSwitch & powerSwitch = PowerSwitch::getInstance (); if (sIsNfaEnabled) { ALOGV("%s: already enabled", __func__); goto TheEnd; } powerSwitch.initialize (PowerSwitch::FULL_POWER); { unsigned long num = 0; NfcAdaptation& theInstance = NfcAdaptation::GetInstance(); theInstance.Initialize(); //start GKI, NCI task, NFC task { SyncEventGuard guard (sNfaEnableEvent); tHAL_NFC_ENTRY* halFuncEntries = theInstance.GetHalEntryFuncs (); NFA_Init (halFuncEntries); stat = NFA_Enable (nfaDeviceManagementCallback, nfaConnectionCallback); if (stat == NFA_STATUS_OK) { num = initializeGlobalAppLogLevel (); CE_SetTraceLevel (num); LLCP_SetTraceLevel (num); NFC_SetTraceLevel (num); RW_SetTraceLevel (num); NFA_SetTraceLevel (num); NFA_P2pSetTraceLevel (num); sNfaEnableEvent.wait(); //wait for NFA command to finish } EXTNS_Init (nfaDeviceManagementCallback, nfaConnectionCallback); } if (stat == NFA_STATUS_OK) { //sIsNfaEnabled indicates whether stack started successfully if (sIsNfaEnabled) { RoutingManager::getInstance().initialize(getNative(e, o)); nativeNfcTag_registerNdefTypeHandler (); NfcTag::getInstance().initialize (getNative(e, o)); PeerToPeer::getInstance().initialize (); PeerToPeer::getInstance().handleNfcOnOff (true); ///////////////////////////////////////////////////////////////////////////////// // Add extra configuration here (work-arounds, etc.) if (gIsDtaEnabled == true) { uint8_t configData = 0; configData = 0x01; /* Poll NFC-DEP : Highest Available Bit Rates */ NFA_SetConfig(NFC_PMID_BITR_NFC_DEP, sizeof(uint8_t), &configData); configData = 0x0B; /* Listen NFC-DEP : Waiting Time */ NFA_SetConfig(NFC_PMID_WT, sizeof(uint8_t), &configData); configData = 0x0F; /* Specific Parameters for NFC-DEP RF Interface */ NFA_SetConfig(NFC_PMID_NFC_DEP_OP, sizeof(uint8_t), &configData); } struct nfc_jni_native_data *nat = getNative(e, o); if ( nat ) { if (GetNumValue(NAME_POLLING_TECH_MASK, &num, sizeof(num))) nat->tech_mask = num; else nat->tech_mask = DEFAULT_TECH_MASK; ALOGV("%s: tag polling tech mask=0x%X", __func__, nat->tech_mask); } // if this value exists, set polling interval. if (GetNumValue(NAME_NFA_DM_DISC_DURATION_POLL, &num, sizeof(num))) nat->discovery_duration = num; else nat->discovery_duration = DEFAULT_DISCOVERY_DURATION; NFA_SetRfDiscoveryDuration(nat->discovery_duration); // get LF_T3T_MAX { SyncEventGuard guard (sNfaGetConfigEvent); tNFA_PMID configParam[1] = {NCI_PARAM_ID_LF_T3T_MAX}; stat = NFA_GetConfig(1, configParam); if (stat == NFA_STATUS_OK) { sNfaGetConfigEvent.wait (); if (sCurrentConfigLen >= 4 || sConfig[1] == NCI_PARAM_ID_LF_T3T_MAX) { ALOGV("%s: lfT3tMax=%d", __func__, sConfig[3]); sLfT3tMax = sConfig[3]; } } } prevScreenState = NFA_SCREEN_STATE_OFF_LOCKED; // Do custom NFCA startup configuration. doStartupConfig(); goto TheEnd; } } ALOGE("%s: fail nfa enable; error=0x%X", __func__, stat); if (sIsNfaEnabled) { EXTNS_Close (); stat = NFA_Disable (FALSE /* ungraceful */); } theInstance.Finalize(); }TheEnd: if (sIsNfaEnabled) PowerSwitch::getInstance ().setLevel (PowerSwitch::LOW_POWER); ALOGV("%s: exit", __func__); return sIsNfaEnabled ? JNI_TRUE : JNI_FALSE;}

可以看到,它调用了NfcAdaptationInitialize()方法和NFA_SetConfig()等在libnfc-nci中定义的API函数,对硬件和GKI、NFA等子系统进行了初始化,最后启动Discovery。再往下就是HAL层调用,这里算是和硬件打上交道了。至此enable过程分析完成。

六、从NCI层入手从上面NFC Service的相关分析也可以看出,安卓系统正是通过NCI层来与NFCC进行交互的。因此我们只要合理调用libnfc-nci.so中的函数,也能达到控制NFCC的目的,当然也应该可以实现设置UID的目的。这里不再对NCI层代码作详细分析,感兴趣的同学可以参考Bluetooth在Android的实现,他们是差不多的。网上关于Bluetooth分析的文章非常多,这里推荐一个CSDN博主风语比较全面的分析。
通过分析我们知道Nfc Service启动Rf Discovery时会调用libnfc-nci中的NFA_StartRfDiscovery()函数,这个函数会发送一个表示事件NFA_DM_API_START_RF_DISCOVERY_EVT的消息,经过消息分发后会执行nfa_dm_start_rf_discover()函数,在此函数中又会调用nfa_dm_set_rf_listen_mode_config()。在nfa_dm_set_rf_listen_mode_config()函数中设置了Listen的参数,但是没有指定NFCID1,将由NFCC自行决定(NCI协议规定为0x80开头的随机值)。下面截取该函数的部分代码:
static tNFA_STATUS nfa_dm_set_rf_listen_mode_config( tNFA_D

为您推荐

发表评论

邮箱地址不会被公开。 必填项已用*标注