首页
社区
课程
招聘
[原创]Android 10属性系统原理,检测与定制源码反检测
发表于: 2022-8-20 22:32 46836

[原创]Android 10属性系统原理,检测与定制源码反检测

2022-8-20 22:32
46836

Android属性是Android系统中重要的设备与运行时信息的来源,也是改机和设备指纹收集绕不开的一个方向,本文就针对我对属性系统的理解,对属性改机的检测与反检测做一个总结.本文代码基于aosp 10.

属性由键值对组成,属性的值有类型,比如string,int,bool等,属性还关联着selinux上下文,因为属性是一种重要的资源,所以由selinux保护,由selinux的策略决定了哪些域可以读取/写哪些属性。

Android属性本质上是进程间通信的一种方式,由init进程创建/dev/__properties__目录,并在此目录下创建和属性相关的文件,然后init进程会调用带着MAP_SHARED标志的mmap()将这些文件映射到vm_area_struct,对属性的初始化和修改会反映到这些文件中去.其他需要读取和修改属性的进程也会调用带着MAP_SHARED标志的mmap()将这些文件映射到自己的vm_area_struct,因此本质上就是共享内存的方式。

在设计上基于安全的考虑,只有init进程可以初始化属性系统,即在/dev/__properties__目录下创建文件,也只有init进程可以修改属性,其他进程想修改属性需要通过/dev/socket/property_service这个socket文件向init进程发起通信请求,而且并不是所有进程都有权限访问这个socket文件,普通的app进程所在的untrusted_app域就是无法访问的,因此普通的app进程无法修改属性。

属性大致分为:
ro开头的只读属性,persist开头的持久化属性,ctl开头的控制属性,selinux.restorecon_recursive属性与普通属性。这么区分是因为init进程中对这些属性有不同的处理逻辑,后面会详细描述不同类型属性的差异。

基于效率的考虑,Android属性在属性文件格式的组织上经过了精心的设计,对于频繁的获取某一属性这种场景Android还提供了属性缓存的机制,这个机制也是改机的一个障碍。

进程间通信必然需要某种同步机制,Android中的属性通过包装futex系统调用实现了属性的同步功能。

系统提供了getprop工具可以获取所有属性值,调用的时候加上-Z参数可以获取属性对应的selinux上下文,加上-T参数可以获取属性值对应的类型。

Android属性的实现核心代码放在了bionic libc中,有两个重要的头文件:<sys/system_properties.h><sys/_system_properties.h>描述了属性的api,这两个文件都处于路径bionic/libc/include/sys/下,这两个头文件不论是系统代码还是基于ndk开发的代码都是可以包含的,只不过包含<sys/_system_properties.h>文件的时候需要定义"#define _REALLY_INCLUDE_SYS__SYSTEM_PROPERTIES_H_",不然编译会报错,这是因为<sys/_system_properties.h>文件开头明确了:

从这里也可以看出系统的开发者不想让你直接包含<sys/_system_properties.h>文件使用里边的接口,而最好使用<sys/system_properties.h>文件中的接口。

这个文件包含的接口主要是设置属性,遍历属性以及读取属性的值,涵盖了正常使用属性系统的需求,其中有些接口需要prop_info结构体,这个结构体表示了属性值所在的区域.只不过调用接口的人无需关心prop_info结构体里边的细节,只需要调用一个接口获取prop_info指针,再将该指针传递给另外的接口获取更多信息。

以下是接口描述:

设置name属性的值为value,它的内部实现是通过/dev/socket/property_service这个socket文件向init进程发起通信请求,因此必须是selinux策略明确同意的进程才有权限发起通信,selinux策略同意设置属性的宏为set_prop,定义在te_macros文件中,这个宏其中之一的作用是允许指定的域connectto init进程域的unix_stream_socket文件.在system_app.te文件中指定了system_app可以设置的一些属性:
set_prop(system_app, bluetooth_audio_hal_prop)
set_prop(system_app, bluetooth_prop)
set_prop(system_app, debug_prop)
set_prop(system_app, system_prop)
表明system_app域中的进程既可以读取u:object_r:bluetooth_audio_hal_prop:s0上下文的属性值,也可以写这些上下文的属性值。
普通的app进程无法调用此函数,会报:
type=1400 audit(0.0:901): avc: denied { write } for name="property_service" dev="tmpfs" ino=25226 scontext=u:r:untrusted_app:s0:c107,c256,c512,c768 tcontext=u:object_r:property_socket:s0 tclass=sock_file permissive=0 app=xxx.xxx.xxx

这两个函数是一对,先调用__system_property_find()函数获取对应属性值的区域结构体指针prop_info*,然后调用__system_property_read_callback()以回调的方式接受属性值,属性值输出参数为value,其中cookie参数由调用方任意设置,最终调用callback的时候会将cookie再传递给回调函数。getprop程序获取单个属性值就是通过调用这两个函数实现的。
将获取属性拆分成两个函数是为了实现属性缓存机制,先将prop_info指针缓存起来,下次再获取属性值直接调用__system_property_read_callback()就行了,bionic/libc/private/CachedProperty.h提供了这种缓存机制,这种机制可以提高属性值获取效率。
其中__serial变量是用于标识属性值有没有变化的一个标识,它和属性的同步机制有关,后面会详细描述。

用于遍历所有属性值,进程只能遍历到自己有权限读取的属性值,对于进程有权限读取到的每一个属性都会回调一次callback,在回调函数中通过得到的const prop_info* pi再调用__system_property_read_callback()函数就可以得到属性值.getprop程序获取所有的属性值就是通过这个函数实现的:

