做移动端安全测试这些年,我最怕听到的一句话是“我们这个App已经加固了,应该没问题”。加固解决的是逆向门槛,跟数据会不会泄露、接口能不能被越权调用、第三方SDK有没有偷偷收集信息,基本是两码事。真正意义上的APP安全性测试,是站在攻击者的视角把应用从安装包、运行过程、网络交互、数据落地到界面交互完整走一遍,找出“能被利用的地方”,再把它翻译成开发能改、产品能懂、合规能过的问题清单。这篇文章适合几类人看:刚转做移动端安全的测试同学、需要自己扛安全验收的客户端开发、以及要给团队搭一套安全检查流程的技术负责人。下面我按实际干活顺序拆开讲——先讲测什么、边界在哪,再讲环境怎么搭、静态怎么扫、动态怎么跑,最后给一份抓不到包之类的排查速查表和几条踩坑经验。
1. APP安全性测试到底在测什么:先把靶子和边界画清楚
很多人一上手就找工具、装证书、开抓包,结果测了两天写不出一份像样的报告。原因不是技术不行,而是开工前没把“测什么”想明白。安全测试跟功能测试最大的差别在于:功能测试有明确的需求文档告诉你正确结果是什么,安全测试没有“正确结果”,只有“可被利用的路径”。所以你必须先自己画一张地图,把资产、入口、信任边界三件事摆清楚,后面所有动作才有落点。
1.1 资产、入口、信任边界:三个维度拆完就不慌了
资产指的是这个App里值钱的东西:登录凭证、支付能力、用户的个人信息、企业的业务数据、以及能调用后端接口的那些密钥和令牌。我在做评估时会先列一张资产清单,按“泄露后影响多大”排序,因为安全测试的时间永远不够,必须把火力集中在最值钱的地方。举个例子,一个电商App里,商品详情页的接口被刷影响有限,但订单接口如果存在越权,能看别人的收货地址和电话,这就是严重级别。
入口指的是外部能接触到应用内部的通道。客户端的入口比大多数人想的要多:对外暴露的页面组件、深链跳转、WebView加载的H5、文件导入、剪贴板读取、通知栏点击、扫码、第三方分享回跳、以及各种隐式跳转参数。每一个入口都值得问一句“这个参数我能不能自己构造”。
信任边界是安全测试里最容易出成绩的地方。客户端和服务端之间、App和第三方SDK之间、App和操作系统之间,都存在一条“我以为对方会校验”的缝。比如客户端把价格算好了传给服务端、服务端直接采信;比如App把用户信息交给某个统计SDK,默认它会脱敏。凡是“假设对方可信”的地方,都是要重点验证的地方。核心原则只有一句:客户端不可信,客户端传来的一切都必须在服务端重新校验。
1.2 客户端测试的边界:很多“客户端漏洞”根子在服务端
刚做这行的时候,我特别执着于“能不能把包解开、能不能改内存里的值”。后来发现,改客户端内存里的金币数量意义不大——真正的问题是这个改动能不能被服务端接受并落库。所以客户端测试很多时候扮演的是“线索发现者”的角色:你在客户端看到一个接口返回了远超页面展示需要的字段,那就要顺着去验证服务端有没有做字段裁剪;你发现客户端本地做了权限判断,那就要验证服务端有没有独立鉴权。
这也是我在报告里坚持写清“验证路径”的原因。只写“客户端本地校验可绕过”意义有限,写成“本地校验绕过之后,服务端未二次校验,导致A用户可以提交B用户的订单号完成查询”,严重等级直接从中危升到高危,开发也没法用“这只是客户端问题”来推掉。实测下来,把客户端线索一路追到服务端的测试,被采纳率和修复率都高得多。
1.3 一套能复用的分层测试模型
我一般把APP安全性测试拆成五层,从上到下依次推进,每层都有明确的产出。分层的好处是进度可控,任何一层中断都不影响前面已经拿到的结论。
| 层级 | 主要目标 | 典型产出 | 大致耗时占比 |
|---|---|---|---|
| 静态分析 | 看清安装包构成、找硬编码与危险配置 | 高危配置清单、密钥泄露清单 | 25% |
| 流量与接口 | 验证传输加密、鉴权、越权、参数可篡改 | 接口问题清单、越权证据 | 30% |
| 数据落地 | 验证本地存储、缓存、日志是否泄露敏感信息 | 存储风险清单 | 20% |
| 运行时观测 | 定位加密逻辑、绕过条件、环境检测 | 关键函数与调用链说明 | 15% |
| 交互与合规 | 截屏、剪贴板、SDK收集、权限申请 | 隐私合规问题清单 | 10% |
这个比例不是死的,遇到金融类、医疗类应用我会把流量和数据落地两层的比例往上提,因为这两类应用的数据敏感度最高;遇到内部工具类应用,静态分析和组件暴露的权重会更高。模型的意义在于让你在被问到“还剩什么没测”时能立刻答上来,而不是凭感觉说“差不多了”。
2. 开工前的准备:授权、环境、第一批“体检”数据
我见过太多人跳过这一步直接开干,最后要么因为碰了生产数据被叫停,要么因为环境没配好,卡在“抓不到包”上耗掉一整天。准备工作做扎实,后面至少省一半时间。
2.1 授权与范围确认:这一步省掉,后面全是白干
安全测试和普通测试的根本区别在于它天然带有“破坏性”的可能。所以开工前必须有一份书面授权,明确测试对象、测试时间窗、允许使用的测试账号、以及哪些动作被禁止。我通常会确认这么几件事:测试包是测试环境还是生产环境,如果是生产环境,是否允许做批量请求和越权验证;账号权限范围是什么,能不能构造第二个测试账号来验证横向越权;测试过程中产生的数据需不需要清理。
提示:任何超出授权范围的动作都不做,包括但不限于对生产环境的批量拉取、对真实用户数据的访问、对其他系统的探测。这一条没有商量余地。
还有一个经常被忽略的点:测试过程中截的图、抓的包、导出的日志,里面大概率含有真实的手机号、身份证、地址。我在交付前会把所有证据材料做一遍脱敏,报告里只留能说明问题的片段。这既是对用户负责,也是对自己的职业习惯负责。
2.2 测试环境怎么搭才顺手
设备这块我建议准备两类:一台系统版本较老、能拿到高权限的实体机或模拟器,用来做深度观测;一台日常主力系统版本的新机器,用来验证真实场景下的表现。老系统的好处是证书可以装进系统信任区,很多工具链跑起来顺畅;新系统则能暴露“证书装了也不生效”之类的真实问题,两者的结论经常不一样。
工具链我一般分成四组,都是通用类别,具体选哪款看团队习惯:
- 包分析类:解包、签名校验、查看清单文件的命令行工具,够用就行,不需要花哨的界面。
- 静态扫描类:开源的移动应用分析框架,能把清单、权限、硬编码、第三方SDK一键扒出来,适合做第一轮粗筛。
- 抓包类:能拦截并修改请求的抓包工具,配合自签证书使用。
- 运行时观测类:动态插桩框架,用于观测方法调用和关键函数入参出参。
网络环境上,我习惯让测试设备和测试机处在同一个局域网,把设备的网络指向测试机上的抓包工具,这样流量路径清晰,出问题也好排查。测试账号至少准备两个,且权限要有差异(比如一个普通用户、一个管理员),否则横向越权根本没法验证。
2.3 拿到安装包先做的三件事
拿到包别急着反编译,先做三件很快但信息量很大的事:
第一件是看包的基本信息,包括包名、版本号、签名信息、支持的架构。命令很简单:
aapt dump badging target.apk | head -30 apksigner verify --print-certs target.apk签名这里重点看是不是使用了调试签名、证书是不是长期有效。我遇到过测试包用调试签名打出去、结果换设备就装不上的情况,浪费半天还以为是设备问题。
第二件是看包内结构:
unzip -l target.apk | head -60重点看有没有lib目录下的原生库、assets里有没有配置文件或模型文件、res/raw里有没有明文资源。原生库的存在意味着有相当一部分逻辑在Java层之外,纯静态搜索能覆盖的面会变窄,后面的运行时观测就更重要。
第三件是用静态扫描框架跑一遍粗筛,主要看三样东西:清单里声明了多少权限、有多少组件是导出的、有没有明显的高危字符串(密钥、测试地址、账号)。这一步通常十几分钟就能出结果,但足够支撑你和开发做第一次沟通了。
3. 静态分析:应用不运行,也能挖出一半问题
静态分析的价值在于“全”,它能把你没注意到的角落一次性摊开。缺点是噪音大,容易误报。所以我的做法是:先用扫描工具做覆盖,再人工确认高价值区域,两头结合,效率最高。
3.1 包结构与反编译:先看地图再看细节
反编译的目的是把编译后的字节码还原成可读的形式。做法不复杂,但关键不在于“能不能还原”,而在于“还原之后看什么”。我的习惯是先看目录结构和包名的命名规律,一个规范的工程,包名基本能反映出模块划分,比如登录、支付、用户中心往往有独立目录,这些就是重点区域。
然后是全局搜索。这一步比逐个文件读要高效得多,我一般会准备一份关键词清单分批搜:密钥相关的词、环境地址相关的词、调试开关相关的词、日志相关的词、以及第三方服务的标识词。搜索的时候注意大小写变体和缩写,比如同一个东西可能写成secretKey、appSecret、sk,只搜一个词很容易漏。
注意:原生库里的字符串不要忽略。很多团队把关键的校验逻辑和密钥放在原生层,以为这样就安全了。实际上字符串表用
strings命令一扫就能看到不少东西,这一步花不了两分钟,值得做。
3.2 高危点速查:硬编码、开关、明文存储
静态分析里最出成绩的就是下面这张表里的内容,几乎每个项目都能命中几条。我把判断方法和风险判断一起列出来,方便你直接照着核对。
| 检查项 | 判断方法 | 风险判断 | 整改方向 |
|---|---|---|---|
| 硬编码密钥 | 全局搜索密钥类关键词,看是否出现可用的固定串 | 高(一旦泄露可长期滥用) | 密钥下发或走服务端签名 |
| 测试环境地址残留 | 搜索内网域名、IP、测试环境标识 | 中(暴露基础设施信息) | 发布包统一清理 |
| 调试开关默认开启 | 清单里调试属性、日志开关常量 | 高(可被挂载调试) | 发布构建强制关闭 |
| 允许备份 | 清单备份属性为真 | 中(本地数据可被导出) | 关闭或做加密 |
| 明文存储路径 | 搜索本地存储相关调用与固定路径 | 高(凭证落盘可读) | 使用系统加密存储 |
| 内嵌测试账号 | 搜索账号密码特征字符串 | 高(可直接登录测试环境) | 全部移除 |
| 弱加密与固定密钥 | 搜索加密调用与常量密钥 | 高(密文等同明文) | 换算法、密钥动态化 |
这里我特别想说一句:硬编码密钥是最常见、也最容易被开发推掉的问题,理由通常是“这个key权限很小”。但权限再小的密钥,只要能调用接口,就能被用来做数据拉取或者刷量,长期成本一点不小。遇上这种争论,我会直接给出利用后的影响面估算,比讲道理有效。
3.3 组件暴露与权限声明:被忽略的重灾区
清单文件里有两个信息非常关键:哪些组件是对外开放的,以及应用申请了哪些权限。组件暴露的风险在于,如果某个页面组件对外开放且没有做调用方校验,其他应用就能直接拉起它并传入参数,参数如果被不当处理,轻则造成信息泄露,重则触发敏感操作。
检测方法不难:用包分析工具把清单完整导出,筛出所有标记为对外暴露的组件,然后逐个问三个问题——这个组件会不会展示敏感数据、会不会接收外部传入的参数、参数会不会被直接用于查询或跳转。三个答案都是“会”的时候,就值得实际验证一次。
权限这块有两个方向的坑。一是申请了和功能不匹配的权限,比如一个记账工具申请读取通讯录,这在隐私合规审查里是硬伤;二是把自定义权限的保护级别声明得太低,导致其他应用可以随意发送广播触发内部逻辑。我在检查权限时会把清单里的权限和隐私政策里的说明逐条对照,对不上的都要提出来。
3.4 第三方SDK与隐私合规扫描
第三方SDK是现在移动应用安全里问题最密集的区域,因为它不在自己团队的代码评审范围内,但会实实在在跑在你的进程里。静态扫描能做的第一件事是列清单:这个App一共集成了多少个SDK、分别属于哪个类别、各自申请了哪些权限、有没有在初始化时就采集设备标识。
第二件事是看初始化时机。合规的要求很明确:用户同意隐私政策之前,不能采集个人信息、不能初始化会采集信息的SDK。我见过不少应用在启动的第一个页面就把所有SDK初始化了个遍,用户还没看到弹窗,信息已经上报了——这类问题在检查中属于高频命中,而且整改成本不高,属于性价比很高的一条。
第三件事是看SDK的版本与已知问题。老版本SDK可能存在公开披露的安全问题,也可能已经停止维护。我在报告里会列一张“SDK名称、用途、版本、风险说明、建议”的表,产品经理看得懂,开发也知道该升哪个。
4. 动态测试:让应用跑起来,看它到底做了什么
静态分析解决“可能有问题”,动态测试解决“确实有问题”。两者的关系有点像读代码和跑测试用例:读代码能发现可疑逻辑,但只有跑起来才知道它到底走不走这条路径、参数长什么样。
4.1 流量抓取:从配置到“抓不到”的预判
抓包是动态测试的基础,配置流程大致是:测试机上启动抓包工具并监听端口,把测试设备的网络指向测试机,在设备上安装并信任抓包工具的自签证书,然后打开应用观察请求是否出现在工具里。听起来简单,实际做的时候十有八九第一次是抓不到的。
能不能抓到,取决于证书被信任的层级。把证书装在用户证书区,很多应用默认不信任;装在系统证书区,成功率会高很多,这也是我建议用能拿到高权限的老系统设备的原因。另一个常见现象是应用只在启动时建立了一条长连接,后面的业务数据都走这条通道,你在工具里只能看到一条握手记录——这种情况要换个思路,从接口文档或者客户端代码里找线索,而不是死等流量。
关于传输层校验,我想强调一个务实的做法:遇到证书绑定(应用只信任自己指定的证书)时,与其花大量时间研究怎么绕过,不如直接向开发要一个关闭校验的调试版本,或者让开发在测试环境用一套专门的测试证书。这不叫偷懒,这叫成本最优——绕过手段依赖具体版本和具体实现,今天绕过了,下个版本又得重来,而调试包一次沟通就能长期可用。当然,如果评估目标本身就包含“校验强度是否足够”,那就必须在正式包上验证,这时候结论要写成“具备该校验能力,绕过成本较高”,而不是硬啃。
4.2 数据落地检查:文件、数据库、缓存、日志
数据落地是我认为最容易被低估的一环。应用跑起来之后,会在设备上留下一堆东西:配置、缓存、数据库、图片、日志、崩溃记录。这些东西如果在有高权限的设备上能直接被读出来,那么在设备丢失、被植入恶意程序、或者被连接电脑调试的场景下,同样存在风险。
检查清单我一般是这么列的:
- 配置存储:登录令牌、用户标识、手机号是否以明文形式存在配置文件里。
- 数据库:本地数据库有没有加密,查询出来的内容是否包含敏感字段。
- 缓存目录:图片缓存里有没有身份证、银行卡、病历这类敏感图片,且没有做访问控制。
- 日志:日志文件里有没有打印请求体、响应体、令牌、密码。这一条命中率极高,而且危害直观。
- 外部存储:有没有把敏感文件写到公共目录,其他应用可读。
日志这条我要多说两句。开发在调试期打印完整请求响应是很常见的做法,问题在于发布构建时忘了关。我实测过好几次,在日志里能完整看到用户的登录返回,包含令牌和手机号。这种问题的整改成本几乎为零(关掉日志开关、加个构建变体),但收益极大,所以在报告里我永远把它放在前面。
4.3 运行时观测:Hook只是手段,不是目的
运行时观测的典型用法是通过动态插桩框架,在应用运行时拦截指定方法的调用,看入参、出参、返回值。它能解决静态分析解决不了的问题:代码被混淆了、逻辑在原生层、关键字符串是运行时拼出来的。
但我一直提醒团队里的同学:Hook是手段不是目的。做观测之前先问自己“我想验证什么结论”。常见的三个目标是:定位加解密函数,搞清楚哪些字段被加密、用什么算法;确认某个校验逻辑的判定条件,判断服务端是否也做了校验;观察应用对运行环境的检测行为,判断这些检测是否影响正常测试。
注意:运行时观测只在获得授权的测试包上进行,且只在测试设备上做。任何把观测手段用于非授权应用的行为都超出测试范畴,不在讨论范围内。
如果团队对插桩比较敏感,还有一个更轻的替代方案:让开发在测试构建里加日志埋点,把关键方法的入参出参打出来。虽然不如插桩灵活,但零风险、零依赖,很多项目我更愿意用这种方式推进。
4.4 界面与交互层的隐形泄露
这一层最容易被忽略,因为它不涉及代码,纯粹是交互设计带来的信息暴露。我通常会验证这么几个场景:
应用切到后台时,任务列表里的缩略图会不会显示敏感内容。银行类、医疗类应用如果不做处理,后台快照里能直接看到余额或者病历,这个问题的修复成本很低(加一个遮罩页面),但没做就是没做。
截屏和录屏有没有做限制。对于展示身份证、银行卡、二维码的页面,很多应用会禁止截屏,这条要实际验证,不能只看代码里有没有调用。
剪贴板有没有被读取。部分应用会在启动时读取剪贴板做“智能识别”,这在合规上是很敏感的行为,检查方法是复制一段特征字符串,看应用有没有在网络请求里把它带出去。
输入框的自动填充与联想、通知栏的消息预览、深链跳转的参数校验、WebView加载的页面是否允许访问本地文件、页面里的JS桥暴露了哪些能力,这些都是要逐个过的。WebView这块我特别提醒一句:桥接接口如果暴露了过多能力,页面一旦被替换或者被注入内容,风险会直接从“页面展示问题”升级为“客户端能力被调用”。
5. 常见问题与排查速查表
这一节是我这些年攒下来的排查经验,基本覆盖了新手最常卡住的地方。遇到问题先查表,比盲目搜索效率高。
5.1 抓不到包的十种原因与对策
| 现象 | 可能原因 | 排查动作 |
|---|---|---|
| 工具里完全没有请求 | 设备网络未指向测试机 | 检查设备网络配置,先用浏览器访问一个已知页面验证 |
| 只有握手没有业务数据 | 走长连接或自有协议 | 从代码或接口文档确认通道类型,改用客户端侧观测 |
| 部分请求有、部分没有 | 某些通道未走统一出口 | 检查是否有独立的网络库或原生层网络实现 |
| 出现证书错误提示 | 证书未安装或未信任 | 重新安装证书,确认装在系统信任区 |
| 应用内一切正常但工具无记录 | 应用只信任指定证书 | 检查是否存在证书绑定,向开发索要调试包 |
| 请求乱码无法解析 | 应用层自行加密 | 通过运行时观测定位加解密函数 |
| 出现双向认证失败 | 客户端证书校验 | 与开发确认测试环境是否提供测试证书 |
| 启动即退出 | 存在运行环境检测 | 用正式设备与调试包复测,排除环境因素 |
| 模拟器上全都不通 | 模拟器特征被识别 | 换实体机验证 |
| 只有首次请求可见 | 请求被缓存或合并 | 清理应用数据后重放操作 |
这张表里我最想强调的是最后一条“向开发索要调试包”。很多同学把抓不到包当成技术挑战,非要自己解决,结果一天过去了什么都没测出来。实际上沟通成本远低于技术成本,而且拿到的调试包往往还能顺带解决日志、环境检测等一系列问题。
5.2 加固、混淆、自检带来的分析阻力
应用做了加固之后,静态分析基本失效:代码被加密、字符串被混淆、类名变成无意义的短串。这时候我的策略是三步走。
第一步,先判断加固类型和强度。看安装包的体积分布、原生库数量、启动时的行为,大致能判断出用的是哪一类方案。判断的意义在于决定后续投入——有些加固只做了基础混淆,静态搜索关键词仍然有效;有些则做了完整的代码加密,纯静态基本没戏。
第二步,主动索要未加固的测试包。这是最省事的路子,绝大多数团队都愿意为安全测试单独出一个包。我会在测试启动会上就把这个需求提出来,避免后面卡住再来回沟通。
第三步,静态走不通就转运行时。既然字符串是运行时解密的、逻辑是运行时还原的,那就在运行时观测,直接看方法调用的入参出参。这条路慢但有效,适合针对少数几个关键功能做深挖,不适合全量覆盖。
至于运行环境自检(检测是否被调试、是否在模拟器、是否被插桩),我的建议是按“是否影响测试推进”来决定投入。如果不影响,就记录下来作为一条结论;如果影响,优先用调试包解决。不要为了绕过检测而绕过检测,那是手段的自我目的化。
5.3 从问题清单到可交付的报告与闭环
测试做完,报告怎么写决定了这些问题能不能被修掉。我踩过的坑是早期报告写得太“技术”,满篇都是复现细节,结果开发看完只说一句“哦,知道了”,然后没有然后了。后来我改成这样的结构:
- 风险分级:按严重、高、中、低四档,分级依据写清楚(影响的数据敏感度、利用难度、影响范围),不要凭感觉定级。
- 复现路径:写清楚前置条件、操作步骤、预期结果与实际结果,让别人能照着复现。
- 证据材料:截图、请求与响应片段、日志摘录,全部脱敏。
- 修复建议:具体到“改哪个配置、换哪种存储方式、在服务端哪个环节加校验”,不要写“建议加强安全防护”这种空话。
- 复测结论:修复后重新验证同一路径,记录结果。
闭环这块我有两个习惯。一是所有高危问题必须当面过一遍,因为文字沟通容易漏掉上下文;二是复测时把原始用例再跑一遍,确认修复没有引入新的问题——我遇到过为了堵一个越权,把接口的参数校验写死,结果正常业务也报错的案例。
6. 几个踩过的坑和长期有效的做法
6.1 越权比“客户端漏洞”更值得花时间
如果只能给一条建议,我会说:把时间优先投在越权和接口鉴权上。原因很现实——客户端的漏洞经常需要特定条件才能触发,而接口越权一旦存在,就是成规模的数据暴露,严重等级和实际损失都不是一个量级。验证方法也不复杂:用两个不同权限的账号,交叉替换请求里的用户标识、订单号、资源ID,看服务端有没有做归属校验。这件事花不了多少时间,但几乎每次都能查出东西。
我印象很深的一次,某个应用在客户端做了非常严格的页面级权限控制,非本人看不到订单详情,看起来无懈可击。结果把请求里的订单号换成另一个账号的,服务端照样返回了完整数据。这类问题在客户端层面永远测不出来,只能靠接口层的交叉验证。所以我现在做评估,接口层的时间占比从不低于三成。
6.2 把安全检查清单变成团队资产
安全测试最容易出现的问题是“每次都是从零开始”。我的做法是把检查项沉淀成一份清单,按应用类型分成模板,每次评估时直接勾选。清单大概长这样:二十条静态检查项、十五条接口检查项、十二条数据落地检查项、十条交互与合规检查项。新同学拿着清单就能上手,老同学用它保证不漏项。
清单还需要定期更新,更新的来源有三个:每次评估中新发现的问题类型、公开披露的移动端安全问题、以及合规要求的变化。我一般每个季度过一遍,把高频命中的项往前排,把已经全行业都修好的项沉下去。这份清单用久了你会发现,它比任何工具都值钱,因为工具解决的是“能不能扫出来”,清单解决的是“有没有想到去扫”。
最后分享一个我觉得很实用的做法:把安全测试往左移。不要等到发版前才做,而是在需求评审阶段就进场,把“这个接口需不需要鉴权”“这个页面会不会展示敏感信息”“这个SDK采集什么数据”这几个问题在文档阶段问出来。改文档的成本是改代码的十分之一,这个账算下来非常划算。我参与的几次早期介入,最后高风险问题的数量比事后评估少了将近一半,而且没有一次因为安全问题卡住发版节奏。