SEAndroid安全机制中的文件安全上下文关联分析

       前面一篇文章提到,SEAndroid是一种基于安全策略的MAC安全机制。这种安全策略实施在主体和客体的安全上下文之上。这意味着安全策略在实施之前,SEAndroid安全机制中的主休和客体是已经有安全上下文的。在SEAndroid安全机制中,主体一般就是进程,而客体一般就是文件。文件的安全上下文的关联有不同的方式。本文主要分析文件安全上下文的设置过程,接下来的一篇文章再分析进程安全上下文的设置过程。

老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!

        在SEAndroid中,文件的安全上下文是在文件的创建过程中设置的。在Android系统中,文件的产生方式主要分为两种,一种是预置在ROM里面的,另外一种是动态创建的,即在系统在运行的过程中创建的。对于预置在ROM里面的文件,例如打包在system.img里面的文件,它们的安全上下文在是制作ROM的过程中设置的。而对于动态创建的文件,它们的安全上下文如果没有特别指定,就与父目录的安全上下文一致。

        假设动态创建的文件的安全上下文来自于父目录。这时候就有一个问题需要解决,就是最开始的父目录的安全上下文是怎么来的呢?如果最开始的父目录是预置在ROM里面的,那么这个问题就很好解决。但是有一些目录,它们并不是预置在ROM的,而是在系统启动或者运行的过程中动态安装的,即我们平时所说的虚拟文件系统,例如我们在前面一篇文章SEAndroid安全机制框架分析中提到的selinux文件系统。这些虚拟文件系统在安装的时候,SEAndroid安全机制会根据安全策略给它们的根目录设置相应的安全上下文,这样以后在里面创建的文件就可以从父目录继承安全上下文了。

       此外,有些文件的安全上下文是不适合使用父目录的安全上下文的,例如应用程序数据文件,它们的安全上文需要根据一定的规则来特别指定。在前面一篇文章SEAndroid安全机制框架分析中提到,SEAndroid安全机制根据应用程序类型的签名来给其数据文件设置不同的安全上下文,以区分系统应用程序和第三方应用程序的数据文件。由于无论是系统应用程序,还是第三方应用程序,它们的数据文件都是位于data分区的data子目录中的,因此我们需要有一种机制给在/data/data目录中创建的数据文件设置不同的安全上下文。我们知道,应用程序在安装的时候,PackageManagerService会通过守护进程installd在/data/data目录中创建相应的数据目录,以后应用程序在运行的过程中默认创建的数据文件就位于对应的数据目录中,因此只要给这些数据目录设置不同的安全上下文,就可以让不同类型的应用程序在运行的过程中创建不同安全上下文的数据文件。

        我们通过图1总结上面描述的文件安全上下文创建方式,如下所示:

SEAndroid安全机制中的文件安全上下文关联分析

图1 文件安全上下文关联方式

       接下来,我们就分别详细分析上述三种文件安全上下文关联方式。

       1. 设置打包在ROM里面的文件的安全上下文

       这里我们以ROM里面的system.img为例,说明打包在ROM里面的文件的安全上下文的设置过程。在前面Android系统镜像文件的打包过程分析一篇文章中,我们已经分析过system.img的制作过程了,因此这里我们只关注与安全上下文设置相关的逻辑。

       生成system.img的命令位于build/core/Makefile文件中,如下所示:

BUILT_SYSTEMIMAGE := $(systemimage_intermediates)/system.img

# $(1): output file
define build-systemimage-target
  @echo "Target system fs image: $(1)"
  @mkdir -p $(dir $(1)) $(systemimage_intermediates) && rm -rf $(systemimage_intermediates)/system_image_info.txt
  $(call generate-userimage-prop-dictionary, $(systemimage_intermediates)/system_image_info.txt, skip_fsck=true)
  $(hide) PATH=$(foreach p,$(INTERNAL_USERIMAGES_BINARY_PATHS),$(p):)$$PATH       ./build/tools/releasetools/build_image.py       $(TARGET_OUT) $(systemimage_intermediates)/system_image_info.txt $(1)
endef

$(BUILT_SYSTEMIMAGE): $(FULL_SYSTEMIMAGE_DEPS) $(INSTALLED_FILES_FILE)
    $(call build-systemimage-target,$@)
        从这里就可以看出,system.img由命令build-system-target生成。build-system-target命令在执行的过程中,又会执行两个子命令。第一个子命令是generate-userimage-prop-dictionary,用来生成一个属性文件system_image_info.txt。第二个子命令是build_image,用来制作system.img镜像文件。注意,第二个命令在制作system.img镜像文件的过程中,会用到第一个命令生成的属性文件system_image_info.txt。

        第一个子命令generate-userimage-prop-dictionary也是实现在build/core/Makefile文件中,如下所示:

SELINUX_FC := $(TARGET_ROOT_OUT)/file_contexts
......