用来等待参数const prop_info* pi的serial成员不再为old_serial值,变化以后的新值放在new_serial_ptr参数中,并且可以指定relative_timeout为等待的超时时间,__relative_timeout为null的时候表示永久等待.
这个接口可以用来实现这样的一个功能: 等待某个属性值变化成为一个新值,具体的代码可以看system/core/base/properties.cpp文件的WaitForProperty()函数。

__system_property_read_callback()的作用差不多,只不过__system_property_read_callback()更灵活,所以这也可能是__system_property_read遭到废弃的原因。

一步就可以获取__name参数对应的属性值,虽然此函数标记为废弃,但事实上Android框架中仍然在使用这个函数。

遍历所有的属性,得到第n个属性对应的prop_info*,system_property_foreach完全可以替代这个函数,事实上Android框架中已经不再使用这个函数了。

这个文件中的函数基本上都是系统在使用,所以一般不会使用到这个头文件:

被废弃的函数,实现只是简单的返回-1。

专门给init进程调用的用于初始化属性区域的函数,调用此函数会以O_RDWR | O_CREAT标志打开/dev/__properties__目录下的文件,而这需要root权限。

获取"global serial"用于实现缓存机制,上面介绍了一个比较常用的函数__system_property_find,它其实是一个比较耗时的函数,__system_property_area_serial函数的目的就是为了减少对__system_property_find函数的调用而将prop_info指针缓存起来,具体的代码在bionic/libc/private/CachedProperty.h文件中,"global serial"存放在/dev/__properties__/properties_serial文件中。

专门给init进程调用的用于添加属性的函数.因为调用这个函数需要root权限才能成功。

专门给init进程调用的用于更新属性的函数.因为调用这个函数需要root权限才能成功。

返回某个属性区域的serial值,当prop_info的serial值发生了变化,就表示属性值发生了变化,此时可以重新调用__system_property_read_callback以获取更新后的属性值,代码同样见上面的CachedProperty.h文件的Get函数。

由普通进程调用的初始化属性区域的函数,它的实现大致可以总结为就是open,mmap /dev/__properties__目录下的文件,只不过这些文件都是-r--r--r-- root root权限,因此普通进程只有读取的权限.需要使用属性系统的进程需要调用该函数。

废弃函数,由__system_property_wait取代。

这两个头文件定义的API讲完了,就可以总结一下属性的基本原理 :
init进程主要使用的是<sys/_system_properties.h>文件定义的接口,init进程在系统启动的时候负责创建/dev/__properties__目录下的文件,调用__system_property_area_init把文件映射到init进程的地址空间,并且通过<sys/_system_properties.h>定义的接口设置一些内置的属性,内置属性来源于系统编译时指定的属性文件以及内核命令行以及代码明确设置的属性.然后init开启socket等待接收其他进程设置属性的请求。

普通的进程主要使用的是<sys/system_properties.h>文件定义的接口,进程会在启动后调用__system_properties_init打开/dev/__properties__目录下的文件并映射到自己进程的地址空间(只读映射),然后通过<sys/system_properties.h>定义的接口来获取属性相关信息。

那么这两个头文件的实现在哪呢?实现由类SystemProperties表示,这个类定义在bionic/libc/system_properties/include/system_properties/system_properties.h文件中,创建这个类对象的文件位于bionic/libc/bionic/system_property_api.cpp,在这个文件中依次将上述接口的实现分发到SystemProperties类。

由于属性的头文件和定义都处于libc中,因此只要使用了libc.so的程序都可以使用这些接口,事实上普通进程的属性初始化动作__system_properties_init就是由libc自己调用的,在bionic/libc/Android.bp文件中,libc.so编译的时候依赖于静态库libc_init_dynamic,这个静态库的源码文件libc_init_dynamic.cpp中有这样的初始化语句:

指明了so的初始化函数,这个函数最终在运行前由linker调用,在这个函数中会调用__libc_init_common,而在__libc_init_common函数中会最终调用__system_properties_init函数。

由于所有app进程都是由zygote fork并且"异化"出来的,zygote虚拟机启动的时候libc会调用__system_properties_init函数,因此所有java进程也同样的映射了/dev/__properties__目录下的文件,且映射区域的起始地址和大小和zygote一样,这一点可以通过cat /proc/pid/maps | grep __prop | sort得到印证。

如果阅读过linker的代码,会发现在linker_main函数中也会调用__system_properties_init函数,这个调用在libc调用之前,按照__system_properties_init函数实现的逻辑,调用两次会有问题.这是怎么回事呢?

事实上linker不可能再依赖于其他共享库,这个__system_properties_init函数最终会被扩展成__dl___system_properties_init,所以如果通过ida分析linker的话,会发现linker_main()中调用的__dl___system_properties_init,它初始化的是linker程序中.bss段的变量__dl__ZL17system_properties,因此这个调用和上面的libc中__system_properties_init的调用是不同的,并不会互相影响。添加前缀的逻辑在linker/Android.bp文件中的:

由于App进程有权限直接打开/dev/__properties__目录下的文件并且自己做解析,所以它并不一定非要使用<sys/system_properties.h>文件定义的接口,完全可以按照接口的实现自己写代码来实现属性操作,这也就可以绕开很多在接口层次做手脚的改属性机制。

对于系统层次的一些和属性相关的接口,基于上都是依靠<sys/system_properties.h>和<sys/_system_properties.h>实现的。

