Android Security | User-Group-Others 权限模型

写在前面

了解本节内容,你会收获:

  1. 系统是如何实现 App 进程之间的数据隔离,换句话说就是为什么 App 进程的工作目录(/data/data/package-name/) 只有本进程能访问,其他进程不能访问
  2. 为什么 普通进程没有权限访问 system 或者 vendor 目录下的文件

UGO (User、Group and Others) 背景

UGO 机制起源于 Linux 的多用户的环境需求,每个文件(客体)只能被特定的一个用户或者一组用户所访问。所以必须对文件的访问权限进行管理。 因此,Linux 对每个文件都打了一个标签,这个标签就是 UID (User ID) 和 GID (Group ID),然后每个用户也有 UID 和 GID,所以系统就可以基于文件的 UID/GID 标识和用户本身的 UID/GID 标识制定一个匹配的规则,来确保不同的用户对不同的文件具有不同的访问权限(权限包括:读、写、执行),而这个规则就是 UGO (User、Group and Others) 权限模型。基于以上的分析,我们可以抽象出基于 UGO 权限和 UID/GID 组成的安全控制策略的顶层模型

Linux UGO 权限模型

根据上图描述的安全控制策略的顶层模型,用一句话来描述就是:主体对于客体的访问规则 ,这个模型由 主体客体规则 三部分组成。

UGO 权限模型的主体(Subject)

从 UGO 的背景来看,UGO 权限模型的主体是用户,但是用户并不能直接访问文件,用户必须通过它的载体-进程才能访问文件,用户的一系列访问行为,最终都演变成这个用户环境下一系列进程的行为,所以用户的 UID/GID 最终标识的就是进程的 UID/GID。所以,在 UGO 权限模型中的主体是进程

当我们使用 chawan 这个用户登录 Linux 系统,然后 run 一个 top 进程,我们通过命令 *ps -eo pid, user, group, comm* 来查看这个 top 进程的 UID/GID 标识

我们可以看到对于 top 这个进程的 UID 是 chawan, GID 是 users

UGO 权限模型的客体(Object)

在 Linux 系统中,UGO 的客体是什么,是文件,文件也需要带有不同的 UID 和 GID 的标识来区分或者隔离不同的主体的访问限制

可以看到,.ssh 文件的 UID 是 chawan,GID 是 user

UGO 权限模型的规则(Access Rule)

主体和客体的 UID 和 GID 有了,接下来分析 UGO 权限模型基于 UID/GID 的访问规则,举个例子

同样以 .ssh 文件夹为例子:

  • 文件的 UID 是 chawan
  • 文件的 GID 是 users
  • d 则标识这是一个文件夹
  • 对于所有者的权限是: rwx, 可读可写可执行
  • 对于组用户的权限是:---, 不可读不可写不可执行
  • 对于其它用户的权限是: ---, 不可读不可写不可执行

所以,只有主体(进程)带有 chawan 这个 UID 这个标识的主体(进程)才能对这 .ssh 这个文件具有读写权限,其它用户 run 的进程就没有读写权限

Android UGO 权限模型

Android UGO 和 Linux GUO 的异同

了解了 Linux 基本的 UGO 机制之后,我们接下来思考一个问题 Android 系统同样也是基于 Linux kernel,所以同样可以把 UGO 机制移植到 Android 系统上。但是在 Linux 中,进程的 UID/GID 是通过登录的用户身份来赋予每个运行的进程相对应的 UID/GID 标识。 Android 虽然也支持多用户,但是与 Linux 的多用户还是有区别的,Android 系统并没有基于多用户给 App进程设置 UIG/GID, 所以把 UGO 机制移植到 Android 系统上最主要的问题就是要解决 App 进程 UID/GID 赋值的问题

那么既然 Linux 是通过当前登录的用户来给予主体(进程)相对应的 UID 和 GID,那么 Android 系统也可以采取相似的策略来给予主体(进程) 相对应的 UID 和 GID。也确实是如此,Android 系统中运行的 App 进程的 UID 和 GID 都是由 PackageManageService 在安装时分配的,相比于 Linux 系统,其实也是"换汤不换药",最终都还是变成了主体(进程)对客体(文件)的访问规则。

App 进程 UID 和 GID 的分配

上面我们提到 Android 的 UID/GID 的分配是 PackageManageService 在安装时,我们从代码角度来看一下,这里我们不谈整个 App 的安装过程,只关注在安装过程中 UID/GID 的分配(以 Android 15 为例子,不同的 Android 版本代码可能不一样),