# $(1): the path of the output dictionary file
# $(2): additional "key=value" pairs to append to the dictionary file.
define generate-userimage-prop-dictionary
$(if $(INTERNAL_USERIMAGES_EXT_VARIANT),$(hide) echo "fs_type=$(INTERNAL_USERIMAGES_EXT_VARIANT)" >> $(1))
$(if $(BOARD_SYSTEMIMAGE_PARTITION_SIZE),$(hide) echo "system_size=$(BOARD_SYSTEMIMAGE_PARTITION_SIZE)" >> $(1))
$(if $(BOARD_USERDATAIMAGE_PARTITION_SIZE),$(hide) echo "userdata_size=$(BOARD_USERDATAIMAGE_PARTITION_SIZE)" >> $(1))
$(if $(BOARD_CACHEIMAGE_FILE_SYSTEM_TYPE),$(hide) echo "cache_fs_type=$(BOARD_CACHEIMAGE_FILE_SYSTEM_TYPE)" >> $(1))
$(if $(BOARD_CACHEIMAGE_PARTITION_SIZE),$(hide) echo "cache_size=$(BOARD_CACHEIMAGE_PARTITION_SIZE)" >> $(1))
$(if $(BOARD_VENDORIMAGE_FILE_SYSTEM_TYPE),$(hide) echo "vendor_fs_type=$(BOARD_VENDORIMAGE_FILE_SYSTEM_TYPE)" >> $(1))
$(if $(BOARD_VENDORIMAGE_PARTITION_SIZE),$(hide) echo "vendor_size=$(BOARD_VENDORIMAGE_PARTITION_SIZE)" >> $(1))
$(if $(INTERNAL_USERIMAGES_SPARSE_EXT_FLAG),$(hide) echo "extfs_sparse_flag=$(INTERNAL_USERIMAGES_SPARSE_EXT_FLAG)" >> $(1))
$(if $(mkyaffs2_extra_flags),$(hide) echo "mkyaffs2_extra_flags=$(mkyaffs2_extra_flags)" >> $(1))
$(hide) echo "selinux_fc=$(SELINUX_FC)" >> $(1)
        这里传过来的第一个参数便是指向上述的属性文件system_image_info.txt,它的内容是通过一系列的echo命令生成的,每一行都是“key=value“形式。其中,与文件安全上下文相关的是最后一行:

selinux_fc=$(SELINUX_FC)
        变量SELINUX_FC指向一个file_contexts文件。这个file_contexts文件就是我们在前面一篇文章SEAndroid安全机制框架分析中提到的file_contexts文件,用来描述文件的安全上下文。我们知道,system.img镜像文件是安装在目标设备上的/system目录的,因此我们就观察一下在file_contexts文件中与/system目录相关的文件的安全上下文是如何设置的。

        文件file_contexts最开始是位于build/external/sepolicy目录中的,经过编译后,就会保存在$OUT/root目录中,其中$OUT指向的是产品输出目录。打开$OUT/root/file_contexts文件,我们就可以看到与/system目录相关的文件的安全上下文的设置规则:

#############################
# System files
#
/system(/.*)?       u:object_r:system_file:s0
/system/bin/ash     u:object_r:shell_exec:s0
/system/bin/mksh    u:object_r:shell_exec:s0
/system/bin/sh      --  u:object_r:shell_exec:s0
/system/bin/run-as  --  u:object_r:runas_exec:s0
/system/bin/app_process u:object_r:zygote_exec:s0
/system/bin/servicemanager  u:object_r:servicemanager_exec:s0
/system/bin/surfaceflinger  u:object_r:surfaceflinger_exec:s0
/system/bin/drmserver   u:object_r:drmserver_exec:s0
/system/bin/vold    u:object_r:vold_exec:s0
/system/bin/netd    u:object_r:netd_exec:s0
/system/bin/rild    u:object_r:rild_exec:s0
/system/bin/mediaserver u:object_r:mediaserver_exec:s0
/system/bin/dbus-daemon u:object_r:dbusd_exec:s0
/system/bin/installd    u:object_r:installd_exec:s0
/system/bin/keystore    u:object_r:keystore_exec:s0
/system/bin/debuggerd   u:object_r:debuggerd_exec:s0
/system/bin/bluetoothd  u:object_r:bluetoothd_exec:s0
/system/bin/wpa_supplicant  u:object_r:wpa_exec:s0
/system/bin/qemud   u:object_r:qemud_exec:s0
/system/bin/sdcard      u:object_r:sdcardd_exec:s0
/system/bin/dhcpcd      u:object_r:dhcp_exec:s0
/system/bin/mtpd    u:object_r:mtp_exec:s0
/system/bin/pppd    u:object_r:ppp_exec:s0
/system/bin/tf_daemon   u:object_r:tee_exec:s0
/system/bin/racoon  u:object_r:racoon_exec:s0
/system/etc/ppp(/.*)?   u:object_r:ppp_system_file:s0
/system/etc/dhcpcd(/.*)? u:object_r:dhcp_system_file:s0
/system/xbin/su     u:object_r:su_exec:s0
/system/vendor/bin/gpsd u:object_r:gpsd_exec:s0
/system/bin/ping    u:object_r:ping_exec:s0
        接下来我们再来看第二个子命令build_image的实现,它是由文件build/tools/releasetools/build_image.py实现的,它的入口函数main的实现如下所示:

def main(argv):
  ......

  in_dir = argv[0]
  glob_dict_file = argv[1]
  out_file = argv[2]

  glob_dict = LoadGlobalDict(glob_dict_file)
  image_filename = os.path.basename(out_file)
  mount_point = ""
  if image_filename == "system.img":
    mount_point = "system"
  elif image_filename == "userdata.img":
    mount_point = "data"
  elif image_filename == "cache.img":
    mount_point = "cache"
  elif image_filename == "vendor.img":
    mount_point = "vendor"
  else:
    print >> sys.stderr, "error: unknown image file name ", image_filename
    exit(1)

  image_properties = ImagePropFromGlobalDict(glob_dict, mount_point)
  if not BuildImage(in_dir, image_properties, out_file):
    print >> sys.stderr, "error: failed to build %s from %s" % (out_file, in_dir)
    exit(1)

