1.本文目的
从本文开始进入Android 启动Kernel后的第1个用户空间进程Init的相关分析。本文是对Init中启用Selinux策略和Selinux启用后系统中业务的验证调用进行简要说明。
2.引入MAC背景
在MAC之前需要提到DAC。DAC简单说是RWX,即执行ls -l见到的类似777的描述。
图中第1列展示的内容即与DAC相关。
拿第1行的部分内容进行说明 “drwxr-xr-x 2 root shell 4096 2009-01-01 08:00 apex”
第1列的d 代表apex是目录,
第1列的d后面的rwx 代表文件所属用户权限是: 可读-可写-可执行
第1列的中间的r-x 代表文件所属用户组权限是: 可读-不可写-可执行
第1列的后面的r-x 代表其他用户的权限是: 可读-不可写-可执行
第3列root 表示文件所属用户为root
第4列shell 表示文件所属用户组为shell
总结一下第1行的内容:apex目录,属于的root用户具备可读、可写、可执行权限,属于的shell用户组具备可读、可执行权限,其他用户具备可读、可执行权限。
顺便一提,Android APK中AndroidManifest.xml配置的permission策略与rwx类似,主要与 用户的ID(UID)、 用户组的ID(GID)有关,后文另说。
DAC的问题,举例说,
1).存在文件1、用户A、用户B
用户A没有文件1的访问权限,用户B有文件1的访问权限,那么,用户A可以通过用户B访问文件1;
2).存在用户root、应用Browser
以root用户启动Browser,那么Browser就有root用户的权限,在Linux系统上能干任何事情
基于以上的举例说明,能看出来,权限容易给的过大,需要做限制。
那么Android就启用了MAC–Linux内核中的Selinux安全模块。
MAC,Mandatory Access Control,强制访问控制。
SELinux, Security-Enhanced Linux,安全增强型 Linux。
SELinux 是一种基于 MAC 的安全机制。
3.启用SELinux
SELinux在init进程中启用,分2部分进行。
3.1.初始化
调用流程:main.cpp#main(无参)–>first_stage_init.cpp#FirstStageMain–>main.cpp#main(“selinux_setup”)–>selinux.cpp#SetupSelinux
int main(int argc, char** argv) {
#if __has_feature(address_sanitizer)
__asan_set_error_report_callback(AsanReportCallback);
#elif __has_feature(hwaddress_sanitizer)
__hwasan_set_error_report_callback(AsanReportCallback);
#endif
// Boost prio which will be restored later
setpriority(PRIO_PROCESS, 0, -20);
if (!strcmp(basename(argv[0]), "ueventd")) {
return ueventd_main(argc, argv);
}
if (argc > 1) {
if (!strcmp(argv[1], "subcontext")) {
android::base::InitLogging(argv, &android::base::KernelLogger);
const BuiltinFunctionMap& function_map = GetBuiltinFunctionMap();
return SubcontextMain(argc, argv, &function_map);
}
if (!strcmp(argv[1], "selinux_setup")) {
return SetupSelinux(argv);
}
if (!strcmp(argv[1], "second_stage")) {
return SecondStageMain(argc, argv);
}
}
return FirstStageMain(argc, argv);
}
int FirstStageMain(int argc, char** argv) {
//...
const char* path = "/system/bin/init";
const char* args[] = {path, "selinux_setup", nullptr};
auto fd = open("/dev/kmsg", O_WRONLY | O_CLOEXEC);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
close(fd);
execv(path, const_cast<char**>(args));
// execv() only returns if an error happened, in which case we
// panic and never fall through this conditional.
PLOG(FATAL) << "execv(\"" << path << "\") failed";
return 1;
}
SetupSelinux–>ReadPolicy–>OpenSplitPolicy,就能看到/system/etc/selinux和/vendor/etc/selinux等内容
int SetupSelinux(char** argv) {
SetStdioToDevNull(argv);
InitKernelLogging(argv);
if (REBOOT_BOOTLOADER_ON_PANIC) {
InstallRebootSignalHandlers();
}
boot_clock::time_point start_time = boot_clock::now();
MountMissingSystemPartitions();
SelinuxSetupKernelLogging();
LOG(INFO) << "Opening SELinux policy";
PrepareApexSepolicy();
// Read the policy before potentially killing snapuserd.
std::string policy;
ReadPolicy(&policy);
CleanupApexSepolicy();
auto snapuserd_helper = SnapuserdSelinuxHelper::CreateIfNeeded();
if (snapuserd_helper) {
// Kill the old snapused to avoid audit messages. After this we cannot
// read from /system (or other dynamic partitions) until we call
// FinishTransition().
snapuserd_helper->StartTransition();
}
LoadSelinuxPolicy(policy);
if (snapuserd_helper) {
// Before enforcing, finish the pending snapuserd transition.
snapuserd_helper->FinishTransition();
snapuserd_helper = nullptr;
}
// This restorecon is intentionally done before SelinuxSetEnforcement because the permissions
// needed to transition files from tmpfs to *_contexts_file context should not be granted to
// any process after selinux is set into enforcing mode.
if (selinux_android_restorecon("/dev/selinux/", SELINUX_ANDROID_RESTORECON_RECURSE) == -1) {
PLOG(FATAL) << "restorecon failed of /dev/selinux failed";
}
SelinuxSetEnforcement();
// We're in the kernel domain and want to transition to the init domain. File systems that
// store SELabels in their xattrs, such as ext4 do not need an explicit restorecon here,
// but other file systems do. In particular, this is needed for ramdisks such as the
// recovery image for A/B devices.
if (selinux_android_restorecon("/system/bin/init", 0) == -1) {
PLOG(FATAL) << "restorecon failed of /system/bin/init failed";
}
setenv(kEnvSelinuxStartedAt, std::to_string(start_time.time_since_epoch().count()).c_str(), 1);
const char* path = "/system/bin/init";
const char* args[] = {path, "second_stage", nullptr};
execv(path, const_cast<char**>(args));
// execv() only returns if an error happened, in which case we
// panic and never return from this function.
PLOG(FATAL) << "execv(\"" << path << "\") failed";
return 1;
}
void ReadPolicy(std::string* policy) {
PolicyFile policy_file;
bool ok = IsSplitPolicyDevice() ? OpenSplitPolicy(&policy_file)
: OpenMonolithicPolicy(&policy_file);
if (!ok) {
LOG(FATAL) << "Unable to open SELinux policy";
}
if (!android::base::ReadFdToString(policy_file.fd, policy)) {
PLOG(FATAL) << "Failed to read policy file: " << policy_file.path;
}
}
bool OpenSplitPolicy(PolicyFile* policy_file) {
// IMPLEMENTATION NOTE: Split policy consists of three or more CIL files:
// * platform -- policy needed due to logic contained in the system image,
// * vendor -- policy needed due to logic contained in the vendor image,
// * mapping -- mapping policy which helps preserve forward-compatibility of non-platform policy
// with newer versions of platform policy.
// * (optional) policy needed due to logic on product, system_ext, odm, or apex.
// secilc is invoked to compile the above three policy files into a single monolithic policy
// file. This file is then loaded into the kernel.
const auto userdebug_plat_sepolicy = GetUserdebugPlatformPolicyFile();
const bool use_userdebug_policy = userdebug_plat_sepolicy.has_value();
if (use_userdebug_policy) {
LOG(INFO) << "Using userdebug system sepolicy " << *userdebug_plat_sepolicy;
}
// Load precompiled policy from vendor image, if a matching policy is found there. The policy
// must match the platform policy on the system image.
// use_userdebug_policy requires compiling sepolicy with userdebug_plat_sepolicy.cil.
// Thus it cannot use the precompiled policy from vendor image.
if (!use_userdebug_policy) {
if (auto res = FindPrecompiledSplitPolicy(); res.ok()) {
unique_fd fd(open(res->c_str(), O_RDONLY | O_CLOEXEC | O_BINARY));
if (fd != -1) {
policy_file->fd = std::move(fd);
policy_file->path = std::move(*res);
return true;
}
} else {
LOG(INFO) << res.error();
}
}
// No suitable precompiled policy could be loaded
LOG(INFO) << "Compiling SELinux policy";
// We store the output of the compilation on /dev because this is the most convenient tmpfs
// storage mount available this early in the boot sequence.
char compiled_sepolicy[] = "/dev/sepolicy.XXXXXX";
unique_fd compiled_sepolicy_fd(mkostemp(compiled_sepolicy, O_CLOEXEC));
if (compiled_sepolicy_fd < 0) {
PLOG(ERROR) << "Failed to create temporary file " << compiled_sepolicy;
return false;
}
// Determine which mapping file to include
std::string vend_plat_vers;
if (!GetVendorMappingVersion(&vend_plat_vers)) {
return false;
}
std::string plat_mapping_file("/system/etc/selinux/mapping/" + vend_plat_vers + ".cil");
std::string plat_compat_cil_file("/system/etc/selinux/mapping/" + vend_plat_vers +
".compat.cil");
if (access(plat_compat_cil_file.c_str(), F_OK) == -1) {
plat_compat_cil_file.clear();
}
std::string system_ext_policy_cil_file("/system_ext/etc/selinux/system_ext_sepolicy.cil");
if (access(system_ext_policy_cil_file.c_str(), F_OK) == -1) {
system_ext_policy_cil_file.clear();
}
std::string system_ext_mapping_file("/system_ext/etc/selinux/mapping/" + vend_plat_vers +
".cil");
if (access(system_ext_mapping_file.c_str(), F_OK) == -1) {
system_ext_mapping_file.clear();
}
std::string system_ext_compat_cil_file("/system_ext/etc/selinux/mapping/" + vend_plat_vers +
".compat.cil");
if (access(system_ext_compat_cil_file.c_str(), F_OK) == -1) {
system_ext_compat_cil_file.clear();
}
std::string product_policy_cil_file("/product/etc/selinux/product_sepolicy.cil");
if (access(product_policy_cil_file.c_str(), F_OK) == -1) {
product_policy_cil_file.clear();
}
std::string product_mapping_file("/product/etc/selinux/mapping/" + vend_plat_vers + ".cil");
if (access(product_mapping_file.c_str(), F_OK) == -1) {
product_mapping_file.clear();
}
std::string vendor_policy_cil_file("/vendor/etc/selinux/vendor_sepolicy.cil");
if (access(vendor_policy_cil_file.c_str(), F_OK) == -1) {
LOG(ERROR) << "Missing " << vendor_policy_cil_file;
return false;
}
std::string plat_pub_versioned_cil_file("/vendor/etc/selinux/plat_pub_versioned.cil");
if (access(plat_pub_versioned_cil_file.c_str(), F_OK) == -1) {
LOG(ERROR) << "Missing " << plat_pub_versioned_cil_file;
return false;
}
// odm_sepolicy.cil is default but optional.
std::string odm_policy_cil_file("/odm/etc/selinux/odm_sepolicy.cil");
if (access(odm_policy_cil_file.c_str(), F_OK) == -1) {
odm_policy_cil_file.clear();
}
// apex_sepolicy.cil is default but optional.
std::string apex_policy_cil_file("/dev/selinux/apex_sepolicy.cil");
if (access(apex_policy_cil_file.c_str(), F_OK) == -1) {
apex_policy_cil_file.clear();
}
const std::string version_as_string = std::to_string(SEPOLICY_VERSION);
// clang-format off
std::vector<const char*> compile_args {
"/system/bin/secilc",
use_userdebug_policy ? *userdebug_plat_sepolicy : plat_policy_cil_file,
"-m", "-M", "true", "-G", "-N",
"-c", version_as_string.c_str(),
plat_mapping_file.c_str(),
"-o", compiled_sepolicy,
// We don't care about file_contexts output by the compiler
"-f", "/sys/fs/selinux/null", // /dev/null is not yet available
};
// clang-format on
//...
return true;
}
3.2.处理restorecon
调用流程:selinux.cpp#SetupSelinux–>main.cpp#main–>init.cpp#SecondStageMain–>selinux.cpp#SelinuxRestoreContext
在selinux.cpp#SetupSelinux函数的末尾我们能看到 继续执行 /system/bin/init带参数”second_stage”
int SetupSelinux(char** argv) {
//...
const char* path = "/system/bin/init";
const char* args[] = {path, "second_stage", nullptr};
execv(path, const_cast<char**>(args));
// execv() only returns if an error happened, in which case we
// panic and never return from this function.
PLOG(FATAL) << "execv(\"" << path << "\") failed";
return 1;
}
也就是继续回到main.cpp#main执行
int main(int argc, char** argv) {
//...
if (argc > 1) {
if (!strcmp(argv[1], "subcontext")) {
android::base::InitLogging(argv, &android::base::KernelLogger);
const BuiltinFunctionMap& function_map = GetBuiltinFunctionMap();
return SubcontextMain(argc, argv, &function_map);
}
if (!strcmp(argv[1], "selinux_setup")) {
return SetupSelinux(argv);
}
if (!strcmp(argv[1], "second_stage")) {
return SecondStageMain(argc, argv);
}
}
return FirstStageMain(argc, argv);
}
此时进入init.cpp#SecondStageMain
int SecondStageMain(int argc, char** argv) {
//...
// Now set up SELinux for second stage.
SelabelInitialize();
SelinuxRestoreContext();
//...
}
void SelinuxRestoreContext() {
LOG(INFO) << "Running restorecon...";
selinux_android_restorecon("/dev", 0);
selinux_android_restorecon("/dev/console", 0);
selinux_android_restorecon("/dev/kmsg", 0);
if constexpr (WORLD_WRITABLE_KMSG) {
selinux_android_restorecon("/dev/kmsg_debug", 0);
}
selinux_android_restorecon("/dev/null", 0);
selinux_android_restorecon("/dev/ptmx", 0);
selinux_android_restorecon("/dev/socket", 0);
selinux_android_restorecon("/dev/random", 0);
selinux_android_restorecon("/dev/urandom", 0);
selinux_android_restorecon("/dev/__properties__", 0);
selinux_android_restorecon("/dev/block", SELINUX_ANDROID_RESTORECON_RECURSE);
selinux_android_restorecon("/dev/dm-user", SELINUX_ANDROID_RESTORECON_RECURSE);
selinux_android_restorecon("/dev/device-mapper", 0);
selinux_android_restorecon("/apex", 0);
selinux_android_restorecon("/linkerconfig", 0);
// adb remount, snapshot-based updates, and DSUs all create files during
// first-stage init.
selinux_android_restorecon(SnapshotManager::GetGlobalRollbackIndicatorPath().c_str(), 0);
selinux_android_restorecon("/metadata/gsi", SELINUX_ANDROID_RESTORECON_RECURSE |
SELINUX_ANDROID_RESTORECON_SKIP_SEHASH);
}
4.权限验证
4.1依赖库
4.2.C++层验证
以ini启动的property_service的getprop和setprop权限验证举例
bool CanReadProperty(const std::string& source_context, const std::string& name) {
const char* target_context = nullptr;
property_info_area->GetPropertyInfo(name.c_str(), &target_context, nullptr);
PropertyAuditData audit_data;
audit_data.name = name.c_str();
ucred cr = {.pid = 0, .uid = 0, .gid = 0};
audit_data.cr = &cr;
return selinux_check_access(source_context.c_str(), target_context, "file", "read",
&audit_data) == 0;
}
static bool CheckMacPerms(const std::string& name, const char* target_context,
const char* source_context, const ucred& cr) {
if (!target_context || !source_context) {
return false;
}
PropertyAuditData audit_data;
audit_data.name = name.c_str();
audit_data.cr = &cr;
bool has_access = (selinux_check_access(source_context, target_context, "property_service",
"set", &audit_data) == 0);
return has_access;
}
4.3.Java层验证
以adb shell am命令调用举例
#!/system/bin/sh
if [ "$1" != "instrument" ] ; then
cmd activity "$@"
else
base=/system
export CLASSPATH=$base/framework/am.jar
exec app_process $base/bin com.android.commands.am.Am "$@"
fi
@Override
public void onShowUsage(PrintStream out) {
try {
runAmCmd(new String[] { "help" });
} catch (AndroidException e) {
e.printStackTrace(System.err);
}
}
@Override
public void onRun() throws Exception {
String op = nextArgRequired();
if (op.equals("instrument")) {
runInstrument();
} else {
runAmCmd(getRawArgs());
}
}
void runAmCmd(String[] args) throws AndroidException {
final MyShellCallback cb = new MyShellCallback();
try {
mAm.asBinder().shellCommand(FileDescriptor.in, FileDescriptor.out, FileDescriptor.err,
args, cb, new ResultReceiver(null) { });
} catch (RemoteException e) {
System.err.println(NO_SYSTEM_ERROR_CODE);
throw new AndroidException("Can't call activity manager; is the system running?");
} finally {
cb.mActive = false;
}
}
static final class MyShellCallback extends ShellCallback {
boolean mActive = true;
@Override public ParcelFileDescriptor onOpenFile(String path, String seLinuxContext,
String mode) {
if (!mActive) {
System.err.println("Open attempt after active for: " + path);
return null;
}
File file = new File(path);
//System.err.println("Opening file: " + file.getAbsolutePath());
//Log.i("Am", "Opening file: " + file.getAbsolutePath());
final ParcelFileDescriptor fd;
try {
fd = ParcelFileDescriptor.open(file,
ParcelFileDescriptor.MODE_CREATE |
ParcelFileDescriptor.MODE_TRUNCATE |
ParcelFileDescriptor.MODE_WRITE_ONLY);
} catch (FileNotFoundException e) {
String msg = "Unable to open file " + path + ": " + e;
System.err.println(msg);
throw new IllegalArgumentException(msg);
}
if (seLinuxContext != null) {
final String tcon = SELinux.getFileContext(file.getAbsolutePath());
if (!SELinux.checkSELinuxAccess(seLinuxContext, tcon, "file", "write")) {
try {
fd.close();
} catch (IOException e) {
}
String msg = "System server has no access to file context " + tcon;
System.err.println(msg + " (from path " + file.getAbsolutePath()
+ ", context " + seLinuxContext + ")");
throw new IllegalArgumentException(msg);
}
}
return fd;
}
}
@UnsupportedAppUsage
public static final native boolean checkSELinuxAccess(String scon, String tcon, String tclass, String perm);
这里通过调用jni,又回到了C++的实现里
/*
* Function: checkSELinuxAccess
* Purpose: Check permissions between two security contexts.
* Parameters: subjectContextStr: subject security context as a string
* objectContextStr: object security context as a string
* objectClassStr: object's security class name as a string
* permissionStr: permission name as a string
* Returns: boolean: (true) if permission was granted, (false) otherwise
* Exceptions: None
*/
static jboolean checkSELinuxAccess(JNIEnv *env, jobject, jstring subjectContextStr,
jstring objectContextStr, jstring objectClassStr, jstring permissionStr) {
if (isSELinuxDisabled) {
return true;
}
ScopedUtfChars subjectContext(env, subjectContextStr);
if (subjectContext.c_str() == NULL) {
return false;
}
ScopedUtfChars objectContext(env, objectContextStr);
if (objectContext.c_str() == NULL) {
return false;
}
ScopedUtfChars objectClass(env, objectClassStr);
if (objectClass.c_str() == NULL) {
return false;
}
ScopedUtfChars permission(env, permissionStr);
if (permission.c_str() == NULL) {
return false;
}
char *tmp1 = const_cast<char *>(subjectContext.c_str());
char *tmp2 = const_cast<char *>(objectContext.c_str());
int accessGranted = selinux_check_access(tmp1, tmp2, objectClass.c_str(), permission.c_str(),
NULL);
ALOGV("checkSELinuxAccess(%s, %s, %s, %s) => %d", subjectContext.c_str(), objectContext.c_str(),
objectClass.c_str(), permission.c_str(), accessGranted);
return (accessGranted == 0) ? true : false;
}
说明一下,业务触发调用的地方不在上面的代码中,
在继承了ShellCommand的ActivityManagerShellCommand中,执行am命令对activity、service、broadcast等操作时存在权限验证,会调用到openFileForSystem,从而调用到 MyShellCallback#onOpenFile
public ParcelFileDescriptor openFileForSystem(String path, String mode) {
if (DEBUG) Slog.d(TAG, "openFileForSystem: " + path + " mode=" + mode);
try {
ParcelFileDescriptor pfd = getShellCallback().openFile(path,
"u:r:system_server:s0", mode);
if (pfd != null) {
if (DEBUG) Slog.d(TAG, "Got file: " + pfd);
return pfd;
}
} catch (RuntimeException e) {
if (DEBUG) Slog.d(TAG, "Failure opening file: " + e.getMessage());
getErrPrintWriter().println("Failure opening file: " + e.getMessage());
}
if (DEBUG) Slog.d(TAG, "Error: Unable to open file: " + path);
getErrPrintWriter().println("Error: Unable to open file: " + path);
String suggestedPath = "/data/local/tmp/";
if (path == null || !path.startsWith(suggestedPath)) {
getErrPrintWriter().println("Consider using a file under " + suggestedPath);
}
return null;
}
public ParcelFileDescriptor openFile(String path, String seLinuxContext, String mode) {
if (DEBUG) Log.d(TAG, "openFile " + this + " mode=" + mode + ": mLocal=" + mLocal
+ " mShellCallback=" + mShellCallback);
if (mLocal) {
return onOpenFile(path, seLinuxContext, mode);
}
if (mShellCallback != null) {
try {
return mShellCallback.openFile(path, seLinuxContext, mode);
} catch (RemoteException e) {
Log.w(TAG, "Failure opening " + path, e);
}
}
return null;
}
5.关于SecureElement
com.android.se进程,是SecureElement APP,安全元素,是se芯片相关的内容
预告,后面2篇文章是APP权限、在AOSP增加新服务并添加Selinux策略。
2024.8.15 :后面PMS里再说权限吧