资讯动态

APP安全检测服务全解析:从静态动态分析到发布门禁

发布时间:2026/10/29 15:45:02 来源:尧图企业网站定制
做移动端的人大概都遇到过这种场面功能都测完了包也打好了上传之后被应用商店打回理由是安装包存在某类风险配置或者更难受一点内测期间有用户反馈账号在别的设备上被登录翻了两天日志最后定位到本地存了一份明文的登录凭证。这类问题的共同点是功能测试根本覆盖不到——你的用例是按正常业务流程写的而安全问题恰恰藏在非预期的输入和非预期的环境里。APP安全检测服务解决的正是这一层它不验证按钮点下去有没有反应它验证的是这个安装包落到别人手里之后会不会把不该给的东西给出去、会不会被不该调用的入口调用起来、会不会在链路上裸奔。这篇内容面向三类人刚接触移动端安全、需要看懂检测报告的产品和研发要自己动手做一轮自查的小团队以及想把检测环节嵌进发布流程的技术负责人。我会把检测在查什么、为什么这么查、自己怎么跑一遍、跑完怎么定级和修尽量讲透。1. APP安全检测服务到底在解决什么问题1.1 用户侧一个安装包背后的信任成本普通用户判断一个 App 安不安全能依据的东西其实非常少图标长得正不正规、商店里有没有、权限弹窗是不是吓人。但真实的风险分布跟这些表象几乎没关系。一个界面做得规规矩矩的应用完全可能在SharedPreferences里存着明文的会话令牌一个权限申请很克制的应用完全可能在 WebView 里挂了一个无鉴权的 JS 桥接接口页面里任意一段脚本都能顺手调起本地文件读取。用户没有能力去看这些东西也不该被要求去看。安全检测服务在这里承担的角色是把看不见的风险翻译成可核对的条目。它不去评价你的产品好不好用只回答几个很具体的问题这个包里的敏感数据落地时是什么形态、进程间暴露了哪些入口、跟服务端说话的通道有没有做身份校验、依赖进来的第三方组件有没有已知的坑。这些答案最后会变成一张带风险等级的表产品能看懂影响面研发能看懂改哪一行。注意检测报告里存在风险和已被利用是两件事。前者是可能性后者是事实。做定级的时候必须区分否则团队要么被吓到过度投入要么觉得全是误报直接无视。1.2 开发侧为什么自测总是漏掉这一类问题功能测试的思维模型是给定输入验证输出。安全测试的思维模型是给定这个系统穷举能进来的路径。两者方向不一样所以哪怕测试覆盖率做到很高安全类问题依然会成片漏掉。举个很典型的例子。登录接口在正常流程下工作良好账号密码对了就发令牌错了就提示。但换三个视角看问题就出来了——把返回体里的用户 ID 改一下能不能拿到别人的资料把同一个请求原样重放二十次服务端会不会照单全收令牌有效期是三十天且不可撤销手机丢了怎么办这三个问题在功能用例里一个都不会出现因为它们都建立在正常用户按正常方式操作这个前提上而攻击者从来不遵守这个前提。再比如组件暴露。开发阶段为了让分享、支付回调、推送跳转能跑通往往会把某个 Activity 或者 Scheme 设成对外可见测试的时候从自家 App 里点进去一切正常没人会想到用adb shell am start从外部直接拉起来试试更没人会想到构造一个畸形参数看它会不会崩或者越权。这类问题的检测成本其实很低但漏掉的概率很高——因为它不在任何人的检查清单上。1.3 检测服务的四层能力地图把市面上的检测服务拆开看不管包装成什么名字能力基本落在四层。理解这四层比记住一堆工具名有用得多。层次关注对象典型手段产出代码与配置层清单文件、字节码、资源、签名反编译、静态扫描、规则匹配配置缺陷清单、硬编码密钥、危险 API 调用运行时行为层进程实际做了什么动态注入、系统调用监控、界面截屏敏感行为时序、本地文件写入内容数据流转层数据在网络与存储间的形态流量拦截、协议分析、存储取证明文传输项、证书校验缺失、落盘明文供应链层引入的 SDK 与依赖成分分析、版本比对、权限合并检查三方组件清单、多余权限来源这四层的检测成本是递增的。第一层几乎可以全自动化跑一遍规则库就出结果第二层需要真机或者模拟器还需要处理各种反调试第三层要处理证书固定和双向校验第四层最麻烦因为很多 SDK 是二进制交付你得先知道它到底做了什么。实际项目里我的建议是分阶段投入静态层做成每次构建必跑的卡点动态层按版本做抽样数据流转层针对核心业务链路登录、支付、个人信息页面重点做供应链层在引入新 SDK 的时候做一次准入。全都做成每次必跑成本撑不住而且大部分时候是重复劳动。2. 检测体系的核心模块拆解2.1 静态分析不运行也能看出门道静态分析是整套检测的地基原因是它便宜、可重复、不需要跑起来。拿到一个安装包先做的事情永远是解包和看清单。Android 侧的入口是AndroidManifest.xml。这个文件在编译后是二进制格式用aapt2可以直接 dump 出可读版本用apktool反编译能得到接近源码的结构。我一般的顺序是先aapt2 dump badging看概览——包名、版本、权限清单、启动组件一分钟内能形成初步印象然后apktool反编译拿资源和小文件最后jadx出 Java 层的伪代码用于搜索特定的 API 调用。# 概览包名、权限、SDK 版本、启动组件 aapt2 dump badging app-release.apk | head -60 # 反编译资源与清单保留可读的 XML apktool d app-release.apk -o out_apktool # 反编译字节码输出 Java 伪代码用于检索 jadx -d out_src --show-bad-code app-release.apk清单里我会重点盯这几项它们几乎覆盖了八成的配置类问题android:debuggable正式包必须是false。这个属性为真意味着任何人接上调试器都能读进程内存、下断点、改返回值。android:allowBackup为真时设备上的应用数据可以通过系统备份通道被导出。如果应用里存了令牌或者聊天记录这就是一条现成的数据外带路径。android:exportedAndroid 12 之后必须显式声明。凡是声明为true的组件都要问一句外部调用它需要什么前提。没有前提就是问题。usesCleartextTraffic与网络安全配置允许明文就意味着一张证书就能把整条链路接管。权限清单里的历史遗留早期为了某个功能申请过、后来功能下线但权限没删的情况非常普遍。代码层的检索目标更集中硬编码的密钥和令牌、用到 ECB 模式的加密、把MD5或SHA1直接当口令哈希、用java.util.Random生成安全相关的随机数、Log里打印了手机号或身份证、WebView 打开了setJavaScriptEnabled同时挂了addJavascriptInterface。这些都是规则能命中的成本极低收益很高。提示静态扫描出来的危险 API 调用里真正需要改的可能只有三成。判断标准是数据来源——如果传进去的参数完全来自常量那通常无所谓如果来自用户输入或者服务端返回就得认真看。2.2 动态分析只有跑起来才暴露的部分静态分析看不到的东西基本都在运行时真实的网络请求长什么样、数据最终写到了哪个文件、某个校验逻辑到底判断了什么条件。这些必须让应用在受控环境里跑一遍。动态分析的常规做法是在测试机上注入一个 Hook 框架拦截关键 API 的调用并打印参数。以 Frida 为例一段最小可用的脚本就能把所有Log.d的输出打到控制台用来确认日志里到底有没有敏感信息这件事比在几万行代码里搜Log.快得多。// hook_log.js拦截 android.util.Log 的全部输出 Java.perform(function () { var Log Java.use(android.util.Log); var methods [v, d, i, w, e]; methods.forEach(function (name) { Log[name].overload(java.lang.String, java.lang.String).implementation function (tag, msg) { console.log([ name ] tag - msg); return this[name](tag, msg); }; }); });# 以 spawn 方式启动目标进程并注入脚本 frida -U -f com.example.demoapp -l hook_log.js --no-pause跑起来之后我会按一条固定的路径走一遍冷启动、登录、进首页、打开个人中心、修改昵称、退出登录。这条路径覆盖了令牌的产生、使用和销毁也覆盖了个人信息的读写是性价比最高的一段。走的过程中同步做三件事第一看本地存储。用adb shell进到/data/data/包名/下面shared_prefs、databases、files、cache四个目录挨个翻。令牌、手机号、地址这类东西如果以明文躺在里面就是明确的落盘问题。第二看临时文件。很多框架和 SDK 会把请求体、图片、日志写到外部存储的应用目录下卸载应用都清不掉。这块经常被忽略。第三看界面保护。涉及敏感信息的页面有没有设置防截屏标志FLAG_SECURE。这个在金融和医疗类应用里是硬性要求普通应用则视数据敏感度而定。2.3 通信链路与数据存储数据在路上的形态链路检测的核心问题只有一个中间人能不能看、能不能改。由此拆出三个子问题——服务端有没有做身份证明证书、客户端有没有做身份校验证书固定、传输的内容有没有额外保护。证书固定的实现方式有好几种效果和成本不一样。常见的是在代码里比对服务端证书的公钥哈希或者把证书打进network_security_config.xml。后者的好处是纯配置、不用改业务代码但也有个明显缺点证书轮换的时候必须发版否则全网旧版本会集体连不上。这在实际运维里是个挺大的坑我见过不止一个团队因为证书到期忘了同步而半夜被叫起来。!-- res/xml/network_security_config.xml同时禁用明文并固定证书 -- network-security-config base-config cleartextTrafficPermittedfalse trust-anchors certificates srcsystem / /trust-anchors /base-config domain-config domain includeSubdomainstrueapi.example.com/domain pin-set expiration2026-06-30 pin digestSHA-256替换为服务端证书公钥的Base64哈希/pin pin digestSHA-256替换为备用证书的哈希/pin /pin-set /domain-config /network-security-config存储侧的原则其实就一条敏感数据不落明文盘。做法上分两档。第一档是系统提供的密钥库。Android 的AndroidKeyStore能把密钥材料放在受硬件保护的区域应用只能拿到一个句柄即使设备被 root导出的难度也远高于普通文件。用它来加密本地存储的令牌改造成本不高。// 用 AndroidKeyStore 生成一把仅在硬件内可用的 AES 密钥 val keyGenerator KeyGenerator.getInstance( KeyProperties.KEY_ALGORITHM_AES, AndroidKeyStore ) keyGenerator.init( KeyGenParameterSpec.Builder( app_local_key, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT ) .setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) .setKeySize(256) .build() ) keyGenerator.generateKey()第二档是算法层面的正确使用。这里最容易踩的坑是加密模式的选择很多人随手写个AES/ECB/PKCS5Padding就上了而 ECB 的特点是相同明文块产出相同密文块对结构化数据来说几乎等于没加密——看一眼密文的重复模式就能猜出内容结构。正确做法是用 GCM 这类带认证的 AEAD 模式它能同时保证机密性和完整性而且在解密时能发现密文被篡改。方案机密性完整性是否需要随机 IV推荐度AES/ECB/PKCS5Padding弱模式泄露无不需要不推荐AES/CBC/PKCS5Padding 随机 IV中无需额外 HMAC需要可用但要配 MACAES/GCM/NoPadding 随机 IV强有认证标签需要且绝不复用推荐明文加 Base64无无不需要等于没做注意GCM 的 IV 绝对不能复用。同一把密钥下重复使用 IV会导致密钥流重复攻击者拿到两份密文做异或就能还原出明文。工程上的做法是每次加密生成 12 字节的随机 IV直接拼接在密文前面一起存。2.4 组件、权限与攻击面这一块是移动端安全里最容易被低估的部分。原因是组件暴露带来的风险不像明文存储那么直观但它的危害往往更大——因为它意味着别人可以直接驱动你的代码。以 Android 的四大组件为例导出意味着任何同设备上的其他应用都能发起调用。一个导出的 Activity 如果直接信任了Intent里传入的参数比如用户 ID、订单号、跳转地址那么调用方是谁根本无从验证。一个导出的 ContentProvider 如果对openFile的路径参数不做规范化校验../../就能穿到应用私有目录外面去。一个导出的 BroadcastReceiver 如果对action不做区分就可能被用来触发本该由内部控制的逻辑。检测的时候我会做这么几件事先列出所有exportedtrue的组件逐个标注它是否需要外部调用对需要外部调用的检查有没有做权限声明android:permission或者调用方校验对 Deep Link构造畸形和越权的参数看返回什么对 Provider尝试带路径穿越的 URI。组件的检测报告里最忌讳的是一刀切。有人看到导出的 Service 就直接标高危但这些组件可能是第三方 SDK 为了推送、分享必须暴露的贸然改掉会直接导致功能失效。合理的做法是标注风险 依赖关系 建议动作把决策权交回给业务方。权限这块的重点不在申请了多少而在申请的和实际用的是否一致。常见的情况是主工程的权限清单很干净但引入了几个三方 SDK 之后合并出来的清单里多了通讯录、位置、应用列表。用户看到的是你的应用名采集行为却来自你没细看过的依赖。2.5 第三方 SDK 的连带风险供应链是最近几年风险增长最快的部分。移动应用的依赖往往是二进制分发看不到源码但拿得到行为。检测的时候我会关注三个维度。一是权限与采集行为SDK 初始化时申请了哪些权限、启动了哪些后台任务、往哪些域名发数据二是版本独有的 SDK 版本可能存在已知问题尤其是那些长期不更新的工具类库三是数据流向SDK 上报的字段里有没有超出业务必要范围的内容比如设备标识、已安装应用列表。这里有个现实问题很多 SDK 的采集行为是默认开启、需要手动关。所以检测报告里应该明确写出关闭方式而不是只写存在采集。研发拿到这份报告需要的是可执行的动作不是一张吓人的清单。3. 一套可复现的检测流程3.1 环境准备与样本管理检测环境的搭建质量直接决定了结果的可信度。我一般准备三类设备一台普通未改动的真机用于跑正常的业务路径一台有 root 权限的测试机用于访问私有目录、安装系统级证书一台模拟器专门用于跑那些会检测真实设备环境的逻辑。样本管理这块容易被忽略但很重要。检测必须针对将要发出去的那个包而不是开发机上的 debug 包。原因很实际debug 包的debuggable为真、有调试符号、混淆配置也可能不一样测出来的结果跟线上完全是两码事。我通常会要求提供签名后的 release 包并且记录包名、版本号、签名指纹这三项写进报告的标题页避免后续扯皮。# 记录包的指纹信息作为检测样本的唯一标识 keytool -printcert -jarfile app-release.apk | head -20 shasum -a 256 app-release.apk工具链的准备不算复杂静态侧是aapt2、apktool、jadx三件套动态侧是 Hook 框架加真机流量侧是一个能拦截 HTTPS 的工具。工具选型的原则是能出可读结果优先比如jadx的反编译质量比手写脚本解析字节码高得多没必要自己造轮子。3.2 反编译与静态排查的具体动作流程上我会按清单→资源→代码的顺序走因为这个顺序的信息密度是递减的。清单阶段花十分钟把前面提到的几个关键属性过一遍同时把导出的组件列成表。资源阶段主要看res/xml下的网络安全配置、res/raw下有没有带着证书和私钥文件、strings.xml里有没有明显像密钥的长字符串。代码阶段用检索的方式定位而不是通读——通读不现实jadx出来的伪代码动辄几十万行。检索的关键词可以直接列成一张清单检索目标关键词典型风险硬编码凭证secret、appkey、token、password凭证泄露弱加密AES/ECB、DES、MD5、SHA1数据可被还原日志泄露Log.d、Log.i、printStackTrace敏感信息外泄WebView 风险addJavascriptInterface、setAllowFileAccess任意脚本调用本地能力存储getExternalStorageDirectory、MODE_WORLD_READABLE数据落在可读位置组件exported、Intent.parseUri外部驱动内部逻辑这个阶段产出的东西是候选问题列表不是最终结论。每一条都要回到代码上下文确认数据来源这一步没法省。3.3 动态环境搭建与 Hook动态阶段的目标是把静态阶段看不懂的逻辑跑明白。典型场景某段代码在if里做了个判断判断通过才继续但你不知道它判断的是什么。Hook 就是用来解决这个的。除了前面那段日志拦截脚本我会再挂几个关键的javax.crypto.Cipher的doFinal用来确认加解密用的算法和密钥java.net.URL的构造函数用来看实际请求了哪些地址有些地址是动态拼接的静态扫不出来android.content.SharedPreferences$Editor的put用来确认写进去的键值对。// hook_sp.js拦截本地存储的写入动作 Java.perform(function () { var Editor Java.use(android.app.SharedPreferencesImpl$EditorImpl); Editor.putString.implementation function (key, value) { console.log([SP putString] key value); return this.putString(key, value); }; });跑完之后对照静态阶段的候选列表能消掉一大半的疑问。剩下的那些就是必须人工判断的部分通常数量不多。提示Hook 框架的版本要和目标系统的架构匹配。在 64 位设备上跑一套 32 位的注入组件是很常见的新手问题表现是进程起来了但脚本完全没反应。3.4 拦截流量与协议分析链路分析的第一步是让流量能过拦截工具。从 Android 7 开始应用默认不再信任用户安装的证书所以要么改包把network_security_config里的信任范围放开但这样就改变了被测对象要么把证书装到系统证书区需要 root也就是测试机的价值所在。我更倾向后者因为它对被测包的改动最小。流量拦到之后重点看四件事有没有明文 HTTP 的请求HTTPS 请求里敏感字段是不是裸的 JSON令牌有没有放在 URL 上会进日志、进缓存、进 Referer请求有没有防重放机制。最后一条经常被忽略。如果接口只靠一个长期有效的令牌做鉴权没有时间戳、没有随机数、没有签名那么一旦这个令牌被抓到攻击者可以无限次重放而且你从服务端日志上根本分不出来是本人还是别人。加时间戳和签名不算复杂但它需要客户端和服务端一起改所以必须在架构阶段就定下来后期补的成本很高。3.5 报告定级与修复验证报告的价值不在于列出多少问题而在于让研发知道先改哪个。我的定级逻辑大致是三个维度相乘危害程度、触发前提、影响范围。危害高、不需要任何前提比如远端直接可达、影响全部用户的是最高优先级危害高但需要拿到设备物理权限才能触发的优先级往下掉。这个逻辑听起来简单但实际操作中很容易被高危两个字带偏——有些人看到高危就要求立刻发版不看触发条件结果把一个需要 root 才能复现的问题当成了紧急线上事故。等级判定特征建议处理节奏严重无前提、可远程触发、影响凭证或资金立即修复单独发版高需邻近条件或特定场景影响敏感数据下个版本内修复中需物理接触或高权限影响有限数据排期修复低信息泄露但利用成本高随版本迭代优化修复之后必须做回归验证而且要用修复前能复现的那套方法再跑一遍。这一步跳过的话很容易出现改了但没改对的情况——比如把allowBackup在清单里关了但某个库在合并清单的时候又给打开了最终产物里还是true。所以验证一定要针对最终打包出来的 APK而不是看源码。4. 常见问题与排查技巧实录4.1 加固之后分析不动了加固会给反编译制造很大阻力jadx出来的结果可能是一堆看不懂的跳转字符串也全是加密的。这时候静态分析基本失效重心必须转到动态侧。具体做法是让应用自己解密自己。加固壳在启动时必然要把原始字节码还原到内存里然后交给虚拟机执行这个时间窗口就是切入的机会。常规思路是在类加载器加载 dex 的时候 dump 内存或者在关键方法被调用时直接 hook 业务逻辑。前者需要一点耐心找时机后者简单直接——你不需要看懂整个包只需要看懂某个方法在做什么。另一个思路是绕开。很多加固对新安装的应用或者特定系统版本的保护强度不一样多试几个环境有时候会有意外收获。这不是取巧是理解加固方案的适配成本之后做出的合理选择。4.2 误报和漏报怎么取舍误报是检测服务最消耗信任的东西。一份报告里如果有三成的条目点开一看是不用改研发很快就会养成整份忽略的习惯那时候再准的报告也没人看。控制误报的核心是上下文校验。规则命中了addJavascriptInterface先别急着报去看看这个 WebView 加载的是不是只有本地file://页面——如果是风险等级可以降一大截。规则命中了MD5去看看它是不是只用来做文件完整性校验而不是口令哈希。漏报相对难控制因为它的成因通常是规则没想到。我的做法是把每次人工发现但规则没覆盖的问题回头补进规则库或者检查清单让这套东西随着项目一起长。4.3 检测环境被识别有些应用会检测运行环境发现是模拟器、发现被 hook、发现有抓包工具就切换行为或者直接退出。这是正常的防护设计但会给检测带来麻烦。应对方式分两层。表层是把环境伪装得像真机一点——改设备指纹、隐藏 hook 框架的特征、把抓包工具的证书伪装成系统内置。深层是接受它会被发现转而观察发现之后的行为它是安静退出还是偷偷降级到明文传输还是把数据发到另一个地址。后者的信息量往往更大。4.4 高频问题速查表现象可能原因排查动作Hook 脚本无任何输出架构不匹配或进程未附加确认frida-ps -U能看到目标进程抓不到 HTTPS 请求证书不受信任或启用了固定检查网络安全配置与固定逻辑反编译报错加固或字节码被裁剪换工具或转动态分析令牌在本地明文未加密存储检查shared_prefs与数据库文件权限清单多出条目SDK 清单合并用manifest-merger报告定位来源改了配置但产物没变清单合并覆盖检查最终 APK 而不是源码还有一个不太算问题但影响很大的点检测报告的时间戳。安全状态是会变的今天测过没问题不代表下个月引入新 SDK 之后还没问题。我一般建议报告上明确标注针对某某版本超过一定周期或者依赖发生重大变更之后重测比一次性做一次大而全的检测有用得多。5. 把检测嵌进研发流程的几个现实做法5.1 门禁位置的选择检测环节放在哪里效果差别很大。放在提测前问题发现得早但那时候包还不完整放在发版前样本最真实但一旦发现问题就得回滚排期。我倾向的做法是两次卡点提测前跑一次轻量的静态扫描只卡最严重的那几类硬编码凭证、debuggable、明文传输成本低、反馈快发版前跑一次完整检测包括动态和链路。两次卡点的规则集不一样这点很关键。提测前用全套规则会把人淹死因为那时候代码还在改扫出来的东西一半是临时的。发版前的规则可以严格因为这时候改动应该收敛了。5.2 自动化流水线的接入方式静态扫描是可以完全自动化的接入方式也很直接在构建产物生成之后跑一遍解包加规则匹配输出一份结构化的结果JSON 或 SARIF 格式然后交给流水线判断是否阻断。关键在于阻断规则的设计。如果一开始就只要有问题就阻断团队很快就会想办法绕过这个卡点比如加个跳过标记。合理的做法是先只阻断严重级别其他的以报告形式呈现等规则稳定、误报降下来之后再逐步提高阻断线。这个过程大概需要两三个版本周期才能调顺。动态检测的自动化难度高很多因为它需要真实设备、需要处理反调试、还有一定的随机性。比较现实的做法是搭一个小规模的设备农场每晚定时跑核心路径结果只作为参考不阻断人工每周看一次趋势。5.3 团队协作与责任划分安全检测最容易出的问题不是技术是责任边界模糊。检测方报了一堆问题研发觉得这些都得改吗产品觉得能上线吗最后不了了之。我的经验是把责任拆清楚检测方负责发现问题并给出可执行的修复建议业务研发负责决定改不改、什么时候改技术负责人负责对不改的那部分签字确认风险。第三件事最容易缺但恰恰是最重要的——不是所有问题都要改但每一个不改的决定都应该有人明确承担。提示报告里的修复建议写到改哪一行这个粒度采纳率会高很多。写建议加强本地数据保护和写把PrefsUtil.saveToken里的明文写入改成用 AndroidKeyStore 加密后写入效果完全不同。还有一点值得说安全检测不该只在出问题的时候被想起来。把它做成季度性的常规动作配合每次引入新依赖时的准入检查整体的风险曲线会比出事之后突击一轮平缓得多。突击检测能解决眼前的问题但解决不了为什么问题会积累到这个程度。我个人在实操中的体会是APP 安全检测这件事最难的从来不是工具而是把一条条技术问题翻译成业务能理解的决策依据。同一个本地明文存储令牌在不同应用里的严重程度可以差出两个数量级——一个纯资讯类应用和一个涉及资产操作的应用判级逻辑完全不同。所以每次出报告之前我会花时间把最严重的几条用自己的话重写一遍写清楚如果这条被利用用户会损失什么。这段文字往往比后面几十页的技术细节更能推动事情往前走。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