下面分析一下属性系统的源码,从init进程开始,它主要负责创建和初始化属性区域.
查看/dev/__properties__目录下的文件,发现主要有三种文件:property_info,properties_serial以及很多以selinux上下文命名的文件

总结起来就是SystemProperties对象成员变量为ContextsSerialized contexts,contexts有成员变量ContextNode contextnodes表示每一个selinux上下文,每个ContextNode又有proparea* pa成员变量表示对应的属性文件内容mmap的内存表示。

属性初始化函数在init进程的SecondStageMain函数中被调用,init.cpp/SecondStageMain():

在property_init函数中,首先调用CreateSerializedPropertyInfo(),它做的事情如下:

了解了CreateSerializedPropertyInfo()做的事情以后,来看一下上面操作的细节.
上面解析的文件指明了属性和selinux上下文对应关系,只不过有三种不同的情况,看文件内容示例:

解析的时候会将信息放入结点,结点由TrieBuilderNode类表示,它有如下几个成员变量:

builderroot为根结点,它的属性值为root,selinux上下文为u:object_r:default_prop:s0。

当解析到dalvik.vm.boot-dex2oat-threads时,会形成如下的父子结点关系(每个结点都是TrieBuilderNode类型),子结点存放在父结点的children_列表中:
root -> dalvik -> vm,然后由于dalvik.vm.boot-dex2oat-threads带有exact指示,所以boot-dex2oat-threads成为了一个PropertyEntryBuilder对象存放在了vm结点的exactmatches列表中。

当解析到net.cdma时,会形成如下的父子结点关系:
root-> net,由于net.cdma不以.结尾,所以它属于前缀属性,cdma成为了一个PropertyEntryBuilder对象存放在了net结点的prefixes_列表中。

当解析到net.时,会形成如下的父子结点关系:
root -> net,然后由于它以.结尾,所以net成为了一个PropertyEntryBuilder对象(上下文为u:object_r:system_prop:s0)存放于net结点自身的propertyentry成员变量。

按上面的规则把所有文件解析完毕以后,TrieBuilder类表示的树就形成了完整的结构。

接下来就调用类TrieSerializer的SerializeTrie()函数来实现将TrieBuilder序列化
序列化后的头由PropertyInfoAreaHeader结构体表示,紧接着PropertyInfoAreaHeader头结构后面的数据就是TrieBuilder序列化后的数据:

序列化TrieBuilder的过程还是有点复杂的,位于TrieSerializer::WriteTrieNode(const TrieBuilderNode& builder_node)函数,序列化的过程中还会对children,prefixes和exactmatches数组进行排序方便后面二分查找,其实我们知道反序列肯定可以还原得到类似于TrieBuilder结构就行了。

经过上面的步骤我们就可以生成了/dev/__properties__/property_info文件,如果想查找某个属性的值,首先需要通过这个文件查询出属性对应的selinux上下文,再通过和上下文同名的文件查询出属性的值。

下面我们再看一下/dev/__properties__/property_info文件的反序列化过程,并且看一下如何根据某个属性的值查询它的selinux上下文。

解析/dev/__properties__/property_info文件Android提供了帮助类,位于system/core/property_service/libpropertyinfoparser目录,其中property_info_parser.h头文件定义了和一些和反序列相关的类,其中PropertyInfoArea类就定义了一些读取出property_info文件信息的函数.看一下它的

调用GetPropertyInfo()函数就可以得到property属性对应的上下文和类型信息,分别做为context和type输出。

PropertyInfoArea::GetPropertyInfo主要通过调用PropertyInfoArea::GetPropertyInfoIndexes函数来得到属性的selinux上下文在所有上下文列表的下标,得到下标以后自然就可以得到上下文字符串了。

PropertyInfoArea::GetPropertyInfoIndexes函数逻辑如下:
首先出发点为树的root结点,然后将属性以.分割为不同的部分,顺着每个部分查找根结点下的子结点,再在子结点下查找下一级子结点,有点像给定一个路径从根路径一步步查找到包含该路径的目录的过程.由于子结点都是排过序的,因此查找都是二分查找.
一直查找到叶子结点(比如如果我们查询的是ro.hardware.egl,egl就为叶子结点的名称),在包含叶子结点的父结点的exactmatches列表中匹配是否有元素和叶子结点名称相同,如果有则直接返回这个元素的下标.否则就在包含叶子结点的父结点的prefixes_列表中匹配是否有元素和叶子结点名称相同,如果还不能满足则以父结点的selinux上下文类型为准。

比如对上面的示例来说net.abc的selinux上下文就为u:object_r:system_prop:s0
net.cdma.abc的selinux上下文就为u:object_r:net_radio_prop:s0

说完了CreateSerializedPropertyInfo,再回到init进程的property_init()函数,接下来调用的__system_property_area_init函数:

接下来会调用property_info_area.LoadDefaultPath(),和上面的第1步执行的操作一样,只不过操作对象是init进程中的static PropertyInfoAreaFile property_info_area;

至此property_init()函数结束,属性区域的初始化就完成了,此时所有属性对应的上下文已经确定,并且属性文件已经创建出来,只不过此时属性文件是空的。

接下来init进程就会调用uint32_t (*property_set)(const std::string& name, const std::string& value)函数来设置属性了,属性的来源主要有如下几方面:

其中系统文件占据属性绝大部分的来源,它由编译时根据编译选项和各个产品的配置mk文件生成。