if __name__ == ‘__main__‘:
  main(sys.argv[1:])
       参数argv[1]指向的就是我们上面提到的属性文件system_image_info.txt,最终保存在本地变量glob_dict_file中。另外一个参数argv[2]指向的要输出的system.img文件路径,最终保存在本地变量out_file中。

       函数LoadGlobalDict用来打开属性文件system_image_info.txt,并且将它每一行的key和value提取出来,并且保在字典glob_dict中。注意,这个字典glob_dict包含有一个key等于selinux_fc、value等于file_contexts文件路径的项。

       接下来再通过os.path.basename将输出的文件路径out_file的最后一项提取出来,就可以得到image_filename的值为”system.img“,因此再接下来就会得到本地变量mount_point的值为”system“,表示我们现在正在打包的是system.img文件。

       函数ImagePropFromGlobalDict用来从字典glob_dict中提取与安装点mount_point相关的项,并且保存在另外一个字典中返回给调用者,它的实现如下所示:

def ImagePropFromGlobalDict(glob_dict, mount_point):
  """Build an image property dictionary from the global dictionary.

  Args:
    glob_dict: the global dictionary from the build system.
    mount_point: such as "system", "data" etc.
  """
  d = {}

  def copy_prop(src_p, dest_p):
    if src_p in glob_dict:
      d[dest_p] = str(glob_dict[src_p])

  common_props = (
      "extfs_sparse_flag",
      "mkyaffs2_extra_flags",
      "selinux_fc",
      "skip_fsck",
      )
  for p in common_props:
    copy_prop(p, p)

  d["mount_point"] = mount_point
  if mount_point == "system":
    copy_prop("fs_type", "fs_type")
    copy_prop("system_size", "partition_size")
  elif mount_point == "data":
    copy_prop("fs_type", "fs_type")
    copy_prop("userdata_size", "partition_size")
  elif mount_point == "cache":
    copy_prop("cache_fs_type", "fs_type")
    copy_prop("cache_size", "partition_size")
  elif mount_point == "vendor":
    copy_prop("vendor_fs_type", "fs_type")
    copy_prop("vendor_size", "partition_size")

  return d
       从这里就可以看出,函数ImagePropFromGlobalDict返回给调用者的字典包含一个以selinux_fc为key值的项,它的值指向上述分析的fiiles_contexts文件。

       回到函数main中,最后它调用另外一个函数BuildImage来生成最终的system.img文件,它的实现如下所示:

def BuildImage(in_dir, prop_dict, out_file):
  """Build an image to out_file from in_dir with property prop_dict.

  Args:
    in_dir: path of input directory.
    prop_dict: property dictionary.
    out_file: path of the output image file.

  Returns:
    True iff the image is built successfully.
  """
  build_command = []
  fs_type = prop_dict.get("fs_type", "")
  run_fsck = False
  if fs_type.startswith("ext"):
    build_command = ["mkuserimg.sh"]
    ......
    build_command.extend([in_dir, out_file, fs_type,
                          prop_dict["mount_point"]])
    ......
    if "selinux_fc" in prop_dict:
      build_command.append(prop_dict["selinux_fc"])
  else:
    build_command = ["mkyaffs2image", "-f"]
    ......
    build_command.append(in_dir)
    build_command.append(out_file)
    if "selinux_fc" in prop_dict:
      build_command.append(prop_dict["selinux_fc"])
      build_command.append(prop_dict["mount_point"])

  exit_code = RunCommand(build_command)
  ......

  return exit_code == 0
       参数prop_dict指向的就是前面调用ImagePropFromGlobalDict获得的字典,如果它里面包含有一个key为"fs_type"的项,并且它的value等于"ext",那么就意味着将要制作ext格式的system.img镜像文件,否则的话,就意味着将要制作yaffs2格式的system.img镜像文件。前者通过命令mkuserimg来生成,而后者通过命令mkyaffs2image来生成。无论生成的是什么格式的system.img镜像文件, 只要参数prop_dict包含有一个key为‘selinux_fc‘的项,那么都会将它的value提取出来,并且作为一个参数传递给命令mkuserimg或者mkyaffs2image使用。

        根据前面我们的分析,参数prop_dict描述的字典包含有key为‘selinux_fc‘的项,并且它的value描述的就是我们在上面提到的file_contexts的路径。当我们将这个file_contexts文件路径传递给命令mkuserimg或者mkyaffs2image时,后者就会根据它设置的规则给打包在system.img里面的文件关联安全上下文。这样我们就获得了一个关联有安全上下文的system.img镜像文件了。

        2. 设置虚拟文件系统的安全上下文

        这里我们以selinux虚拟文件系统的安装过程为例,说明虚拟文件系统的安全上下文的设置过程。在前面的一篇文章SEAndroid安全机制框架分析中提到,系统启动之后,会由init进程在/sys/fs/selinux中安装一个selinux虚拟文件系统,接着再加载SEAndroid安全策略到内核空间的SELinux LSM模块中去。

        SEAndroid安全策略是由external/sepolicy模块生成的。观察external/sepolicy模块的编译脚本Android.mk,我们就会发现,SEAndroid安全策略包含有文件genfs_contexts定义的安全上下文设置规则,如下所示:

##################################
include $(CLEAR_VARS)

LOCAL_MODULE := sepolicy
LOCAL_MODULE_CLASS := ETC
LOCAL_MODULE_TAGS := optional
LOCAL_MODULE_PATH := $(TARGET_ROOT_OUT)

include $(BUILD_SYSTEM)/base_rules.mk

sepolicy_policy.conf := $(intermediates)/policy.conf
$(sepolicy_policy.conf): PRIVATE_MLS_SENS := $(MLS_SENS)
$(sepolicy_policy.conf): PRIVATE_MLS_CATS := $(MLS_CATS)
$(sepolicy_policy.conf) : $(call build_policy, security_classes initial_sids access_vectors global_macros mls_macros mls policy_capabilities te_macros attributes *.te roles users initial_sid_contexts fs_use genfs_contexts port_contexts)
    @mkdir -p $(dir $@)
    $(hide) m4 -D mls_num_sens=$(PRIVATE_MLS_SENS) -D mls_num_cats=$(PRIVATE_MLS_CATS) -s $^ > $@
    $(hide) sed ‘/dontaudit/d‘ $@ > $@.dontaudit

