24、构建系统安全:签名机制、权限控制、安全编译选项
说到构建系统的安全,很多人第一反应是「哦,不就是签个名嘛」。嗯,这话对了一半。签名确实是安全的第一道门,但如果你只盯着签名,那后面还有一堆坑等着你踩。我在AOSP里摸爬滚打这些年,见过太多因为权限配置漏了、编译选项没开,导致系统被攻破的案例。今天咱们就把这块彻底捋清楚。
签名机制:你的APK到底是谁的?
签名说白了就是给APK盖个章。但这个章怎么盖、用什么钥匙盖、盖完能不能换,这里面的门道可不少。
签名类型与演进
Android的签名方案从v1一路走到v4,每个版本解决一个核心问题:
| 签名方案 | 引入版本 | 核心特点 | 我踩过的坑 |
|---|---|---|---|
| v1 (JAR签名) | Android 1.0 | 基于ZIP条目签名,不保护META-INF | 有人把META-INF删了重新打包,签名校验直接绕过 |
| v2 (APK签名) | Android 7.0 | 对整个APK文件进行签名,保护所有字节 | 升级后忘了兼容旧设备,低版本手机装不上 |
| v3 (密钥轮换) | Android 9.0 | 支持签名密钥轮换,保留历史签名 | 轮换时没保留旧密钥,应用升级直接崩了 |
| v4 (增量更新) | Android 11.0 | 支持增量更新时的签名验证 | 配合ADB增量安装时,签名文件没传全 |
核心原则:从Android 11开始,新应用必须同时使用v2和v3签名。v1签名已经被标记为废弃,但为了兼容旧设备,我建议你还是保留。
在构建系统中配置签名
在AOSP里,签名配置主要在Android.bp或Android.mk里搞定。我个人习惯用Android.bp,语法更清爽:
// Android.bp
android_app {
name: "MySecureApp",
// 使用平台签名
certificate: "platform",
// 或者指定自定义签名
// certificate: "my_company",
// 签名配置
sign_config: {
// 指定签名方案
min_sdk_version: "29",
signing_config_version: 2,
// 启用v2和v3签名
enable_v2_signature: true,
enable_v3_signature: true,
},
// 私钥和证书路径
local_certificate: "certs/mykey.x509.pem",
local_private_key: "certs/mykey.pk8",
}
避坑指南:我曾经在项目里直接用debug签名发布预发布版本,结果测试设备死活装不上。后来才发现debug签名的有效期只有365天,过期后所有依赖这个签名的应用都会崩溃。记住:发布版本一定要用release签名,有效期至少25年。
权限控制:谁可以碰我的系统?
权限控制是Android安全模型的基石。说白了就是「谁可以做什么事」。但AOSP里的权限控制比普通应用开发要复杂得多,因为你不仅要管应用权限,还要管系统服务、原生代码的权限。
系统级权限声明
在AOSP里,系统权限是在frameworks/base/core/res/AndroidManifest.xml里声明的。这里有个关键点:权限的protectionLevel决定了谁能申请这个权限。
<!-- 系统级权限示例 -->
<permission
android:name="android.permission.INSTALL_PACKAGES"
android:protectionLevel="signature|privileged"
android:label="@string/permlab_installPackages"
android:description="@string/permdesc_installPackages" />
<!-- 自定义系统权限 -->
<permission
android:name="com.mycompany.permission.ACCESS_SECURE_SERVICE"
android:protectionLevel="signature"
android:label="访问安全服务"
android:description="允许应用访问公司的安全服务接口" />
protectionLevel的取值决定了权限的严格程度:
- normal:普通权限,应用直接申请就行
- dangerous:危险权限,需要用户运行时授权
- signature:只有相同签名的应用才能申请
- privileged:只有系统应用才能申请
- signatureOrSystem:签名相同或系统应用才能申请(已废弃)
注意:千万别把系统级权限设成normal或dangerous。我见过有人把INSTALL_PACKAGES设成normal,结果第三方应用直接静默安装APK,整个系统安全防线瞬间崩塌。
SELinux策略:最后的防线
权限控制只是第一层,SELinux才是真正的硬核防线。在AOSP里,SELinux策略文件放在system/sepolicy目录下。每个系统服务、每个守护进程都有自己的安全上下文。
# 自定义SELinux策略示例
# 文件:system/sepolicy/private/my_daemon.te
# 定义类型
type my_daemon, domain;
type my_daemon_exec, exec_type, file_type;
# 初始化进程
init_daemon_domain(my_daemon)
# 允许访问的权限
allow my_daemon my_data_file:dir rw_dir_perms;
allow my_daemon my_data_file:file create_file_perms;
# 禁止访问系统敏感资源
neverallow my_daemon system_data_file:file write;
嗯,这里要注意:SELinux策略写错了,系统可能直接起不来。我刚开始搞SELinux时,有一次加了一条allow规则,结果编译出来的系统在设备上直接kernel panic。后来查了半天,发现是类型定义冲突了。
安全编译选项:把漏洞扼杀在摇篮里
安全编译选项,说白了就是在编译阶段就堵住常见的安全漏洞。这些选项大部分是编译器(Clang/GCC)提供的,我们只需要在构建系统里把它们打开就行。
关键安全编译选项
| 选项 | 作用 | 在AOSP中的配置 |
|---|---|---|
| -fstack-protector-strong | 检测栈缓冲区溢出 | 默认开启,在build/core/config.mk中 |
| -D_FORTIFY_SOURCE=2 | 运行时缓冲区溢出检测 | 在build/core/combo/select.mk中配置 |
| -Wl,-z,relro | 只读重定位,防止GOT覆写 | 链接器默认选项 |
| -Wl,-z,now | 立即绑定,延迟GOT解析 | 在build/core/definitions.mk中 |
| -fPIE | 生成位置无关可执行文件 | Android 5.0+ 默认开启 |
在Android.bp里,你可以为特定模块配置安全选项:
// 为原生模块配置安全编译选项
cc_binary {
name: "my_secure_daemon",
// 启用栈保护
cflags: [
"-fstack-protector-strong",
"-D_FORTIFY_SOURCE=2",
"-O2",
],
// 链接器选项
ldflags: [
"-Wl,-z,relro",
"-Wl,-z,now",
"-Wl,-z,noexecstack",
],
// 启用ASLR
sanitize: {
address: true,
undefined: true,
},
}
我的经验:安全编译选项不是越多越好。比如-O2优化级别虽然安全,但会增大二进制体积。我曾经在一个存储空间只有128MB的设备上,因为开了太多安全选项,导致系统分区装不下。后来只能权衡取舍,对关键服务开满安全选项,对普通工具适当放宽。
知识体系总览
下面这张图把构建系统安全的三个核心维度串起来了。你想想看,签名机制管的是「你是谁」,权限控制管的是「你能做什么」,安全编译选项管的是「你的代码有没有漏洞」。三者缺一不可。
避坑指南:我曾经在一个定制ROM项目里,只关注了签名机制,觉得权限控制和编译选项是「默认的不用管」。结果上线后,第三方应用通过一个未保护的系统服务接口,直接获取了root权限。从那以后,我每次构建系统都会跑一遍安全审计脚本,检查这三个维度是否都到位了。
构建系统安全不是一锤子买卖。签名要定期轮换,权限要持续审计,编译选项要跟着Android版本升级。嗯,说白了就是「安全是个过程,不是个结果」。你在AOSP里每加一个模块、每改一行配置,都要想想:这个改动会不会打开一个安全缺口?