property_set函数最终会调用到property_service.cpp文件的PropertySet函数:

一开始属性是不存在的,因此会调用__system_property_add函数:

prop_area结构描述了具有同一个selinux上下文的所有属性区域,它的成员变量为:

prop_area区域的属性区域由变种的字典树/二叉搜索树组成,树的每个结点类型为prop_bt:

prop_bt代码上面的注释也说明了:
假设有ro.secure=1属性

再来看一下prop_info结构:

再回到prop_area类的bool add(const char* name, unsigned int namelen, const char* value, unsigned int valuelen)函数,这个函数用来往prop_area中添加属性,也是二叉搜索树中的插入操作,理解这个操作以后遍历和更新属性也就好理解了,因为它们就是二叉搜索树的遍历和更新操作而已:
增加属性的prop_area::add函数调用的是find_property函数,查找属性的prop_area::find函数调用的也是find_property函数,两者主要区别在于最后一个参数bool alloc_if_needed,alloc_if_needed为true时,查找不到对应结点的时候会创建结点。

回到init进程的SecondStageMain()函数,接下来调用StartPropertyService函数开启socket等待其他进程的设置属性请求:PROP_MSG_SETPROP和PROP_MSG_SETPROP2,至此App就可以使用属性系统了。

普通APP对于/dev/__properties__/properties_serial/dev/__properties__/property_info都是有权限读取的,但是以selinx上下文命名的文件并不是所有的文件app都可以读取,这也就表示有的属性app是无法读取的。在app遍历属性的时候会调用ContextNode::CheckAccessAndOpen函数依次检查并打开所有的/dev/__properties__目录下的selinux上下文文件,对于selinux禁止读取的文件该函数返回false,app就遍历不到对应的属性。app有权限读取的属性在system/sepolicy/public/domain.te中指定,如:

上面提到了属性的几种分类,下面看一下分类的不同含义:

由于属性是进程间共享的资源,而不同的进程可以读写属性,因此属性需要一种同步机制。属性同步依赖于futex系统调用,futex系统调用的第一个参数需要利用mmap机制在各个进程间共享,在属性实现中有如下几个变量提供这个参数:

serial类型都为atomic_uint_least32_t,原子变量。

futex系统调用不仅可以实现多线程的事件通知机制,也可以实现互斥锁机制,bionic库封装了这个系统调用,比如futex_wait,futex_wake。

对于第1种情况/dev/__properties__/properties_serial文件中的serial,初始值为0,当有属性被添加/更新时,serial会自增1,并且通过futex_wake唤醒所有等待着的进程,这些等待的进程是通过调用`system_propertywait`函数(第一个参数为nullptr就可以等待这个serial)从而调用__futex_wait进行等待的。由于属性被添加/更新时serial_会变化,所以__system_property_wait函数可以实现等待任意属性添加/更新的功能。serial_就是"global serial"。

对于第2种情况prop_info结构的成员变量serial初始值为属性值长度<<24,当更新了某个属性值的时候先将serial值|=1通知其他进程正在修改此属性,修改完属性以后将serial的值设置为(len << 24) | ((serial + 1) & 0xffffff),然后通知其他所有等待serial值发生变化的进程。

假设一个进程正在修改某个属性,而另外一个进程要读取这个属性会发生什么?
看一下system_properties.cpp文件中SystemProperties::Get函数对同步的处理:

上面说完了属性的原理,下面谈一下我对属性改机的理解,属性改机最好是只针对某些app,只有这些app看到的属性是修改过的,不影响全局的属性,全局的属性如果修改了很可能系统工作就不正常了。且只针对某些app修改属性,修改属性的控制逻辑也比较好设计,可以更方便的实现一键改机,不需要重启机器。
先来看一下属性改机检测,主要分为两方面:

这里说的主要是第一个方面:检测属性实现机制有没有问题,主要检测点有以下:

先来看第1条检测selinux,厂商开启了selinux以后android系统部分一般都会遵守谷歌制定的策略,先调用getenforce判断selinux开启状态,然后去访问公开的被selinux策略禁止的文件,如果可以访问就表示selinux实现有问题,比如/dev/socket/property_service这个socket文件app是没办法open的,有一些属性文件selinux策略也是禁止访问的,系统中还有很多的文件可以用来做这种检测。读取/proc/self/mounts判断selinuxfs有没有挂载到/sys/fs/selinux。

最后一种方式可以把系统和属性相关的代码拷贝到jni中,自己生成接口去获取属性,完全不依赖于系统的运行时。看下图:

有很多改属性的逻辑都会将/dev/__properties__下的文件进行IO重定向,你读取/dev/__properties__下的文件,偷梁换柱让你读取到的是其他目录下的文件。IO重定向有很多种实现方式,这里检测其中的一些。

有些IO重定向会直接修改内核do_sys_open函数,调用do_filp_open函数的时候传递另外一个filename过去,虽然可以实现IO重定向,但会在/proc/pid/fd下面暴露自己,打开文件的fd会指向被重定向的文件,检测手段:

然后调用

逻辑是:存在这种IO重定向时,当你打开/proc/cpuinfo的时候,给你重定向到/xxx文件,假设open("/proc/cpuinfo")得到的fd为5,此时调用readlink("/proc/pid/fd/5")如果得到的路径是/xxx而不是/proc/cpuinfo,表示有问题。