$(LOCAL_BUILT_MODULE) : $(sepolicy_policy.conf) $(HOST_OUT_EXECUTABLES)/checkpolicy
    @mkdir -p $(dir $@)
    $(hide) $(HOST_OUT_EXECUTABLES)/checkpolicy -M -c $(POLICYVERS) -o $@ $<
    $(hide) $(HOST_OUT_EXECUTABLES)/checkpolicy -M -c $(POLICYVERS) -o $(dir $<)/$(notdir $@).dontaudit $<.dontaudit

built_sepolicy := $(LOCAL_BUILT_MODULE)
sepolicy_policy.conf :=
       生成的SEAndroid安全策略保存在一个名称为policy.conf文件中。在生成这个policy.conf文件的过程中,会调用一个build_policy函数。传递给函数build_policy的参数包含了生成SEAndroid安全策略所需要的源文件。其中,源文件genfs_contexts描述的是虚拟文件系统的安全上下文设置规则,它的内容如下所示:

# Label inodes with the fs label.
genfscon rootfs / u:object_r:rootfs:s0
# proc labeling can be further refined (longest matching prefix).
genfscon proc / u:object_r:proc:s0
genfscon proc /net/xt_qtaguid/ctrl u:object_r:qtaguid_proc:s0
# selinuxfs booleans can be individually labeled.
genfscon selinuxfs / u:object_r:selinuxfs:s0
genfscon cgroup / u:object_r:cgroup:s0
# sysfs labels can be set by userspace.
genfscon sysfs / u:object_r:sysfs:s0
genfscon inotifyfs / u:object_r:inotify:s0
genfscon vfat / u:object_r:sdcard_external:s0
genfscon debugfs / u:object_r:debugfs:s0
genfscon fuse / u:object_r:sdcard_internal:s0
       从这里我们就可以看出selinux虚拟文件系统关联的安全上下文为”u:object_r:selinuxfs:s0“,这意味着只有那些对Type为”selinuxfs“的文件有访问权限的进程才可以访问selinux虚拟文件系统,也就是/sys/fs/selinux目录下的文件。

       3. 设置应用程序数据文件的安全上下文

       在Android系统中,每一个应用程序在/data/data目录下都有一个以包名命名的目录,用来作为数据保存目录。这个数据目录是在应用程序安装的时候由守护进程installd创建的。守护进程installd在创建应用程序数据目录的时候,会同时设置它的安全上下文,以便可以对它进行保护。Android应用程序的详细安装过程可以参考前面Android应用程序安装过程源代码分析一文,这里只关注应用程序数据目录的创建及其安全上下文设置的过程。

       PackageManagerService负责安装Android应用程序,它在启动的时候会通过SELinuxMMAC类的静态成员函数readInstallPolicy读取我们在前面SEAndroid安全机制框架分析一文分析的mac_permissions.xml文件,如下所示:

public class PackageManagerService extends IPackageManager.Stub {
    ......

    public PackageManagerService(Context context, Installer installer,
            boolean factoryTest, boolean onlyCore) {
        ......

        synchronized (mInstallLock) {
        // writer
        synchronized (mPackages) {
            ......

            mFoundPolicyFile = SELinuxMMAC.readInstallPolicy();
 
            ......
        } // synchronized (mPackages)
        } // synchronized (mInstallLock)
    }

    ......
}
        这个函数定义在文件frameworks/base/services/java/com/android/server/pm/PackageManagerService.java文件中。

        SELinuxMMAC类的静态成员函数readInstallPolicy的实现如下所示:

public final class SELinuxMMAC {
    ......

    // Signature seinfo values read from policy.
    private static final HashMap<Signature, String> sSigSeinfo =
        new HashMap<Signature, String>();

    // Package name seinfo values read from policy.
    private static final HashMap<String, String> sPackageSeinfo =
        new HashMap<String, String>();

    // Locations of potential install policy files.
    private static final File[] INSTALL_POLICY_FILE = {
        new File(Environment.getDataDirectory(), "system/mac_permissions.xml"),
        new File(Environment.getRootDirectory(), "etc/security/mac_permissions.xml"),
        null};
    ......

    public static boolean readInstallPolicy() {

        return readInstallPolicy(INSTALL_POLICY_FILE);
    }
    .......

    private static boolean readInstallPolicy(File[] policyFiles) {
        FileReader policyFile = null;
        int i = 0;
        while (policyFile == null && policyFiles != null && policyFiles[i] != null) {
            try {
                policyFile = new FileReader(policyFiles[i]);
                break;
            } catch (FileNotFoundException e) {
                Slog.d(TAG,"Couldn‘t find install policy " + policyFiles[i].getPath());
            }
            i++;
        }

        ......
        
        try {
            XmlPullParser parser = Xml.newPullParser();
            parser.setInput(policyFile);

            XmlUtils.beginDocument(parser, "policy");
            while (true) {
                XmlUtils.nextElement(parser);
                if (parser.getEventType() == XmlPullParser.END_DOCUMENT) {
                    break;
                }

                String tagName = parser.getName();
                if ("signer".equals(tagName)) {
                    String cert = parser.getAttributeValue(null, "signature");
                    ......
                    Signature signature;
                    try {
                        signature = new Signature(cert);
                    } catch (IllegalArgumentException e) {
                        ......
                    }
                    String seinfo = readSeinfoTag(parser);
                    if (seinfo != null) {
                        ......
                        sSigSeinfo.put(signature, seinfo);
                    }
                } else if ("default".equals(tagName)) {
                    String seinfo = readSeinfoTag(parser);
                    if (seinfo != null) {
                        ......
                        // The ‘null‘ signature is the default seinfo value
                        sSigSeinfo.put(null, seinfo);
                    }
                } else if ("package".equals(tagName)) {
                    String pkgName = parser.getAttributeValue(null, "name");
                    ......
                    String seinfo = readSeinfoTag(parser);
                    if (seinfo != null) {
                        ......
                        sPackageSeinfo.put(pkgName, seinfo);
                    }
                } else {
                    XmlUtils.skipCurrentTag(parser);
                    continue;
                }
            }
        } catch (XmlPullParserException e) {
            Slog.w(TAG, "Got execption parsing ", e);
        } catch (IOException e) {
            Slog.w(TAG, "Got execption parsing ", e);
        }
        try {
            policyFile.close();
        } catch (IOException e) {
            //omit
        }
        return true;
    }

