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 的情况下也能有效的保护系统资源。

相关推荐
咖啡の猫13 小时前
Android开发-设计规范
android·设计规范
CYRUS STUDIO15 小时前
Frida + FART 联手:解锁更强大的 Android 脱壳新姿势
android·逆向
阿华的代码王国18 小时前
【Android】Retrofit2发起GET请求 && POST请求
android·retrofit2
私人珍藏库18 小时前
[Android] 京墨 v1.15.2 —— 古诗词文、汉语字典、黄历等查询阅读学习宝典(可离线)
android·学习·安卓
阿华的代码王国20 小时前
【Android】JSONObject和Gson的使用
android·java·json·gson·jsonobject
weixin_588531151 天前
android studio 同步慢问题解决
android·ide·android studio
AI2中文网1 天前
别再说AppInventor2只能开发安卓了!苹果iOS现已支持!
android·ios·跨平台·苹果·appstore·app inventor 2·appinventor
xiayiye51 天前
Android原生HttpURLConnection上传图片方案
android·android原生方案上传图片·httpurl上传图片·android原生api上传
fatiaozhang95271 天前
晶晨线刷工具下载及易错点说明:生成工作流程XML失败
android·xml·网络·电视盒子·刷机固件·机顶盒刷机