检测通过open和openat得到的文件内容是否相同,有些重定向判断某个文件是否需要重定向是通过检查内核里边do_sys_open的filename参数来实现的而忽略了dfd参数,此时将路径分隔开调用openat,如果得到的内容和调用open得到的内容不同则表明有问题。

调用: checkOpenAt("/proc/cpuinfo", "/proc", "cpuinfo")

上面的检查可以很大程度上检测出属性是否被改机。

那么如何实现自己的一套改属性的方案呢?
需要实现的目标如下:

在这里说一下我的实现方案:
要想绕过上面的所有检测,那么这个属性必须做的和真实的属性一样:selinux上下文是匹配的,属性区域也是存在并且匹配的,所有接口结果一致,除了里边内容是假的所有行为都和系统属性没什么差别。

先准备一份属性文件以及/system/etc/selinux/plat_property_contexts,/vendor/etc/selinux/vendor_property_contexts文件,这三个文件可以从真实的手机比如华为,oppo手机中获取。后面这两个文件需要先用脚本处理一下将厂商定义的selinux上下文修改为改机手机上已存在的selinux上下文,不然就得修改selinux策略文件添加这些上下文。

需要构建一个新的属性目录,假设为/dev/xxx/__properties__,init进程既初始化/dev/__properties__区域也初始化/dev/xxx/__properties__区域
<sys/system_properties.h>和<sys/_system_properties.h>头文件需要准备两份拷贝,我这里命名为system_properties_ext.h和_system_properties_ext.h,里边所有的函数都添加_ext后缀。

有了接口以后还需要准备一份接口实现文件system_property_api_ext.cpp,实现类为static SystemProperties system_properties_ext;这个类的实现依赖路径从/dev/__properties__修改为/dev/xxx/__properties__
有了接口和实现类以后,init进程就可以利用新的接口初始化新区域:

CreateSerializedPropertyInfoExt函数,__system_property_area_init_ext函数都已经切换为/dev/xxx/__properties__区域。

init_ext_properties函数用来设置属性的值,实现为解析上面准备的属性文件,调用InitPropertySetExt函数设置属性:

property_init_ext函数调用以后新的区域/dev/xxx/__properties__中的属性就可以工作了。此时可以复制getprop程序,将其中所有的接口修改为加上_ext后缀,然后编译,调用这个程序如果能正确显示出改过的属性以及selinx上下文信息,表示新的属性区域是没有问题的。

接下来处理需要改机的app,上面说过zygote中已经调用过__system_properties_init初始化了属性区域,其他所有的app进程都是被zygote fork出来的,因此app继承了和zygote相同的属性区域,此时我们需要将app的属性区域切到新的属性区域/dev/xxx/__properties__下而且不能在mmap中留下痕迹。这里采用的机制是unshare+bind mount:
com_android_internal_os_Zygote.cpp文件的SpecializeCommon函数是fork一个新的app进程即将执行的"异化"函数,这个函数前仍然是以root权限运行,后面才放弃了权能,权限资源。我选择在这个函数动手,调用新的函数bind_mount_early:

需要先判断是否为AID_WEBVIEW_ZYGOTE,webview zygote不需要处理。

调用unshare(CLONE_NEWNS):正常的app会在下面的MountEmulatedStorage函数中调用unshare(CLONE_NEWNS)创建出新的mount namespace,这里我把unshare调用提前,为下面的bind mount做准备,将/dev/xxx/__properties__ bind mount到/dev/__properties__。这样这个app的/dev/xxx/__properties__就会被重定向到/dev/__properties__,而且由于创建了新的mount namespace,所以只会对当前进程生效。

接着调用函数重新初始化属性区域:

__system_properties_abandon是我新添加的新函数为了释放掉之前属性区域的所有内存映射,接着调用__system_properties_init,这个函数是原先系统提供的函数,由于已经实现了bind mount,所以其实初始化的是/dev/xxx/__properties__区域,只不过对app来说它感知不到这点。

需要额外处理android.os.Build类,这个类初始化的比SpecializeCommon要早很多,我这里在ActivityThread类的handleBindApplication方法中处理:
判断如果当前进程需要修改属性,则用反射将Build类中的所有属性更新。

由于bind mount也会有特征: /proc/pid/mountinfo,/proc/pid/mounts,/proc/pid/mountstats中会显示出/dev/__properties__经过了Bind mount,因此需要修改内核去除该特征:
/proc文件系统中显示出这三个文件的逻辑最终都会走到fs/seq_file.c的seq_path_root函数中,其中p = __d_path(path, root, buf, size)返回的指针p就表示着目标bind点的字符串,只要修改这个字符串即可实现去除bind mount特征,比如判断如果p为/dev/__properties__,就修改p为其他字符串即可。

经过上面的调整基本上就实现了预期的目标,但是实际上还有遇到以下问题:

第1条报错的原因是因为app的RenderThread线程使用了libhwui,libhwui依赖于opengl实现,opengl实现则依赖于原始的属性,它根据这些属性来查找厂商提供的opengl实现,具体的逻辑在系统的egl分发库EGL/Loader.cpp文件中。opengl实现还会依赖于gralloc实现,这个实现也是依赖于原始属性的。因此如果读取到了修改后的属性就会出错。

不论是Android还是Linux,都有一套opengl壳用来封装厂商提供的opengl/egl实现,壳会有自己的查找规则加载厂商的实现库。Android系统它的实现位于frameworks/native/opengl/libs/EGL目录,对于Linux来说使用的一般是libglvnd: the GL Vendor-Neutral Dispatch library。Android查找opengl实现用到的属性主要是:
ro.hardware.egl
ro.board.platform