    ......
}
       这个函数定义在文件frameworks/base/services/java/com/android/server/pm/SELinuxMMAC.java文件中。

       先看SELinuxMMAC类的两个静态成员变量sSigSeinfo和sPackageSeinfo,它们指向的都是一个HashMap。其中,sSigSeinfo是以应用程序签名为Key值的,而sPackageSeinfo是以应用程序包名为Key值,并且两者的Value描述的都是一个seinfo字符串。这个seinfo字符串决定了应用程序数据目录的安全上下文设置。

       再看SELinuxMMAC类的静态成员函数readInstallPolicy,这里列出了两个重载版本的实现。其中,无参数版本的readInstallPolicy以静态成员INSTALL_POLICY_FILE
描述的一个数组为参数,调用File数组参数版本的readInstallPolicy。

       SELinuxMMAC类的静态成员变量INSTALL_POLICY_FILE指向的是一个File数组,它里包含了两个文件路径,分别是/data/system/mac_permissions.xml和/etc/security/mac_permissions.xml。从File数组参数版本的readInstallPolicy的开始实现可以知道,它会依次检查/data/system/mac_permissions.xml和/etc/security/mac_permissions.xml文件的存在性。只要其中的一个文件存在,那么就会停止检查过程,并且将已经存在的文件打工,以便接下来可以对它进行读取和解析。一般来说,mac_permissions.xml文件都是保存在/etc/security目录下的。

       打开了要读取的解析的mac_permissions.xml文件之后,接下来就开始对它进行解析了。mac_permissions.xml是一个xml文件,它的内容可以参考前面SEAndroid安全机制框架分析一文。对mac_permissions.xml的解析是比较简单的,逻辑如下所示:

       A. 如果碰到一个以“signer”为名称的Xml标签,那么就将它的signature属性值读取出来,并且以此为参数创建了一个Signature对象。接下来再通过另外一个函数readSeinfoTag在以“signer”为名称的Xml标签中,寻找一个名称为“seinfo”的Xml标签,并且将它的value属性值读取出来,作为一个seinfo字符串。最后,就以刚才创建的Signature对象为Key值,并且以通过函数readSeinfoTag读取出来的seinfo字符串为Value值,保存在SELinuxMMAC类的静态成员函数sSigSeinfo所描述的一个HashMap中。

       B. 如果碰到一个以"package"为名称的Xml标签,那么就将它的name属性值读取出来,作为包名。接下来同样是通过函数readSeinfoTag在以“package”为名称的Xml标签中,寻找一个名称为“seinfo”的Xml标签,并且将它的value属性值读取出来,作为一个seinfo字符串。最后,就以刚才读取的包名为Key值,并且以通过函数readSeinfoTag读取出来的seinfo字符串为Value值,保存在SELinuxMMAC类的静态成员函数sPackageSeinfo所描述的一个HashMap中。

       C. 如果碰到一个以“default”为名称的Xml标签,那么就直接通过函数readSeinfoTag在其子标签中,寻找一个名称为“seinfo”的Xml标签,并且将它的value属性值读取出来,作为一个seinfo字符串。最后,以null为Key值,并且以通过函数readSeinfoTag读取出来的seinfo字符串为Value值,保存在SELinuxMMAC类的静态成员函数sSigSeinfo所描述的一个HashMap中。

       也就是说,我们可以在mac_permissions.xml文件中通过签名或者包名来为某一个应用程序设置特定的seinfo字符串,而对于没有显示通过签名或者包名来设置seinfo字符串的应用程序,那么它们的seinfo字符串就由mac_permissions.xml文件中的default标签来确定。

       PackageManagerService通过调用SELinuxMMAC类的静态成员函数readInstallPolicy,就初始化好了接下来安装应用程序时要用到的seinfo字符串。

       从前面Android应用程序安装过程源代码分析一文可以知道,PackageManagerService在安装应用程序的时候,会调用到PackageManagerService类的成员函数scanPackageLI来解析应用程序的信息。这个函数会同时也会给正在安装的应用程序分配一个seinfo字符串,如下所示:

public class PackageManagerService extends IPackageManager.Stub {
    ......

    private PackageParser.Package scanPackageLI(PackageParser.Package pkg,
            int parseFlags, int scanMode, long currentTime, UserHandle user) {
        ......

        synchronized (mPackages) {
            ......

            if (mFoundPolicyFile) {
                SELinuxMMAC.assignSeinfoValue(pkg);
            }
  
            ......
        }

        ......
   }

   ......
}
        这个函数定义在文件frameworks/base/services/java/com/android/server/pm/PackageManagerService.java中。

        在PackageManagerService类的成员函数scanPackageLI中,参数pkg指向的是一个Package对象,用来描述正在安装的应用程序。如果PackageManagerService类的成员变量mFoundPolicyFile的值等于true,那么就说明前面已经成功地读取和解析到了一个mac_permissions.xml文件,这时候就会调用SELinuxMMAC类的静态成员函数assignSeinfoValue给正在安装的应用程序设置一个seinfo字符串。

        SELinuxMMAC类的静态成员函数assignSeinfoValue的实现如下所示:

