在 SEAndroid 中,主体指的是一系列的进程,每个进程都需要和安全上下文关联起来,也叫做"打标签",这样 sepolicy 才能依据进程的标签决定是否具有访问某个客体(文件)的权限。
接下来我们分析 Android 各种进程是如何被"打标签"的。
在 Android 系统中,进程大致可以分为两种,
- 第一种是 daemon 进程,比如 init 进程,adb 进程吗Zygote 进程等等
- 第二种进程是 App 进程
上述两种进程设置安全上下文的方式是完全不一样的,决定 daemon 进程的安全上下文的重要配置文件是 file_contexts
, 而决定 App 进程的安全上下的核心配置文件是 seapp_contexts
,接下来我们一一进行分析。
daemon 进程的安全上下文设置
daemon 进程的安全上下文设置基本是类似的,这里我以 init 进程为例。
init
进程是由 Kernel
执行 kernel_execve
启动的,正常来说,那么 init
进程的 domian
(安全上下文中一种特殊的 type) 应该继承了 kernel
的 domian
,但是我们通过 ps -AZ
查看
csharp
ps -AZ | grep init
u:r:init:s0 root
init
进程的安全上下文是 u:r:init:s0, domain
是 init
,为什么 init
进程的 domian
不是 kernel
而是 init
呢?
这里就涉及到了 domian transition
(域转换) 在 system/sepolicy/private/kernel.te 中
scss
...
domain_auto_trans(kernel, init_exec, init)
...
domain_auto_trans
三个参数:
- 参数1: 当前进程的 domian
- 参数2: 执行文件的安全上下文的 type
- 参数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
中:
- 根据当前的 APK 的签名信息+包名和 mac_permissions.xml 中的内容相匹配,找到对应的 seinfo
- 根据 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
分为两步:
- 通过
seapp_context_lookup
找到需要设置的安全上下文ctx_str
- 通过
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
的每一行代码,依据 user ,seInfo ,name 等字段来进行匹配,如果和 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 启动时使用。
- PMS 确实会读取 APK 的签名信息、包名,然后和
-
启动阶段(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
) 。
-
-