而对于查找gralloc实现主要和以下几个属性有关:
ro.hardware.gralloc
ro.hardware
ro.product.board
ro.board.platform
ro.arch

解决方案:
RenderThread是系统在app启动过程中创建的线程,创建点为ActivityThread类的handleLaunchActivity函数中的HardwareRenderer.preload();调用。
如果可以让系统创建出的线程读取到的属性是原始属性,而App创建出来的线程读取到的是改过的属性就可以解决这个问题了。
pthread的线程局部存储机制pthread_once,pthread_setspecific,pthread_getspecific正好可以实现:
system_properties.h文件创建接口:

并创建static pthread_key_t my_pthread_key变量,在获取属性的时候判断调用者线程是否设置了线程局部存储,如果设置了就让调用者线程获取的所有属性都是原始的属性。获取原始属性又有两个方案:创建一个新的通道可以从init进程获取原始属性的值或者在__system_properties_abandon之前保存原始属性的值。这里我采用了后者,因为第一种方案selinux是不允许的,原则上尽量不去修改selinux策略。
然后在RenderThread启动以后调用__system_property_pthread_setspecific函数,这样就解决了第1个问题。

第2条报错原因是CachedProperty类实现了属性的缓冲机制,__system_properties_abandon释放掉之前属性区域的内存映射以后,CachedProperty还保存着之前的prop_info指针,解决方案就是判断如果已经释放了内存区域就清空CachedProperty类的prop_info_,cached_area_serial_以及cached_property_serial_成员。

经过上面对源码的修改,就可以通过之前所有的测试,且验机软件也会显示被修改后的属性:-)

ps:某大厂app会提前在别的线程中初始化egl,遇到这种报错直接在eglInitialize函数中调用__system_property_pthread_setspecific即可。

能力有限,如果有什么遗漏或者不准确的地方,欢迎一起讨论!

 
 
 
 
 
 
#ifndef _REALLY_INCLUDE_SYS__SYSTEM_PROPERTIES_H_
#error you should #include <sys/system_properties.h> instead
#endif
#ifndef _REALLY_INCLUDE_SYS__SYSTEM_PROPERTIES_H_
#error you should #include <sys/system_properties.h> instead
#endif
 
int __system_property_set(const char* __name, const char* __value);
int __system_property_set(const char* __name, const char* __value);
const prop_info* __system_property_find(const char* __name);
void __system_property_read_callback(const prop_info* __pi,
    void (*__callback)(void* __cookie, const char* __name, const char* __value, uint32_t __serial),
    void* __cookie)
