
现在市面上的 APP 安全测试我见过两种走法一种是把安装包丢进扫描工具等它吐出一份 PDF 报告然后逐条往缺陷系统里提另一种是干脆不做等灰度期间有人把接口扒出来再说。前者看着专业实际上报告里一半是通用规则刷出来的噪音真正能被利用的那几条往往藏在业务逻辑和 SDK 的行为里后者的问题不用我多说一次数据泄露就够团队忙半年。APP安全性测试这件事本质上不是“跑个工具”而是围绕一个具体产品把“谁能碰到什么、碰到之后能拿走什么”这条链子理清楚。这篇内容我按自己带项目时的实际流程拆开讲从攻击面怎么画、安装包怎么拆、本地数据怎么查、链路怎么验、业务逻辑怎么设计用例一直讲到上线前怎么收口。适合刚接安全测试任务的测试同学也适合想自查的移动端开发基础不需要很深但最好对 Android 或 iOS 的目录结构有个模糊印象。1. 别急着开扫描器先给 APP 画一张攻击面地图扫描器最大的问题是它不认识你的业务。它看到 WebView 开了 JS 接口就报高危看到允许备份就报中危但这两条在你这个产品里到底能不能被利用它不知道只有你知道。所以我在动手之前会先花半天时间画一张攻击面地图把“谁可能攻击、从哪里进来、进来能碰到什么”三件事写在纸上。这张图不用多漂亮一页纸足够但后面所有测试项的优先级都从它来。1.1 把资产和入口分开列先列资产账号体系、支付链路、用户隐私数据通讯录、位置、相册、身份证、业务凭证优惠券、积分、会员权益、内部配置接口地址、灰度开关。再列入口客户端本体安装包、本地存储、组件导出、通信链路、服务端接口、第三方 SDK、系统交互面Deep Link、剪贴板、分享回调、通知栏。这两列交叉一下你就知道哪些组合是真正要测的。举个例子一个只做内容浏览的资讯类 APP“相册权限”可能根本不申请那这条测试项直接划掉但它的 Deep Link 如果能把外部参数直接拼进 WebView 的地址那就是一条实实在在的入口。我自己常用的打分方式是给每个交叉点打两个分利用门槛1 到 3 分1 分代表普通用户拿现成工具就能做3 分代表需要 root 设备加逆向能力和损失量级1 到 3 分1 分是信息泄露但影响有限3 分是资金损失或批量账号风险。两个分数相乘排在前面的先测。这套土办法不严谨但比“按扫描器等级排序”靠谱得多因为它把业务上下文塞进去了。攻击面典型问题利用门槛损失量级优先级服务端接口水平越权查他人订单低高最先测本地存储明文存 token中需设备权限高次优先通信链路证书校验缺失中中第三组件导出任意页面被外部拉起低低最后业务逻辑优惠券并发领取低高最先测1.2 分清“设备已被控制”和“设备未被控制”两种前提这一步特别容易被忽略。很多扫描报告里的高危项前提都是攻击者已经拿到了 root 权限或者能物理接触设备。如果你的产品是面向普通消费者的那么“攻击者已 root”这个前提下的问题风险等级要往下调一档因为绝大多数真实攻击者不会为了一个普通账号去 root 手机。但反过来如果产品面向的是企业内网用户或者是金融、办公类应用那这个前提就完全成立因为攻击者有的是动机去搞一台能读沙箱的设备。我的做法是把测试项分成两组A 组是“不需要特殊权限就能触发”的比如接口越权、逻辑漏洞、明文传输B 组是“需要设备权限或逆向能力”的比如本地数据库明文、内存中的密钥。A 组的问题一律按真实风险上报B 组的问题要写清楚前提条件让决策的人自己判断。这么分开的好处是修复排期的时候不会出现“一个需要 root 才能利用的本地存储问题把接口越权问题挤下队列”的荒唐事。1.3 扫描器输出的正确用法我不否认扫描器的价值它能快速把“明显不该出现的配置”筛一遍省掉很多手工翻文件的时间。但它的输出必须经过两道过滤第一道是配置层面的确认比如报“允许备份”你要去清单文件里看android:allowBackup到底是不是 true有些构建变体里已经关掉了只是扫描的是旧的包第二道是可达性验证比如报“导出组件”你实际用 adb 拉起一次看看它有没有权限校验很多组件虽然 exported但内部第一行就是登录态判断外部根本进不去。剩下那些过滤不掉的才是真正要花时间深挖的。我一般会把扫描结果和攻击面地图叠在一起看图上没标但扫描器报了的回头看是不是漏了图上标了但扫描器没报的那才是重点因为那部分只能靠手工。2. 静态解剖安装包从清单文件到硬编码密钥的排查顺序拆包这件事顺序比工具重要。我见过有人一上来就用反编译工具把整个 dex 转成 Java 源码几万个文件铺开然后就不知道从哪看了。正确的顺序应该是先看外层结构再看清单配置再搜字符串最后才进代码。这个顺序的逻辑是越靠外层的越容易改也越容易成为入口越往里的越费时间所以要等问题定位到具体模块再进去。2.1 拆包后的第一眼签名、清单、权限拿到安装包第一件事是确认它是不是加固过的。判断方法很朴素正常包解出来的 dex 通常有好几个体积分布也比较均匀加固包往往只有一个壳 dex里面类很少而且会出现一些奇怪的段名。这一步决定了后面走哪条路——没加固的可以静态看完加固的就得考虑在运行时下手。接着看清单文件我会重点盯六个字段android:debuggable是不是被误开到了正式包、android:allowBackup的值、android:usesCleartextTraffic和网络安全配置文件、所有android:exportedtrue的组件、android:permission的声明、以及应用申请的全部权限清单。这几项里debuggable被误开是最常见也最要命的一旦开了任何能连上设备的人都能直接调试进程、读取内存几乎等于把门敞开。权限清单要对照功能看。一个纯离线的记账工具申请了读取通讯录和后台定位这就不是“多申请了一个权限”的问题而是要追问它到底在采集什么。我在实际项目里遇到过一次某个第三方统计 SDK 在初始化时偷偷申请了位置权限业务方自己都不知道这类问题只能靠对着权限清单逐个回溯来源才能发现。2.2 字符串搜索找密钥、找地址、找开关反编译完之后我最先做的不是读逻辑而是搜字符串。关键词表大概这么几类凭证类secret、appkey、access_key、token、password、private、地址类http://、内网网段、测试域名、.internal、开关类debug、test、mock、switch、加密类AES、DES、ECB、MD5、SHA1。搜索的时候要注意大小写和命名风格的变体光搜secret会漏掉sEcReT和appSecret。搜出来的一堆结果里绝大多数是噪音。我的过滤规则是这样的先看它所在的类名和包名如果是测试用例类、单元测试目录、日志格式化工具类基本可以直接排除再看这个字符串有没有被引用没有任何引用的常量大概率是废弃代码最后看它的形态长度和字符集符合密钥特征的比如 32 位十六进制、44 位 Base64才值得深挖。真正需要担心的是那种“看起来像正常业务参数、实际上是签名盐值”的字符串这种只能靠读调用链确认。提醒一句搜到硬编码密钥不要急着当高危上报先确认它是不是生产环境的。很多团队在代码里留的是沙箱环境的测试 key泄露了也没什么影响真正的问题是生产 key 和测试 key 共用同一套字符串常量。2.3 加固包怎么继续往下走遇到加固包静态这条路基本走不通。这时候要换思路确认加固方案覆盖了哪些层面——是只有 dex 加固还是 so 也加固了有没有做 VMP。不同的覆盖范围决定了你还能做什么。如果只是 dex 加固、so 是明文那核心算法很可能就在 so 里直接分析动态库就行如果 so 也加固了那基本只能从运行时内存和调用行为入手。这里必须强调授权边界。任何动态调试、内存分析的操作都必须在客户书面授权、使用测试账号、测试环境的前提下进行而且要在测试报告里写清楚操作方式方便开发复现。我见过有人为了证明漏洞存在直接拿线上账号做验证结果触发了风控把真实用户给锁了这种事故比漏洞本身更麻烦。测试包里做验证、线上只做只读确认这是我给自己定的死规矩。另外一个常被忽略的点是签名校验。改包重签名之后应用闪退八成是校验没过。校验可能在三处Java 层的checkSignatures对比、so 层的签名摘要比对、以及服务端下发的签名指纹比对。前两种在测试环境里可以定位并说明第三种是改包也绕不过去的遇到这种我会在报告里明确写“客户端加固有效该路径不可利用”这也是有价值的结论。3. 本地数据落地沙箱目录里那些不该出现的明文本地存储这块测试成本极低、发现问题极多性价比很高。但很多人只查shared_prefs漏掉了数据库、缓存目录、外部存储和系统级泄露点。我的习惯是把沙箱目录整个拉一遍先按文件大小和修改时间排个序最近改动的大文件优先看。这一步在测试设备上做使用测试账号产生的数据不要去碰真实用户的数据。3.1 该翻的几个目录Android 侧的路径相对固定/data/data/包名/shared_prefs/放键值对配置databases/放 SQLite 库files/和cache/放业务文件no_backup/也值得看一眼有些库会把不该备份的东西放这儿。外部存储上的/sdcard/Android/data/包名/是另一个重灾区因为这里的文件卸载应用后不一定会被清掉而且早期版本里其他应用可以读。iOS 侧对应的是Documents/、Library/Caches/、Library/Preferences/以及钥匙串。钥匙串的特殊之处在于它的生命周期跟应用卸载无关如果往里面写了不该写的明文卸载重装依然还在。平台路径常见问题Androidshared_prefs明文 token、手机号、设备标识AndroiddatabasesSQLite 中明文存聊天记录、订单Androidfiles/cache下载的业务文件未加密Android外部存储导出文件残留、日志文件iOSDocuments用户文档明文、备份泄露iOS钥匙串密钥写入后不随卸载清除查这些目录有个小技巧不要只看文件名用文件头判断类型。SQLite 文件开头是SQLite format 3XML 开头是?xml剩下的用file命令看。我遇到过把 SQLite 库改名为.dat的情况光看后缀会直接漏掉。3.2 日志、剪贴板、截屏这三个隐蔽出口日志泄露是最容易被低估的。开发阶段为了方便排查往往会把接口返回、登录参数整段打印出来上线前忘了关。测试方法是把应用的主要流程走一遍同时抓设备日志按包名和关键词过滤看有没有敏感字段。判断标准很简单真实手机号、身份证、完整 token、完整订单金额这几样出现任何一个都算问题。剪贴板的问题在于用户主动复制的内容和应用偷偷写入的内容是两回事。有些应用会在启动时把设备指纹或者推广参数写进剪贴板用来做跨应用追踪这在隐私合规上是明确的红线。测试时可以复制一段固定的测试文本然后走一遍主要流程再看剪贴板内容有没有变化。截屏泄露分两个层面一是应用允许在后台任务列表里显示完整界面截图导致敏感页面比如账单详情在任务切换时被拍到二是应用没有对敏感页面设置防截屏标记。前者几乎所有应用都有后者只有金融类才强制要求。这两条我不一定会当漏洞报但会在报告里作为改进建议提因为修复成本很低加一行标记的事。3.3 加密实现里最常见的几种错法查到加密存储不等于安全关键看怎么用的。我遇到的错法大概这么几类按出现频率排把 Base64 编码当加密解码就还原、用固定 IV 的 AES-CBC相同明文出相同密文可以对比推断、用 ECB 模式加密相同分组出相同密文图片加密后还能看出轮廓、密钥硬编码在代码或 so 里等于没加密、用 MD5 或 SHA1 直接存密码彩虹表一查就出来。判断方法不难看存储的密文长度有没有变化。如果同一个字段在两条记录里长度不同大概率用了分组加密如果长度固定但每次内容不同可能有随机 IV如果两次写入同一个值密文完全一样那 IV 就是固定的。发现固定 IV 之后可以构造两条内容相似但关键字段不同的记录对比密文差异能推断出加密结构的弱点。这类验证要在测试设备上用测试数据做别去动真数据。4. 通信链路验证抓不到包时该往哪里想抓包抓不到是安全测试里最常卡住的环节。我见过不少人在这一步怀疑工具、重装证书、换设备折腾一下午没进展。其实抓不到包的原因就那么几种按排查成本从低到高排一遍基本十分钟能定位到方向。前提再说一次抓包操作仅在你负责的测试环境里进行测试设备、测试账号、测试网络缺一不可。4.1 排查顺序从最外层往里走第一层确认设备的流量到底有没有经过抓包工具。方法很简单先拿一个确定走明文的测试接口打一次看工具里有没有记录。没有的话问题在转发配置不在应用。第二层确认协议。如果应用走的是自定义的 TCP 通道、WebSocket 或者 gRPC那 HTTP 抓包工具当然什么都看不到。这时候要换工具或者先用端口监听看看连接目标。第三层证书信任。Android 7.0 之后应用默认只信任系统证书用户手动装的证书不再生效。所以工具里明明装好了证书应用却报网络错误八成就是这个原因。解决办法有两个方向一是在测试设备上把证书装进系统证书区需要设备权限测试环境下可行二是让开发出一个信任测试证书的 debug 包——后者更干净也更容易复现。第四层应用自己做了证书绑定。这是最常见也最正当的一种防护表现是换了证书就握手失败。这时候要区分它是绑了证书指纹、绑了公钥还是用了网络库自带的绑定功能。不同实现绕开的方式不一样而“绕开”本身要在授权范围内做通常的做法是用动态插桩改变校验逻辑的返回值或者让服务端在测试环境放行测试证书。第五层双向认证。服务端也要求客户端出示证书这种情况客户端手里一定有一份证书文件可以在安装包资源里找出来。找到之后配置到抓包工具里就能继续看流量了。现象最可能的原因处理方向工具里完全没有记录转发配置错误或走了非 HTTP 协议先验证工具链路再查协议有记录但应用报错证书不被信任系统证书或改用 debug 包握手直接失败证书绑定或双向认证定位绑定实现测试环境放行部分接口能抓、部分不能特定模块用了独立网络栈按模块分别处理4.2 接口层的越权与重放怎么设计用例能看到流量之后重头戏才开始。接口测试里我优先做三类水平越权、垂直越权、重放。水平越权就是拿 A 账号的凭证去访问 B 账号的资源。做法是准备两个测试账号用 B 的会话抓一个查询请求然后把请求里的用户标识换成 A 的看返回的是不是 A 的数据。这里的坑在于很多接口的用户标识不是明文 ID而是加密串或者哈希值直接改会报参数错误得先从别的接口拿到 A 的对应标识再替换。垂直越权是低权限账号访问高权限接口。做法是拿普通账号的 token 去调管理端接口看服务端是不是只在前端做了菜单隐藏。这类问题在后台类应用里特别多因为开发往往认为“用户看不到入口就不会调”。重放测试要关注两类接口有实际副作用但没做幂等控制的比如领券、下单、提现以及依赖客户端计算结果的比如金额、折扣。验证方法是原样重发一次请求看服务端有没有做去重、有没有校验时间戳和随机数。4.3 签名机制的实际破解路径很多接口会带一个sign参数用来防篡改。测试的时候改了参数签名对不上请求就被拒了。这时候不要急着下结论说“有签名所以安全”先看签名是怎么算的。常见实现是sign MD5(排序后的参数 salt)salt 要么硬编码在客户端要么藏在 so 里。如果是前者静态搜索就能找到如果是后者得分析动态库或者从运行时取值。还有一种更弱的设计salt 是固定的而且不参与时间戳和随机数那么只要参数组合固定签名就固定攻击者可以无限制复用这个签名——这等于签名形同虚设。更稳妥的设计是把时间戳和一次性随机数一起纳入签名服务端维护一个短时间窗口去重。测试这类机制时我会专门验证三件事把时间戳改成很久以前的值服务端收不收把随机数重复使用服务端收不收把签名去掉只留参数服务端收不收。三件事里任何一件通过了都说明这套机制有缺口。5. 接口与业务逻辑自动化扫不出来的越权和重放工具能扫出配置问题扫不出业务问题。而业务问题恰恰是造成真实资金损失的那一类。我做过统计一个中等复杂度的交易类应用逻辑漏洞的数量通常比配置类漏洞多而且修复起来更麻烦因为它往往不是“改个开关”而是要改状态机设计。所以这部分我会单独留出时间用状态图的方式把关键链路画出来再逐个节点找边界。5.1 会话与凭证的边界用例这块的测试点比较固定我一般列成清单逐条过登录后 token 的有效期是多久、过期后能不能续期、退出登录后旧 token 还能不能用、修改密码后其他设备上的会话有没有被踢掉、同一账号多端并发登录会不会互相挤掉、账号被禁用后已签发的 token 是否立即失效。这里最常出问题的是“退出登录后 token 仍有效”和“改密码后旧 token 不失效”两条。从用户视角看退出登录就意味着别人拿不到我的账号了如果旧 token 还能用这个心理预期就被打破了。验证方法很直接登录拿到 token退出登录再用这个 token 手动发一个请求看返回 200 还是 401。还有一个隐蔽的点是 token 的存储位置。如果 token 存在本地明文文件里而且没有绑定设备指纹那么把文件复制到另一台设备上就能直接登录这类问题在检测时容易被当成“本地存储问题”实际上是会话设计问题。5.2 状态机视角下的业务漏洞我习惯把关键业务画成状态机待支付、已支付、待发货、已发货、已完成、已退款每个状态之间有哪些允许的跳转哪些操作会触发状态变更。画完之后专门去找“不应该存在的跳转”比如从待支付直接跳到已完成、从已退款再跳回已支付。举个典型场景优惠券的领取和使用。正常流程是领取后进入“未使用”下单时锁定支付成功后置为“已使用”。如果服务端只在支付回调里判断券的状态而没有在下单时锁定那么同一张券可以并发用在多个订单上。验证方法是同时发两个使用同一张券的下单请求看是不是两个都成功。这类问题单靠一次一次点界面是发现不了的必须并发。金额篡改也是老问题。核心判断只有一条服务端有没有把客户端传来的金额当准。测试方法是把下单请求里的金额改成一个明显不合理的值再看订单实际创建的金额。有些系统会在服务端重新计算有些则直接采信后者就是漏洞。还有一个变体是把商品数量改成负数看总价会不会变成负的。5.3 支付链路里几个必查项支付这块我会单独列一个检查表因为它的容错空间最小。第一支付回调有没有验签。第二回调是不是幂等的重复回调会不会重复发货。第三订单金额以哪一侧为准。第四支付超时后的订单状态怎么处理会不会出现“钱付了但订单已关闭”。第五退款接口有没有校验订单归属能不能退别人的单。其中“回调幂等”是最容易出事的。做法是截获一次支付成功回调原样重发若干次观察订单状态和账户余额的变化。如果每次重发都触发一次发货或者一次加币那就是实打实的问题。修复方式通常是在服务端加一个基于订单号的唯一约束或者状态前置判断成本不高但必须在设计阶段就想清楚。6. 隐私合规与上线收口把测试结论变成可验证的修复项技术漏洞修完不等于能上线还有一层是隐私合规。这块的检查逻辑跟技术漏洞不太一样技术漏洞看的是“能不能被利用”合规看的是“有没有超出告知范围去采集”。两者有交集但不重合有些行为技术上没漏洞数据加密传输、存在合规的服务器上但采集本身没有获得用户同意照样是问题。6.1 权限申请时机与 SDK 行为核查第一个要查的是时机。应用刚启动、用户还没看到任何说明就先弹一堆权限申请这是最典型的违规。正确做法是在用户触发对应功能时再申请并且说明用途。测试时可以清空数据重装一次观察首次启动后 10 秒内弹了几个权限框、弹之前有没有展示说明。第二个要查的是 SDK。第三方 SDK 是隐私问题的重灾区因为它的初始化往往在应用启动时就完成了而且采集行为不受业务方控制。核查方法是把安装包里所有 SDK 的清单列出来逐个确认它有没有在初始化时读取设备标识、有没有在后台采集位置、有没有把数据传到外部地址。这一步要结合抓包一起看看启动阶段有没有往陌生域名发请求。第三个要查的是共享和导出。应用内如果有“分享到外部”的功能要确认分享出去的内容里有没有夹带用户标识或者设备信息。文件导出功能要确认导出的文件里有没有超出用户预期的字段比如导出一份对账单结果里面带了内部用户 ID。6.2 从问题清单到修复验证的闭环测试做完最怕的是报告交上去然后没有下文。我的做法是把每个问题写成开发能直接动手的格式问题描述、复现步骤精确到点了哪个按钮、发了哪个请求、影响范围、修复建议、验证方法。其中“验证方法”这一栏经常被省略但它其实最重要因为开发修完之后得有人能快速确认修没修好。回归验证的时候有个经验不要只验证报告里那一条。同一个成因往往会在多个地方出现比如你发现 A 接口存在水平越权那么 B、C 接口大概率有同样的问题因为它们可能共用同一套权限校验中间件。所以修完之后我会把同类接口全扫一遍而不是只测原来那一个。还有一个容易被忽略的点是修复方式本身引入的新问题。比如为了修本地明文存储开发改成把数据加密后存在本地但密钥又硬编码在代码里这就等于把问题从“明文”变成了“看起来加密了但其实能解开”风险等级没降多少。验证的时候要看实现不能只看现象。6.3 一份能落地的自查清单如果团队暂时没有专职安全测试我建议至少把下面这张表过一遍成本不高但能挡掉大部分低级问题。检查项判断标准常见错法正式包调试开关debuggable 为 false构建变体配置串了备份开关allowBackup 按需设置默认 true 未改明文传输全站 HTTPS 且有配置约束部分图片域名走 HTTP本地敏感数据无明文 token、手机号日志打印未关闭接口鉴权每个接口都校验归属只在前端隐藏入口会话失效退出后旧 token 失效只清本地不清服务端权限时机功能触发时申请启动即申请全套SDK 采集与隐私政策一致初始化即读设备标识这张表的价值不在于它有多全而在于它能被反复执行。每次发版前跑一遍比做一次深度渗透然后半年不管要有用得多。最后分享一个我在实际项目里的做法把安全测试的用例按“能否自动化”分成两堆配置类、权限类、明文存储类这些能用脚本固定下来的全部塞进流水线每次构建自动跑接口越权、业务逻辑、支付链路这些必须人工设计的做成发版前的固定检查单由固定的人负责。这样分工之后重复劳动被压缩人的精力集中在那 20% 真正需要思考的地方。踩过几次“临时抓个包就上线”的坑之后我现在宁可多花半天把清单过完也不想在灰度第一天被电话叫起来。