java 复制代码
    //frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
    void installPackagesTraced(List<InstallRequest> requests) {
        mInstallPackageHelper.installPackagesTraced(requests);
    }
    
    //frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
    void installPackagesTraced(List<InstallRequest> requests) {
        ...
        if (prepareInstallPackages(requests)
                    && scanInstallPackages(requests, createdAppId, versionInfos)) {}
        ...            
    }
    
    // frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
     private boolean scanInstallPackages(List<InstallRequest> requests,
            Map<String, Boolean> createdAppId, Map<String, Settings.VersionInfo> versionInfos) {
       
        for (InstallRequest request : requests) {
            final ParsedPackage packageToScan = request.getParsedPackage();
            final String packageName = packageToScan.getPackageName();
                ...
                if (isApex || (isSdkLibrary && disallowSdkLibsToBeApps())) {
                    request.getScannedPackageSetting().setAppId(Process.INVALID_UID);
                } else {
                    createdAppId.put(packageName, optimisticallyRegisterAppId(request));
                }
                ...
        }
        return true;
    }
    
   // frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
   private boolean optimisticallyRegisterAppId(@NonNull InstallRequest installRequest) {
           ...
           PMS 持有 Settings 这个变量,里面存储所有的 App 的安装信息
           mPm.mSettings.registerAppIdLPw(installRequest.getScannedPackageSetting(),
                        installRequest.needsNewAppId());
            
        }
  
    }
    
    //frameworks/base/services/core/java/com/android/server/pm/Settings.java
    boolean registerAppIdLPw(PackageSetting p, boolean forceNew) throws PackageManagerException {
        if (p.getAppId() == 0 || forceNew) {
            // Assign new user ID
            p.setAppId(mAppIds.acquireAndRegisterNewAppId(p));
        } else {
            // Add new setting to list of user IDs
            createdNew = mAppIds.registerExistingAppId(p.getAppId(), p, p.getPackageName());
        }
    }
    
    // frameworks/base/services/core/java/com/android/server/pm/AppIdSettingMap.java
    public int acquireAndRegisterNewAppId(SettingBase obj) {
        final int size = mNonSystemSettings.size();
        // 考虑到有 app 被 uninstall 的情况,复用之前的 UID
        for (int i = mFirstAvailableAppId - Process.FIRST_APPLICATION_UID; i < size; i++) {
            if (mNonSystemSettings.get(i) == null) {
                mNonSystemSettings.set(i, obj);
                return Process.FIRST_APPLICATION_UID + i;
            }
        }
        mNonSystemSettings.add(obj);
        // 如果不能复用,则UID 在已安装的 app 的UID 上递增
        return Process.FIRST_APPLICATION_UID + size;
    }
    

App 在安装过程中的分配,是在 Process.FIRST_APPLICATION_UID = 10000 的基础上递增,并且保存到 Settings 中,这也就解释了普通应用的 UID 都大于 10000 了

Runtime UID/GID 设置

PMS 在安装的时候为每个 App 进程分配了 UID 和 GID,真正设置 App 进程的 UID 和 GID 是发生 Zygote fork App 进程的阶段。 Android App 进程都是由 Zygote 进程 fork 出来,Zygote 是 App 进程的父进程,如果我们不对 App 进程重新设置 UIG/GID 的话,那么所以 App 都会继承 Zygote 的 UID/GID , 这样会导致进程隔离性失效。因此,Zygote 在 fork 出 App 进程之后,立马重新设置了 App 进程的 UID 和 GID

C++ 复制代码
static jint com_android_internal_os_Zygote_nativeForkSystemServer(
        JNIEnv* env, jclass, uid_t uid, gid_t gid, jintArray gids,
        jint runtime_flags, jobjectArray rlimits, jlong permitted_capabilities,
        jlong effective_capabilities) {

  ...
  // 这里会通过 fork 出一个子进程
  pid_t pid = zygote::ForkCommon(env, true,
                                 fds_to_close,
                                 fds_to_ignore,
                                 true);
  // pid == 0 代表代码进入了子进程
  if (pid == 0) {
      ...
      SpecializeCommon(env, uid, gid, gids, runtime_flags, rlimits, permitted_capabilities,
                       effective_capabilities, 0, MOUNT_EXTERNAL_DEFAULT, nullptr, nullptr, true,
                       false, nullptr, nullptr, /* is_top_app= */ false,
                       /* pkg_data_info_list */ nullptr,
                       /* allowlisted_data_info_list */ nullptr, false, false, false);
  } else if (pid > 0) {
    ...
  }
  return pid;
}

