新书上市《深入解析Android 5.0系统》
以下内容节选自本书
理解了守护进程的安全上下文的创建过程后,我们再看看应用进程的安全上下文是如何创建的。应用进程是通过Zygote进程fork出来的,但是不会调用exec。在前面介绍Zygote进程时我们已经知道了创建应用进程时会调用函数ForkAndSpecializeCommon(),这个函数则通过调用selinux_android_setcontext()函数来设置应用进程的安全上下文,如下所示:
rc =selinux_android_setcontext(uid,is_system_server,se_info_c_str,se_name_c_str);
selinux_android_setcontext()函数的代码如下:
intselinux_android_setcontext(uid_t uid, int isSystemServer,
const char *seinfo, const char *pkgname)
{
char *orig_ctx_str = NULL, *ctx_str;
context_t ctx = NULL;
int rc = -1;
......
__selinux_once(once, seapp_context_init); // 调用一次seapp_context_init函数
rc = getcon(&ctx_str); // 得到当前进程从Zygote进程继承的安全上下文
......
ctx = context_new(ctx_str); // 以ctx_str为模板创建一个新的安全上下文
orig_ctx_str = ctx_str;
......
// 在SEAPP_DOMAIN中查找新的安全上下文
rc = seapp_context_lookup(SEAPP_DOMAIN, uid,isSystemServer, seinfo, pkgname, ctx);
...... // 检查新的安全上下文
if (strcmp(ctx_str, orig_ctx_str)) { // 如果新的安全上下文和原来的不相同
rc = setcon(ctx_str); // 为当前进程设置安全上下文。
if (rc < 0)
goto err;
}
......
return rc;
}
selinux_android_setcontext()函数首先调用了seapp_context_init()函数来装载“seapp_context”文件的内容。然后通过函数seapp_context_lookup()查找合适的安全上下文,最后调用setcon()函数来设置进程的安全上下文。我们先看看seapp_context_init()函数的代码:
static voidseapp_context_init(void)
{
selinux_android_seapp_context_reload();
}
seapp_context_init()函数只是调用了selinux_android_seapp_context_reload()函数,代码如下:
intselinux_android_seapp_context_reload(void)
{
......
while ((fp==NULL) &&seapp_contexts_file[i])
fp =fopen(seapp_contexts_file[i++], "r"); // 打开文件
...... // 把文件的内容读进来并解析
}
selinux_android_seapp_context_reload()函数的作用是打开数组seapp_contexts_file中指定的文件,读取它的内容并解析,解析后的内容放到全局变量seapp_contexts中,这段解析比较长,也比较简单,我们就不逐行分析了,这里主要是希望了解所打开的文件的定义,如下所示:
static char const *const seapp_contexts_file[] = {
"/data/security/current/seapp_contexts",
"/seapp_contexts",
0 };
constseapp_contexts_file数组中定义了两个文件,/seapp_contexts是原始的文件,如果对这个文件的内容进行了修改,会保存到/data/security/current/seapp_contexts文件中,所以需要先检查这个文件是否存在。这个文件的内容是:
isSystemServer=truedomain=system
user=systemdomain=system_app type=system_data_file
user=bluetoothdomain=bluetooth type=bluetooth_data_file
user=nfc domain=nfctype=nfc_data_file
user=radiodomain=radio type=radio_data_file
user=_appdomain=untrusted_app type=app_data_file levelFrom=none
user=_appseinfo=platform domain=platform_app type=platform_app_data_file
user=_appseinfo=shared domain=shared_app type=platform_app_data_file
user=_appseinfo=media domain=media_app type=platform_app_data_file
user=_appseinfo=release domain=release_app type=platform_app_data_file
user=_isolateddomain=isolated_app
user=shelldomain=shell type=shell_data_file
seapp_contexts文件定义了用户名和domain之间的联系。下面我们看看seapp_context_lookup()函数是如何根据这个文件的信息来得到应用的安全上下文的。
static intseapp_context_lookup(enum seapp_kind kind, uid_t uid, int isSystemServer,
const char *seinfo, const char*pkgname, context_t ctx)
{
......
userid = uid / AID_USER;
appid = uid % AID_USER;
// 通过uid在android_ids表中查找进程名,android_ids表中定义的系统应用
if (appid < AID_APP) {
for (n = 0; n < android_id_count; n++) {
if (android_ids[n].aid == appid) {
username = android_ids[n].name;
break;
}
}
if (!username)
goto err;
} else if (appid < AID_ISOLATED_START) { // 如果进程号小于AID_ISOLATED_START
username = "_app"; // 说明是普通进程,设置用户名为"_app"
appid -= AID_APP;
} else {
username = "_isolated"; // 沙箱进程,设置用户名为"_isolated"
appid -= AID_ISOLATED_START;
}
if (appid >= CAT_MAPPING_MAX_ID || userid>= CAT_MAPPING_MAX_ID)
goto err;
for (i = 0; i < nspec; i++) { // 在seapp_contexts表中进行查找
cur = seapp_contexts[i];
if (cur->isSystemServer !=isSystemServer) // 不处理systemserver进程
continue;
if (cur->user) { // 先比较文件中的user项和进程的username
if (cur->prefix) {
if (strncasecmp(username,cur->user, cur->len-1))
continue;
} else {
if (strcasecmp(username, cur->user))
continue;
}
}
if (cur->seinfo) { // 再比较两者的seinfo是否相同
if (!seinfo || strcasecmp(seinfo,cur->seinfo))
continue;
}
if (cur->name) { // seapp_contexts中没有哪行定义了name,所以这里不用比较
if (!pkgname || strcasecmp(pkgname,cur->name))
continue;
}
if (kind == SEAPP_TYPE &&!cur->type) // 参数kind等于SEAPP_DOMAIN
continue;
else if (kind == SEAPP_DOMAIN &&!cur->domain)
continue; // 跳过文件中没有domain的项
if (cur->sebool) { // 如果文件中定义了sebool项(文件中也没有哪行定义它)
......
}
if (kind == SEAPP_TYPE) {
if (context_type_set(ctx,cur->type))
goto oom;
} else if (kind == SEAPP_DOMAIN) {
if (context_type_set(ctx,cur->domain)) // 设置安全上下文的域
goto oom;
}
......
}
seapp_context_lookup()函数首先根据进程的id得到进程的username,如果是系统进程,通过查找android_ids表可以得到进行用户名,如果是普通进程,通过判断id的大小来区分是普通的app(用户名为_app)还是沙箱应用(用户名为_isolated)。然后根据进程的用户名和seinfo在seapp_contexts表中进行查找对应的项,如果找到了,把找到的domain设置到安全上下文中就完成了。
现在又有了一个新问题,进程的seinfo是什么,进程又是从哪里获得值的?我们前面介绍了PackageManagerService中扫描apk文件时会调用方法scanPackageLI(),这个方法中有下面一段代码:
if (mFoundPolicyFile){
SELinuxMMAC.assignSeinfoValue(pkg);
}
SELinuxMMAC 的assignSeinfoValue()方法就是用来设置应用的seinfo。我们看看assignSeinfoValue()方法的代码:
public static voidassignSeinfoValue(PackageParser.Package pkg) {
if(((pkg.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) ||
((pkg.applicationInfo.flags&ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0)){
for (Signature s :pkg.mSignatures) {
if (s == null)
continue;
if (sSigSeinfo.containsKey(s)) { // 查找sSigSeinfo表中的签名
Stringseinfo = pkg.applicationInfo.seinfo = sSigSeinfo.get(s);
return; // 返回sSigSeinfo中对应的seinfo
}
}
if(sPackageSeinfo.containsKey(pkg.packageName)) {
String seinfo= pkg.applicationInfo.seinfo =
sPackageSeinfo.get(pkg.packageName);
return; //根据包名来得到seinfo
}
}
String seinfo =pkg.applicationInfo.seinfo = sSigSeinfo.get(null);
}
assignSeinfoValue()方法首先根据文件的签名在sSigSeinfo表中匹配相同签名的项,如果找到了相同的项,则返回对应的seinfo。否则再根据应用的包名在sPackageSeinfo表中进行匹配,找到相同的项则返回对应的seinfo。所以问题又变成了sSigSeinfo和sPackageSeinfo两张表中的内容从那里得到的。看看SELinuxMMAC中对这个两个全局变量进行初始化的方法readInstallPolicy方法就知道了,这个方法会对INSTALL_POLICY_FILE数组中定义的xml文件进行解析,解析的结果将保存到这两张表中。INSTALL_POLICY_FILE数组的内容如下:
private static finalFile[] INSTALL_POLICY_FILE = {
newFile(Environment.getDataDirectory(), "security/mac_permissions.xml"),
newFile(Environment.getRootDirectory(),"etc/security/mac_permissions.xml"),
null};
我们打开一个mac_permissions.xml文件看看,
<?xmlversion="1.0" encoding="utf-8"?>
<policy>
<signer signature="......">
<seinfo value="platform"/>
</signer>
<signer signature="......">
<seinfo value="media" />
</signer>
<signer signature="......">
<seinfo value="shared" />
</signer>
<signer signature="......">
<seinfo value="release"/>
</signer>
<default>
<seinfo value="default"/>
</default>
</policy>
上面列出内容中签名项的内容太长,我们就省略了。这个文件的格式很好理解,实际上是给某个签名赋予了一个名称,seinfo用来表示这个名称。
因此应用进程获得安全上下文实际上是根据应用的签名先得到对应的seinfo的值,再通过比较seinfo和用户名得到domain的值。Domain是安全上下文中最关键的项,得到它就等于得到了安全上下文。
从这里也可以看到,Android对SELinux的策略的实现基本上是维持了以前的功能,对app的安全上下文只是根据以前的定义划分成四种类型,还没有进一步的对每个系统app的指定不同的安全上下文。