一、导出组件漏洞 Android 四大组件(Activity、Service、BroadcastReceiver、ContentProvider)若设置 android:exported="true" 且未做权限保护,可能被第三方应用恶意调用。
组件导出规则(Android 12+ 变更) Android 11 及以前: - 有 <intent-filter> 的组件默认 exported="true" - 无 <intent-filter> 的组件默认 exported="false" Android 12 (API 31)+: - 有 <intent-filter> 的组件必须显式声明 android:exported - 否则编译失败或运行时崩溃 - 目的是强制开发者明确组件的导出意图
Activity 劫持 Intent intent = new Intent ();intent.setClassName("com.victim.app" , "com.victim.app.SecretActivity" ); intent.putExtra("token" , "attacker_controlled_value" ); startActivity(intent);
修复:设置 android:exported="false" 或添加 android:permission 保护。
Task Hijacking(任务劫持)攻击:
<activity android:name=".FakeLoginActivity" android:taskAffinity="com.victim.app" android:launchMode="singleTask" android:exported="true" > </activity> Intent intent = new Intent ();intent.setClassName("com.victim.app" , "com.victim.app.LoginActivity" ); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); new Handler ().postDelayed(() -> { Intent fake = new Intent (this , FakeLoginActivity.class); startActivity(fake); }, 100 );
修复:设置 android:taskAffinity=""(空字符串)或设置 android:launchMode="singleInstance"。
Service 劫持 导出 Service 可直接绑定并调用其方法:
Intent intent = new Intent ();intent.setClassName("com.victim.app" , "com.victim.app.SensitiveService" ); bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
AIDL Service 攻击:
ServiceConnection conn = new ServiceConnection () { @Override public void onServiceConnected (ComponentName name, IBinder service) { ISensitiveService api = ISensitiveService.Stub.asInterface(service); String secret = api.getSecretData(); api.sendSms("555-1234" , "malicious message" ); } };
BroadcastReceiver 劫持 Intent intent = new Intent ();intent.setAction("com.victim.app.ACTION_SEND_SMS" ); intent.putExtra("phone" , "5551234567" ); intent.putExtra("message" , "malicious" ); sendBroadcast(intent);
有序广播拦截(Ordered Broadcast Interception):
<receiver android:name=".SmsInterceptReceiver" android:exported="true" > <intent-filter android:priority="999" > <action android:name="com.victim.app.ACTION_SEND_SMS" /> </intent-filter> </receiver> public void onReceive (Context context, Intent intent) { String phone = intent.getStringExtra("phone" ); abortBroadcast(); }
二、ContentProvider 漏洞 SQL 注入 public Cursor query (Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { SQLiteDatabase db = mDbHelper.getReadableDatabase(); String sql = "SELECT * FROM users WHERE name = '" + selection + "'" ; return db.rawQuery(sql, null ); }
攻击者构造 selection = "admin' OR '1'='1" 实现注入,获取全表数据。
ContentProvider SQL 注入的完整攻击面:
┌───────────────────────────────────────────────────────┐ │ ContentProvider 注入点检测方法 │ ├───────────────────────────────────────────────────────┤ │ │ │ 1. 路径遍历注入 │ │ content://com.victim.provider/files/../../../etc/ │ │ → 检查 openFile() 是否正确过滤路径 │ │ │ │ 2. 投影注入(Projection Injection) │ │ query() 的 projection 参数 │ │ → new String[]{"* FROM sqlite_master--"} │ │ → 如果 projection 被拼接到 SQL │ │ │ │ 3. 排序注入(Order By Injection) │ │ query() 的 sortOrder 参数 │ │ → "name ASC; DROP TABLE users--" │ │ │ │ 4. URI 匹配漏洞 │ │ UriMatcher 的匹配逻辑缺陷 │ │ → content://.../user/../admin → 访问 admin 数据 │ │ │ └───────────────────────────────────────────────────────┘
自动检测 ContentProvider 注入点:
public static void testInjection (Uri uri) { String[] projections = { "'* FROM SQLITE_MASTER--" , "COLUMN_NAME FROM (SELECT * FROM SQLITE_MASTER)--" , "* FROM USERS--" }; String[] selections = { "1=1" , "' OR '1'='1" , "admin'--" , "1) UNION SELECT * FROM SQLITE_MASTER--" }; String[] sortOrders = { "name ASC--" , "(SELECT name FROM SQLITE_MASTER LIMIT 1)--" }; for (String proj : projections) { for (String sel : selections) { try { Cursor c = resolver.query(uri, new String []{proj}, sel, null , null ); } catch (Exception e) { } } } }
文件遍历漏洞 public ParcelFileDescriptor openFile (Uri uri, String mode) { String path = uri.getPath(); File file = new File (getContext().getFilesDir(), path); return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); }
修复:
public ParcelFileDescriptor openFile (Uri uri, String mode) throws FileNotFoundException { String path = uri.getPath(); File requestedFile = new File (getContext().getFilesDir(), path); String canonicalPath = requestedFile.getCanonicalPath(); if (!canonicalPath.startsWith(getContext().getFilesDir().getCanonicalPath())) { throw new SecurityException ("Path traversal attempt detected" ); } return ParcelFileDescriptor.open(requestedFile, ParcelFileDescriptor.MODE_READ_ONLY); }
三、WebView 远程代码执行 Android API 16 及以下,使用 addJavascriptInterface 存在严重 RCE 漏洞:
webView.addJavascriptInterface(new JSInterface (), "Android" );
恶意网页可执行:
Android .getClass ().forName ("java.lang.Runtime" ) .getMethod ("getRuntime" ).invoke (null ) .exec ("rm -rf /data/data/com.victim.app/*" );
WebView 安全漏洞全景 ┌────────────────────────────────────────────────────┐ │ WebView 安全漏洞分类 │ ├────────────────────────────────────────────────────┤ │ │ │ 1. Javascript Interface 注入 │ │ - API 16-: 无条件反射访问所有 Java 方法 │ │ - API 17+: @JavascriptInterface 仅标记方法 │ │ - 攻击:注入恶意 JS → 反射调用系统 API │ │ │ │ 2. 文件访问漏洞 │ │ - setAllowFileAccess(true) 默认开启 │ │ - 恶意页面可通过 file:// 读取本地文件 │ │ - setAllowFileAccessFromFileURLs(true) │ │ → 允许 file:// 页面访问其他 file:// 资源 │ │ │ │ 3. SSL 绕过 │ │ - onReceivedSslError() 直接 proceed() │ │ - 可被中间人攻击劫持 HTTPS 通信 │ │ │ │ 4. URL 跳转劫持 │ │ - shouldOverrideUrlLoading() 未校验 URL │ │ - 通过 intent:// scheme 启动本地组件 │ │ - 通过 javascript: scheme 执行 XSS │ │ │ │ 5. Cookie 泄露 │ │ - CookieManager 未设置 secure/httpOnly │ │ - 跨域策略配置不当 │ │ │ └────────────────────────────────────────────────────┘
修复方案:
将 targetSdkVersion 提高到 17+,使用 @JavascriptInterface 注解限制
避免在 WebView 中暴露敏感 Java 对象
使用 WebView 安全配置:setAllowFileAccess(false)
WebView 安全配置最佳实践 public class SecureWebViewActivity extends Activity { private WebView webView; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); webView = new WebView (this ); WebSettings settings = webView.getSettings(); settings.setAllowFileAccess(false ); settings.setAllowFileAccessFromFileURLs(false ); settings.setAllowUniversalAccessFromFileURLs(false ); settings.setJavaScriptEnabled(true ); settings.setSavePassword(false ); settings.setSaveFormData(false ); webView.setWebViewClient(new WebViewClient () { @Override public void onReceivedSslError (WebView view, SslErrorHandler handler, SslError error) { handler.cancel(); } @Override public boolean shouldOverrideUrlLoading (WebView view, WebResourceRequest request) { Uri url = request.getUrl(); String scheme = url.getScheme(); if ("intent" .equals(scheme) || "javascript" .equals(scheme)) { return true ; } if (!url.getHost().endsWith(".trusted-domain.com" )) { return true ; } return false ; } }); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { webView.addJavascriptInterface(new SafeJSInterface (), "Android" ); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { CookieManager.getInstance().setAcceptThirdPartyCookies(webView, false ); } } public class SafeJSInterface { @JavascriptInterface public String getDeviceInfo () { return Build.MODEL; } } }
四、不安全的文件存储 SharedPreferences sp = getSharedPreferences("config" , MODE_WORLD_READABLE);sp.edit().putString("password" , "admin123" ).apply(); FileOutputStream fos = new FileOutputStream ( Environment.getExternalStorageDirectory() + "/secrets.txt" );
Android 存储安全清单
存储方式
风险
安全替代方案
MODE_WORLD_READABLE
任何应用可读取
MODE_PRIVATE(默认)
MODE_WORLD_WRITEABLE
任何应用可写入/注入
MODE_PRIVATE
外部存储明文文件
所有应用 + 用户可访问
EncryptedFile + 内部存储
SharedPreferences 明文
root 设备可读
EncryptedSharedPreferences
SQLite 明文数据
root 设备可读
SQLCipher 加密数据库
logcat 输出
任何应用通过 READ_LOGS 读取
发布前移除所有日志
EncryptedSharedPreferences(Jetpack Security) MasterKey masterKey = new MasterKey .Builder(context) .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .build(); SharedPreferences sharedPreferences = EncryptedSharedPreferences.create( context, "secure_prefs" , masterKey, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ); sharedPreferences.edit() .putString("auth_token" , token) .apply();
修复:使用 MODE_PRIVATE + 加密存储,敏感文件放 /data/data/ 内部目录。
五、Logcat 信息泄露 Log.d("LoginActivity" , "User login: " + username + ":" + password); Log.e("Payment" , "Card number: " + cardNum);
检查命令:
adb logcat | grep -iE "password|token|secret|apikey|card"
自动化日志审计脚本 """监听 logcat 并标记敏感信息泄露""" import subprocessimport reSENSITIVE_PATTERNS = { 'PASSWORD' : r'(?:password|passwd|pwd)\s*[:=]\s*\S+' , 'TOKEN' : r'(?:token|auth|bearer)\s*[:=]\s*\S+' , 'CREDIT_CARD' : r'\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b' , 'EMAIL' : r'[\w\.-]+@[\w\.-]+\.\w+' , 'API_KEY' : r'(?:api[_-]?key|apikey)\s*[:=]\s*\S+' , 'SESSION' : r'(?:session|jsessionid)\s*[:=]\s*\S+' , 'JSON_OBJECT' : r'\{[^}]*"(?:password|token|secret|key)"[^}]*\}' , 'BASE64_LONG' : r'[A-Za-z0-9+/]{40,}={0,2}' , } def monitor_logcat (package_name=None ): cmd = ['adb' , 'logcat' , '-v' , 'brief' ] if package_name: cmd.extend(['--pid' , str (get_pid(package_name))]) proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True ) for line in proc.stdout: for category, pattern in SENSITIVE_PATTERNS.items(): if re.search(pattern, line, re.IGNORECASE): print (f"[!] {category} LEAK: {line.strip()} " ) break def get_pid (package_name ): result = subprocess.run(['adb' , 'shell' , 'pidof' , package_name], capture_output=True , text=True ) return result.stdout.strip() if __name__ == "__main__" : monitor_logcat("com.target.app" )
六、Intent 注入与 Sniffing Intent intent = new Intent ("com.victim.app.RESULT" );intent.putExtra("auth_token" , tokenString); sendBroadcast(intent);
使用 setPackage() 限定接收者,或使用 LocalBroadcastManager。
Intent 安全漏洞分类 ┌─────────────────────────────────────────────────┐ │ Intent 相关安全漏洞 │ ├─────────────────────────────────────────────────┤ │ │ │ 1. Intent Sniffing(广播嗅探) │ │ - 发送隐式广播时,任何应用注册对应的 │ │ BroadcastReceiver 都能接收 │ │ - 可能泄露 auth token, session id, │ │ user info 等 │ │ │ │ 2. Intent Injection(意图注入) │ │ - 导出的 Activity 接受任意 Intent │ │ - 攻击者可构造 Intent extras 触发 │ │ 非预期的代码路径 │ │ - 例如: intent.putExtra("url", │ │ "javascript:alert(document.cookie)") │ │ 传给 WebView Activity │ │ │ │ 3. Intent Redirection(意图重定向) │ │ - Activity 收到 Intent 后将其转发给 │ │ 另一个组件 │ │ - 攻击者可在原始 Intent 中嵌套 │ │ 目标 Intent,通过重定向攻击内部组件 │ │ │ │ 4. PendingIntent 劫持 │ │ - PendingIntent 中的 Intent 可被 │ │ 恶意应用修改 extras │ │ - PendingIntent.FLAG_IMMUTABLE │ │ (Android 12+)解决此问题 │ │ │ └─────────────────────────────────────────────────┘
PendingIntent 安全:
PendingIntent pi = PendingIntent.getActivity(context, 0 , intent, 0 );PendingIntent pi = PendingIntent.getActivity( context, 0 , intent, PendingIntent.FLAG_IMMUTABLE); PendingIntent pi = PendingIntent.getActivity( context, 0 , intent, PendingIntent.FLAG_MUTABLE);
七、其他常见漏洞 7.1 不安全的加密实现 private static final String KEY = "1234567890abcdef" ; Cipher cipher = Cipher.getInstance("DES" ); Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding" );byte [] iv = {0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 }; IvParameterSpec ivSpec = new IvParameterSpec (iv);KeyGenerator keyGenerator = KeyGenerator.getInstance( KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore" ); keyGenerator.init(new KeyGenParameterSpec .Builder( "my_key_alias" , KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_CBC) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) .build()); SecretKey key = keyGenerator.generateKey();Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding" );SecureRandom random = new SecureRandom ();byte [] iv = new byte [12 ]; random.nextBytes(iv);
7.2 不安全的反序列化 ObjectInputStream ois = new ObjectInputStream (inputStream);MyObject obj = (MyObject) ois.readObject();
7.3 SSL/TLS 中间人攻击 TrustManager[] trustAllCerts = new TrustManager []{ new X509TrustManager () { public void checkClientTrusted (X509Certificate[] chain, String authType) {} public void checkServerTrusted (X509Certificate[] chain, String authType) {} public X509Certificate[] getAcceptedIssuers() { return new X509Certificate [0 ]; } } }; SSLContext sc = SSLContext.getInstance("TLS" );sc.init(null , trustAllCerts, new SecureRandom ()); HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true );
7.4 不安全的 IPC(进程间通信) public class MyService extends Service { private final IMyAidlInterface.Stub binder = new IMyAidlInterface .Stub() { @Override public String getSecretData () { return "SENSITIVE_DATA" ; } }; } public class SecureService extends Service { private final IMyAidlInterface.Stub binder = new IMyAidlInterface .Stub() { @Override public String getSecretData () { int callingUid = Binder.getCallingUid(); String[] packages = getPackageManager().getPackagesForUid(callingUid); for (String pkg : packages) { if ("com.trusted.caller" .equals(pkg)) { return "SENSITIVE_DATA" ; } } throw new SecurityException ("Unauthorized caller" ); } }; }
面试常考问题 Q1:如何快速判断一个应用是否存在导出组件漏洞?
A:使用 drozer(https://github.com/WithSecureLabs/drozer)扫描应用的攻击面:`run app.package.attacksurface com.target.app。它可以列出所有导出组件、可读取 ContentProvider URI、可调用的 Service 等。也可用 aapt dump xmltree target.apk AndroidManifest.xml | grep exported手动检查。更系统的方法:(1)aapt dump badging获取包名和权限列表;(2) 使用jadx打开 APK,查看 AndroidManifest.xml 中标记exported=”true”的组件;(3) 使用drozer的app.activity.info、app.provider.info、app.service.info、app.broadcast.info模块逐一审查;(4) 对于 ContentProvider,使用app.provider.finduri列出所有可访问的 URI,并使用scanner.provider.injection和scanner.provider.traversal` 检测 SQL 注入和路径遍历。
Q2:WebView 安全除了 addJavascriptInterface 还有哪些常见问题?
A:(1)setAllowFileAccess(true) 允许 WebView 通过 file:// 协议访问本地文件;(2) SSL 错误处理不当,onReceivedSslError 中直接 handler.proceed();(3) 未验证 URL,导致 URL 跳转劫持——攻击者可以通过 intent:// scheme 启动本地组件,或通过 javascript: scheme 执行 XSS;(4) WebView 中泄露 Cookie 或 Session 信息——未设置 Cookie 的 secure 和 httpOnly 标志;(5) 使用 addJavascriptInterface 暴露了具有反射能力的对象(即使加了 @JavascriptInterface 注解也存在风险);(6) WebView 加载任意 URL 而缺少 URL 白名单;(7) 在 API 23+ 使用了被废弃的 setJavaScriptEnabled(true) 配合 file:// URL 加载(存在通用 XSS 风险)。
Q3:ContentProvider 的 SQL 注入和 Web SQL 注入有什么异同?
A:原理相同,都是通过未经过滤的用户输入拼接 SQL 语句。不同点在于攻击入口:Web SQL 注入通过 HTTP 参数传入,ContentProvider SQL 注入通过其他应用构造的 ContentResolver 调用传入。防御方式也类似:使用参数化查询(selectionArgs),对输入做严格校验。ContentProvider 的特殊性在于:(1) 攻击者需要先确定 Provider 的 URI 和权限要求;(2) 注入点不仅限于 selection 参数,还包括 projection(投影列注入)和 sortOrder(排序注入);(3) 攻击可通过 adb shell content query 命令直接测试;(4) ContentProvider 还额外面临路径遍历(Path Traversal)风险——不同于 Web SQL 注入,ContentProvider 的 openFile() 方法可能允许攻击者通过构造 URI 读/写任意文件。
Q4:什么是不安全的 Intent 处理?如何防御?
A:不安全的 Intent 处理包括:(1) 通过隐式广播发送敏感数据,任何应用注册相同的 action 即可接收——防御:使用 LocalBroadcastManager(仅进程内发送)、setPackage() 指定目标包、或使用权限保护;(2) 导出的 Activity 信任 Intent extras,可能导致功能劫持——防御:校验 getCallingActivity() 确认调用者身份、对 extras 做白名单校验;(3) PendingIntent 被恶意修改 extras——防御:使用 PendingIntent.FLAG_IMMUTABLE 标志;(4) Intent Redirection——导出的 Activity 将收到的 Intent 转发给内部组件——防御:检查原始 Intent 的来源,避免直接转发不可信 Intent。
Q5:如何在 Android 应用中安全地存储敏感数据?
A:分层防御策略:(1) 首选 Android Keystore 系统——密钥在 TEE(Trusted Execution Environment)或 StrongBox 中生成和操作,密钥材料永不离开安全硬件;(2) 使用 Jetpack Security 库的 EncryptedFile 和 EncryptedSharedPreferences——它们底层使用 Keystore 管理的 AES-256 GCM 加密;(3) 如果使用 SQLite,使用 SQLCipher 进行全数据库加密;(4) 绝不使用外部存储(getExternalStorageDirectory())存放敏感文件——该目录对设备上的所有应用和通过 USB 连接 PC 后的用户可读;(5) 内部存储(getFilesDir())的默认权限是 MODE_PRIVATE,只有本应用可访问;(6) 对于极敏感的密钥材料(如支付签名私钥),采用白盒加密(White-box Cryptography)或拆分为多段分散存储,并配合代码混淆和完整性校验(检测应用是否被重新打包)。