public final class SELinuxMMAC {
    ......

    public static void assignSeinfoValue(PackageParser.Package pkg) {
        ......

        if (((pkg.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) ||
            ((pkg.applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0)) {

            // We just want one of the signatures to match.
            for (Signature s : pkg.mSignatures) {
                ......

                if (sSigSeinfo.containsKey(s)) {
                    String seinfo = pkg.applicationInfo.seinfo = sSigSeinfo.get(s);
                    ......

                    return;
                }
            }

            // Check for seinfo labeled by package.
            if (sPackageSeinfo.containsKey(pkg.packageName)) {
                String seinfo = pkg.applicationInfo.seinfo = sPackageSeinfo.get(pkg.packageName);
                ......

                return;
            }
        }
        
        String seinfo = pkg.applicationInfo.seinfo = sSigSeinfo.get(null);
        ......
    }
}

        这个函数定义在文件frameworks/base/services/java/com/android/server/pm/SELinuxMMAC.java文件中。

        从SELinuxMMAC类的静态成员函数assignSeinfoValue的实现可以看出:

        A. 对于系统应用程序来说,首先根据签名来确定它的seinfo字符串。如果不能通过签名来确定它的seinfo字符串,那就再通过包名来确定它的seinfo字符串。如果通过包名也不能确定它的seinfo字符串,那么就使用默认的seinfo字符串。

        B. 对于第三方应用程序来说,它们的seinfo字符串均为默认seinfo字符串。

        无论如何,最终获得的seinfo字符串都保存在用来描述应用程序安装信息的Package对象的成员变量applicationInfo所描述的一个ApplicationInfo对象的成员变量seinfo中。

        当PackageManagerService解析完成要安装的应用程序的信息之后,接下来就会调用PackageManagerService类的成员函数createDataDirsLI来给正在安装的应用程序创建数据目录,如下所示:

public class PackageManagerService extends IPackageManager.Stub {
    ......

    private int createDataDirsLI(String packageName, int uid, String seinfo) {
        int[] users = sUserManager.getUserIds();
        int res = mInstaller.install(packageName, uid, uid, seinfo);
        ......
    }

    ......
}
        这个函数定义在frameworks/base/services/java/com/android/server/pm/PackageManagerService.java文件中。

        参数packageName描述的是正在安装的应用程序的包名,而参数uid和seinfo描述的是分配给正在安装的应用程序的uid和seinfo。PackageManagerService类的成员函数createDataDirsLI通过调用成员变量mInstaller指向的一个Installer对象的成员函数install来为正在安装的应用程序创建数据目录,并且会将包名、uid和seinfo等信息传递给它。

        Installer类的成员函数install的实现如下所示:

public final class Installer {
    ......

    public int install(String name, int uid, int gid, String seinfo) {
        StringBuilder builder = new StringBuilder("install");
        builder.append(‘ ‘);
        builder.append(name);
        builder.append(‘ ‘);
        builder.append(uid);
        builder.append(‘ ‘);
        builder.append(gid);
        builder.append(‘ ‘);
        builder.append(seinfo != null ? seinfo : "!");
        return execute(builder.toString());
    }

    ......
}
        这个函数定义在frameworks/base/services/java/com/android/server/pm/Installer.java文件中。

        Installer类的成员函数install通过socket向守护进程installd发送一个install命令,请求为正在安装的应用程序创建数据目录,并且会将包名、uid和seinfo等信息传递给它。

        守护进程installd的源码位于frameworks/native/cmds/installd目录中,当它收到PackageManagerService发送过来的intall命令后,就会调用里面的一个函数install来为正在安装的应用程序创建数据目录,它的实现如下所示:

int install(const char *pkgname, uid_t uid, gid_t gid, const char *seinfo)
{
    char pkgdir[PKG_PATH_MAX];
    ......

    if (create_pkg_path(pkgdir, pkgname, PKG_DIR_POSTFIX, 0)) {
        ALOGE("cannot create package path\n");
        return -1;
    }

    ......

    if (selinux_android_setfilecon2(pkgdir, pkgname, seinfo, uid) < 0) {
        ALOGE("cannot setfilecon dir ‘%s‘: %s\n", pkgdir, strerror(errno));
        ......
        return -errno;
    }

    ......

    return 0;
}

       这个函数定义在rameworks/native/cmds/installd/commands.c文件中。

       函数install首先是调用另外一个函数create_pkg_path在/data/data目录下创建一下名称等于包名pkgname的子目录,接着再调用由libselinux库提供的函数selinux_android_setfilecon2为刚才创建的目录设置安全上下文。

       函数selinux_android_setfilecon2的实现如下所示:

int selinux_android_setfilecon2(const char *pkgdir,
                const char *pkgname,
                const char *seinfo,
                uid_t uid)
{
    char *orig_ctx_str = NULL, *ctx_str;
    context_t ctx = NULL;
    int rc;

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

    __selinux_once(once, seapp_context_init);

    rc = getfilecon(pkgdir, &ctx_str);
    if (rc < 0)
        goto err;

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

    rc = seapp_context_lookup(SEAPP_TYPE, uid, 0, 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 = setfilecon(pkgdir, ctx_str);
        if (rc < 0)
            goto err;
    }

    rc = 0;
out:
    freecon(orig_ctx_str);
    context_free(ctx);
    return rc;
err:
    selinux_log(SELINUX_ERROR, "%s:  Error setting context for pkgdir %s, uid %d: %s\n",
            __FUNCTION__, pkgdir, uid, strerror(errno));
    rc = -1;
    goto out;
oom:
    selinux_log(SELINUX_ERROR, "%s:  Out of memory\n", __FUNCTION__);
    rc = -1;
    goto out;
}
         这个函数定义在external/libselinux/src/android.c文件中。

         函数selinux_android_setfilecon2的实现逻辑如下所示:

         A. 调用函数is_selinux_enabled检查系统是否启用了SELinux。如果没有启用的话,那就什么也不用做就返回。否则的话,就继续往下执行。

         B. 调用函数seapp_context_init读取和解析我们在前面SEAndroid安全机制框架分析一文提到的seapp_contexts文件。注意,__selinux_once是一个宏,它实际上是利用了pthread库提供的函数pthread_once保证函数seapp_context_init在进程内有且仅有一次会被调用到,适合用来执行初始化工作。

         C. 调用函数getfilecon获得要设置新的安全上下文的目录pkgdir原来已有的安全上下文,保存在变量ctx_str。我们在SEAndroid安全机制框架分析一文提到,文件或者目录在创建的时候,默认设置的是父目录的安全上下文。

         D. 调用函数context_new在原来的安全上下文的基础上创建一个新的安全上下文ctx,以便可以获得原来安全上下文的SELinux用户、角色和安全级别等信息。

         E. 调用函数seapp_context_lookup根据传进来的参数seinfo在seapp_contexts文件中找到对应的Type,并且将其设置为新的安全上下文ctr的Type。

         F. 调用函数context_str获得新创建的安全上下文的字符串描述,以便可以调用函数ecurity_check_context来验证新创建的安全上下文的正确性。如果不正确的话,就出错返回。否则的话,继续往下执行。

        G. 比较原来的安全上下文和新创建的安全上下文。如果不一致的话,那么就调用函数setfilecon将目录pkgdir的安全上下文设置为新创建的安全上下文。

        在上面几个步骤中,最重要的就是B、E和G三步,因此接下来我们就继续分析函数seapp_context_init、seapp_context_lookup和seapp_context_lookup的实现。

        函数seapp_context_init实现在external/libselinux/src/android.c文件中。如下所示:

static void seapp_context_init(void)
{
        selinux_android_seapp_context_reload();
}
        它通过调用另外一个函数selinux_android_seapp_context_reload来读取和解析seapp_contexts文件。

        函数selinux_android_seapp_context_reload也是实现在external/libselinux/src/android.c文件中。如下所示:

int selinux_android_seapp_context_reload(void)
{
    FILE *fp = NULL;
    char line_buf[BUFSIZ];
    ......
    struct seapp_context *cur;
    ......

    while ((fp==NULL) && seapp_contexts_file[i])
        fp = fopen(seapp_contexts_file[i++], "r");  
    ......

    seapp_contexts = calloc(nspec, sizeof(struct seapp_context *));
    ......

    while (fgets(line_buf, sizeof line_buf - 1, fp)) {
        ......

        cur = calloc(1, sizeof(struct seapp_context));
        ......

        token = strtok_r(p, " \t", &saveptr);
        ......

        while (1) {
            name = token;
            value = strchr(name, ‘=‘);
            ......

            if (!strcasecmp(name, "isSystemServer")) {
                if (!strcasecmp(value, "true"))
                    cur->isSystemServer = 1;
                else if (!strcasecmp(value, "false"))
                    cur->isSystemServer = 0;
                ......
            } else if (!strcasecmp(name, "user")) {
                cur->user = strdup(value);
                ......
            } else if (!strcasecmp(name, "seinfo")) {
                cur->seinfo = strdup(value);
                ......
            } else if (!strcasecmp(name, "name")) {
                cur->name = strdup(value);
                ......
            } else if (!strcasecmp(name, "domain")) {
                cur->domain = strdup(value);
                ......
            } else if (!strcasecmp(name, "type")) {
                cur->type = strdup(value);
                ......
            } else if (!strcasecmp(name, "levelFromUid")) {
                if (!strcasecmp(value, "true"))
                    cur->levelFrom = LEVELFROM_APP;
                else if (!strcasecmp(value, "false"))
                    cur->levelFrom = LEVELFROM_NONE;
                ......
            } else if (!strcasecmp(name, "levelFrom")) {
                if (!strcasecmp(value, "none"))
                    cur->levelFrom = LEVELFROM_NONE;
                else if (!strcasecmp(value, "app"))
                    cur->levelFrom = LEVELFROM_APP;
                else if (!strcasecmp(value, "user"))
                    cur->levelFrom = LEVELFROM_USER;
                else if (!strcasecmp(value, "all"))
                    cur->levelFrom = LEVELFROM_ALL;
                .......
            } else if (!strcasecmp(name, "level")) {
                cur->level = strdup(value);
                ......
            } else if (!strcasecmp(name, "sebool")) {
                cur->sebool = strdup(value);
                ......
            }
            ......

            token = strtok_r(NULL, " \t", &saveptr);
            ......
        }

        seapp_contexts[nspec] = cur;
        ......
    }

    ......

    ret = 0;
out:
    fclose(fp);
    return ret;

    ......
}
        seapp_contexts_file是一个全局变量,也是定义在external/libselinux/src/android.c文件中。如下所示:

static char const * const seapp_contexts_file[] = {
    "/data/security/current/seapp_contexts",
    "/seapp_contexts",
    0 };
        从这里我们就可以看到,seapp_contexts可能保存在目录/data/security/current中,也有可能保存在佷目录中,函数selinux_android_seapp_context_reload依次进行检查。只要其中一个地方存在,那么就对它进行打开。打开之后,接下来就按行进行解析。

        文件seapp_contexts的内容如下所示:

isSystemServer=true domain=system
user=system domain=system_app type=system_data_file
user=bluetooth domain=bluetooth type=bluetooth_data_file
user=nfc domain=nfc type=nfc_data_file
user=radio domain=radio type=radio_data_file
user=_app domain=untrusted_app type=app_data_file levelFrom=none
user=_app seinfo=platform domain=platform_app type=platform_app_data_file
user=_app seinfo=shared domain=shared_app type=platform_app_data_file
user=_app seinfo=media domain=media_app type=platform_app_data_file
user=_app seinfo=release domain=release_app type=platform_app_data_file
user=_isolated domain=isolated_app
        函数selinux_android_seapp_context_reload将每一行的内容都用一个seapp_context结构体来描述,并且将这些结构体全部缓存在全局变量seapp_contexts之中。通过这种方式,以后需要从文件seapp_contexts中查找文件的Type或者进程的Domain时,就可以直接在内存进行,加快了查找的速度。

        函数seapp_context_lookup就是用来从全局变量seapp_contexts中查找文件的Type或者进程的Domain,它定义在external/libselinux/src/android.c文件中。如下所示:

static int seapp_context_lookup(enum seapp_kind kind,
                uid_t uid,
                int isSystemServer,
                const char *seinfo,
                const char *pkgname,
                context_t ctx)
{
    const char *username = NULL;
    ......
    struct seapp_context *cur;
    ......

    userid = uid / AID_USER;
    appid = uid % AID_USER;
    if (appid < AID_APP) {
        for (n = 0; n < android_id_count; n++) {
            if (android_ids[n].aid == appid) {
                username = android_ids[n].name;
                break;
            }
        }
        ......
    } else if (appid < AID_ISOLATED_START) {
        username = "_app";
        ......
    } else {
        username = "_isolated";
        ......
    }

    ......

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

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

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

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

        if (cur->name) {
            if (!pkgname || strcasecmp(pkgname, cur->name))
                continue;
        }

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

        if (cur->sebool) {
            int value = security_get_boolean_active(cur->sebool);
            if (value == 0)
                continue;
            ......
        }

        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;
        }

        ......

        break;
    }

