Android Security | SEAndroid 的主体

在 SEAndroid 中,主体指的是一系列的进程,每个进程都需要和安全上下文关联起来,也叫做"打标签",这样 sepolicy 才能依据进程的标签决定是否具有访问某个客体(文件)的权限。

接下来我们分析 Android 各种进程是如何被"打标签"的。

在 Android 系统中,进程大致可以分为两种,

  1. 第一种是 daemon 进程,比如 init 进程,adb 进程吗Zygote 进程等等
  2. 第二种进程是 App 进程

上述两种进程设置安全上下文的方式是完全不一样的,决定 daemon 进程的安全上下文的重要配置文件是 file_contexts, 而决定 App 进程的安全上下的核心配置文件是 seapp_contexts,接下来我们一一进行分析。

daemon 进程的安全上下文设置

daemon 进程的安全上下文设置基本是类似的,这里我以 init 进程为例。

init 进程是由 Kernel 执行 kernel_execve 启动的,正常来说,那么 init 进程的 domian (安全上下文中一种特殊的 type) 应该继承了 kerneldomian,但是我们通过 ps -AZ 查看

csharp 复制代码
ps -AZ | grep init

u:r:init:s0                    root

init 进程的安全上下文是 u:r:init:s0, domaininit,为什么 init 进程的 domian 不是 kernel 而是 init 呢?

这里就涉及到了 domian transition (域转换) 在 system/sepolicy/private/kernel.te 中

scss 复制代码
...
domain_auto_trans(kernel, init_exec, init)
...

domain_auto_trans 三个参数:

  1. 参数1: 当前进程的 domian
  2. 参数2: 执行文件的安全上下文的 type
  3. 参数3: 目标 domian

所以 domain_auto_trans 的作用是: 当进程尝试执行一个文件时,如果该文件的安全上下文的 type 与 domain_auto_trans 的第二个参数相匹配,那么 SEAndroid 会将当前进程的 domian 转换成目标 domian。 所以结合传入的三个参数,这个 te 的语法就是: 当具有 kernel domian 的进程执行 type 为 init_exec 的文件的时候,会将该进程的 domain 从 kernel 转换成 init

根据 init 的启动流程,在 FirstStageMain (此时的 init 进程还是运行的 kernel domian) 执行的最后会加载 /system/bin/init 这个可执行文件启动 init 的 SecondStageMain

arduino 复制代码
int FirstStageMain(int argc, char** argv) {

    const char* path = "/system/bin/init";
    ...
    execv(path, const_cast<char**>(args));
    ...
    return 1;
}

而 /system/bin/init 文件的安全上下文的 type 正好是 init_exec

perl 复制代码
// system/sepolicy/microdroid/system/private/file_contexts
...
/system/bin/init		u:object_r:init_exec:s0
...

所以在此时,通过 domain_auto_trans(kernel, init_exec, init) 把 init 进程的 domian 从 kernel 转换成了 init。

其它的 demon 进程都是采用 domian transition 的方式,来实现不同的进程具有不同的 domain,其中就包括 Zygote 进程,它的 domian 是 zygote

makefile 复制代码
u:r:zygote:s0    root  1492   1   17267772 192984 poll_schedule_timeout 0 S zygote64

App 进程的安全上下文设置

APP 进程都是通过 Zygote 进程 fork 出来的,同样的也需要设置安全上下文,如果不特殊处理的话,所有的 App 进程都会继承 Zygote 进程的 domain,那么会导致普通的 App 进程也具有 Zygote 进程一样的权限,这显然这是不合理的。所以 App 进程同样需要进行 domian transition ,但是 App 进程和其他 daemon 进程的 domian transition 完全不一样,所以接下来了解 App 进程的安全上下文设置

App 进程的启动是由 AMS 发起请求,调用链:

rust 复制代码
ActivityManagerService.startProcessLocked ->
  ProcessList.startProcessLocked ->
   Process.start ->
    ZygoteProcess.start ->
     ZygoteProcess.startViaZygote ->
      ZygoteProcess.zygoteSendArgsAndGetResult ->
       ZygoteProcess.attemptUsapSendArgsAndGetResult,

最后基于 Socket IPC 的方式把请求的参数发给 Zygote 进程

Zygote 进程内部会监听 Socket 请求,当收到请求之后,会调用 Zygote.forkAndSpecialize

