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.bpAndroid.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的设备上,因为开了太多安全选项,导致系统分区装不下。后来只能权衡取舍,对关键服务开满安全选项,对普通工具适当放宽。

知识体系总览

下面这张图把构建系统安全的三个核心维度串起来了。你想想看,签名机制管的是「你是谁」,权限控制管的是「你能做什么」,安全编译选项管的是「你的代码有没有漏洞」。三者缺一不可。

构建系统安全知识体系 签名机制 v1/v2/v3/v4签名 密钥管理与轮换 APK完整性校验 平台签名 vs 应用签名 签名过期处理 多签名共存 权限控制 protectionLevel 系统权限声明 SELinux策略 权限分离原则 最小权限原则 运行时权限检查 安全编译选项 栈保护 (-fstack-protector) FORTIFY_SOURCE RELRO/GOT保护 PIE/ASLR Sanitizer (ASan/UBSan) 编译优化与安全权衡 三者协同:签名确认身份 → 权限控制行为 → 编译选项防漏洞

避坑指南:我曾经在一个定制ROM项目里,只关注了签名机制,觉得权限控制和编译选项是「默认的不用管」。结果上线后,第三方应用通过一个未保护的系统服务接口,直接获取了root权限。从那以后,我每次构建系统都会跑一遍安全审计脚本,检查这三个维度是否都到位了。

构建系统安全不是一锤子买卖。签名要定期轮换,权限要持续审计,编译选项要跟着Android版本升级。嗯,说白了就是「安全是个过程,不是个结果」。你在AOSP里每加一个模块、每改一行配置,都要想想:这个改动会不会打开一个安全缺口?

公众号:蓝海资料掘金营,微信deep3321