    ......

    return 0;

    ......
}
        函数seapp_context_lookup首先根据参数uid来确定接下来的查找中要用到的关键字username,这对应于seapp_contexts文件中的user字段。

        Android从4.2开始支持多用户。支持多用户之后,应用程序在安装的时候分配到的UID由两部分组成,一部分是User Id,另一部分是App Id。常量AID_USER的值定义为100000,用UID除以该值获得的整数就为User Id,而用UID对该值进行取模得到的就为App Id。

        App Id小于AID_APP(10000)是保留给系统使用的,它们对应的username保存在数组android_ids中,具体可以参考文件system/core/include/private/android_filesystem_config.h。

        App Id大于等于AID_APP(10000)并且小于AID_ISOLATED_START(99000)是分配给应用程序使用的,它们对应的username统一设置为“_app”。

        App Id大于等于AID_ISOLATED_START(99000)是分配给运行在独立沙箱(没有任何自己的Permission)的应用程序使用的,它们对应的username统一设置为“_isolated”。

       接下来就根据username、isSystemServer、seinfo和pkgname等查找关键字在全局变量seapp_contexts中寻找目标文件的Type或者目标进程的Domain,这是由参数kind的值决定的。如果kind的值等于SEAPP_TYPE,那么就表明要寻找的是文件的Type。如果kind的值等于SEAPP_DOMAIN,那么就表明要查找的是文件的Domain。在我们这个场景中,我们要查找的是文件的Type。在接下来的一篇文章分析进程的安全上下文时,我们就会通过函数seapp_context_lookup查找目标进程的Domain。

       查找的过程是相当直觉的。通过遍历保存在数组seapp_contexts中的每一个seapp_context,如果该seapp_context定义了相应的字段,那么就要求它与对应的查找关键字相等。如果某一个字段与对应的查找关键字不相等,就说明当前遍历的seapp_context不匹配,要继续下一个seapp_context的查找。有一点需要特别注意的是,如果当前正在遍历的seapp_context的某一个字段没有定义,那么它就默认匹配对应的查找关键字。

       最后,如果查找到了匹配的seapp_context,那么就将它的Type或者Domain字段值提取出来,并且设置到参数ctx所描述的安全上下文中去,以便返回给调用者使用。

       我们再来看函数setfilecon的实现,它定义在文件external/libselinux/src/setfilecon.c中,如下所示:

int setfilecon(const char *path, const security_context_t context)
{
    return setxattr(path, XATTR_NAME_SELINUX, context, strlen(context) + 1,
            0);
}
        参数path描述的就是要设置安全上下文的目录或者文件,而参数context描述的就是要设置的安全上下文。

        函数setfilecon实际上是通过调用系统接口setxattr将参数context描述的安全上下文设置为参数path描述的目录或者文件名称为XATTR_NAME_SELINUX(“security.selinux”)的扩展属性中。也就是说,在SEAndroid中,目录或者文件的安全上下文其实是保存在名称为security.selinux扩展属性中的。这要求对应的文件系统支持扩展属性。

        至此,我们就分析完成在SEAndroid安全机制中,文件安全上下文的关联过程了。在接下来的一篇文章中,我们将继续分析SEAndroid安全机制中的进程安全上下文的关联过程了。当进程和文件的安全上下文都设置好之后,SEAndroid安全策略就有用武之地了。敬请关注!更多信息可以关注老罗的新浪微博:http://weibo.com/shengyangluo

SEAndroid安全机制中的文件安全上下文关联分析

上一篇:Android launcher动态Icon的实现方法


下一篇:Android Fragment 真正的完全解析(下)