赞
踩
因为业务需求, 我们需要获取到Android设备的序列号(SerialNo)。首先看一下Android提供的用于获取序列号的Api以及Api实现。
/**
* A hardware serial number, if available. Alphanumeric only, case-insensitive.
* This field is always set to {@link Build#UNKNOWN}.
*
* @deprecated Use {@link #getSerial()} instead.
**/
@Deprecated
// IMPORTANT: This field should be initialized via a function call to
// prevent its value being inlined in the app during compilation because
// we will later set it to the value based on the app's target SDK.
public static final String SERIAL = getString("no.such.thing");
早期的Android版本中,可以直接通过调用Build.SERIAL来获取序列号,在高版本中,为了保护个人隐私, 不让第三方应用轻易获取序列号。所以该Api已经过时, 并且它的值也被设置成了"unknown"。
先看一下该Api的源码, 该源码实现在frameworks/base/core/java/android/os/Build.java
:
@SuppressAutoDoc // No support for device / profile owner.
@RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
public static String getSerial() {
IDeviceIdentifiersPolicyService service = IDeviceIdentifiersPolicyService.Stub
.asInterface(ServiceManager.getService(Context.DEVICE_IDENTIFIERS_SERVICE));
try {
Application application = ActivityThread.currentApplication();
String callingPackage = application != null ? application.getPackageName() : null;
return service.getSerialForPackage(callingPackage);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
return UNKNOWN;
}
调用该Api需要READ_PRIVILEGED_PHONE_STATE权限, 该权限是一个protectLevel为signature的系统级权限,只有系统签名的系统App才能获取该权限, 第三方App是无法获取的。
该权限定义在frameworks/base/core/res/AndroidManifest.xml
中:
<!-- @SystemApi Allows read access to privileged phone state.
@hide Used internally. -->
<permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE"
android:protectionLevel="signature|privileged" />
所以第三方应用无法通过调用getSerial() 来获取序列号。
接下来, 我们看一下DeviceIdentifiersPolicyService中getSerial()的实现。 代码位置在:
frameworks/base/services/core/java/com/android/server/os/DeviceIdentifiersPolicyService.java
getSerial方法的实现为:
@Override public @Nullable String getSerial() throws RemoteException { // Since this invocation is on the server side a null value is used for the // callingPackage as the server's package name (typically android) should not be used // for any device / profile owner checks. The majority of requests for the serial number // should use the getSerialForPackage method with the calling package specified. if (!TelephonyPermissions.checkCallingOrSelfReadDeviceIdentifiers(mContext, /* callingPackage */ null, "getSerial")) { return Build.UNKNOWN; } return SystemProperties.get("ro.serialno", Build.UNKNOWN); } @Override public @Nullable String getSerialForPackage(String callingPackage) throws RemoteException { if (!TelephonyPermissions.checkCallingOrSelfReadDeviceIdentifiers(mContext, callingPackage, "getSerial")) { return Build.UNKNOWN; } return SystemProperties.get("ro.serialno", Build.UNKNOWN); }
TelephonyPermissions.checkCallingOrSelfReadDeviceIdentifiers
即是校验调用方的权限,没有权限的话,会直接返回"unknown"。
从这句代码return SystemProperties.get("ro.serialno", Build.UNKNOWN);
来看,DeviceIdentifiersPolicyService也是通过读取系统属性ro.serialno来获取序列号的。这里调用SystemProperties.get的进程为system_server。所以我们想要验证一下, 普通App是否也可以直接读取到这个属性。
因为SystemProperties是隐藏Api, 所以我们通过反射来调用。代码如下:
Class<?> clazz = null;
try {
Class<?> clazz = Class.forName("android.os.SystemProperties");
Method m = clazz.getDeclaredMethod("get", String.class);
return (String)m.invoke(null, "ro.serialno");
} catch (Exception e) {
e.printStackTrace();
}
但是执行该代码后,发现无法获取ro.serialno的值,并且会打印如下日志:
E/libc: Access denied finding property "ro.serialno"
该日志说明,我们读取ro.serialno属性的操作被拒绝了。
下面我们深入解析一下属性相关的源码。我们从读取属性的流程来跟踪源码,也会解析一下App中属性的初始化以及关键数据结构的创建。
接下来理一下读取系统属性的流程。
首先看一下入口SystemProperties.get方法,代码所在文件为frameworks/base/core/java/android/os/SystemProperties.java
/** * Get the String value for the given {@code key}. * * @param key the key to lookup * @return an empty string if the {@code key} isn't found * @hide */ @NonNull @SystemApi @TestApi public static String get(@NonNull String key) { if (TRACK_KEY_ACCESS) onKeyAccess(key); return native_get(key); } @UnsupportedAppUsage private static native String native_get(String key);
接下来我们找到native_get的native实现的入口函数,经过搜索, 发现它实现在frameworks/base/core/jni/android_os_SystemProperties.cpp
中:
int register_android_os_SystemProperties(JNIEnv *env) { const JNINativeMethod method_table[] = { { "native_get", "(Ljava/lang/String;)Ljava/lang/String;", (void*) SystemProperties_getS }, { "native_get", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", (void*) SystemProperties_getSS }, { "native_get_int", "(Ljava/lang/String;I)I", (void*) SystemProperties_get_integral<jint> }, { "native_get_long", "(Ljava/lang/String;J)J", (void*) SystemProperties_get_integral<jlong> }, { "native_get_boolean", "(Ljava/lang/String;Z)Z", (void*) SystemProperties_get_boolean }, { "native_set", "(Ljava/lang/String;Ljava/lang/String;)V", (void*) SystemProperties_set }, { "native_add_change_callback", "()V", (void*) SystemProperties_add_change_callback }, { "native_report_sysprop_change", "()V", (void*) SystemProperties_report_sysprop_change }, }; return RegisterMethodsOrDie(env, "android/os/SystemProperties", method_table, NELEM(method_table)); } jstring SystemProperties_getSS(JNIEnv *env, jclass clazz, jstring keyJ, jstring defJ) { // Using ConvertKeyAndForward is sub-optimal for copying the key string, // but improves reuse and reasoning over code. auto handler = [&](const std::string& key, jstring defJ) { std::string prop_val = android::base::GetProperty(key, ""); if (!prop_val.empty()) { return env->NewStringUTF(prop_val.c_str()); }; if (defJ != nullptr) { return defJ; } // This function is specified to never return null (or have an // exception pending). return env->NewStringUTF(""); }; return ConvertKeyAndForward(env, keyJ, defJ, handler); } jstring SystemProperties_getS(JNIEnv *env, jclass clazz, jstring keyJ) { return SystemProperties_getSS(env, clazz, keyJ, nullptr); }
SystemProperties_getSS方法中,真正执行获取属性的代码为android::base::GetProperty(key, "")
,该方法实现在system/core/base/properties.cpp
中:
std::string GetProperty(const std::string& key, const std::string& default_value) { std::string property_value; #if defined(__BIONIC__) const prop_info* pi = __system_property_find(key.c_str()); if (pi == nullptr) return default_value; __system_property_read_callback(pi, [](void* cookie, const char*, const char* value, unsigned) { auto property_value = reinterpret_cast<std::string*>(cookie); *property_value = value; }, &property_value); #else auto it = g_properties.find(key); if (it == g_properties.end()) return default_value; property_value = it->second; #endif // If the property exists but is empty, also return the default value. // Since we can't remove system properties, "empty" is traditionally // the same as "missing" (this was true for cutils' property_get). return property_value.empty() ? default_value : property_value; }
GetProperty函数的内部逻辑,会根据__BIONIC__
这个宏是否定义,分两个不同的逻辑。可以看到如果该宏已定义的话,会继续调用__system_property_find, 如果该宏未定义,则从全局变量g_properties中读取。
从逻辑的复杂度来看,g_properties 逻辑比较简单,并且也没有什么保活措施,属性应该是随意读取的,应该是兼容的旧版本。g_properties的值也没有被初始化。
并且经过追踪__BIONIC__
的定义,发现system/core/base/properties.cpp
引用了头文件#include "android-base/properties.h"
, 这个头文件又引用了#include <sys/cdefs.h>
,这个头文件中定义了__BIONIC__
:
#define __BIONIC__ 1
所以GetProperty会继续调用__system_property_find。__system_property_find实现在bionic/libc/bionic/system_property_api.cpp
中:
__BIONIC_WEAK_FOR_NATIVE_BRIDGE
const prop_info* __system_property_find(const char* name) {
return system_properties.Find(name);
}
system_properties是一个全局对象,对应的类为SystemProperties:
static SystemProperties system_properties;
static_assert(__is_trivially_constructible(SystemProperties),
"System Properties must be trivially constructable");
SystemProperties类定义在bionic/libc/system_properties/include/system_properties/system_properties.h
中,实现在bionic/libc/system_properties/system_properties.cpp
中, 实现代码比较多, 我们直接看Find方法的实现:
const prop_info* SystemProperties::Find(const char* name) {
if (!initialized_) {
return nullptr;
}
prop_area* pa = contexts_->GetPropAreaForName(name);
if (!pa) {
async_safe_format_log(ANDROID_LOG_ERROR, "libc", "Access denied finding property \"%s\"", name);
return nullptr;
}
return pa->find(name);
}
Find方法调用成员变量contexts_的GetPropAreaForName方法来获取一个prop_area,如果无法获取,则打印Access denied finding property
日志。 这跟我们上面通过反射SystemProperties.get方法时,打印出来的日志是一致的,这也证明我们跟踪代码的方向是正确的。
所以可以确定,Android拦截读取属性的逻辑,是在contexts_->GetPropAreaForName
里面实现的。
我们接着上面的流程继续深入分析。首先看一下contexts_是如何被初始化的。 该成员变量的初始化,在成员函数Init中:
bool SystemProperties::Init(const char* filename) { // This is called from __libc_init_common, and should leave errno at 0 (http://b/37248982). ErrnoRestorer errno_restorer; if (initialized_) { contexts_->ResetAccess(); return true; } if (strlen(filename) >= PROP_FILENAME_MAX) { return false; } strcpy(property_filename_, filename); // property_filename_ 的值为PROP_FILENAME #define PROP_FILENAME "/dev/__properties__" if (is_dir(property_filename_)) { if (access("/dev/__properties__/property_info", R_OK) == 0) { contexts_ = new (contexts_data_) ContextsSerialized(); if (!contexts_->Initialize(false, property_filename_, nullptr)) { return false; } } else { contexts_ = new (contexts_data_) ContextsSplit(); if (!contexts_->Initialize(false, property_filename_, nullptr)) { return false; } } } else { contexts_ = new (contexts_data_) ContextsPreSplit(); if (!contexts_->Initialize(false, property_filename_, nullptr)) { return false; } } initialized_ = true; return true; }
初始化的时候,首先会将构造方法中的参数filename拷贝到成员变量property_filename_中,可以从调用构造方法的地方追代码,可以得知property_filename_的值为"/dev/__properties__"
, 是一个目录 :
ls -l /dev | grep properties
drwx--x--x 2 root root 8920 2021-11-02 17:42 __properties__
尽然它是一个目录, 则会走到if (access("/dev/__properties__/property_info", R_OK) == 0)
这个if语句。 这个if语句会判断/dev/__properties__/property_info
这个文件是否可读。我们看一下这个文件的权限信息:
ls -l /dev/__properties__/property_info
-r--r--r-- 1 root root 98768 2021-11-02 17:42 /dev/__properties__/property_info
这个文件对于所有的用户都有读权限。 所以会继续执行以下逻辑 :
contexts_ = new (contexts_data_) ContextsSerialized();
if (!contexts_->Initialize(false, property_filename_, nullptr)) {
return false;
}
我们之所以分析这个逻辑, 就是为了要知道, contexts_对象的真实类型。 现在我们知道, 它的真实类型是ContextsSerialized。
所以结合上一节的分析, 拦截读取属性的逻辑, 是在ContextsSerialized类的GetPropAreaForName方法中实现的。下面我们去看这个方法的实现。 在文件bionic/libc/system_properties/contexts_serialized.cpp
中:
prop_area* ContextsSerialized::GetPropAreaForName(const char* name) { uint32_t index; property_info_area_file_->GetPropertyInfoIndexes(name, &index, nullptr); if (index == ~0u || index >= num_context_nodes_) { async_safe_format_log(ANDROID_LOG_ERROR, "libc", "Could not find context for property \"%s\"", name); return nullptr; } auto* context_node = &context_nodes_[index]; if (!context_node->pa()) { // We explicitly do not check no_access_ in this case because unlike the // case of foreach(), we want to generate an selinux audit for each // non-permitted property access in this function. context_node->Open(false, nullptr); } return context_node->pa(); }
该方法的核心逻辑有三个。
property_info_area_file_和context_nodes_都是在ContextsSerialized的Initialize方法中初始化的。我们看一下Initialize方法,以便可以得知property_info_area_file_和context_nodes_到底是做什么的。
bool ContextsSerialized::Initialize(bool writable, const char* filename, bool* fsetxattr_failed) { // filename_ 的值为PROP_FILENAME #define PROP_FILENAME "/dev/__properties__" filename_ = filename; if (!InitializeProperties()) { return false; } if (writable) { mkdir(filename_, S_IRWXU | S_IXGRP | S_IXOTH); bool open_failed = false; if (fsetxattr_failed) { *fsetxattr_failed = false; } for (size_t i = 0; i < num_context_nodes_; ++i) { if (!context_nodes_[i].Open(true, fsetxattr_failed)) { open_failed = true; } } if (open_failed || !MapSerialPropertyArea(true, fsetxattr_failed)) { FreeAndUnmap(); return false; } } else { if (!MapSerialPropertyArea(false, nullptr)) { FreeAndUnmap(); return false; } } return true; }
Initialize方法又调用了InitializeProperties方法。 InitializeProperties方法代码如下:
bool ContextsSerialized::InitializeProperties() {
if (!property_info_area_file_.LoadDefaultPath()) {
return false;
}
if (!InitializeContextNodes()) {
FreeAndUnmap();
return false;
}
return true;
}
从property_info_area_file_.LoadDefaultPath()来看, 是从一个文件中加载数据。InitializeContextNodes用来初始化ContextNodes。
我们先看一下LoadDefaultPath这个方法,实现在system/core/property_service/libpropertyinfoparser/property_info_parser.cpp
中:
bool PropertyInfoAreaFile::LoadDefaultPath() { return LoadPath("/dev/__properties__/property_info"); } bool PropertyInfoAreaFile::LoadPath(const char* filename) { int fd = open(filename, O_CLOEXEC | O_NOFOLLOW | O_RDONLY); struct stat fd_stat; if (fstat(fd, &fd_stat) < 0) { close(fd); return false; } if ((fd_stat.st_uid != 0) || (fd_stat.st_gid != 0) || ((fd_stat.st_mode & (S_IWGRP | S_IWOTH)) != 0) || (fd_stat.st_size < static_cast<off_t>(sizeof(PropertyInfoArea)))) { close(fd); return false; } auto mmap_size = fd_stat.st_size; void* map_result = mmap(nullptr, mmap_size, PROT_READ, MAP_SHARED, fd, 0); if (map_result == MAP_FAILED) { close(fd); return false; } auto property_info_area = reinterpret_cast<PropertyInfoArea*>(map_result); if (property_info_area->minimum_supported_version() > 1 || property_info_area->size() != mmap_size) { munmap(map_result, mmap_size); close(fd); return false; } close(fd); mmap_base_ = map_result; mmap_size_ = mmap_size; return true; }
LoadDefaultPath又调用LoadPath, 将/dev/__properties__/property_info
这个文件, 通过mmap映射到当前进程的内存空间中, 起始地址为map_result 。然后通过auto property_info_area = reinterpret_cast<PropertyInfoArea*>(map_result);
这句代码来看,/dev/__properties__/property_info
这个文件中,存放的是序列化的PropertyInfoArea对象。整个文件映射到内存中后,就是一个PropertyInfoArea数组。
所以到现在我们可以知道,为什么SystemProperties中的contexts_的类型为ContextsSerialized。
但是现在我们还不知道/dev/__properties__/property_info
中到底是什么对象。
下面我们接着看InitializeContextNodes的实现:
bool ContextsSerialized::InitializeContextNodes() { auto num_context_nodes = property_info_area_file_->num_contexts(); auto context_nodes_mmap_size = sizeof(ContextNode) * num_context_nodes; // We want to avoid malloc in system properties, so we take an anonymous map instead (b/31659220). void* const map_result = mmap(nullptr, context_nodes_mmap_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (map_result == MAP_FAILED) { return false; } prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, map_result, context_nodes_mmap_size, "System property context nodes"); context_nodes_ = reinterpret_cast<ContextNode*>(map_result); num_context_nodes_ = num_context_nodes; context_nodes_mmap_size_ = context_nodes_mmap_size; for (size_t i = 0; i < num_context_nodes; ++i) { new (&context_nodes_[i]) ContextNode(property_info_area_file_->context(i), filename_); } return true; }
首先从property_info_area_file_中获取contexts的个数。这个property_info_area_file_我们上面说过,他是读取的/dev/__properties__/property_info
文件,文件中是序列化的对象,所以可以认为,对象的个数,就是num_context_nodes 。
然后根据num_context_nodes 计算创建ContextNode所需要的内存空间, 通过mmap匿名映射一块内存,映射后内存的起始地址为map_result, 通过context_nodes_ = reinterpret_cast<ContextNode*>(map_result)
这行代码, 将这块内存当做context_nodes_ 数组。
最后通过for循环,创建各个ContextNode。
总结一下上面的逻辑:
从property_info_area_file_中读取序列化的对象PropertyInfoArea,通过这些序列化的对象,创建多个ContextNode,并将这些ContextNode对象,放到一个数组context_nodes_中。可以认为一个PropertyInfoArea对象,对应一个ContextNode对象。
但是分析到这里,我们还是不知道这些对象到底是什么。要搞明白这个问题, 我们就要知道/dev/__properties__/property_info
文件中存放的到底是什么数据。 但是这个文件是序列化的,通过cat命令,打印出来的都是乱码:
device_type�Dwake_on_hotplug0�D�D�D�@������hw`���x�x�p���������hwui|���
Luse_vulkan��
���
���
��������imei_match����������recovery������status(�`�D�`�8� ��������imei_rpmbH�X������status|���������
��������incremental����a����enable������������������init�P�,�,����������userspace_reboot0�@�
�is_supportedl�������|���������kernel����Lqemu���T������������������android���L bootanim�T�4�4�,���������ebpf8�H� Lsupportedp���������L����qemu���������� e����khungtask��$���$�����������lib_gui������frame_event_history_size@�T�T�T�P�e����llkp�����������������lmk������<�`�����������(�H�p����D critical��Dcritical_upgrade�Ddebug(�D downgrade_pressureL�Dkill_heaviest_taskp�D kill_timeout_ms��D low��D medium��D psi_complete_stall_ms��D psi_partial_stall_ms
所以我们还是只能通过读代码去分析。
下面我们看一下这个文件是如何初始化的。 属性的初始化是在init中, 下面我们继续分析init中属性初始化相关的代码。
init的代码所在文件为system/core/init/init.cpp
,从这个文件中搜索property
关键字,可以找到如下代码:
int SecondStageMain(int argc, char** argv) {
//......
property_init();
//......
// Propagate the kernel variables to internal variables
// used by init as well as the current required properties.
export_kernel_boot_props();
//......
property_load_boot_defaults(load_debug_prop);
//......
}
首先看property_init方法的实现:
property_init这个函数实现在system/core/init/property_service.cpp
中:
void property_init() {
mkdir("/dev/__properties__", S_IRWXU | S_IXGRP | S_IXOTH);
CreateSerializedPropertyInfo();
if (__system_property_area_init()) {
LOG(FATAL) << "Failed to initialize property area";
}
if (!property_info_area.LoadDefaultPath()) {
LOG(FATAL) << "Failed to load serialized property info file";
}
}
这个方法中的逻辑还算清晰。分四步
为了得到更多细节, 我们分别看一下这几个方法。
该方法代码如下:
void CreateSerializedPropertyInfo() { auto property_infos = std::vector<PropertyInfoEntry>(); if (access("/system/etc/selinux/plat_property_contexts", R_OK) != -1) { if (!LoadPropertyInfoFromFile("/system/etc/selinux/plat_property_contexts", &property_infos)) { return; } // Don't check for failure here, so we always have a sane list of properties. // E.g. In case of recovery, the vendor partition will not have mounted and we // still need the system / platform properties to function. if (!LoadPropertyInfoFromFile("/vendor/etc/selinux/vendor_property_contexts", &property_infos)) { // Fallback to nonplat_* if vendor_* doesn't exist. LoadPropertyInfoFromFile("/vendor/etc/selinux/nonplat_property_contexts", &property_infos); } if (access("/product/etc/selinux/product_property_contexts", R_OK) != -1) { LoadPropertyInfoFromFile("/product/etc/selinux/product_property_contexts", &property_infos); } if (access("/odm/etc/selinux/odm_property_contexts", R_OK) != -1) { LoadPropertyInfoFromFile("/odm/etc/selinux/odm_property_contexts", &property_infos); } } else { if (!LoadPropertyInfoFromFile("/plat_property_contexts", &property_infos)) { return; } if (!LoadPropertyInfoFromFile("/vendor_property_contexts", &property_infos)) { // Fallback to nonplat_* if vendor_* doesn't exist. LoadPropertyInfoFromFile("/nonplat_property_contexts", &property_infos); } LoadPropertyInfoFromFile("/product_property_contexts", &property_infos); LoadPropertyInfoFromFile("/odm_property_contexts", &property_infos); } auto serialized_contexts = std::string(); auto error = std::string(); if (!BuildTrie(property_infos, "u:object_r:default_prop:s0", "string", &serialized_contexts, &error)) { LOG(ERROR) << "Unable to serialize property contexts: " << error; return; } constexpr static const char kPropertyInfosPath[] = "/dev/__properties__/property_info"; if (!WriteStringToFile(serialized_contexts, kPropertyInfosPath, 0444, 0, 0, false)) { PLOG(ERROR) << "Unable to write serialized property infos to file"; } selinux_android_restorecon(kPropertyInfosPath, 0); }
首先通过access来判断文件/system/etc/selinux/plat_property_contexts是否可读。这里的代码是运行在init进程中的, init进程是root用户的,肯定可以读。我们可以看一下这个文件的权限信息:
ls -lh /system/etc/selinux/plat_property_contexts
-rw-r--r-- 1 root root 46K 2009-01-01 08:00 /system/etc/selinux/plat_property_contexts
对所有用户都是可读的。
既然这个文件可读, 就会执行到if语句里面。 if语句里面,会将多个目录下的xxx_property_contexts文件,读取到property_infos中, property_infos是一个std::vector<PropertyInfoEntry>
集合。
这些文件的是一致的。我们来看一下/system/etc/selinux/plat_property_contexts中的内容。这个文件很长,我们截取一部分来看:
persist.sys.hdmi.keep_awake u:object_r:exported2_system_prop:s0 exact bool
persist.sys.sf.color_mode u:object_r:exported2_system_prop:s0 exact int
persist.sys.sf.color_saturation u:object_r:exported2_system_prop:s0 exact string
persist.sys.sf.native_mode u:object_r:exported2_system_prop:s0 exact int
pm.dexopt.ab-ota u:object_r:exported_pm_prop:s0 exact string
可以看到,第一列是属性名称, 第二列是selinux的安全上下文信息。所以到这里,基本上可以确定,我们之所以读取不到ro.serialno属性,是被selinux限制了。
我用了一台vivo手机,进入adb shell, 读取这几个文件,并没有ro.serialno对象的selinux context信息。但是从Android源码的system/sepolicy/private/property_contexts
文件中找到了相关信息:
ro.serialno u:object_r:serialno_prop:s0
ro.boot.serialno u:object_r:serialno_prop:s0
回到代码逻辑, 我们接着看CreateSerializedPropertyInfo方法中的逻辑。 从上面的部分可以了解到,会将各个xxx_property_contexts文件中的属性以及属性对应的selinux安全上下文读取到property_infos集合中,下面看一下集合中的数据会被存放到何处:
auto serialized_contexts = std::string();
auto error = std::string();
if (!BuildTrie(property_infos, "u:object_r:default_prop:s0", "string", &serialized_contexts,
&error)) {
LOG(ERROR) << "Unable to serialize property contexts: " << error;
return;
}
这段代码通过BuildTrie函数, 将property_infos集合中的数据,转变成字符串serialized_contexts 。我们暂且不看它是如何转换的,我们先去看最后被存到哪里:
constexpr static const char kPropertyInfosPath[] = "/dev/__properties__/property_info";
if (!WriteStringToFile(serialized_contexts, kPropertyInfosPath, 0444, 0, 0, false)) {
PLOG(ERROR) << "Unable to write serialized property infos to file";
}
这段代码是关键信息。说明这些数据正是被写到/dev/__properties__/property_info
中了。所以我们可以反推上面的BuildTrie方法正是将对象进行序列化,然后转变成乱码字符串的。如果没有序列化, /dev/__properties__/property_info
中的内容应该就是下面这样的格式:
ro.serialno u:object_r:serialno_prop:s0
ro.boot.serialno u:object_r:serialno_prop:s0
所以做一下总结:
/dev/__properties__/property_info
文件中存储的不是属性的值, 而是属性的selinux上下文。也就是说,每个属性是被哪个selinux上下文来保护的。
上面只是初始化的属性对应的selinux信息,我们还没有看到属性值是怎么存储的。我们接着看__system_property_area_init方法。该方法实现在bionic/libc/bionic/system_property_api.cpp
文件中:
__BIONIC_WEAK_FOR_NATIVE_BRIDGE
int __system_property_area_init() {
bool fsetxattr_failed = false;
return system_properties.AreaInit(PROP_FILENAME, &fsetxattr_failed) && !fsetxattr_failed ? 0 : -1;
}
这里的PROP_FILENAME的值为/dev/__properties__
#define PROP_FILENAME "/dev/__properties__"
__system_property_area_init调用system_properties.AreaInit。这个system_properties我们上面说过,是一个SystemProperties对象,实现在bionic/libc/system_properties/system_properties.cpp
中:
bool SystemProperties::AreaInit(const char* filename, bool* fsetxattr_failed) {
if (strlen(filename) >= PROP_FILENAME_MAX) {
return false;
}
strcpy(property_filename_, filename);
contexts_ = new (contexts_data_) ContextsSerialized();
if (!contexts_->Initialize(true, property_filename_, fsetxattr_failed)) {
return false;
}
initialized_ = true;
return true;
}
这个代码我们看上去有点熟悉。 上面我们分析属性读取流程的时候,分析过SystemProperties的Init方法。注意这里的AreaInit和Init是不一样的。 这里的AreaInit是运行在init进程中的,上面的Init是运行在普通应用进程中的。
这里也会创建ContextsSerialized对象,并且调用该对象的Initialize方法。该方法的前两个参数为传入参数:
/dev/__properties__
下面我们再次看一下ContextsSerialized的Initialize方法:
bool ContextsSerialized::Initialize(bool writable, const char* filename, bool* fsetxattr_failed) { // filename_ 的值为PROP_FILENAME #define PROP_FILENAME "/dev/__properties__" filename_ = filename; if (!InitializeProperties()) { return false; } if (writable) { mkdir(filename_, S_IRWXU | S_IXGRP | S_IXOTH); bool open_failed = false; if (fsetxattr_failed) { *fsetxattr_failed = false; } for (size_t i = 0; i < num_context_nodes_; ++i) { if (!context_nodes_[i].Open(true, fsetxattr_failed)) { open_failed = true; } } if (open_failed || !MapSerialPropertyArea(true, fsetxattr_failed)) { FreeAndUnmap(); return false; } } else { if (!MapSerialPropertyArea(false, nullptr)) { FreeAndUnmap(); return false; } } return true; }
InitializeProperties的执行,前面已经讲过了, 就是从/dev/__properties__/property_info
文件中读取信息, 然后创建ContextNode数组。 现在我们知道了, 每个ContextNode对象, 就代表一个属性的selinux信息,而不是属性的值。属性的值是单独存到其他地方的,至于存到哪里, 正是我们现在正在分析的问题。
因为现在是在init进程中执行,writable传入的值为true, 所以会走到对应的分支。 该分支中的关键代码是调用每个ContextNode的Open函数,且第一个参数传入的是true。
接下来我们看一下ContextNode的Open函数, 实现在bionic/libc/system_properties/context_node.cpp
中:
bool ContextNode::Open(bool access_rw, bool* fsetxattr_failed) { lock_.lock(); if (pa_) { lock_.unlock(); return true; } // 格式为 /dev/__properties__/u:object_r:serialno_prop:s0 char filename[PROP_FILENAME_MAX]; int len = async_safe_format_buffer(filename, sizeof(filename), "%s/%s", filename_, context_); if (len < 0 || len >= PROP_FILENAME_MAX) { lock_.unlock(); return false; } if (access_rw) { pa_ = prop_area::map_prop_area_rw(filename, context_, fsetxattr_failed); } else { pa_ = prop_area::map_prop_area(filename); } lock_.unlock(); return pa_; }
第一句关键代码是async_safe_format_buffer(filename, sizeof(filename), "%s/%s", filename_, context_);
, 将filename_和context_这两个成员变量,拼接成filename 。 filename_我们已经知道了,值为/dev/__properties__
, 那么这个context_到底是什么呢?
让我们回忆一下,ContextNode可以看做是和/dev/__properties__/property_info
中的序列化对象是对应的。而/dev/__properties__/property_info
中每个对象的关键信息如下:
persist.sys.hdmi.keep_awake u:object_r:exported2_system_prop:s0 exact bool
persist.sys.sf.color_mode u:object_r:exported2_system_prop:s0 exact int
persist.sys.sf.color_saturation u:object_r:exported2_system_prop:s0 exact string
persist.sys.sf.native_mode u:object_r:exported2_system_prop:s0 exact int
pm.dexopt.ab-ota u:object_r:exported_pm_prop:s0 exact string
其中第一列为name, 也就是属性名。 第二列为selinux上下文信息, 也就是这里的context_成员变量。
所以filename_和context_拼接成的filename的值的格式为 /dev/__properties__/u:object_r:serialno_prop:s0
, 当然, 最后的部分context_是根据ContextNode而变化的。
我找了个root手机, 看了下/dev/__properties__/
目录下, 有很多这种格式的文件:
ls -l /dev/__properties__
total 796
-r--r--r-- 1 root root 131072 1970-05-01 06:58 properties_serial
-r--r--r-- 1 root root 26968 1970-05-01 06:58 property_info
-r--r--r-- 1 root root 131072 1970-05-01 06:58 u:object_r:adsprpc_prop:s0
-r--r--r-- 1 root root 131072 1970-05-01 06:58 u:object_r:audio_prop:s0
-r--r--r-- 1 root root 131072 1970-05-01 06:58 u:object_r:bg_boot_complete_prop:s0
-r--r--r-- 1 root root 131072 1970-05-01 06:58 u:object_r:bg_daemon_prop:s0
-r--r--r-- 1 root root 131072 1970-05-01 06:58 u:object_r:bluetooth_prop:s0
-r--r--r-- 1 root root 131072 1970-05-01 06:58 u:object_r:bootloader_boot_reason_prop:s0
//......
每个文件的权限为-r--r--r--
,好像是这些文件对所有用户均可读。 但是经过测试发现, 根本就读不到。这是因为每个文件都具有selinux上下文, selinux会禁止普通进程读取这些文件。我们通过ls -lZ再次查看一下(-Z参数会打印出来每个文件的selinux上下文):
sagit:/ # ls -lZ /dev/__properties__
total 796
-r--r--r-- 1 root root u:object_r:properties_serial:s0 131072 1970-05-01 06:58 properties_serial
-r--r--r-- 1 root root u:object_r:property_info:s0 26968 1970-05-01 06:58 property_info
-r--r--r-- 1 root root u:object_r:adsprpc_prop:s0 131072 1970-05-01 06:58 u:object_r:adsprpc_prop:s0
-r--r--r-- 1 root root u:object_r:audio_prop:s0 131072 1970-05-01 06:58 u:object_r:audio_prop:s0
-r--r--r-- 1 root root u:object_r:bg_boot_complete_prop:s0 131072 1970-05-01 06:58 u:object_r:bg_boot_complete_prop:s0
-r--r--r-- 1 root root u:object_r:bg_daemon_prop:s0 131072 1970-05-01 06:58 u:object_r:bg_daemon_prop:s0
-r--r--r-- 1 root root u:object_r:bluetooth_prop:s0 131072 1970-05-01 06:58 u:object_r:bluetooth_prop:s0
-r--r--r-- 1 root root u:object_r:bootloader_boot_reason_prop:s0 131072 1970-05-01 06:58 u:object_r:bootloader_boot_reason_prop:s0
-r--r--r-- 1 root root u:object_r:boottime_prop:s0 131072 2021-12-16 13:26 u:object_r:boottime_prop:s0
-r--r--r-- 1 root root u:object_r:bservice_prop:s0 131072 1970-05-01 06:58 u:object_r:bservice_prop:s0
可以看出,文件名和selinux的上下文是一致的。看到这里我们大概已经知道了,具体的属性值可能就是存放到这些文件中的。
我们回到ContextNode的Open函数,已经知道filename的值为/dev/__properties__/u:object_r:serialno_prop:s0
这种格式。下面我们继续看代码。
因为access_rw值为true, 所以会执行pa_ = prop_area::map_prop_area_rw(filename, context_, fsetxattr_failed);
这行代码。
传入的值filename 和 context_我们已经分析过。 接下来我们看map_prop_area_rw的实现,在bionic/libc/system_properties/prop_area.cpp
中:
prop_area* prop_area::map_prop_area_rw(const char* filename, const char* context, bool* fsetxattr_failed) { /* dev is a tmpfs that we can use to carve a shared workspace * out of, so let's do that... */ const int fd = open(filename, O_RDWR | O_CREAT | O_NOFOLLOW | O_CLOEXEC | O_EXCL, 0444); if (fd < 0) { if (errno == EACCES) { /* for consistency with the case where the process has already * mapped the page in and segfaults when trying to write to it */ abort(); } return nullptr; } if (context) { if (fsetxattr(fd, XATTR_NAME_SELINUX, context, strlen(context) + 1, 0) != 0) { async_safe_format_log(ANDROID_LOG_ERROR, "libc", "fsetxattr failed to set context (%s) for \"%s\"", context, filename); /* * fsetxattr() will fail during system properties tests due to selinux policy. * We do not want to create a custom policy for the tester, so we will continue in * this function but set a flag that an error has occurred. * Init, which is the only daemon that should ever call this function will abort * when this error occurs. * Otherwise, the tester will ignore it and continue, albeit without any selinux * property separation. */ if (fsetxattr_failed) { *fsetxattr_failed = true; } } } if (ftruncate(fd, PA_SIZE) < 0) { close(fd); return nullptr; } pa_size_ = PA_SIZE; pa_data_size_ = pa_size_ - sizeof(prop_area); void* const memory_area = mmap(nullptr, pa_size_, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (memory_area == MAP_FAILED) { close(fd); return nullptr; } prop_area* pa = new (memory_area) prop_area(PROP_AREA_MAGIC, PROP_AREA_VERSION); close(fd); return pa; }
首先调用open打开文件(文件格式为 /dev/__properties__/u:object_r:serialno_prop:s0
), 这里有一个O_CREAT,意思是如果文件不存在的话, 就创建文件。因为这是init第一次初始化系统属性,可以得知,之前肯定是不存在的, 执行完open之后才创建出来的。
context就是调用该函数时传入的属性对应的selinux上下文,如u:object_r:serialno_prop:s0。然后调用fsetxattr为文件设置selinux上下文。
到此为止,/dev/__properties__/u:object_r:serialno_prop:s0
文件也创建出来了,对应的selinux上下文也设置好了。
接下来调用mmap将文件映射成一个prop_area对象。
ContextsSerialized::Initialize中是循环调用ContextNode的,所以会将/dev/__properties__/
下所有的文件均创建出来, 并且都会设置好selinux上下文,创建所有的prop_area对象。
所以可以得知, prop_area, ContextNode,/dev/__properties__/
下的每个文件, 还有 /dev/__properties__/property_info
中的每个对象, 是一一对应的。
到现在为止,属性对应的文件和数据结构都已经创建完了,但是我们发现, 这些文件和数据结构都是空的。这里并没有将真正的属性值存储进来。我们接着往下分析。
property_init中调用完__system_property_area_init()之后, 接下来会调用property_info_area.LoadDefaultPath(), 这个逻辑也是将 /dev/__properties__/property_info
中的内容读取出来。
bool PropertyInfoAreaFile::LoadDefaultPath() { return LoadPath("/dev/__properties__/property_info"); } bool PropertyInfoAreaFile::LoadPath(const char* filename) { int fd = open(filename, O_CLOEXEC | O_NOFOLLOW | O_RDONLY); struct stat fd_stat; if (fstat(fd, &fd_stat) < 0) { close(fd); return false; } if ((fd_stat.st_uid != 0) || (fd_stat.st_gid != 0) || ((fd_stat.st_mode & (S_IWGRP | S_IWOTH)) != 0) || (fd_stat.st_size < static_cast<off_t>(sizeof(PropertyInfoArea)))) { close(fd); return false; } auto mmap_size = fd_stat.st_size; void* map_result = mmap(nullptr, mmap_size, PROT_READ, MAP_SHARED, fd, 0); if (map_result == MAP_FAILED) { close(fd); return false; } auto property_info_area = reinterpret_cast<PropertyInfoArea*>(map_result); if (property_info_area->minimum_supported_version() > 1 || property_info_area->size() != mmap_size) { munmap(map_result, mmap_size); close(fd); return false; } close(fd); mmap_base_ = map_result; mmap_size_ = mmap_size; return true; }
感觉和上面__system_property_area_init的逻辑重复了。不知道是不是多余的逻辑。没有看到更多的有用信息,先暂且不管。我们继续往下分析,找到属性值是如何存储到文件中的。
上面我们分析过,init.cpp中,调用完property_init之后,会调用export_kernel_boot_props。该函数也定义在system/core/init/init.cpp
中:
static void export_kernel_boot_props() { constexpr const char* UNSET = ""; struct { const char *src_prop; const char *dst_prop; const char *default_value; } prop_map[] = { { "ro.boot.serialno", "ro.serialno", UNSET, }, { "ro.boot.mode", "ro.bootmode", "unknown", }, { "ro.boot.baseband", "ro.baseband", "unknown", }, { "ro.boot.bootloader", "ro.bootloader", "unknown", }, { "ro.boot.hardware", "ro.hardware", "unknown", }, { "ro.boot.revision", "ro.revision", "0", }, }; for (const auto& prop : prop_map) { std::string value = GetProperty(prop.src_prop, prop.default_value); if (value != UNSET) property_set(prop.dst_prop, value); } }
这里看到了我们所关心的ro.serialno, 可见它是从内核启动参数ro.boot.serialno而来的。可见这个属性有特别之处。我们先不管这中特殊情况,继续分析下面的代码,看一下普通的属性是如何写入的。
上面我们分析过,init.cpp中,调用完export_kernel_boot_props之后,会调用property_load_boot_defaults,下面我们继续分析这个函数。
和property_init一样,该函数也定义在system/core/init/property_service.cpp
中:
void property_load_boot_defaults(bool load_debug_prop) { // TODO(b/117892318): merge prop.default and build.prop files into one // We read the properties and their values into a map, in order to always allow properties // loaded in the later property files to override the properties in loaded in the earlier // property files, regardless of if they are "ro." properties or not. std::map<std::string, std::string> properties; if (!load_properties_from_file("/system/etc/prop.default", nullptr, &properties)) { // Try recovery path if (!load_properties_from_file("/prop.default", nullptr, &properties)) { // Try legacy path load_properties_from_file("/default.prop", nullptr, &properties); } } load_properties_from_file("/system/build.prop", nullptr, &properties); load_properties_from_file("/vendor/default.prop", nullptr, &properties); load_properties_from_file("/vendor/build.prop", nullptr, &properties); if (SelinuxGetVendorAndroidVersion() >= __ANDROID_API_Q__) { load_properties_from_file("/odm/etc/build.prop", nullptr, &properties); } else { load_properties_from_file("/odm/default.prop", nullptr, &properties); load_properties_from_file("/odm/build.prop", nullptr, &properties); } load_properties_from_file("/product/build.prop", nullptr, &properties); load_properties_from_file("/product_services/build.prop", nullptr, &properties); load_properties_from_file("/factory/factory.prop", "ro.*", &properties); if (load_debug_prop) { LOG(INFO) << "Loading " << kDebugRamdiskProp; load_properties_from_file(kDebugRamdiskProp, nullptr, &properties); } for (const auto& [name, value] : properties) { std::string error; if (PropertySet(name, value, &error) != PROP_SUCCESS) { LOG(ERROR) << "Could not set '" << name << "' to '" << value << "' while loading .prop files" << error; } } property_initialize_ro_product_props(); property_derive_build_fingerprint(); update_sys_usb_config(); }
这个函数的逻辑也很清晰。
首先将系统各个位置的prop文件读取到std::map<std::string, std::string> properties
中。
非root手机是无法读取这些文件的:
ls -lh /system/etc/prop.default
-rw------- 1 root root 1.5K 2009-01-01 08:00 /system/etc/prop.default
ls -lh /system/build.prop
-rw------- 1 root root 7.7K 2009-01-01 08:00 /system/build.prop
这些文件只对root用户有读写权限。为了编译理解代码, 我找来一台root的手机, 来看下文件中的内容:
ro.mi.development=false
ro.miui.version.code_time=1567440000
ro.rom.zone=1
ro.miui.ui.version.code=8
ro.miui.ui.version.name=V10
ro.miui.has_security_keyboard=1
ro.fota.oem=Xiaomi
可见这些文件才是真正的属性。
将这些文件读取到std::map<std::string, std::string> properties
中之后, 会通过for循环调用PropertySet将所有属性写入。我们继续分析PropertySet。该函数定义在system/core/init/property_service.cpp
中:
static uint32_t PropertySet(const std::string& name, const std::string& value, std::string* error) { size_t valuelen = value.size(); if (!IsLegalPropertyName(name)) { *error = "Illegal property name"; return PROP_ERROR_INVALID_NAME; } if (valuelen >= PROP_VALUE_MAX && !StartsWith(name, "ro.")) { *error = "Property value too long"; return PROP_ERROR_INVALID_VALUE; } if (mbstowcs(nullptr, value.data(), 0) == static_cast<std::size_t>(-1)) { *error = "Value is not a UTF8 encoded string"; return PROP_ERROR_INVALID_VALUE; } prop_info* pi = (prop_info*) __system_property_find(name.c_str()); if (pi != nullptr) { // ro.* properties are actually "write-once". if (StartsWith(name, "ro.")) { *error = "Read-only property was already set"; return PROP_ERROR_READ_ONLY_PROPERTY; } __system_property_update(pi, value.c_str(), valuelen); } else { int rc = __system_property_add(name.c_str(), name.size(), value.c_str(), valuelen); if (rc < 0) { *error = "__system_property_add failed"; return PROP_ERROR_SET_FAILED; } } // Don't write properties to disk until after we have read all default // properties to prevent them from being overwritten by default values. if (persistent_properties_loaded && StartsWith(name, "persist.")) { WritePersistentProperty(name, value); } property_changed(name, value); return PROP_SUCCESS; }
这个函数中的核心代码其实很少。 首先通过__system_property_find来获取对应的prop_info, 但是因为是第一次存储属性, 肯定会返回nullptr, 所以会继续调用__system_property_add来增加属性。
我们继续分析__system_property_add函数,该函数定义在bionic/libc/bionic/system_property_api.cpp
中:
__BIONIC_WEAK_FOR_NATIVE_BRIDGE
int __system_property_add(const char* name, unsigned int namelen, const char* value,
unsigned int valuelen) {
return system_properties.Add(name, namelen, value, valuelen);
}
该函数又调用SystemProperties类的Add方法。该方法实现在bionic/libc/system_properties/system_properties.cpp
中:
int SystemProperties::Add(const char* name, unsigned int namelen, const char* value, unsigned int valuelen) { if (valuelen >= PROP_VALUE_MAX && !is_read_only(name)) { return -1; } if (namelen < 1) { return -1; } if (!initialized_) { return -1; } prop_area* serial_pa = contexts_->GetSerialPropArea(); if (serial_pa == nullptr) { return -1; } prop_area* pa = contexts_->GetPropAreaForName(name); if (!pa) { async_safe_format_log(ANDROID_LOG_ERROR, "libc", "Access denied adding property \"%s\"", name); return -1; } bool ret = pa->add(name, namelen, value, valuelen); if (!ret) { return -1; } // There is only a single mutator, but we want to make sure that // updates are visible to a reader waiting for the update. atomic_store_explicit(serial_pa->serial(), atomic_load_explicit(serial_pa->serial(), memory_order_relaxed) + 1, memory_order_release); __futex_wake(serial_pa->serial(), INT32_MAX); return 0; }
首先调用contexts_->GetPropAreaForName来获取prop_area,因为当前代码是在init中执行,init进程是root用户,所以肯定有权限获取prop_area。
根据前面的分析,我们已经知道一个prop_area其实就对应/dev/__properties__/
下面的一个文件。比如 ro.serialno属性对应的prop_area,就是/dev/__properties__/u:object_r:serialno_prop:s0
文件的内容映射(mmap)。
然后调用pa->add, 写入属性。可以认为这里就是真正将属性写入到prop_area中, 因为prop_area是/dev/__properties__/
下文件的映射, 所以会直接同步到文件中。
prop_area的实现比较复杂,使用了trie树,并不是普通的键值对, /dev/__properties__/
下的属性文件也不是普通文本文件,而是序列化的对象。所以我们就不再继续跟进代码了。大家可以看下bionic/libc/system_properties/include/system_properties/prop_area.h
中的介绍。
// Properties are stored in a hybrid trie/binary tree structure. // Each property's name is delimited at '.' characters, and the tokens are put // into a trie structure. Siblings at each level of the trie are stored in a // binary tree. For instance, "ro.secure"="1" could be stored as follows: // // +-----+ children +----+ children +--------+ // | |-------------->| ro |-------------->| secure | // +-----+ +----+ +--------+ // / \ / | // left / \ right left / | prop +===========+ // v v v +-------->| ro.secure | // +-----+ +-----+ +-----+ +-----------+ // | net | | sys | | com | | 1 | // +-----+ +-----+ +-----+ +===========+ // Represents a node in the trie.
还可以参考一下维基百科上面对trie树的介绍:
https://zh.wikipedia.org/wiki/Trie
到这里我们就已经分析完了init中对属性的初始化过程。总结一下包括如下几个步骤:
/dev/__properties__
目录。/dev/__properties__/property_info
文件中。/dev/__properties__/property_info
,创建各种数据结构。其中ContextsSerialized可以看做属性selinux上下文的管理者。它会创建一个ContextNode数组,每个ContextNode代表一个属性以及属性的selinux上下文信息。然后每个ContextNode在Open的时候,会创建对应的/dev/__properties__/${selinux context}
文件,为这个文件设置selinux context,然后将这个文件通过mmap映射到内存中,形成一个prop_area对象。/dev/__properties__/${selinux context}
文件的内存映射, 所以同时也会写到文件中。从上面的 App读取属性流程分析 一节中我们已经得知了一些属性拦截相关流程,再加上上一节对init中属性系统初始化的分析,我们对整个属性系统的细节已经有了更清晰的了解。
现在我们对App读取属性流程分析这一节进行一些总结:
/dev/__properties__/property_info
文件创建ContextNode数组。ContextNode中保存的是属性名和属性selinux context的对应关系。下面我们接着下面的代码来分析:
const prop_info* SystemProperties::Find(const char* name) {
if (!initialized_) {
return nullptr;
}
prop_area* pa = contexts_->GetPropAreaForName(name);
if (!pa) {
async_safe_format_log(ANDROID_LOG_ERROR, "libc", "Access denied finding property \"%s\"", name);
return nullptr;
}
return pa->find(name);
}
首先进入ContextsSerialized的GetPropAreaForName方法,实现在bionic/libc/system_properties/contexts_serialized.cpp
中:
prop_area* ContextsSerialized::GetPropAreaForName(const char* name) { uint32_t index; property_info_area_file_->GetPropertyInfoIndexes(name, &index, nullptr); if (index == ~0u || index >= num_context_nodes_) { async_safe_format_log(ANDROID_LOG_ERROR, "libc", "Could not find context for property \"%s\"", name); return nullptr; } auto* context_node = &context_nodes_[index]; if (!context_node->pa()) { // We explicitly do not check no_access_ in this case because unlike the // case of foreach(), we want to generate an selinux audit for each // non-permitted property access in this function. context_node->Open(false, nullptr); } return context_node->pa(); }
该方法中, 会根据属性名name, 获取对应的ContextNode对象, 通过调用pa方法获取一个prop_area指针,如果为空指针,则会调用Open来初始化这个prop_area指针,然后再次调用pa方法获取prop_area指针,并返回这个指针。
我们已经知道,因为没有权限读取属性,返回的prop_area肯定是空指针。也就是说,在Open方法中,无法成功创建这个prop_area对象。
Open方法有两个参数:
下面我们去看一下Open方法为什么无法创建prop_area对象,该方法实现在bionic/libc/system_properties/context_node.cpp
中:
bool ContextNode::Open(bool access_rw, bool* fsetxattr_failed) { lock_.lock(); if (pa_) { lock_.unlock(); return true; } // 格式为 /dev/__properties__/u:object_r:serialno_prop:s0 char filename[PROP_FILENAME_MAX]; int len = async_safe_format_buffer(filename, sizeof(filename), "%s/%s", filename_, context_); if (len < 0 || len >= PROP_FILENAME_MAX) { lock_.unlock(); return false; } if (access_rw) { pa_ = prop_area::map_prop_area_rw(filename, context_, fsetxattr_failed); } else { pa_ = prop_area::map_prop_area(filename); } lock_.unlock(); return pa_; }
首先根据filename_, context_两个参数拼接出filename, 拼接出的filename的格式为/dev/__properties__/${selinux context}
, 比如/dev/__properties__/u:object_r:serialno_prop:s0
。
因为access_rw为false, 会通过prop_area::map_prop_area来获取prop_area指针。下面我们分析一下这个方法。该方法实现在bionic/libc/system_properties/prop_area.cpp
中
prop_area* prop_area::map_prop_area(const char* filename) {
int fd = open(filename, O_CLOEXEC | O_NOFOLLOW | O_RDONLY);
if (fd == -1) return nullptr;
prop_area* map_result = map_fd_ro(fd);
close(fd);
return map_result;
}
逻辑比较简单。 首先通过open打开/dev/__properties__/${selinux context}
文件,然后将该文件映射成prop_area。
之所以会无法获取prop_area对象,是因为open打开文件失败了。因为没有权限访问这个文件。回顾一下init初始化属性系统的时候,会创建这个文件, 并设置selinux上下文。比如:
文件 /dev/__properties__/u:object_r:serialno_prop:s0
对应的selinux context为 u:object_r:serialno_prop:s0
我们看一下Android中定义的和 u:object_r:serialno_prop:s0
相关的策略,定义在system/sepolicy/prebuilts/api/29.0/public/domain.te
文件中:
# Do not allow reading device's serial number from system properties except form # a few whitelisted domains. neverallow { domain -adbd -dumpstate -fastbootd -hal_camera_server -hal_cas_server -hal_drm_server -init -mediadrmserver -recovery -shell -system_server -vendor_init } serialno_prop:file r_file_perms;
这个策略的意义是:除了init,adbd等特殊的域(domain)之外,其他域无法访问selinux context为serialno_prop的文件。
这里的域可以认为是selinux给进程设置的一个标签,每个进程有不同的标签, 每个标签对应不同的策略, 所以就可以实现不同的进程访问不同的文件。
通过ps -Z可以显示进程的selinux域:
我们看一下init进程的域为 init:
1901:/ $ ps -eZ | grep init
u:r:init:s0 root 1 0 2221092 3296 0 0 S init
adbd进程的域为adbd:
1901:/ $ ps -eZ | grep adb
u:r:adbd:s0 shell 12956 1 2323304 4588 0 0 S adbd
所以跟根据selinux策略, 他们可以访问/dev/__properties__/u:object_r:serialno_prop:s0
读取序列号。
再看一下普通App的selinux域:
1901:/ $ ps -eZ | grep com.smile.gifmaker
u:r:untrusted_app_27:s0:c512,c768 u0_a299 6305 842 10498244 476308 0 0 S com.smile.gifmaker
u:r:untrusted_app_27:s0:c512,c768 u0_a299 6557 842 6627940 193148 0 0 S com.smile.gifmaker:messagesdk
u:r:untrusted_app_27:s0:c512,c768 u0_a299 6773 842 6705184 202628 0 0 S com.smile.gifmaker:pushservice
1901:/ $ ps -eZ | grep com.tencent.mobileqq
u:r:untrusted_app_27:s0:c512,c768 u0_a296 6094 845 1639688 150228 0 0 S com.tencent.mobileqq:MSF
u:r:untrusted_app_27:s0:c512,c768 u0_a296 6117 845 1668920 203432 0 0 S com.tencent.mobileqq
1901:/ $ ps -eZ | grep com.ss.android.ugc.aweme
u:r:untrusted_app_29:s0:c41,c257,c512,c768 u0_a297 8504 842 11194652 473736 0 0 S com.ss.android.ugc.aweme
u:r:untrusted_app_29:s0:c41,c257,c512,c768 u0_a297 8888 842 6901604 213232 0 0 S com.ss.android.ugc.aweme:push
u:r:untrusted_app_29:s0:c41,c257,c512,c768 u0_a297 8964 842 6903492 214656 0 0 S com.ss.android.ugc.aweme:pushservice
普通App的selinux域都是untrusted_app,根据selinux策略, 普通App是无法访问/dev/__properties__/u:object_r:serialno_prop:s0
文件的,也就无法读取文件中的ro.serialno属性。
到此为止, 我们就明白了Android是如何通过selinux来保护ro.serialno属性不被随意读取的。
不光是ro.serialno属性,其他属性也会有独立的selinux策略来保护。
可见,Android对用户隐私数据的保护力度越来越大了,也越来越细。
本文我们主要通过跟踪init初始化属性系统和App读取属性这两个关键流程,来对系统属性进行了较为全面的分析,厘清了为什么普通App无法读取关键属性。同时对属性系统中的关键细节进行了分析, 包括一些关键目录,关键文件,关键对象和关键数据结构。
frameworks/base/core/java/android/os/Build.java
frameworks/base/core/res/AndroidManifest.xml
frameworks/base/services/core/java/com/android/server/os/DeviceIdentifiersPolicyService.java
frameworks/base/core/java/android/os/SystemProperties.java
frameworks/base/core/jni/android_os_SystemProperties.cpp
system/core/base/properties.cpp
system/core/base/include/android-base/properties.h
bionic/tools/versioner/current/sys/cdefs.h
bionic/libc/bionic/system_property_api.cpp
bionic/libc/system_properties/include/system_properties/system_properties.h
bionic/libc/system_properties/system_properties.cpp
bionic/libc/system_properties/contexts_serialized.cpp
system/core/property_service/libpropertyinfoparser/property_info_parser.cpp
system/core/init/init.cpp
system/core/init/property_service.cpp
system/sepolicy/private/property_contexts
bionic/libc/bionic/system_property_api.cpp
bionic/libc/system_properties/context_node.cpp
bionic/libc/system_properties/prop_area.cpp
bionic/libc/bionic/system_property_api.cpp
bionic/libc/system_properties/include/system_properties/prop_area.h
system/sepolicy/prebuilts/api/29.0/public/domain.te
该文中引用的源码是Android 10的,不同的Android版本可能会有差异
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。