FrokCommon 这个方法中,会调用 fork 系统命令,创建一个新的子进程,接下来新的 子进程会执行 SpecializeCommon 内部的代码,在 SpecializeCommon 内部中会分别调用 setresgidsetresuid 来设置当前子进程的 UID 和 GID

C++ 复制代码
static void SpecializeCommon() { 
    ...
    if (setresgid(gid, gid, gid) == -1) {
        fail_fn(CREATE_ERROR("setresgid(%d) failed: %s", gid, strerror(errno)));
    }
    ...
    if (setresuid(uid, uid, uid) == -1) {
        fail_fn(CREATE_ERROR("setresuid(%d) failed: %s", uid, strerror(errno)));
    }
}

这里注意到,无论是 setgid 还是 setuid 都传入了三个一样的参数。这里为什么要传入三个呢? 实际上这三个参数分别是 RUID(real UID), EUID(effective UID) 和 SUID (Saved UID)

RUID

  1. RUID 是进程的身份标识,它在进程创建时被设定为启动该进程的用户 UID
  2. 操作系统对于访问权限的判断和 RUID 无关,所以 RUID 没有真正的权利
  3. 子进程会继承父进程的 RUID

EUID

  1. 有效用户ID,对文件系统和其他资源的访问都以 EUID 为依据
  2. 通常情况下 EUID = RUID = 父进程的 RUID
  3. 在特殊情况下,EUID 会被临时修改,比如二进制文件设置了 set-UID 位 的时候,如果该进程执这个二进制文件的时候,那么这个进程的 EUID 会被设置为文件的 UID,这样说有点绕,举个例子:

在Linux 中我们可以查看 /usr/bin/passwd 这个可执行文件的 UGO 权限

diff 复制代码
...
-rwxr-xr-x  1 root   root       63952 Dec  9 10:54 ldappasswd
-rwsr-xr-x  1 root   root       59976 Feb  6  2024 passwd
-rwxr-xr-x  1 root   root       39224 Jan  6  2024 smbpasswd
...

可以看到, passwd 相比于其他二进制文件,对于 User 的权限是rws而不是 rwx ,结合上面提到的 EUID 的作用,当一个进程执行这个二进制文件的时候,会 fork 一个子进程,子进程的 RUID 会继承父进程 RUID不会改变,但是这个子进程的 EUID 会被修改为文件的 UID,而这个文件的 UID 是 root,那么意味着这个子进程的 EUID 会临时变成 root。前面提到,所有的权限相关的判断都只跟 EUID 有关系,所以这个子进程就拥有了 root 的权限。

  1. EUID 不会被子进程继承,所以即使父进程的 EUID 为 root,但是 RUID 为普通用户,那么 fork 出来的子进程的 EUID 依旧为普通用户的权限

SUID

  1. 保存用户用于在进程运行过程中暂存原有的 EUID
  2. 用于恢复权限修改之前的 EUID

APP工作目录创建和权限设置

给 App 创建了 UID/GID 之后,系统会给每一个 App 创建一个单独的工作目录,位于 /data/user/user-id/package-name,并且给每个 App 的工作目录都设置了 UID/GID。 APP工作目录创建和权限设置也是发生在 APP 安装时候,由 PMS 发起,但是并不是 PMS 完成,而是借助了installd 这个 deamon 进程来完成。这里同样也是考虑到了权限的问题,因为 PMS 是运行在 system_server 进程的,system_server 这个进程的 UID 和 GID 是 system, 而 installd 的 UID 和 GID 是 root,所以 installd 有 "超能力" 为 APP 创建工作目录并且设置权限。

PMS 安装的代码很长,前面的代码我们不分析了,我们直接看 installd 是如何创建目录并且设置对应的权限

