1. 为什么这篇叫“二”而不是“进阶”上一篇我们聊了安卓应用安全的基础面Android 的系统架构、沙箱机制、数据目录划分、四大组件的基本概念以及为什么说一个应用从安装到运行安全边界比想象中要窄。很多朋友看过之后觉得意犹未尽因为那些内容更多回答的是“安卓安全是怎么回事”而不是“我写代码时到底该防什么、怎么防”。这篇正好补上第二层。我会沿用之前的风格尽量用做过项目的人都会经历的案例来讲不堆术语定义。核心围绕几条线展开APK 解包之后你能看到什么、签名机制到底在防谁、危险权限申请的真实成本、组件导出为什么会成为漏洞、混淆加固能做到什么程度。这些内容不会让你立刻成为逆向专家但能让你在开发、测试、上架前自查的时候知道该把注意力放在哪。如果你正在做安卓开发或者刚接触应用安全测试这篇可以直接当操作手册用。如果你已经有一定基础那后面几节关于签名校验、组件导出攻击、客户端安全边界的思考应该能帮你把零散经验串起来。无论哪种情况请记住一个前提安卓应用安全没有“绝对安全”所有的加固和防护都是在提高攻击者的成本而不是把成本变成无穷大。2. 拆开 APK你的应用在攻击者眼里长什么样2.1 APK 内部结构与每个文件的价值APK 本质上是个 zip 压缩包但里面装的不是随便几个文件每一部分都有明确的职责也对应着不同的攻击面。我们直接解压看一个典型 APKunzip demo.apk -d demo/解压后至少会出现这些内容路径/文件作用攻击者关注点classes.dexDalvik/ART 虚拟机字节码业务代码的核心用 jadx/jeb 反编译后基本可以获得接近 Java 源码的逻辑lib/armeabi-v7a 等native so 库核心算法、混淆后的计算逻辑、签名校验逻辑常在这里resources.arsc编译后的资源索引表可以还原字符串资源配合反编译结果定位功能点AndroidManifest.xml组件声明、权限声明二进制 AXML判断导出组件、权限申请、备份标志等关键配置META-INF/签名信息与证书v1 签名时代篡改 APK 后重签名的关键对象校验签名的手段就在这附近找res/布局、图片、原始资源可提取 UI 设计稿也可定位相关的字符串第一次做安全评估的朋友经常会犯一个错误只盯着 dex 反编译觉得代码都被看光了那加固还有什么意义。实际上攻击者也会从资源文件入手。比如你在res/values/strings.xml里直接写了 API 网关地址、AK/SK 前缀甚至把测试环境的密钥明文写在assets/下的配置文件里那 dex 里还没分析攻击面已经暴露了。我的习惯是拿到 APK 先用aapt或 Android Studio 自带的 APK Analyzer 快速过一遍整体结构再看 manifest最后才碰 dex。顺序反过来很容易被代码细节淹没忘了从全局看暴露面。2.2 静态体检几个命令快速了解目标这里分享一套我经常用的基础体检流程。先通过 aapt 看包名、版本和目标 SDKaapt dump badging demo.apk输出里会有package、sdkVersion、targetSdkVersion、permission等关键字段。特别注意targetSdkVersion它决定了你走老权限模式还是新权限模式也决定了某些系统安全策略对你是否生效。Android 8 之后很多应用还停留在旧 target 上那系统会帮你把不安全行为“兼容”掉但这不等于安全。再看 manifest 里的组件导出情况这一步比看代码更重要aapt dump xmltree demo.apk AndroidManifest.xml导出的信息会比较长我通常会 grepexported和intent-filter。一个 manifest 里如果 Activity、Service、Receiver 大量出现android:exportedtrue而且没有搭配合理的权限保护那这个应用的攻击面就很大。后面第四大节会专门讲组件暴露的问题。最后用 jadx 打开 dex重点搜索几个模式getSharedPreferences、SQLiteDatabase、Log.d、WebView、addJavascriptInterface、Base64、Cipher。这些关键词对应的代码区域要么是敏感数据存储要么是前端交互入口要么是加解密逻辑都是安全评估里必看的位置。其实这一步做完你对一个 APK 的“健壮度”已经有大致判断了。如果 manifest 导出泛滥、代码里明文密钥一堆、数据库操作没加密那后面即便做了签名校验也顶不住。基础不牢加固只是心理安慰。3. 签名机制完整链路才是防篡改的关键3.1 v1/v2/v3/v4 签名演进与取舍签名这事很容易被开发同学误判觉得“我已经用正式签名打包了所以安全”。但签名解决的核心问题是“应用来源可验证”它防的是别人拿你的 App 改名重打包去钓鱼不是防攻击者把你的代码逻辑扒出来。先理清几代签名方案签名方案覆盖范围特点适用场景v1JAR 签名对 APK 内每个条目单独签名兼容性好但可被篡改后重签只要不校验签名低版本系统、多渠道打包工具依赖v2APK Signing Scheme v2对 APK 整个文件做摘要校验更快防篡改更强Android 7.0 及以上强烈推荐v3在 v2 基础上增加密钥轮换支持密钥更换而不丢失应用身份需要证书替换的应用v4基于 fs-verity 的流式签名主要用于增量更新少数特殊场景很多团队还在用 v1 v2 双签名原因是国内渠道多需要兼容老的微信/应用宝分包逻辑。但注意Android 11API 30开始如果 targetSdkVersion 是 30系统默认不再信任 v1 签名只校验 v2/v3。如果你只打了 v1 签名的包在老设备上能装新设备上反而装不上这类是从 Android 11 升级后才暴露的问题。我的建议是v2 是底线能上 v3 就上 v3。只有当确认目标用户里有大量 Android 6 以下设备时才考虑保留 v1 签名兼容。3.2 防二次打包的签名校验到底怎么写签名校验本身不复杂但网上大量写法是错的。比如在 Java 层获取签名再比对字符串攻击者只要在 smali 里把比较函数改成恒真就绕过了。更弱的做法是把签名字符串明文写在代码里等于告诉攻击者你要比什么。我从实际项目里总结出的比较靠谱的做法是把签名校验放到 native 层并且不只是比对一次而是在多个关键路径上分散调用。比如应用启动时校验一次进入核心业务模块时校验一次调用支付前校验一次。这样攻击者只 patch 一个点是不够的。下面是 Java 层获取签名的参考代码fun getSignatureMd5(context: Context): String { val info context.packageManager.getPackageInfo( context.packageName, PackageManager.GET_SIGNATURES ) val signature info.signatures[0].toByteArray() val md MessageDigest.getInstance(MD5) return md.digest(signature).joinToString() { %02x.format(it) } }注意在 Android 11 之后GET_SIGNATURES已经标记为废弃建议用GET_SIGNING_CERTIFICATES获取signingInfo再计算摘要。这块的坑不少网上很多老代码直接跑不通。再说一个更容易忽略的问题签名校验逻辑不能依赖网络。很多方案是启动时请求服务端校验签名这等于把你的应用安全交到了网络稳定性和服务端接口安全性上离线环境下直接失效。正确思路是“本地为主、服务端为辅”。本地校验负责提高破解成本服务端校验负责在敏感操作登录、支付、发布时二次确认。最后提醒一点没必要把所有签名逻辑都拿来做“防破解”。现实中大多数应用面对的是普通用户误装恶意包不是职业逆向工程师。签名校验能挡住最粗放的重打包已足够。过度设计反而影响兼容性和升级频率得不偿失。4. 权限体系每一个权限申请都在扩大攻击面4.1 权限分级与运行时权限的机制细节Android 权限按保护级别分三类normal、dangerous、signature/signature|privileged。普通权限在安装时直接授予危险权限从 Android 6.0 开始需要运行时动态申请签名权限则要求申请方与应用持有相同签名。很多崩溃和用户投诉都出在权限申请阶段。危险权限不是“你申请了就一定给”用户拒绝一次、拒绝两次甚至勾选“不再询问”每一种状态都要在代码里处理。别想当然地认为弹一次窗用户就会点允许。运行时权限从机制上可以分为三个层次应用层检查checkSelfPermission的结果决定是否发起requestPermissions系统层系统根据权限分组、用户选择、应用 targetSdk 决定授权行为厂商层国产 ROM 经常把“自启动”“后台弹窗”“通知栏”等也做成独立权限Android 标准权限之外还要过一道厂商审核层与层之间有大量“意外”。比如用户给了一个属于同一权限组的权限系统会默认把同组其他权限也授予可某些厂商定制 ROM 上没有按这个逻辑处理。再比如 targetSdk 23 以下的老应用危险权限全部在安装时授予用户根本没有拒绝入口这在合规审核里很容易被卡。4.2 危险权限申请的标准姿势与检查命令申请权限最忌讳“一上来就一次弹七八个”。我见过很多产品为了省事在主界面启动时就把存储、定位、相机、通讯录全部要一遍结果用户感觉被冒犯拒绝率极高后面核心功能反而无法使用。推荐的做法是按功能场景申请。比如用户要发布带图内容时才申请存储权限要扫二维码时才申请相机权限。代码里先判断shouldShowRequestPermissionRationale决定要不要给用户解释用途再发起申请。同时要处理“永久拒绝”的情况引导用户去设置页开启。我在项目里会保留一份完整的权限清单方便自查adb shell dumpsys package package_name | grep permission这个命令能看到应用当前被授予的权限列表。对比一下 manifest 里申请了哪些、运行时实际给了哪些可以快速发现过度申请的问题。申请了但没有被使用、被授予但功能根本不需要的权限都应该从 manifest 里删掉。多一个权限就多一个被攻击和被审核质疑的可能。危险权限组里值得特别关注的几个权限攻击风险备注READ_SMS / RECEIVE_SMS短信劫持、验证码泄露非必要不申请READ_CONTACTS通讯录批量泄露常见于恶意收集用户信息的应用ACCESS_FINE_LOCATION位置追踪很多应用只需要粗略位置就够了CAMERA / RECORD_AUDIO音视频偷录后台使用场景要格外谨慎READ_EXTERNAL_STORAGE文件数据泄露尽量用 SAF / MediaStore 替代全盘读我在上一个项目里因为地图功能申请了精确定位结果隐私合规审核被打回来三次。后来把业务改成只需要城市级定位权限从ACCESS_FINE_LOCATION降到ACCESS_COARSE_LOCATION审核一次过。权限设计不合理不只是安全问题还会直接影响业务上线节奏。5. 四大组件导出组件就是给攻击者留门5.1 exported 判定规则与隐式 Intent 风险四大组件的android:exported属性决定了外部应用能不能通过startActivity、startService、sendBroadcast、ContentResolver等方式访问它。这个属性是应用安全里最容易出问题的地方因为它的默认值有历史包袱没有配置 intent-filter 的组件默认exportedfalse但配置了 intent-filter 的组件在 Android 12 之前默认是导出的。从 Android 12API 31开始只要 manifest 里声明了 intent-filter就必须显式声明android:exported否则编译直接报错。这个改动帮了很多项目堵住了疏忽但存量项目升级 targetSdk 时经常会崩在这一点上得逐个组件确认导出意图。组件导出的直接风险是攻击者可以绕过正常入口调用你的内部功能。举几个实际例子导出的 Activity 可能是内部跳转页攻击者直接拉起它进入某个业务状态跳过登录校验导出的 BroadcastReceiver 如果处理的是敏感广播攻击者可以伪造广播触发逻辑甚至携带恶意 extra 数据导出的 ContentProvider 如果没做权限保护攻击者通过query、update、delete直接操作数据导出的 Service 可能被其他应用绑定消耗资源或触发耗时操作测试方法也很直接adb 一条命令就能启动任意组件adb shell am start -n package/component如果没加权限保护、没做来源校验组件就会直接响应。这里说的“来源校验”不只是检查包名因为包名也能伪造更稳妥的是校验调用方签名是否匹配。5.2 实际攻击演示与防护配置举一个简单的攻击场景。某应用有个导出的 ActivityShareActivity它接收Intent.getStringExtra(share_url)加载后在 WebView 里展示。正常业务是用户分享链接给朋友打开但攻击者可以构造adb shell am start -n com.example.app/.ShareActivity --es share_url file:///data/data/com.example.app/databases/user.db如果 WebView 没有禁用 file 访问数据库里的敏感内容就可能被读出来展示在页面上攻击者配合截图或录屏就能获取数据。这类问题在真实环境里出现过很多次核心原因不是 WebView 本身不安全而是组件不该导出。防护配置看起来很简单但执行起来要有清单组件类型推荐配置额外措施Activity只有启动入口 Activity 导出内部页面一律不导出跳转前校验调用来源Service默认不导出需要 IPC 时用自定义权限保护Receiver根据广播类型决定导出系统级广播如 BOOT_COMPLETED要防伪造动态注册更好Provider显式声明权限或设置android:exportedfalse共享数据用 FileProvider加grantUriPermissions控制临时授权我再补充一个容易被忽视的点隐式 Intent 也可能把数据发给恶意应用。比如你startActivity发起一个ACTION_SEND的分享弹窗让用户选择目标时恶意应用也能收到这个 Intent 里的数据。如果分享内容涉及敏感信息最好定制分享面板或校验目标包名、签名而不是直接用系统分享。处理组件导出安全问题我总结出一个简单可执行的验收标准除了一屏主入口和系统必须回调的组件其他组件一律exportedfalse。有跨应用调用需求时先用最少组件 最严格权限的方案不要图方便全量导出。6. 代码保护混淆、加固与客户端安全的边界6.1 R8/ProGuard 做了什么没做什么很多团队的“安全方案”就是开了 ProGuard/R8 混淆觉得代码已经“加密”了。真实情况是混淆只改了类名、方法名和字段名字符串和调用逻辑都还是明文的。攻击者用 jadx 反编译后看到的是一堆a.b.c()但结合字符串资源和调用顺序依然能还原出业务逻辑只是阅读成本高了一些。R8 相比 ProGuard 的优势是压缩更好、内联更多还能做资源收缩。但要注意它默认只做名称混淆和死代码移除不会处理字符串加密。如果你的核心算法直接以字符串形式保存在 dex 里混淆等于没混淆。我的习惯是区分“混淆等级”。普通应用用 R8 默认配置就够了省心且稳定涉及支付、算法、核心业务逻辑的应用要考虑字符串加密插件或 so 库下沉。但无论哪种方案都要保留mapping.txt映射文件否则线上崩溃日志里全是混淆后的符号名排查问题会让你怀疑人生。6.2 加固、反调试、完整性校验的合理姿势加固比如乐固、360 加固、爱加密核心思路是把 dex 加密后放到 assets 或 so 里运行时从 native 层解密加载。这个方案能挡住大部分静态反编译攻击但挡不住动态调试和内存 dump。攻击者拿到运行中的内存把解密后的 dex dump 出来依然可以还原代码。所以更合理的分层是这样的静态防护混淆 字符串加密 加固解决“拿到 APK 就能看代码”的问题动态防护反调试、防注入、内存完整性校验解决“运行起来再抓”的问题业务防护服务端校验、敏感逻辑放服务端解决“客户端被完全控制”的问题这三层里最可靠的是第三层。客户端里的所有逻辑理论上都能被逆向和 hook只是成本问题。把真正敏感的东西放到服务端才是安全的正解。反调试常用手段有几种但都不完美。Debug.isDebuggerConnected()可以检测调试器但容易被 hook检查/proc/self/status里的TracerPid可以检测 ptrace但对 Frida 这类注入式框架效果有限native 层的ptrace(PTRACE_TRACEME)自跟踪能挡住一部分调试器但又可能被 unptrace 绕过。我建议不要把精力全花在“让攻击者解不开”上而是花在“让攻击者很难批量获利”上。比如关键数据接口的鉴权放服务端、加密密钥放服务端下发、检测到模拟器或 root 环境时降低功能可用性。这样攻击者破解单个 APK 的成本高批量自动化脚本的收益低攻击动机自然就小了。7. 常见安全故障排查与测试技巧7.1 从崩溃日志反推安全配置问题安全配置不合理不只导致漏洞也会导致崩溃。我整理了一些高频问题对照着查能省不少时间现象常见原因排查方向安装提示“已安装”但启动闪退新旧包签名不一致用apksigner verify对比两次签名64 位设备运行崩溃只打了 armeabi-v7a 的 so缺少 arm64-v8a检查 APK 里 lib 目录架构是否完整混淆之后反射报错反射调用的类/方法被 R8 改名在 proguard-rules.pro 里保留对应类的 keep 规则升级到 targetSdk 31 后崩溃组件没声明android:exported检查 manifest 中所有带 intent-filter 的组件Android 10 无法读写公共目录文件分区存储机制生效改用 MediaStore 或应用私有目录WebView 白屏或加载失败明文流量被默认禁止targetSdk 28 默认禁明文需要显式声明或用 https收到“应用未安装”提示APK 被篡改或签名校验失败用 zipalign 检查对齐用 apksigner 检查签名这些问题的共同特点是它们不在编译期报错只在上线后暴露测试同学很难覆盖到。所以在发布前强烈建议把“安全配置检查”做成独立测试项而不是靠功能测试顺带验证。7.2 一条命令搞定签名校验签名问题排查用 apksigner 非常方便apksigner verify --print-certs demo.apk输出会包含签名算法、证书指纹和摘要。想比较两个 APK 是不是同一个签名分别打印出 SHA-256 指纹对比就行。这比解包 META-INF 里的 cert 文件直观得多。再看是否启用了 v2/v3 签名可以用apksigner verify --verbose demo.apk如果只显示 v1 签名就要考虑升级。不是说 v1 不能用而是新设备上可能不认应用上架审核或灰度用户反馈会出问题。另外提一句zipalign它管的是资源对齐虽然和签名没直接关系但 Google Play 要求 APK 在签名前完成 zipalign。很多本地手工打包流程里漏了这一步导致开发自测没问题、上架审核报错。对齐检查也很简单zipalign -c -v 4 demo.apk看到Verification succeeded就说明没问题。7.3 安全测试的几个实用检查项日常安全测试不需要一上来就上 Frida 和 Xposed。我比较推荐按下面这套顺序走能覆盖大部分基础风险检查 manifest 是否配置备份标志。android:allowBackuptrue意味着用户可以通过 adb backup 导出应用数据尽量改为 false检查日志输出。Log.d、Log.i里打敏感信息是最低级的泄露搜索代码关键词就能发现检查网络是否全部走 HTTPS。okhttp日志、抓包工具都能直观看到明文 HTTP 请求在现在的环境里基本没有合格理由检查本地存储。SharedPreferences、SQLite、私有文件是否存了密码、token、身份证号等敏感数据检查 WebView 配置。setAllowFileAccess(true)、addJavascriptInterface没做白名单都是高危项这些检查项不需要逆向功底照着代码 review 就能做完。安全测试不是只有攻防高手才能做基础排查做到位很多应用的风险已经能下降一半。8. 一点个人经验做了这么多年安卓开发和安全评估我最大的感受是安全问题从来不是只靠安全工程师就能解决的它需要开发者在每个技术决策里脑子里多一根弦。组件导出之前多问一句“这个真的要被别人调用吗”申请权限之前多问一句“这个权限真的需要吗”写日志之前多问一句“这句日志会不会泄露信息”。这根弦绷住了比任何加固方案都管用。最后分享一个我一直在用的土办法。每次发布前我会故意用 debug 签名重新打一个包装到测试机上跑一遍。如果应用的核心功能在这个“假包”里还能正常跑通说明签名校验是摆设需要回去补。这个方法虽然糙但它能让你从攻击者的视角看自己的应用很多“我觉得没问题”的隐患一测就原形毕露。