`
const prop_info* __system_property_find(const char* __name);
void __system_property_read_callback(const prop_info* __pi,
    void (*__callback)(void* __cookie, const char* __name, const char* __value, uint32_t __serial),
    void* __cookie)
`
int __system_property_foreach(void (*__callback)(const prop_info* __pi, void* __cookie), void* __cookie);
int __system_property_foreach(void (*__callback)(const prop_info* __pi, void* __cookie), void* __cookie);
void PrintAllProperties(ResultType result_type) {
    std::vector<std::pair<std::string, std::string>> properties;
    __system_property_foreach(
        [](const prop_info* pi, void* cookie) {
            __system_property_read_callback(
                pi,
                [](void* cookie, const char* name, const char* value, unsigned) {
                    auto properties =
                        reinterpret_cast<std::vector<std::pair<std::string, std::string>>*>(cookie);
                    properties->emplace_back(name, value);
                },
                cookie);
        },
        &properties);
 
    std::sort(properties.begin(), properties.end());
void PrintAllProperties(ResultType result_type) {
    std::vector<std::pair<std::string, std::string>> properties;
    __system_property_foreach(
        [](const prop_info* pi, void* cookie) {
            __system_property_read_callback(
                pi,
                [](void* cookie, const char* name, const char* value, unsigned) {
                    auto properties =
                        reinterpret_cast<std::vector<std::pair<std::string, std::string>>*>(cookie);
                    properties->emplace_back(name, value);
                },
                cookie);
        },
        &properties);
 
    std::sort(properties.begin(), properties.end());
bool __system_property_wait(const prop_info* __pi, uint32_t __old_serial, uint32_t* __new_serial_ptr, const struct timespec* __relative_timeout)
bool __system_property_wait(const prop_info* __pi, uint32_t __old_serial, uint32_t* __new_serial_ptr, const struct timespec* __relative_timeout)
int __system_property_read(const prop_info* __pi, char* __name, char* __value);
int __system_property_read(const prop_info* __pi, char* __name, char* __value);
int __system_property_get(const char* __name, char* __value);
int __system_property_get(const char* __name, char* __value);
const prop_info* __system_property_find_nth(unsigned __n);
const prop_info* __system_property_find_nth(unsigned __n);
int __system_property_set_filename(const char* __filename);
int __system_property_set_filename(const char* __filename);
int __system_property_area_init(void);
int __system_property_area_init(void);
uint32_t __system_property_area_serial(void);
uint32_t __system_property_area_serial(void);
int __system_property_add(const char* __name, unsigned int __name_length, const char* __value, unsigned int __value_length);
int __system_property_add(const char* __name, unsigned int __name_length, const char* __value, unsigned int __value_length);
int __system_property_update(prop_info* __pi, const char* __value, unsigned int __value_length);
int __system_property_update(prop_info* __pi, const char* __value, unsigned int __value_length);
uint32_t __system_property_serial(const prop_info* __pi);
uint32_t __system_property_serial(const prop_info* __pi);
int __system_properties_init(void);
int __system_properties_init(void);
uint32_t __system_property_wait_any(uint32_t __old_serial);
uint32_t __system_property_wait_any(uint32_t __old_serial);
 
 
 
__attribute__((constructor(1))) static void __libc_preinit() {
__attribute__((constructor(1))) static void __libc_preinit() {
 
 
 
// Insert an extra objcopy step to add prefix to symbols. This is needed to prevent gdb
// looking up symbols in the linker by mistake.
 prefix_symbols: "__dl_",
// Insert an extra objcopy step to add prefix to symbols. This is needed to prevent gdb
// looking up symbols in the linker by mistake.
 prefix_symbols: "__dl_",
 
 
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 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";
    }
}
PropertyEntryBuilder property_entry_;
std::vector<TrieBuilderNode> children_;
std::vector<PropertyEntryBuilder> prefixes_;
std::vector<PropertyEntryBuilder> exact_matches_;
PropertyEntryBuilder property_entry_;
std::vector<TrieBuilderNode> children_;
std::vector<PropertyEntryBuilder> prefixes_;
std::vector<PropertyEntryBuilder> exact_matches_;
 
 
 
 
 
struct PropertyInfoAreaHeader {
  // The current version of this data as created by property service.
  uint32_t current_version; //值为1
  // The lowest version of libc that can properly parse this data.
  uint32_t minimum_supported_version; //值为1
  uint32_t size; //文件的大小
  uint32_t contexts_offset; //所有contexts字符串在文件中的偏移,偏移处内容为context的长度数组紧接着context字符串数组
  uint32_t types_offset; //所有contexts类型的字符串在文件中的偏移,偏移处内容为类型的长度数组紧接着类型字符串数组
  uint32_t root_offset; //TrieBuilder序列化后在文件中的偏移
};
struct PropertyInfoAreaHeader {
  // The current version of this data as created by property service.
  uint32_t current_version; //值为1
  // The lowest version of libc that can properly parse this data.
  uint32_t minimum_supported_version; //值为1
  uint32_t size; //文件的大小
  uint32_t contexts_offset; //所有contexts字符串在文件中的偏移,偏移处内容为context的长度数组紧接着context字符串数组
  uint32_t types_offset; //所有contexts类型的字符串在文件中的偏移,偏移处内容为类型的长度数组紧接着类型字符串数组
  uint32_t root_offset; //TrieBuilder序列化后在文件中的偏移
};
 
 
 
void PropertyInfoArea::GetPropertyInfo(const char* property, const char** context,const char** type) const
void PropertyInfoArea::GetPropertyInfo(const char* property, const char** context,const char** type) const
 
 
 
 
uint32_t bytes_used_;
atomic_uint_least32_t serial_;
uint32_t magic_;
uint32_t version_;
uint32_t reserved_[28];
char data_[0];
uint32_t bytes_used_;
atomic_uint_least32_t serial_;
uint32_t magic_;
uint32_t version_;
uint32_t reserved_[28];
char data_[0];
 
 
 
uint32_t bytes_used_;  //prop_area总共分配的字节大小
atomic_uint_least32_t serial_; //序列号,初始为0,当添加了新的属性或者属性值更新以后会自增1
uint32_t magic_; //0x504f5250
uint32_t version_; //0xfc6ed0ab
uint32_t reserved_[28]; //全为0的保留区域
char data_[0]; //属性区域,由二叉搜索树构成,树的每个结点类型为prop_bt
uint32_t bytes_used_;  //prop_area总共分配的字节大小
atomic_uint_least32_t serial_; //序列号,初始为0,当添加了新的属性或者属性值更新以后会自增1
uint32_t magic_; //0x504f5250
uint32_t version_; //0xfc6ed0ab
uint32_t reserved_[28]; //全为0的保留区域
char data_[0]; //属性区域,由二叉搜索树构成,树的每个结点类型为prop_bt
struct prop_bt {
  uint32_t namelen;  //名称长度
  atomic_uint_least32_t prop; //如果这个结点对应于一个属性,那么prop就是指向prop_area内存区域的prop_info结构的偏移
 
  atomic_uint_least32_t left; //左子树,其中所有结点的名称 < 此结点的名称
  atomic_uint_least32_t right;//右子树,其中所有结点的名称 > 此结点的名称
 
  atomic_uint_least32_t children;//下一级子结点
 
  char name[0];//名称所在的可变数组
 }
struct prop_bt {
  uint32_t namelen;  //名称长度
  atomic_uint_least32_t prop; //如果这个结点对应于一个属性,那么prop就是指向prop_area内存区域的prop_info结构的偏移
 
  atomic_uint_least32_t left; //左子树,其中所有结点的名称 < 此结点的名称
  atomic_uint_least32_t right;//右子树,其中所有结点的名称 > 此结点的名称
 
  atomic_uint_least32_t children;//下一级子结点
 
  char name[0];//名称所在的可变数组
 }
struct prop_info {
    atomic_uint_least32_t serial;//修改属性值时用到的同步变量,同时也用来描述属性值的长度。
    union {
    char value[PROP_VALUE_MAX];//属性值字符串
    struct {
      char error_message[kLongLegacyErrorBufferSize];
      uint32_t offset;
    } long_property; //和长属性相关,属性值长度>=PROP_VALUE_MAX的为长属性
  };
  char name[0]; //属性名称字符串
}
struct prop_info {
    atomic_uint_least32_t serial;//修改属性值时用到的同步变量,同时也用来描述属性值的长度。
    union {
    char value[PROP_VALUE_MAX];//属性值字符串
    struct {
      char error_message[kLongLegacyErrorBufferSize];
      uint32_t offset;
    } long_property; //和长属性相关,属性值长度>=PROP_VALUE_MAX的为长属性
  };
  char name[0]; //属性名称字符串
}

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2022-10-8 14:37 被飞翔的猫咪编辑 ,原因:
收藏
免费 39
支持
分享
打赏 + 150.00雪花
打赏次数 1 雪花 + 150.00
 
赞赏  Editor   +150.00 2022/09/01 恭喜您获得“雪花”奖励,安全圈有你而精彩!
最新回复 (32)
雪    币: 102
活跃值: (2115)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
3
大佬666,火前留名。
2022-8-21 15:55
0
雪    币: 28
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
4
大佬有做出具体产品吗
2022-8-21 21:59
0
雪    币: 986
活跃值: (6167)
能力值: ( LV7,RANK:115 )
在线值:
发帖
回帖
粉丝
5

学到了,太强了

最后于 2022-8-22 10:23 被Ssssone编辑 ,原因:
2022-8-22 10:21
0
雪    币: 3084
活跃值: (4177)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
猫神tql
2022-8-22 14:31
0
雪    币: 156
活跃值: (3811)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
7
其实设备指纹或者风控对设备的真实性是很多维度来判断,比如你改了内核,那我通过maps中的libc相关文件做check,记录签名,后端做备份,只要app在下线足够的多,那么判断还是能够做的
2022-8-22 14:35
1
雪    币: 47
活跃值: (437)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
TUGOhost 其实设备指纹或者风控对设备的真实性是很多维度来判断,比如你改了内核,那我通过maps中的libc相关文件做check,记录签名,后端做备份,只要app在下线足够的多,那么判断还是能够做的
没看懂,这样怎么判断呢
2022-8-22 14:39
0
雪    币: 1110
活跃值: (3334)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
9
wx_白熊 没看懂,这样怎么判断呢
杀软常规做法,收集白名单,只要一个文件不在白名单中,它就很可能是新的木马病毒。
2022-8-22 18:03
0
雪    币: 4527
活跃值: (6791)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
写这个么多,肯定发了不少时间,赞一个
2022-8-22 22:14
0
雪    币: 6573
活跃值: (3933)
能力值: (RANK:200 )
在线值:
发帖
回帖
粉丝
11
赞一个
2022-8-23 10:37
0
雪    币: 1671
活跃值: (215852)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
12
风控对抗门槛又提升了
2022-8-23 11:08
0
雪    币: 19
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
13
虽然看不懂,但是好像挺牛逼的,点赞
2022-8-23 13:12
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
14
Amun 杀软常规做法,收集白名单,只要一个文件不在白名单中,它就很可能是新的木马病毒。
建立白名单的方式有哪些呢
2022-8-24 08:38
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
15
Amun 杀软常规做法,收集白名单,只要一个文件不在白名单中,它就很可能是新的木马病毒。
有哪些大佬可以单独咨询一下吗,我的联系方式qq 2337283043
2022-8-24 08:41
0
雪    币: 669
活跃值: (1667)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
16
哈哈哈,前段时间刚研究完这个,还没来得及写。喵神厉害
2022-8-29 12:00
0
雪    币: 1495
活跃值: (9953)
能力值: ( LV9,RANK:240 )
在线值:
发帖
回帖
粉丝
17
双击666
2022-8-29 16:49
0
雪    币: 181
活跃值: (2978)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
18

牛,思考了很多

最后于 2022-9-13 18:34 被huaerxiela编辑 ,原因:
2022-9-13 18:34
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
19
Amun 杀软常规做法,收集白名单,只要一个文件不在白名单中,它就很可能是新的木马病毒。
文件列表白名单不够的,加载地址都获得了,直接dump段.text抽取特征不就完了。
2022-9-17 15:21
0
雪    币: 70
活跃值: (2066)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
20
牛逼!!!!!!卷出花了
2022-9-20 16:40
0
雪    币: 228
活跃值: (115)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
21

__system_properties_abandon是我新添加的新函数为了释放掉之前属性区域的所有内存映射,接着调用__system_properties_init,这个函数是原先系统提供的函数,由于已经实现了bind mount,所以其实初始化的是/dev/xxx/__properties__区域,只不过对app来说它感知不到这点。



既然这样上面那些操作的意义是什么??这不是重新初始化了吗?想看看实现代码

2022-11-2 22:12
0
雪    币: 562
活跃值: (4145)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
22
少了一些细节,... 
2022-12-8 19:50
0
雪    币: 562
活跃值: (4145)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
23
DirtyAngle 少了一些细节,...
只有一点没有写, 完成度很高 666 
已用magisk实现
2022-12-9 10:33
0
雪    币: 477
活跃值: (1412)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
24
DirtyAngle 只有一点没有写, 完成度很高 666 已用magisk实现
magisk本来就支持resetprop, 楼主这一通花里胡哨的检测根本没啥用
2022-12-9 13:33
0
雪    币: 562
活跃值: (4145)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
25
mb_foyotena magisk本来就支持resetprop, 楼主这一通花里胡哨的检测根本没啥用
全局改不太好吧
2022-12-9 16:34
0
游客
登录 | 注册 方可回帖
返回
//