C++ 复制代码
   //frameworks/native/cmds/installd/InstalldNativeService.cpp
   Binder::Status InstalldNativeService::createAppDataLocked(...) {
    ...
    // pkgname 就是 app 的包名,例如:com.android.example
    const char* pkgname = packageName.c_str();
    ...
    // 为工作目录创建读写权限,
    // 0700: rwx------, 只有 Owner 才有读写权限
    // 0751: rwxr-x--x, 只有 Owner 才有读写权限,但是 Group 也有读的权限
    mode_t targetMode = targetSdkVersion >= MIN_RESTRICTED_HOME_SDK_VERSION ? 0700 : 0751;
    //  FLAG_STORAGE_CE 和 FLAG_STORAGE_DE 是加密相关,这里暂时不扩展,默认是使用 CE(Credential Encrypted)
    if (flags & FLAG_STORAGE_CE) {
        // path 就是 app 的工作目录:/data/user/userid/package_name/
        auto path = create_data_user_ce_package_path(uuid_, userId, pkgname);
        // 内部会调用 fs_prepare_dir_strict
        1. 来创建目录
        2. 根据 Apk 的 UID 和 GID,修改文件的 UID 和 GID 属性
        3. 重新设置文件的访问权限为 0700 或 0751
        auto status = createAppDataDirs(path, uid, uid, previousUid, cacheGid, seInfo, targetMode,
                                        projectIdApp, projectIdCache);
        ...
    }
    if (flags & FLAG_STORAGE_DE) {
        ...
    }
    ...
    return ok();
}

createAppDataDirs 这个方法最终会调用 system/core/libcutils/fs.cpp 的 fs_prepare_path_impl

fs.cpp 复制代码
static int fs_prepare_path_impl(const char* path, mode_t mode, uid_t uid, gid_t gid,
        int allow_fixup, int prepare_as_dir) {
    ...
    create_result = prepare_as_dir
        ? TEMP_FAILURE_RETRY(mkdir(path, mode))
        : TEMP_FAILURE_RETRY(open(path, O_CREAT | O_CLOEXEC | O_NOFOLLOW | O_RDONLY, 0644));
    
    if (TEMP_FAILURE_RETRY(chmod(path, mode)) == -1) {
        ALOGE("Failed to chmod(%s, %d): %s", path, mode, strerror(errno));
        return -1;
    }
    if (TEMP_FAILURE_RETRY(chown(path, uid, gid)) == -1) {
        ALOGE("Failed to chown(%s, %d, %d): %s", path, uid, gid, strerror(errno));
        return -1;
    }
    ...
}

fs_prepare_path_impl 中,

  1. 通过 mkdir 创建了文件,也就是 /data/user/user-id/package-name/ 目录
  2. 通过 chown 修改文件的 UID 和 GID
  3. 通过 chmod 修改文件的访问权限

文件的创建以及权限的设置就分析完了,通过 App 本身的 UID/GID 设置 App工作目录的 UID/GID,同时设置权限为 rwx------ 或者 rwxr-x--x 这样就确保了只有 App 进程本身才有权限读写自己的工作目录从而达到了 App 进程之间的数据隔离。 Android 对于 App 进程之间的数据隔离,整个流程如下

可以看出,Android 对于 App 进程之间的数据保护/隔离,本质上还是依赖于 Linux Kernel 的 UGO 权限模型。

UGO 权限模型的缺点

  1. 颗粒度太大,导致权限过剩/溢出,比如某个进程需要访问某几个文件,不同的文件具有不同的 UID/GID 标识,我们有的时候不得不给 root 权限来满足我们的需求,这样会导致这个进程的权利太大
  2. root 权利太大,如果被"不法分子"拿到了 root 权限,会导致系统毫无安全性可言

为了解决这个问题,Android 引入了 SELinux (Security-Enhanced Linux),并且基于自身情况扩展成了 SEAndroid,即使在设备被 root 的情况下也能有效的保护系统资源。

相关推荐
双桥wow21 小时前
Android Framework开机动画开发
android
fanged1 天前
天马G前端的使用
android·游戏
molong9311 天前
Kotlin 内联函数、高阶函数、扩展函数
android·开发语言·kotlin
叶辞树1 天前
Android framework调试和AMS等服务调试
android
慕伏白1 天前
【慕伏白】Android Studio 无线调试配置
android·ide·android studio
低调小一1 天前
Kuikly 小白拆解系列 · 第1篇|两棵树直调(Kotlin 构建与原生承载)
android·开发语言·kotlin
跟着珅聪学java1 天前
spring boot 整合 activiti 教程
android·java·spring
川石课堂软件测试2 天前
全链路Controller压测负载均衡
android·运维·开发语言·python·mysql·adb·负载均衡
2501_915921432 天前
iOS 26 电耗监测与优化,耗电问题实战 + 多工具 辅助策略
android·macos·ios·小程序·uni-app·cocoa·iphone
2501_915921432 天前
苹果软件混淆与 iOS 应用加固白皮书,IPA 文件加密、反编译防护与无源码混淆方案全解析
android·ios·小程序·https·uni-app·iphone·webview