English version: Mobile App Sideloading Attacks: Trust Chains, Weak Points and Defences on Android and iOS
侧载(sideloading)本身不是漏洞,而是一个被刻意留下的信任例外。本文从"操作系统凭什么决定一段代码能不能装、能不能跑"这个第一性问题出发,分别拆开 Android 和 iOS 的安装信任链,看攻击者如何利用这个例外,最后落到银行类 App 的防护设计。
本文只讲机制与防御,不提供可直接落地的利用步骤。
缩写对照表
| 缩写 | 英文全称 | 中文 |
|---|---|---|
| OS | Operating System | 操作系统 |
| APK | Android Package Kit | Android 安装包 |
| IPA | iOS App Store Package | iOS 安装包 |
| DEX | Dalvik Executable | Android 字节码文件 |
| SDK | Software Development Kit | 软件开发工具包 |
| CA | Certificate Authority | 证书颁发机构 |
| TOFU | Trust On First Use | 首次使用即信任 |
| AMFI | Apple Mobile File Integrity | Apple 移动文件完整性(内核扩展) |
| UDID | Unique Device Identifier | 设备唯一标识 |
| MDM | Mobile Device Management | 移动设备管理 |
| OCSP | Online Certificate Status Protocol | 在线证书状态协议 |
| DMA | Digital Markets Act | 欧盟《数字市场法》 |
| SEP | Secure Enclave Processor | 安全隔区处理器 |
| SSV | Signed System Volume | 签名系统卷 |
| OTP | One-Time Password | 一次性密码 |
| RASP | Runtime Application Self-Protection | 运行时应用自我保护 |
| CVE | Common Vulnerabilities and Exposures | 通用漏洞披露编号 |
1. 第一性问题:OS 凭什么决定一段代码能不能运行
一个移动 OS 面对一个安装包,要回答三个问题:
- 它是谁写的?(身份)
- 它有没有被改过?(完整性)
- 我该不该相信这个作者?(授权 / 信任根)
两个平台都用代码签名回答前两个问题:签名者用私钥对包内容的摘要签名,OS 用公钥验证------改一个字节,签名就失效。分歧出在第三个问题:
- Android :签名证书可以自签 ,OS 不问"作者是不是好人",只保证"同一个包名之后的更新必须来自同一把钥匙"。信任根是用户的安装决定 加上分发渠道的审核(Google Play 等)。
- iOS :签名证书必须由 Apple 颁发 ,内核在每次执行代码时都检查签名链是否通往 Apple。信任根是 Apple 本身。
官方商店 把三个问题打包回答:商店审核过作者和代码,商店保证分发过程不被篡改。侧载就是绕开商店、直接把安装包交给 OS------前两个问题签名依然回答得了,但第三个问题的答案从"商店审核过"退化成了"用户(或某个证书持有者)说可以"。
那为什么还要留口子? 因为完全封死会伤害正当需求:开发调试、企业内部分发、测试分发、开源与替代商店、以及(在欧盟等地)法律要求。所以两家的设计都是"默认关闭 + 有门槛地打开"。攻击者的工作,就是让受害者(或被盗用的证书)替他迈过这道门槛。
#mermaid-svg-O599gCOewgBgrIKQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-O599gCOewgBgrIKQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-O599gCOewgBgrIKQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-O599gCOewgBgrIKQ .error-icon{fill:#552222;}#mermaid-svg-O599gCOewgBgrIKQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-O599gCOewgBgrIKQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-O599gCOewgBgrIKQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-O599gCOewgBgrIKQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-O599gCOewgBgrIKQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-O599gCOewgBgrIKQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-O599gCOewgBgrIKQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-O599gCOewgBgrIKQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-O599gCOewgBgrIKQ .marker.cross{stroke:#333333;}#mermaid-svg-O599gCOewgBgrIKQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-O599gCOewgBgrIKQ p{margin:0;}#mermaid-svg-O599gCOewgBgrIKQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-O599gCOewgBgrIKQ .cluster-label text{fill:#333;}#mermaid-svg-O599gCOewgBgrIKQ .cluster-label span{color:#333;}#mermaid-svg-O599gCOewgBgrIKQ .cluster-label span p{background-color:transparent;}#mermaid-svg-O599gCOewgBgrIKQ .label text,#mermaid-svg-O599gCOewgBgrIKQ span{fill:#333;color:#333;}#mermaid-svg-O599gCOewgBgrIKQ .node rect,#mermaid-svg-O599gCOewgBgrIKQ .node circle,#mermaid-svg-O599gCOewgBgrIKQ .node ellipse,#mermaid-svg-O599gCOewgBgrIKQ .node polygon,#mermaid-svg-O599gCOewgBgrIKQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-O599gCOewgBgrIKQ .rough-node .label text,#mermaid-svg-O599gCOewgBgrIKQ .node .label text,#mermaid-svg-O599gCOewgBgrIKQ .image-shape .label,#mermaid-svg-O599gCOewgBgrIKQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-O599gCOewgBgrIKQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-O599gCOewgBgrIKQ .rough-node .label,#mermaid-svg-O599gCOewgBgrIKQ .node .label,#mermaid-svg-O599gCOewgBgrIKQ .image-shape .label,#mermaid-svg-O599gCOewgBgrIKQ .icon-shape .label{text-align:center;}#mermaid-svg-O599gCOewgBgrIKQ .node.clickable{cursor:pointer;}#mermaid-svg-O599gCOewgBgrIKQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-O599gCOewgBgrIKQ .arrowheadPath{fill:#333333;}#mermaid-svg-O599gCOewgBgrIKQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-O599gCOewgBgrIKQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-O599gCOewgBgrIKQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-O599gCOewgBgrIKQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-O599gCOewgBgrIKQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-O599gCOewgBgrIKQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-O599gCOewgBgrIKQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-O599gCOewgBgrIKQ .cluster text{fill:#333;}#mermaid-svg-O599gCOewgBgrIKQ .cluster span{color:#333;}#mermaid-svg-O599gCOewgBgrIKQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-O599gCOewgBgrIKQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-O599gCOewgBgrIKQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-O599gCOewgBgrIKQ .icon-shape,#mermaid-svg-O599gCOewgBgrIKQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-O599gCOewgBgrIKQ .icon-shape p,#mermaid-svg-O599gCOewgBgrIKQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-O599gCOewgBgrIKQ .icon-shape .label rect,#mermaid-svg-O599gCOewgBgrIKQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-O599gCOewgBgrIKQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-O599gCOewgBgrIKQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-O599gCOewgBgrIKQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
Android:
用户允许该来源
iOS:
签名链通往 Apple
且描述文件有效
否
安装包
签名有效?
(完整性)
拒绝安装 / 运行
信任根认可?
安装
这张图回答:两个平台在安装时分别把"信任"交给了谁?
2. Android 侧载
2.1 签名:身份等于钥匙,而不是证书上的名字
Android 不验证签名证书是否由 CA 签发。一个 App 的身份就是 "包名 + 签名证书":
- 更新:新 APK 的签名证书必须与已安装版本一致(或有合法的密钥轮换证明),否则安装失败。
- 签名级权限 (
protectionLevel="signature")与早期的sharedUserId:只有同一签名者的 App 才能共享。 - 这是一种 TOFU(Trust On First Use,首次使用即信任) 模型:第一次安装时信了谁,之后就只认这把钥匙。
TOFU 的含义很直接:如果用户第一次装的就是攻击者的版本,签名机制会忠实地保护攻击者。
签名方案的演进,本质是在修补"签名到底覆盖了哪些字节":
| 方案 | 引入版本 | 覆盖范围 | 关键点 |
|---|---|---|---|
| v1(JAR 签名) | Android 1.0 | 逐个 ZIP 条目签名 | ZIP 的元数据和条目之外的字节不受保护;校验要解压,慢 |
| v2 | Android 7.0 | 整个文件(在中央目录前插入 APK Signing Block) | 任何字节改动都会失效;安装时无需解压即可校验 |
| v3 | Android 9 | 同 v2 | 增加密钥轮换(proof-of-rotation),可以换签名钥匙而保持身份 |
| v3.1 | Android 13 | 同 v3 | 轮换可以按 SDK 版本定向生效 |
| v4 | Android 11 | 基于 fs-verity 的 Merkle 树,放在单独文件 | 支持增量安装(ADB incremental),需同时有 v2/v3 |
Janus(CVE-2017-13156) 是"签名没覆盖到的字节"最典型的教训:一个文件可以同时是合法的 DEX(从文件头解析)和合法的 APK/ZIP(从文件尾的中央目录解析)。对只有 v1 签名 的 APK,在文件头部前置一段 DEX 不会破坏 v1 签名,但运行时(在 Android 5.0--8.0 的部分路径上)会把它当 DEX 执行。于是攻击者能在不改变签名身份 的前提下替换代码,甚至以"合法更新"的形式覆盖已安装 App。2017 年 12 月的安全补丁修复了它;v2/v3 签名因为覆盖整个文件,从机制上免疫。对开发者的结论:必须启用 v2 及以上签名,并把 minSdk 设得足够高,避免只靠 v1 的旧设备路径。
2.2 安装流程:PackageManager 与"未知来源"开关
APK 最终都要交给系统的 PackageManager / PackageInstaller 安装。区别在于谁来发起:
- Google Play 等预装商店:持有系统级安装权限,可静默安装和更新。
- 普通 App(浏览器、文件管理器、聊天软件):只能通过 PackageInstaller 弹出系统确认界面,由用户点"安装"。
"允许这个来源安装"的门槛经历过一次关键变化:
| 时期 | 机制 | 后果 |
|---|---|---|
| Android 7.1 及以前 | 全局开关 "未知来源"(Unknown sources) | 一旦打开,任何 App 都能发起安装 |
| Android 8.0 起 | 按来源授权:发起方需声明 REQUEST_INSTALL_PACKAGES,且用户在"安装未知应用"(install_unknown_apps)里逐个 App 授权 |
用户给了浏览器权限,不等于给了聊天软件权限 |
| Android 12 起 | 安装器可通过 setRequireUserAction 对自己装的 App 做免确认更新 |
第三方商店的体验接近官方,但只限它自己的 App |
| Android 13 起 | 受限设置(Restricted settings) :从非会话式安装器(典型如浏览器下载后点开)装的 App,默认不能直接被授予无障碍服务、通知监听等高危能力,需用户额外手动解除 | 直接针对"侧载木马 + 滥用无障碍"的套路 |
| Android 14 起 | 拒绝安装 targetSdkVersion < 23 的 App |
封死"用老 targetSdk 绕过运行时权限"的路 |
| Android 15 起 | 受限设置扩大覆盖面(更多安装方式与权限) | 继续收紧 |
"受限设置"值得多说一句:它不是不让你装,而是把"装上"和"拿到危险能力"拆成两道门。侧载银行木马的核心收益来自无障碍服务(读屏、代点击)和通知/短信读取(截 OTP),这两样正是被单独加锁的。
2.3 安全边界在哪里被突破
侧载本身只是"装上"。真正让攻击成立的,是装上之后 App 能拿到多大的能力,以及目标 App 自身有没有漏洞可钻:
| 突破口 | 机制 | 现状 |
|---|---|---|
低 targetSdkVersion |
Android 6.0 之前的 App 在安装时一次性获得全部危险权限;系统对旧 targetSdk 的 App 保留兼容行为 | Android 14 起禁止安装 targetSdk < 23;但兼容行为在更高版本间仍有差异 |
requestLegacyExternalStorage |
Android 10 中可以申请退出分区存储(scoped storage),获得宽泛的外部存储访问 | targetSdk ≥ 30 时被忽略 |
| 签名方案降级 | 只有 v1 签名的 APK 在旧系统上受 Janus 类问题影响 | 已打补丁 + 现代工具默认 v2/v3 |
sharedUserId |
同签名的 App 可共享同一 Linux UID、互访数据;钥匙泄露就等于全家沦陷 | API 29 起废弃 |
| 导出组件 | exported="true" 的 Activity / Service / Receiver / Provider 可被任意 App(包括侧载的恶意 App)调用;缺权限保护时成为跳板 |
Android 12 起,带 intent-filter 的组件必须显式 声明 exported |
| Manifest 错误配置 | debuggable="true"、allowBackup="true"、过宽的 FileProvider 路径、明文流量许可等 |
靠开发者自查与 lint |
最后两行说明一点:侧载的恶意 App 是"同设备上的另一个攻击者进程"。它不需要攻破 OS,只需要找到目标 App 自己开着的门。
2.4 实际威胁模型
| 攻击 | 机制 | 为什么能成 |
|---|---|---|
| 重打包(repackaging) | 反编译正版 APK → 注入恶意代码或替换收款信息 → 用攻击者自己的钥匙重新签名 → 通过钓鱼链接、仿冒网站、第三方市场分发 | TOFU:用户没装过正版,或者先卸载了正版;签名只保证"没被再改过" |
| Janus 类签名覆盖缺口 | 利用签名没覆盖的字节,在保持原签名身份的情况下改代码 | 只依赖 v1 签名的旧 App + 未打补丁的旧系统 |
| 木马化的"加固包"/"破解版"/"去广告版" | 打着"官方加固"或"VIP 破解"旗号的 APK,本身就是重打包产物 | 用户主动寻求侧载 |
| 银行木马(短信/社交工程投递) | 短信或聊天消息诱导下载 APK → 诱导开启无障碍服务 → 覆盖层钓鱼、截取 OTP、远程代操作 | 社交工程迈过"未知来源"与"无障碍"两道门 |
| 滴管(dropper) | 第一阶段 App 看似无害(可能甚至上架商店),运行后再下载并请求安装真正的载荷 | 把危险行为推迟到审核与扫描之后 |
2.5 Play Protect 与"验证应用":作用和局限
Google Play Protect(前身"验证应用",Verify Apps)是 Google 移动服务(GMS)的一部分,在认证设备上运行:
- 安装前:对侧载 APK 做本地 + 云端扫描,命中已知恶意样本就警告或阻止。
- 安装后:周期性扫描,可禁用或移除恶意 App。
- 实时代码级扫描:对从未见过的侧载 App 提取特征送云端分析(Android 14 及以后逐步推出)。
- 增强欺诈防护(自 2024 年起在部分国家/地区试点):从浏览器、聊天软件、文件管理器等"互联网侧载"来源安装、且申请短信、通知监听、无障碍等敏感权限的 App,会被直接拦截。
- 开发者验证:Google 在 2025 年宣布,认证设备上的 App(包括侧载)将逐步要求来自已验证身份的开发者,计划自 2026 年起先在部分国家生效、此后扩展到全球。具体范围与豁免(例如面向高级用户的安装流程)以 Google 当前公告为准。
局限,也就是攻击者的绕过面:
- 签名与样本库是"已知"导向:新的重打包变种、强混淆、按地区或时间延迟激活的载荷,都能在初次扫描时躲过。
- 用户可以关闭它,也可以无视警告:社交工程脚本里常有"关闭 Play Protect"这一步。
- 只存在于带 GMS 的认证设备上:国内大量设备、AOSP 定制系统、无 GMS 的设备上根本没有这一层,由各厂商的安全中心替代,能力参差。
- dropper 模式把恶意行为推迟到扫描之后。
结论:Play Protect 是提高成本的一层,而不是 App 可以依赖的安全边界。
3. iOS 侧载
3.1 官方约束:内核强制的代码签名
iOS 的规则一句话就能说完:任何可执行代码页,签名链都必须通往 Apple,否则内核不让它执行。 机制分几层:
- 安全启动链:Boot ROM → iBoot → 内核,每一级验证下一级的签名。这条链保证运行的是 Apple 的内核,后面的一切强制才可信。
- 签名系统卷(SSV):系统分区以密码学方式封存,篡改即无法启动。
- AMFI + 内核代码签名强制 :每个 Mach-O 文件带有 CodeDirectory(各页的哈希)和签名;页面被换入内存时,内核核对该页的哈希。未签名或签名不符的代码页无法执行------这同时堵死了"运行时下载并执行新代码"(JIT 只对少数被授予特定权限的进程开放)。
- dyld 与库验证:动态加载的库也必须签名,且默认必须与主程序同一 Team ID(或由 Apple 签名),这挡住了往进程里塞第三方 dylib。
- 描述文件(Provisioning Profile)+ 授权(Entitlements):非 App Store 签名的代码要附带 Apple 签发的描述文件,写明 Team ID、允许的设备(或企业范围)、允许的能力、有效期。
App Store 渠道 :开发者上传 → 审核 → Apple 用自己的证书重新签名,并加上 FairPlay DRM 加密 → 设备只认 Apple 的签名。
关于 Secure Enclave(SEP) :它负责的是密钥保护(Touch ID / Face ID、数据保护密钥、App Attest 密钥),并不参与 App 代码签名的校验 。代码签名的信任根是 Boot ROM 中烧录的 Apple 根公钥和由它启动的内核。SEP 在侧载语境里真正重要的作用,是第 5 节讲的 App Attest------它能证明"发起请求的是 Apple 签过名的、属于你的 App ID 的那个 App"。
3.2 合法的例外口子
| 例外 | 证书 / 描述文件 | 设备范围 | 门槛 |
|---|---|---|---|
| 开发(Development) | 开发证书 + 列出 UDID 的描述文件 | 注册过的设备 | 付费账号;免费 Apple ID 也可以,但签出的 App 7 天过期,数量有限 |
| Ad Hoc | 发布证书 + 列出 UDID 的描述文件 | 每个账号每年有限数量的注册设备 | 付费开发者账号 |
| 企业内部分发(In-House / Enterprise) | 企业证书 + 不限设备的描述文件 | 任何设备 | Apple Developer Enterprise Program,只允许发给本企业员工;用户需在"设置 → 通用 → VPN 与设备管理"中手动信任该开发者 |
| TestFlight | Apple 签名 | 受邀测试者 | 外部测试需经 Beta 审核;构建有有效期 |
| 欧盟替代分发(DMA) | Apple 签名 + 公证(Notarization) | 欧盟地区账户与设备 | 见 3.4 |
这些口子共同的前提是:最终签名仍然通往 Apple,而 Apple 能随时吊销。
3.3 攻击路径与实例
① 滥用企业证书。 企业证书是唯一"不限设备、不需审核"的口子,因此最常被滥用:赌博、色情、盗版、仿冒金融 App 都曾借用(或购买、盗用)企业证书分发。用户被引导去"信任"一个陌生企业开发者------这一步就是社交工程迈过信任门槛。Apple 的反制是吊销证书:设备在联网启动 App 时会通过 OCSP 检查证书状态,吊销后 App 将无法启动。实际效果取决于设备是否联网及本地缓存的状态,所以吊销生效会有时间差,不是瞬间全网失效。
② "超级签名"(滥用 Ad Hoc / 开发证书)。 分发方用(往往是批量注册或买来的)个人开发者账号,通过安装描述文件收集用户设备的 UDID,把设备加进账号的设备列表,再为这一台设备签一个 Ad Hoc 包。因为不是企业证书,不会因企业证书吊销而批量失效;代价是每个账号的设备名额有限,所以是"按设备付费"的灰色生意。这同样违反 Apple 的开发者协议,账号会被封禁。
③ 免费账号自签与第三方"商店"(AltStore / SideStore 一类)。 它们利用免费 Apple ID 的开发签名能力,在本地或配套程序里为用户重新签名 IPA,并在 7 天到期前自动续签。机制上合法,但它把"签什么"完全交给了用户:用户拿到的 IPA 是否被改过,没有任何第三方审核。(在欧盟,AltStore PAL 以经过公证的替代市场形态存在,属于另一条合规路径。)
④ 重签名的克隆 App(iOS 上的重打包)。 流程是:拿到已解密 的 IPA(App Store 包有 FairPlay 加密,需要先在越狱设备上脱壳)→ 修改二进制或注入一个 dylib (修改 Mach-O 的加载命令)→ 用攻击者自己的证书(企业、Ad Hoc 或免费账号)整体重签名。注入的 dylib 与主程序一起被重签名,所以同 Team ID 的库验证照样通过。结果是一个签名完全合法、但 Team ID 已经不是你的"你的 App"。 这是"非越狱环境下的 Tweak"的本质,也是第 5 节服务端校验 Team ID 的原因。
⑤ 利用签名校验本身的漏洞。 TrollStore 是代表:它利用 CoreTrust 的签名验证漏洞(CVE-2022-26766、CVE-2023-41991),让任意 App 以伪造的签名永久安装 、不过期,并可携带任意 entitlements。这类漏洞修补后只在旧版本 iOS 上可用,但它说明了一点:签名强制的强度,等于签名校验代码的正确性。
⑥ 地域与资格绕过(DMA 语境)。 欧盟替代分发的资格由账户地区和设备所在地共同决定,离开欧盟有宽限期。资格判断依赖的地区信号可以被伪造或"借用",从而把本应只在欧盟出现的分发渠道带到别处。
3.4 欧盟 DMA 侧载:开了门,但留着 Apple 这道闸
iOS 17.4(2024 年 3 月)起,欧盟用户可以从替代应用市场安装 App,iOS 17.5 起还可以从开发者网站直接下载。关键约束:
- 公证(Notarization) :所有在欧盟以替代方式分发的 iOS App,仍需提交给 Apple 做自动化 + 人工的基线审查(恶意软件、诈骗、基本功能),通过后由 Apple 签名。不审内容合规,但审"是不是恶意的、是不是在冒充别人"。
- 市场开发者授权:开商店的主体需要获得 Apple 授权,并满足财务与合规条件。
- 系统安装界面:安装时由系统显示 App 名称、开发者、截图、年龄分级等信息,而不是由商店自绘------防止商店伪造这些信息。
- 吊销:公证后发现是恶意软件,Apple 仍可吊销,已安装的 App 将无法启动。
所以 DMA 并没有改变 iOS 的信任根------签名仍然通往 Apple,只是把"审核"从全面的 App Review 缩小成了安全基线的公证。(日本依据《移动软件竞争法》要求的类似开放,Apple 也已公布相应方案;细节以 Apple 当前公告为准。)
3.5 越狱:彻底拆掉代码签名强制
越狱的本质,是利用内核或更底层的漏洞,获得修改内核行为的能力,然后把上面 3.1 的几层强制挨个关掉或旁路:
- 修补 AMFI / 代码签名检查,或把自签的二进制哈希注入内核信任缓存(trust cache),让内核把它们当作"Apple 认可"。
- 打开动态注入:通过 Substrate / ElleKit 一类框架,让 Tweak 的 dylib 被注入到任意进程(包括你的银行 App),在运行时挂钩(hook)任何函数。
- 开放文件系统与包管理器:Cydia、Sileo 这类包管理器从任意源安装软件。
- "无根"越狱(rootless):iOS 15 以后由于 SSV,越狱不再修改系统卷,改为把文件放在独立目录中------但内核层面的签名强制同样被绕过。
越狱之后,"侧载"已经不是问题:任何代码都能运行,任何 App 的内存都能被读写和挂钩。 这是为什么越狱检测在金融 App 里是标配,也是为什么它永远是"猫鼠游戏"------检测代码自己也运行在一个已被攻破的内核上。
4. 两个平台的威胁模型对比
#mermaid-svg-2LFTXWvv4bdbAdFR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2LFTXWvv4bdbAdFR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2LFTXWvv4bdbAdFR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2LFTXWvv4bdbAdFR .error-icon{fill:#552222;}#mermaid-svg-2LFTXWvv4bdbAdFR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2LFTXWvv4bdbAdFR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2LFTXWvv4bdbAdFR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2LFTXWvv4bdbAdFR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2LFTXWvv4bdbAdFR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2LFTXWvv4bdbAdFR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2LFTXWvv4bdbAdFR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2LFTXWvv4bdbAdFR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2LFTXWvv4bdbAdFR .marker.cross{stroke:#333333;}#mermaid-svg-2LFTXWvv4bdbAdFR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2LFTXWvv4bdbAdFR p{margin:0;}#mermaid-svg-2LFTXWvv4bdbAdFR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2LFTXWvv4bdbAdFR .cluster-label text{fill:#333;}#mermaid-svg-2LFTXWvv4bdbAdFR .cluster-label span{color:#333;}#mermaid-svg-2LFTXWvv4bdbAdFR .cluster-label span p{background-color:transparent;}#mermaid-svg-2LFTXWvv4bdbAdFR .label text,#mermaid-svg-2LFTXWvv4bdbAdFR span{fill:#333;color:#333;}#mermaid-svg-2LFTXWvv4bdbAdFR .node rect,#mermaid-svg-2LFTXWvv4bdbAdFR .node circle,#mermaid-svg-2LFTXWvv4bdbAdFR .node ellipse,#mermaid-svg-2LFTXWvv4bdbAdFR .node polygon,#mermaid-svg-2LFTXWvv4bdbAdFR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2LFTXWvv4bdbAdFR .rough-node .label text,#mermaid-svg-2LFTXWvv4bdbAdFR .node .label text,#mermaid-svg-2LFTXWvv4bdbAdFR .image-shape .label,#mermaid-svg-2LFTXWvv4bdbAdFR .icon-shape .label{text-anchor:middle;}#mermaid-svg-2LFTXWvv4bdbAdFR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2LFTXWvv4bdbAdFR .rough-node .label,#mermaid-svg-2LFTXWvv4bdbAdFR .node .label,#mermaid-svg-2LFTXWvv4bdbAdFR .image-shape .label,#mermaid-svg-2LFTXWvv4bdbAdFR .icon-shape .label{text-align:center;}#mermaid-svg-2LFTXWvv4bdbAdFR .node.clickable{cursor:pointer;}#mermaid-svg-2LFTXWvv4bdbAdFR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2LFTXWvv4bdbAdFR .arrowheadPath{fill:#333333;}#mermaid-svg-2LFTXWvv4bdbAdFR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2LFTXWvv4bdbAdFR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2LFTXWvv4bdbAdFR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2LFTXWvv4bdbAdFR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2LFTXWvv4bdbAdFR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2LFTXWvv4bdbAdFR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2LFTXWvv4bdbAdFR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2LFTXWvv4bdbAdFR .cluster text{fill:#333;}#mermaid-svg-2LFTXWvv4bdbAdFR .cluster span{color:#333;}#mermaid-svg-2LFTXWvv4bdbAdFR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2LFTXWvv4bdbAdFR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2LFTXWvv4bdbAdFR rect.text{fill:none;stroke-width:0;}#mermaid-svg-2LFTXWvv4bdbAdFR .icon-shape,#mermaid-svg-2LFTXWvv4bdbAdFR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2LFTXWvv4bdbAdFR .icon-shape p,#mermaid-svg-2LFTXWvv4bdbAdFR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2LFTXWvv4bdbAdFR .icon-shape .label rect,#mermaid-svg-2LFTXWvv4bdbAdFR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2LFTXWvv4bdbAdFR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2LFTXWvv4bdbAdFR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2LFTXWvv4bdbAdFR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} iOS
签名必须通往 Apple
(内核强制)
描述文件限定
设备 / 企业 / 期限
用户手动信任
企业开发者
Apple 可吊销
(OCSP / 公证)
Android
用户授权来源
(install_unknown_apps)
签名可自签
TOFU 身份
Play Protect 扫描
(仅 GMS 设备)
受限设置拦截
无障碍 / 通知监听
这张图回答:一个侧载 App 在两个平台上分别要过哪几道门?
| 维度 | Android | iOS |
|---|---|---|
| 典型攻击者 | 银行木马团伙、诈骗团伙、重打包刷量者 | 企业证书 / 超级签名分发商、克隆 App 制作者、越狱 Tweak 作者 |
| 入口 | 钓鱼短信 / 聊天链接下载 APK;第三方市场;"破解版" | 诱导信任企业开发者;超级签名网站;越狱源;自签工具 |
| OS 防御边界 | 应用沙箱 + 权限模型 + 按来源授权 + 受限设置 | 内核代码签名强制 + 沙箱 + 库验证 + 描述文件 |
| 签名的信任根 | 开发者自己(自签证书,TOFU) | Apple(所有签名必须通往 Apple 根) |
| 集中撤销能力 | 弱:Play Protect 可远程禁用已知恶意 App,但无法"吊销签名" | 强:吊销证书或公证,已装 App 无法启动 |
| 侧载默认难度 | 低:几次点击即可 | 高:需要开发账号、企业证书或越狱 |
| 最大单点风险 | 用户被说服授予无障碍服务 | 企业证书外泄或被滥用;越狱 |
| 用户能否自防 | 能,但依赖警惕性:不开未知来源、不授无障碍、保持 Play Protect 开启 | 较能:不信任陌生企业开发者、不越狱、不装来路不明的描述文件 |
一句话:Android 的身份由开发者自证,安全靠"用户决定 + 扫描兜底";iOS 的身份由 Apple 背书,安全靠"Apple 可以随时收回背书"。 前者被攻破在用户那一步,后者被攻破在证书管理或内核漏洞那一步。
5. 给银行 App 开发者的防护
设计原则只有一条:客户端的任何检查都运行在攻击者可能完全控制的环境里,只能当减速带;真正的判定要放在服务端,基于平台签发、客户端无法伪造的证明。
5.1 服务端可验证的完整性证明(主防线)
| 平台 | 机制 | 能证明什么 |
|---|---|---|
| Android | Play Integrity API:服务端解密并校验 Google 签发的 verdict | appIntegrity:是否为 Play 认可的、你签名的正版二进制(重打包版本会显示未识别);deviceIntegrity:设备是否通过完整性检查(root、改机、非认证系统通常不过);accountDetails:授权状态;appAccessRiskVerdict:设备上是否有可能截屏、录屏或控制界面的 App 在运行 |
| Android | Key Attestation:在 Android Keystore 硬件中生成密钥,由硬件出具证书链 | 密钥确实在可信硬件中;证书扩展里带有调用方 App 的包名与签名摘要、设备启动状态 |
| iOS | App Attest(DeviceCheck 框架):在 SEP 中生成密钥,由 Apple 出具证明 | 请求来自你的 Team ID + Bundle ID 的正版 App,运行在真实 Apple 设备上;重签名的克隆 App Team ID 不同,拿不到有效证明 |
| iOS | DeviceCheck | 每台设备的少量持久状态位,可用于标记滥用设备 |
做法:登录、绑卡、转账等高风险接口要求附带新鲜的完整性证明(服务端下发一次性 nonce,防重放),服务端校验失败就降级或拒绝。
5.2 客户端检查(纵深防御,不可单独依赖)
- 签名自检:Android 在运行时读取自身签名证书摘要,与内置的期望值比对;iOS 检查描述文件与 Team ID。容易被 hook 掉,但能挡住低成本的重打包。
- 安装来源检查(Android) :通过
PackageManager.getInstallSourceInfo()判断安装者是否为预期的商店;侧载来源可以触发风险评分上调,而不是直接拒绝(企业 MDM 分发等是正当来源)。 - Root / 越狱 / 模拟器检测 :文件与路径特征、可疑进程与端口、
su可执行文件、越狱残留、系统属性等。结论应上报服务端参与风控,而不只是本地弹窗。 - Hook 与调试检测:检测 Frida、Substrate 等注入框架的特征,以及调试器附加。
- 无障碍与覆盖层风险(Android) :检查已启用的无障碍服务,对敏感页面设置
FLAG_SECURE防截屏,使用setFilterTouchesWhenObscured防覆盖层点击劫持。 - 代码混淆与加固 :提高逆向和重打包的成本(R8/ProGuard、字符串加密、native 层关键逻辑)。商业 RASP 方案集成了以上多项,见 RASP 提纲。
- 证书固定 :防止重打包或越狱环境中的流量被中间人改写,见 Certificate pinning 与 Mobile App Certificate Pinning。
5.3 构建与发布侧
- Android 启用 v2 + v3 签名 ,
minSdk不低于团队能支持的最高水平;签名私钥放入 HSM 或使用 Play App Signing,私钥泄露 = 身份被冒充。 - Manifest 审查:组件
exported显式且最小化,敏感组件加签名级权限;关闭debuggable、审慎设置allowBackup。 - iOS 只走 App Store(或受控的 MDM 企业分发);不要用企业证书对公众分发------这既违规,也会让你的证书和你的用户一起暴露在吊销风险里。
- 在官网和 App 内明确告知"唯一官方下载渠道",并监测仿冒 App 与仿冒下载站。
5.4 风险响应与合规
#mermaid-svg-Q20P9iR8zbhuf9uf{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Q20P9iR8zbhuf9uf .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Q20P9iR8zbhuf9uf .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Q20P9iR8zbhuf9uf .error-icon{fill:#552222;}#mermaid-svg-Q20P9iR8zbhuf9uf .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Q20P9iR8zbhuf9uf .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Q20P9iR8zbhuf9uf .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Q20P9iR8zbhuf9uf .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Q20P9iR8zbhuf9uf .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Q20P9iR8zbhuf9uf .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Q20P9iR8zbhuf9uf .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Q20P9iR8zbhuf9uf .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Q20P9iR8zbhuf9uf .marker.cross{stroke:#333333;}#mermaid-svg-Q20P9iR8zbhuf9uf svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Q20P9iR8zbhuf9uf p{margin:0;}#mermaid-svg-Q20P9iR8zbhuf9uf .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Q20P9iR8zbhuf9uf .cluster-label text{fill:#333;}#mermaid-svg-Q20P9iR8zbhuf9uf .cluster-label span{color:#333;}#mermaid-svg-Q20P9iR8zbhuf9uf .cluster-label span p{background-color:transparent;}#mermaid-svg-Q20P9iR8zbhuf9uf .label text,#mermaid-svg-Q20P9iR8zbhuf9uf span{fill:#333;color:#333;}#mermaid-svg-Q20P9iR8zbhuf9uf .node rect,#mermaid-svg-Q20P9iR8zbhuf9uf .node circle,#mermaid-svg-Q20P9iR8zbhuf9uf .node ellipse,#mermaid-svg-Q20P9iR8zbhuf9uf .node polygon,#mermaid-svg-Q20P9iR8zbhuf9uf .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Q20P9iR8zbhuf9uf .rough-node .label text,#mermaid-svg-Q20P9iR8zbhuf9uf .node .label text,#mermaid-svg-Q20P9iR8zbhuf9uf .image-shape .label,#mermaid-svg-Q20P9iR8zbhuf9uf .icon-shape .label{text-anchor:middle;}#mermaid-svg-Q20P9iR8zbhuf9uf .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Q20P9iR8zbhuf9uf .rough-node .label,#mermaid-svg-Q20P9iR8zbhuf9uf .node .label,#mermaid-svg-Q20P9iR8zbhuf9uf .image-shape .label,#mermaid-svg-Q20P9iR8zbhuf9uf .icon-shape .label{text-align:center;}#mermaid-svg-Q20P9iR8zbhuf9uf .node.clickable{cursor:pointer;}#mermaid-svg-Q20P9iR8zbhuf9uf .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Q20P9iR8zbhuf9uf .arrowheadPath{fill:#333333;}#mermaid-svg-Q20P9iR8zbhuf9uf .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Q20P9iR8zbhuf9uf .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Q20P9iR8zbhuf9uf .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Q20P9iR8zbhuf9uf .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Q20P9iR8zbhuf9uf .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Q20P9iR8zbhuf9uf .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Q20P9iR8zbhuf9uf .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Q20P9iR8zbhuf9uf .cluster text{fill:#333;}#mermaid-svg-Q20P9iR8zbhuf9uf .cluster span{color:#333;}#mermaid-svg-Q20P9iR8zbhuf9uf div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Q20P9iR8zbhuf9uf .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Q20P9iR8zbhuf9uf rect.text{fill:none;stroke-width:0;}#mermaid-svg-Q20P9iR8zbhuf9uf .icon-shape,#mermaid-svg-Q20P9iR8zbhuf9uf .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Q20P9iR8zbhuf9uf .icon-shape p,#mermaid-svg-Q20P9iR8zbhuf9uf .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Q20P9iR8zbhuf9uf .icon-shape .label rect,#mermaid-svg-Q20P9iR8zbhuf9uf .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Q20P9iR8zbhuf9uf .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Q20P9iR8zbhuf9uf .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Q20P9iR8zbhuf9uf :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 通过
应用不完整
(重打包)
设备不可信
(root / 越狱)
存在控制类风险
(无障碍 / 录屏)
App 启动 / 高风险操作
服务端校验
完整性证明
正常服务
拒绝服务
引导官方渠道重装
降权:
只读查询,
禁止转账与改密
提示风险 +
加强验证
这张图回答:服务端拿到完整性结论后,应如何分级处置?
- 分级而不是一刀切:重打包 App 应直接拒绝;root/越狱设备通常降权(例如允许查询、禁止转账);存在可疑无障碍服务时提示并加强验证。
- 合规:多地金融监管对移动银行有"检测设备是否被 root/越狱、检测 App 是否被篡改、并据此提示或限制交易"一类要求。具体条款以所在辖区的监管文件为准,安全设计评审时应逐条对照。
- 可观测性:把完整性结论、安装来源、root/越狱信号作为风控特征入库,用于发现成批出现的重打包版本或仿冒分发。
6. 结论
- 侧载不是漏洞,而是信任例外:签名保证"是谁、有没有被改",侧载改变的是"凭什么信"。
- Android 的身份由开发者自签、首次安装即信任;攻击的主战场是社交工程------让用户允许来源、授予无障碍。Android 8 的按来源授权、Android 13 的受限设置、Play Protect 的增强欺诈防护和开发者验证,都是在给这两道门加锁。
- iOS 的每一页代码都必须通往 Apple 的签名;攻击要么借用 Apple 签发的身份(企业证书、超级签名、免费账号、重签名克隆),要么攻破签名校验或内核本身(TrollStore 类漏洞、越狱)。Apple 的核心反制是吊销,DMA 也没有改变这个信任根。
- 对银行 App,客户端检查只是减速带。 主防线是服务端校验平台签发的完整性证明------Android 的 Play Integrity / Key Attestation,iOS 的 App Attest------并据此做分级的风险处置。
参考资料
- Android Developers:APK Signature Scheme v2 / v3 / v3.1 / v4 ;Install unknown apps ;Restricted settings ;Play Integrity API ;Key and ID attestation。
- Android 安全公告,2017 年 12 月(CVE-2017-13156,Janus);GuardSquare, "New Android vulnerability allows attackers to modify apps without affecting their signatures", 2017。
- Google Security Blog:Play Protect 实时代码级扫描、增强欺诈防护试点;Android Developers Blog:开发者验证要求(2025)。
- Apple Platform Security Guide:Secure boot、Signed System Volume、App code signing process in iOS、Secure Enclave。
- Apple Developer:Establishing your app's integrity (App Attest);DeviceCheck ;Distributing proprietary in-house apps;Apple Developer Enterprise Program License Agreement。
- Apple Developer:Getting started with alternative app distribution in the European Union ;Notarization for iOS apps。
- CVE-2022-26766、CVE-2023-41991(CoreTrust 签名校验漏洞)。
- 同目录相关:Certificate pinning · Mobile App Certificate Pinning: Underlying Principle and a Swift Example · Mobile API Token Security · Runtime application self-protection。