arduino 复制代码
    static int forkAndSpecialize(int uid, int gid, int[] gids, int runtimeFlags,
            int[][] rlimits, int mountExternal, String seInfo, String niceName, int[] fdsToClose,
            int[] fdsToIgnore, boolean startChildZygote, String instructionSet, String appDataDir,
            boolean isTopApp, String[] pkgDataInfoList, String[] allowlistedDataInfoList,
            boolean bindMountAppDataDirs, boolean bindMountAppStorageDirs,
            boolean bindMountSyspropOverrides) {
        ZygoteHooks.preFork();

        int pid = nativeForkAndSpecialize(
                uid, gid, gids, runtimeFlags, rlimits, mountExternal, seInfo, niceName, fdsToClose,
                fdsToIgnore, startChildZygote, instructionSet, appDataDir, isTopApp,
                pkgDataInfoList, allowlistedDataInfoList, bindMountAppDataDirs,
                bindMountAppStorageDirs, bindMountSyspropOverrides);
        ...
    }

这里注意到,AMS 通过 socket 发送请求给 Zygote 进程的参数中有一个 String seInfo , 这个 seInfo 非常重要,在进程的安全上下文扮演了一个非常重要的角色,我们先了解 seInfo 是如何得来的,然后再继续往下面分析:

那么这个 seinfo 是从哪里得到的呢?

答案是在 PMS 安装的时候,在 PMS 安装 App 的过程中,会调用 SELinuxMMAC.getSeInfo。根据 JVM 的规则,在执行 SELinuxMMAC.getSeInfo 这个方法之前,会执行 static {} 里面的代码,在 SELinuxMMAC 的 static {} 中会把多个 mac_permissions.xml 加载到内存中并解析保存到 Policy 这个对象中。

scss 复制代码
    static {
        // Platform mac permissions.
        sMacPermissions.add(new File(
            Environment.getRootDirectory(), "/etc/selinux/plat_mac_permissions.xml"));

        // SystemExt mac permissions (optional).
        final File systemExtMacPermission = new File(
                Environment.getSystemExtDirectory(), "/etc/selinux/system_ext_mac_permissions.xml");
        if (systemExtMacPermission.exists()) {
            sMacPermissions.add(systemExtMacPermission);
        }

        // Product mac permissions (optional).
        final File productMacPermission = new File(
                Environment.getProductDirectory(), "/etc/selinux/product_mac_permissions.xml");
        if (productMacPermission.exists()) {
            sMacPermissions.add(productMacPermission);
        }

        // Vendor mac permissions.
        final File vendorMacPermission = new File(
            Environment.getVendorDirectory(), "/etc/selinux/vendor_mac_permissions.xml");
        if (vendorMacPermission.exists()) {
            sMacPermissions.add(vendorMacPermission);
        }

        // ODM mac permissions (optional).
        final File odmMacPermission = new File(
            Environment.getOdmDirectory(), "/etc/selinux/odm_mac_permissions.xml");
        if (odmMacPermission.exists()) {
            sMacPermissions.add(odmMacPermission);
        }
    }

接下来执行 SELinuxMMAC.getSeInfo

