安卓⼿机NFC模拟门禁卡(设置UID)的⼀种⽅法
*本⽂作者:新希望鲜⽜奶,本⽂属FreeBuf原创奖励计划,未经许可禁⽌转载。
本⽂通过对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通常是以 0×08开头的随即值,这是ISO/IEC 14443-3标准的Anticollision部分要求的。当然这⼀点,不同的⼚家有不同的实现,并且⽬前流⾏于Android平台的Broadcom和NXP这两家公司的芯⽚通常都可以通过修改配置⽂件的⽅式来指定UID。
如果在配置⽂件中没有指定UID,将由NFCC(NFC Controler)产⽣随机值。基于这点,⽹上有很多热⼼⽹友写了指定UID的教程,可以参见这⾥和这⾥。甚⾄有⼈写了相应软件来更⽅便的修改UID。后来有些⼿机⼚商甚⾄在⾃家应⽤中添加了门禁卡模拟的功能,⽐如(18年初?)更新的⼩⽶钱包。有些门禁是要读取卡内的除UID以外的其他信息的,M1卡它可能读取加密或不加密的Sector,⽽CPU卡你也很难知道它会读取哪个DF⾥的信息,以及是否需要密钥认证。因此通⽤的门禁模拟软件还⼤多停留在UID的模拟上,本⽂也只讨论如何设置固定的NFCID1。
三、修改配置⽂件
android模拟点击经过前⾯的分析,我开始在Mi 5s Plus⼿机上进⾏尝试。这款⼿机采⽤了NXP的 pn551 芯⽚,在⽂档AN11690.pdf中介绍了NXP的NFC芯⽚在Android下的移植过程。从⽂档中我们得知在Android O平台上的移植需要⽤到 libnfc-
NFA_DM_START_UP_CFG和NXP_CORE_CONF,杀死 com.android.nfc进程重启NFC服务。
NFC服务有个 android:persistent=”true”属性, ActivityManager检测到进程被杀死后会⾃动重启它。从logcat中可以看到两个配置⽂件均被加载了,但是读卡器读到的UID仍然是 0×08开头的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@。
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。当然,我们也可以⼀步步把它出来。
在 l到如下与设置界⾯⼀致的字符串:
"已连接的设备"
搜索以下看哪些布局⽤到这个字符串,在 l中到:
以上布局是如何被加载的这⾥不⽤关⼼,知道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 implem
ents
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 =
可以看到在这个Listener中创建了⼀个Brodcasteceiver,当我们点击NFC设置项那个SwitchPreference(相当于ListView的⾃定义item)时,它就会收到⼴播,并通过NfcAdapter来开关NFC。
前⾯我们知道,通过调⽤able()⽅法来进⾏NFC硬件的开关。它具体⼜做了些什么事呢?
我们来看看Lineageos/frameworks/base/core/java/android/nfc/NfcAdapter.java:
@SystemApi @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean enable() { try { able(); } 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 { forceAdminPermissions(mContext); saveNfcOnSetting(true); new EnableDisableTask().execute(TASK_ENABLE); return true; }
NfcService作为系统服务,由NfcNci.apk提供,并在开机时启动由NfcApplication启动。下⾯我们来看看NfcService在这个异步任务⾥⾯⼜做了些什么。
class EnableDisableTask extends AsyncTask { @Override protected Void
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 (Boolean(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 { lease(); } } finally { watchDog.cancel(); } if (mIsHceCapable) { // Generate the initial card emulation routing NfcEnabled(); } nci_version = getNciVersion(); Log.d(TAG, "NCI_Version: " +
nci_version); synchronized (NfcService.this) { mObjectMap.clear();
在 enableInternal⽅法⾥调⽤了mDeviceHost的initialize()⽅法。
mDeviceHost = new NativeNfcManager(mContext, this);
在nci和nxp⽬录下都有相应的 NativeNfcManager.java实现了DeviceHost接⼝。从 Android.mk中可以看出他们分属于两个不同的Package:NfcNci和Nfc。这⾥有两个包是因为以前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得到印证。其中包含的这部分代码:
# NFC PRODUCT_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)
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; }
可以看到,它调⽤了NfcAdaptation的Initialize()⽅法和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协议规定为 0×80开头的随机值)。下⾯截取该函数的部分代码:
static tNFA_STATUS nfa_dm_set_rf_listen_mode_config( tNFA_DM_DISC_TECH_PROTO_MASK
tech_proto_mask) { uint8_t params[40], *p; uint8_t platform = 0; uint8_t sens_info = 0; ...... p = params; /* ** for Listen A ** ** Set ATQA 0x0C00 for T1T listen ** If the ATQA values are 0x0000, then the FW will use 0x0400 ** which works for ISODEP, T2T and NFCDEP. */ if (nfa_dm_cb.disc_cb.liste
n_RT[NFA_DM_DISC_LRT_NFC_A] ==
NFA_DM_DISC_HOST_ID_DH) { UINT8_TO_STREAM(p, NFC_PMID_LA_BIT_FRAME_SDD);
UINT8_TO_STREAM(p, NCI_PARAM_LEN_LA_BIT_FRAME_SDD); UINT8_TO_STREAM(p, 0x04);
UINT8_TO_STREAM(p, NFC_PMID_LA_PLATFORM_CONFIG); UINT8_TO_STREAM(p,
NCI_PARAM_LEN_LA_PLATFORM_CONFIG); UINT8_TO_STREAM(p, platform); UINT8_TO_STREAM(p,
NFC_PMID_LA_SEL_INFO); UINT8_TO_STREAM(p, NCI_PARAM_LEN_LA_SEL_INFO); UINT8_TO_STREAM(p, sens_info); } ...... if (p > params) { nfa_dm_check_set_config((uint8_t)(p - params), params, false); } return
NFA_STATUS_OK; }
从以上代码可以看出在设置的参数中没有NFCID1,我们在UINT8_TO_STREAM(p, sens_info);之后加⼊设置NFCID1的代码:
UINT8_TO_STREAM(p, NFC_PMID_LA_NFCID1);// parameter type is nfcid1 UINT8_TO_STREAM(p, 0x04); // parameter length UINT8_TO_STREAM(p, 0x01); UINT8_TO_STREAM(p, 0x02); UINT8_TO_STREAM(p, 0x03); UINT8_TO_STREAM(p, 0x04);
在LineageOS代码根⽬录使⽤mmm system/nfc即可编译这个模块。使⽤adb push将⽣成的 libnfc-nci.so传送到⼿机的/system/lib64/,通过kill命令杀死 com.android.nfc进程,NFC Service将⾃动重启。通过读卡器读取⼿机模拟的NFC卡⽚UID为:01020304。实验成功。
将UID写死可不是我们想要的,既然通过上⾯的函数将UID写⼊到NFCC就会⽣效,那么我们⾃⼰写软件来调⽤这个函数设置UID可以不能?答案是可以的。下⾯我们将通过写程序来动态控制UID。
从上⼀节的分析我们可以看出NFA模块的初始化是⽐较复杂的,因此我们直接在程序中加载libnfc-nci.so来调⽤它提供的API是会崩溃的,除⾮我们也如同NFC Service那样进⾏以系列初始化⼯作。我们应该在初始化完成的环境中来调⽤API,所以我们需要注⼊到 com.android.nfc进程中去。我在demo中⽤的注⼊⼯具是TinyInjector,当然我们的⽬的是仅仅是把动态库加载到⽬标进程中去,⽤xposed等框架也是可以的。寻⽬标函数在进程空间的地址也是个⿇烦事,我直接使⽤了iqiyi团队开源的xHook将⽬标函数地址替换为我的函数地址,然后在我的函数⾥调⽤⽬标函数,也算是⼀种曲线救国的⽅式。我选择调⽤nfa_dm_set_config来设置参数,这个函数会在NFA_SetConfig调⽤后作为消息处理函
数被调⽤。设置UID后需要重启Listening来使配置⽣效,这⾥通过调⽤NFC_Deactivate函数将NFCC设置为IDLE状态再设置为DISCOVERY状态实现重启,通过其他如Stop/StartRf函数也是可以的。测试代码在这⾥。
七、总结
为了给NFCC设置固定的UID,从⽽达到模拟门禁卡的⽬的。本⽂先尝试了⽹上⼴泛流传的修改配置⽂件的⽅式,在尝

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。