ini 复制代码
    //frameworks/base/services/core/java/com/android/server/pm/SELinuxMMAC.java
    public static String getSeInfo(PackageState packageState, AndroidPackage pkg,
            boolean isPrivileged, int targetSdkVersion) {
        String seInfo = null;
        synchronized (sPolicies) {
                for (Policy policy : sPolicies) {
                    // 根据安装包的信息从 mac_permissions.xml 中匹配对应的 seInfo
                    seInfo = policy.getMatchedSeInfo(pkg);
                    if (seInfo != null) {
                        break;
                }
        }

        if (seInfo == null) {
            seInfo = DEFAULT_SEINFO;
        }

        if (isPrivileged) {
            seInfo += PRIVILEGED_APP_STR;
        }

        seInfo += TARGETSDKVERSION_STR + targetSdkVersion;

        String partition = getPartition(packageState);
        if (!partition.isEmpty()) {
            seInfo += PARTITION_STR + partition;
        }
        return seInfo;
    }

    public String getMatchedSeInfo(AndroidPackage pkg) {
        // Check for exact signature matches across all certs.
        Signature[] certs = mCerts.toArray(new Signature[0]);
        if (pkg.getSigningDetails() != SigningDetails.UNKNOWN
                && !Signature.areExactMatch(pkg.getSigningDetails(), certs)) {
            if (certs.length > 1 || !pkg.getSigningDetails().hasCertificate(certs[0])) {
                return null;
            }
        }
        String seinfoValue = mPkgMap.get(pkg.getPackageName());
        if (seinfoValue != null) {
            return seinfoValue;
        }
        return mSeinfo;
    }

SELinuxMMAC.getSeInfo 中:

  1. 根据当前的 APK 的签名信息+包名和 mac_permissions.xml 中的内容相匹配,找到对应的 seinfo
  2. 根据 isPrivileged 以及 partition 信息共同决定当前安装 App 的 seInfo

那么这个 mac_permissions.xml 到底是什么呢?

xml 复制代码
// system/sepolicy/private/mac_permissions.xml
<policy>

    <!-- Platform dev key in AOSP -->
    <signer signature="@PLATFORM" >
      <seinfo value="platform" />
    </signer>

    <!-- Sdk Sandbox key -->
    <signer signature="@SDK_SANDBOX" >
      <seinfo value="sdk_sandbox" />
    </signer>

    <!-- Bluetooth key in AOSP -->
    <signer signature="@BLUETOOTH" >
      <seinfo value="bluetooth" />
    </signer>

    <!-- Media key in AOSP -->
    <signer signature="@MEDIA" >
      <seinfo value="media" />
    </signer>

    <signer signature="@NETWORK_STACK" >
      <seinfo value="network_stack" />
    </signer>

    <!-- NFC key in AOSP -->
    <signer signature="@NFC" >
      <seinfo value="nfc" />
    </signer>
</policy>

举一个例子,如果 PMS 在安装 App 的时候,发现这个 App 的签名是 @PLATFORM,那么匹配返回的 **seinfo** 就是 platform

了解了 seinfo 的来龙去脉之后,接下来继续分析 Zygote 如何给 App 进程设置安全上下文

scss 复制代码
    // frameworks/base/core/jni/com_android_internal_os_Zygote.cpp
    static jint com_android_internal_os_Zygote_nativeForkAndSpecialize(
        JNIEnv* env, jclass, jint uid, jint gid, jintArray gids, jint runtime_flags,
        jobjectArray rlimits, jint mount_external, jstring se_info, jstring nice_name,
        jintArray managed_fds_to_close, jintArray managed_fds_to_ignore, jboolean is_child_zygote,
        jstring instruction_set, jstring app_data_dir, jboolean is_top_app,
        jobjectArray pkg_data_info_list, jobjectArray allowlisted_data_info_list,
        jboolean mount_data_dirs, jboolean mount_storage_dirs, jboolean mount_sysprop_overrides) {
    jlong capabilities = CalculateCapabilities(env, uid, gid, gids, is_child_zygote);
    jlong bounding_capabilities = CalculateBoundingCapabilities(env, uid, gid, gids);

    if (UNLIKELY(managed_fds_to_close == nullptr)) {
      zygote::ZygoteFailure(env, "zygote", nice_name,
                            "Zygote received a null fds_to_close vector.");
    }

    std::vector<int> fds_to_close =
        ExtractJIntArray(env, "zygote", nice_name, managed_fds_to_close).value();
    std::vector<int> fds_to_ignore =
        ExtractJIntArray(env, "zygote", nice_name, managed_fds_to_ignore)
            .value_or(std::vector<int>());

    std::vector<int> usap_pipes = MakeUsapPipeReadFDVector();

    fds_to_close.insert(fds_to_close.end(), usap_pipes.begin(), usap_pipes.end());
    fds_to_ignore.insert(fds_to_ignore.end(), usap_pipes.begin(), usap_pipes.end());

    fds_to_close.push_back(gUsapPoolSocketFD);

    if (gUsapPoolEventFD != -1) {
      fds_to_close.push_back(gUsapPoolEventFD);
      fds_to_ignore.push_back(gUsapPoolEventFD);
    }

    if (gSystemServerSocketFd != -1) {
        fds_to_close.push_back(gSystemServerSocketFd);
        fds_to_ignore.push_back(gSystemServerSocketFd);
    }

    if (gPreloadFds && gPreloadFdsExtracted) {
        fds_to_ignore.insert(fds_to_ignore.end(), gPreloadFds->begin(), gPreloadFds->end());
    }
    // 当前 app 进程已经 fork 出来了
    pid_t pid = zygote::ForkCommon(env, /* is_system_server= */ false, fds_to_close, fds_to_ignore,
                                   true);
    // 进入 app 进程的一系列设置,其中就包括安全上下文的设置
    if (pid == 0) {
        SpecializeCommon(env, uid, gid, gids, runtime_flags, rlimits, capabilities, capabilities,
                         bounding_capabilities, mount_external, se_info, nice_name, false,
                         is_child_zygote == JNI_TRUE, instruction_set, app_data_dir,
                         is_top_app == JNI_TRUE, pkg_data_info_list, allowlisted_data_info_list,
                         mount_data_dirs == JNI_TRUE, mount_storage_dirs == JNI_TRUE,
                         mount_sysprop_overrides == JNI_TRUE);
    }
    return pid;
}

// Utility routine to specialize a zygote child process.
static void SpecializeCommon(JNIEnv* env, uid_t uid, gid_t gid, jintArray gids, jint runtime_flags,
                             jobjectArray rlimits, jlong permitted_capabilities,
                             jlong effective_capabilities, jlong bounding_capabilities,
                             jint mount_external, jstring managed_se_info,
                             jstring managed_nice_name, bool is_system_server, bool is_child_zygote,
                             jstring managed_instruction_set, jstring managed_app_data_dir,
                             bool is_top_app, jobjectArray pkg_data_info_list,
                             jobjectArray allowlisted_data_info_list, bool mount_data_dirs,
                             bool mount_storage_dirs, bool mount_sysprop_overrides) {
    const char* process_name = is_system_server ? "system_server" : "zygote";

    auto se_info = extract_fn(managed_se_info);
    ...
    if (selinux_android_setcontext(uid, is_system_server, se_info_ptr, nice_name_ptr) == -1) {
        fail_fn(CREATE_ERROR("selinux_android_setcontext(%d, %d, \\"%s\\", \\"%s\\") failed", uid,
                             is_system_server, se_info_ptr, nice_name_ptr));
    }
    ...
}

这里会调用 SELinux 相关的库函数 selinux_android_setcontext 来设置安全上下文

ini 复制代码
int selinux_android_setcontext(uid_t uid,
			       bool isSystemServer,
			       const char *seinfo,
			       const char *pkgname)
{
	char *orig_ctx_str = NULL;
	const char *ctx_str = NULL;
	context_t ctx = NULL;
	int rc = -1;

	if (is_selinux_enabled() <= 0)
		return 0;

	rc = getcon(&orig_ctx_str);
	if (rc)
		goto err;

	ctx = context_new(orig_ctx_str);
	if (!ctx)
		goto oom;

	rc = seapp_context_lookup(SEAPP_DOMAIN, uid, isSystemServer, seinfo, pkgname, ctx);
	if (rc == -1)
		goto err;
	else if (rc == -2)
		goto oom;

	ctx_str = context_str(ctx);
	if (!ctx_str)
		goto oom;

	rc = security_check_context(ctx_str);
	if (rc < 0)
		goto err;

	if (strcmp(ctx_str, orig_ctx_str)) {
		rc = selinux_android_setcon(ctx_str);
		if (rc < 0)
			goto err;
	}

	rc = 0;
        ...
}

selinux_android_setcontext 分为两步:

  1. 通过 seapp_context_lookup 找到需要设置的安全上下文 ctx_str
  2. 通过 selinux_android_setcon 设置安全上下文

seapp_context_lookup

arduino 复制代码
int seapp_context_lookup(enum seapp_kind kind,
				uid_t uid,
				bool isSystemServer,
				const char *seinfo,
				const char *pkgname,
				context_t ctx)
{
	// Ensure the default context files are loaded.
	selinux_android_seapp_context_init();
	return seapp_context_lookup_internal(kind, uid, isSystemServer, seinfo, pkgname, ctx);
}

seapp_context_lookup 也分为两步 A. selinux_android_seapp_context_init 加载 seapp_contexts 文件 B. seapp_context_lookup_internal 通过 seinfo 在 seapp_contexts 匹配对应的安全上下文

selinux_android_seapp_context_init

javascript 复制代码
void selinux_android_seapp_context_init(void) {
	__selinux_once(seapp_once, seapp_context_init);
}

/* indirection to support pthread_once */
static void seapp_context_init(void)
{
	selinux_android_seapp_context_reload();
}

int selinux_android_seapp_context_reload(void)
{
	return seapp_context_reload_internal(&seapp_context_paths);
}

static const path_alts_t seapp_context_paths = { .paths = {
	{
		"/system/etc/selinux/plat_seapp_contexts",
		"/plat_seapp_contexts"
	},
	{
		"/system_ext/etc/selinux/system_ext_seapp_contexts",
		"/system_ext_seapp_contexts"
	},
	{
		"/product/etc/selinux/product_seapp_contexts",
		"/product_seapp_contexts"
	},
	{
		"/vendor/etc/selinux/vendor_seapp_contexts",
		"/vendor_seapp_contexts"
	},
	{
		"/odm/etc/selinux/odm_seapp_contexts",
		"/odm_seapp_contexts"
	}
}, .partitions= {
	"system",
	"system_ext",
	"product",
	"vendor",
	"odm"
}};

selinux_android_seapp_context_init 中加载了各个分区的 seapp_contexts ,这个 seapp_contexts 是什么呢? 以 plat_seapp_contexts 为例子,它的源码定义在 system/sepolicy/private/seapp_contexts

ini 复制代码
...
user=_app seinfo=platform name=com.android.traceur domain=traceur_app type=app_data_file levelFrom=all
user=system seinfo=platform domain=system_app type=system_app_data_file
...

seapp_contexts定义了进程的安全上下文的 type 比如 system_app,也定义了文件的安全上下文的 type 比如 system_app_data_file

seapp_context_lookup_internal

seapp_context_lookup_internal 基于以上加载的 seapp_contexts 和传入的参数 seinfo 等进行匹配,找到最终的安全上下文的 type

ini 复制代码
int seapp_context_lookup_internal(enum seapp_kind kind,
				uid_t uid,
				bool isSystemServer,
				const char *seinfo,
				const char *pkgname,
				context_t ctx)
{
	struct passwd *pwd;
	const char *username = NULL;
	struct seapp_context *cur = NULL;
	int i;
	uid_t userid;
	uid_t appid;
	struct parsed_seinfo info;
	memset(&info, 0, sizeof(info));

	if (seinfo) {
		int ret = parse_seinfo(seinfo, &info);
                ...
	}

	userid = uid / AID_USER_OFFSET;
	appid = uid % AID_USER_OFFSET;
        // 根据 UID 计算逻辑用户名,如 _app, _isolated
	if (appid < AID_APP_START) {
		pwd = seapp_getpwuid(appid);
		if (!pwd)
			goto err;
		username = pwd->pw_name;
	} else if (appid < AID_SDK_SANDBOX_PROCESS_START) {
		username = "_app";
		appid -= AID_APP_START;
	} else if (appid < AID_ISOLATED_START) {
		username = "_sdksandbox";
		appid -= AID_SDK_SANDBOX_PROCESS_START;
	} else {
		username = "_isolated";
		appid -= AID_ISOLATED_START;
	}

	for (i = 0; i < nspec; i++) {
		cur = seapp_contexts[i];

		if (cur->isSystemServer != isSystemServer)
			continue;

		if (cur->isEphemeralAppSet && cur->isEphemeralApp != ((info.is & IS_EPHEMERAL_APP) != 0))
			continue;

		if (cur->user.str) {
			if (cur->user.is_prefix) {
				if (strncasecmp(username, cur->user.str, cur->user.len-1))
					continue;
			} else {
				if (strcasecmp(username, cur->user.str))
					continue;
			}
		}

		if (cur->seinfo) {
			if (!seinfo || strcasecmp(info.base, cur->seinfo))
				continue;
		}

		if (cur->name.str) {
			if(!pkgname)
				continue;

			if (cur->name.is_prefix) {
				if (strncasecmp(pkgname, cur->name.str, cur->name.len-1))
					continue;
			} else {
				if (strcasecmp(pkgname, cur->name.str))
					continue;
			}
		}

		if (cur->isPrivAppSet && cur->isPrivApp != ((info.is & IS_PRIV_APP) != 0))
			continue;

		if (cur->minTargetSdkVersion > info.targetSdkVersion)
			continue;

		if (cur->fromRunAs != ((info.is & IS_FROM_RUN_AS) != 0))
			continue;

		if (cur->isIsolatedComputeApp != ((info.is & IS_ISOLATED_COMPUTE_APP) != 0))
			continue;

		if (cur->isSdkSandboxAudit != ((info.is & IS_SDK_SANDBOX_AUDIT) != 0))
			continue;

		if (cur->isSdkSandboxNext != ((info.is & IS_SDK_SANDBOX_NEXT) != 0))
			continue;

		if (kind == SEAPP_TYPE && !cur->type)
			continue;
		else if (kind == SEAPP_DOMAIN && !cur->domain)
			continue;

		if (kind == SEAPP_TYPE) {
			context_type_set(ctx, cur->type);
		} else if (kind == SEAPP_DOMAIN) {
			context_type_set(ctx, cur->domain);
		}

		if (cur->levelFrom != LEVELFROM_NONE) {
                        // 设置安全上下文的 level
			int res = set_range_from_level(ctx, cur->levelFrom, userid, appid);
		} else if (cur->level) {
			if (context_range_set(ctx, cur->level))
				goto oom;
		}

		if (info.isPreinstalledApp
				&& !is_preinstalled_app_partition_valid(cur->partition, info.partition)) {...}

		break;
	}
	return 0;
}

匹配过程是这样子的: 从上到上遍历上面提到的 seapp_contexts 的每一行代码,依据 userseInfoname 等字段来进行匹配,如果和 seapp_contexts 的每一行匹配上了,那么

如果是文件 :那么匹配的结果就是 seapp_contexts 所匹配上的那一行的 type 作为该文件的安全上下文

如果是进程 :那么匹配的结果就是 seapp_contexts 所匹配上的那一行的 domian 作为该进程的安全上下文

举个例子,如果匹配的 user 是 system,并且 seInfo 是 platform,依据 seapp_contexts 的规则,如果是文件,那么这个文件的安全上下文就是 system_app_data_file,如果是进程,那么这个进程的安全上下文就是 system_app

回到selinux_android_setcontext 函数,通过 seapp_context_lookup_internal 找到安全上下文之后,就通过 selinux_android_setcon 来真正设置当前进程的安全上下文。

到这里为止,App 进程的安全上下文设置就结束了,总结一下:

  • 安装阶段(PackageManagerService)

    • PMS 确实会读取 APK 的签名信息、包名,然后和 mac_permissions.xml 里的规则匹配,得到 seinfo 标签。
    • 这个 seinfo 会保存在 PackageSetting 中,并写入 /data/system/packages.xml,供后续 App 启动时使用。
  • 启动阶段(Zygote → system_server → Zygote fork App)

    • App 启动时,ActivityManagerService 调用 Zygote fork 出新的进程。

    • Zygote 在 fork 完之后,会调用 libselinux 的 selinux_android_setcontext()

    • 这个函数会结合:

      • UID/GID(system、app_XXX 等)

      • 包名(部分情况下使用)

      • PMS 提供的 seinfo

      • /system/etc/selinux/ 下的 seapp_contexts 配置

        来选择并设置最终的 SELinux 安全上下文(如 u:r:untrusted_app:s0:c512,c768

相关推荐
马 孔 多 在下雨5 小时前
安卓旋转屏幕后如何防止数据丢失-ViewModel入门
android
Just_Paranoid11 小时前
【Settings】恢复出厂设置密码校验
android·python·settings·sha256·hmac-sha256
肥肥呀呀呀13 小时前
flutter配置Android gradle kts 8.0 的打包名称
android·flutter
平生不喜凡桃李16 小时前
C++ 异常
android·java·c++
Propeller17 小时前
【Android】View 交互的事件处理机制
android·java
吴Wu涛涛涛涛涛Tao17 小时前
Flutter 实现「可拖拽评论面板 + 回复输入框 + @高亮」的完整方案
android·flutter·ios
雨声不在17 小时前
使用android studio分析cpu开销
android·ide·android studio
程序leo源18 小时前
Linux_基础指令(二)
android·linux·运维·服务器·青少年编程