一、前言
今天我们来看一下Android中一个众人熟悉的一个属性:shareUserId,关于这个属性可能大家都很熟悉了,最近在开发项目,用到了这个属性,虽然知道一点知识,但是感觉还是有些迷糊,所以就写篇文章来深入研究一下。
关于Android中的sharedUserId的概念这里就简单介绍一下:
Android给每个APK进程分配一个单独的空间,manifest中的userid就是对应一个分配的Linux用户ID,并且为它创建一个沙箱,以防止影响其他应用程序(或者其他应用程序影响它)。用户ID 在应用程序安装到设备中时被分配,并且在这个设备中保持它的永久性。
通常,不同的APK会具有不同的userId,因此运行时属于不同的进程中,而不同进程中的资源是不共享的,在保障了程序运行的稳定。然后在有些时候,我们自己开发了多个APK并且需要他们之间互相共享资源,那么就需要通过设置shareUserId来实现这一目的。
通过Shared User id,拥有同一个User id的多个APK可以配置成运行在同一个进程中.所以默认就是可以互相访问任意数据. 也可以配置成运行成不同的进程, 同时可以访问其他APK的数据目录下的数据库和文件.就像访问本程序的数据一样。
用法也很简单:
在需要共享资源的项目的每个AndroidMainfest.xml中添加shareuserId的标签。
android:sharedUserId="com.example"
id名自由设置,但必须保证每个项目都使用了相同的sharedUserId。一个mainfest只能有一个Shareuserid标签。
1. SELinux 背景知识
详细了解 Android 8.0 SELinux,可以参阅 Google 官方文档
1.1 DAC 与 MAC
在 SELinux 出现之前,Linux 上的安全模型叫 DAC,全称是 Discretionary Access Control,翻译为自主访问控制。
DAC 的核心思想很简单,就是:进程理论上所拥有的权限与执行它的用户的权限相同。比如,以 root 用户启动 Browser,那么 Browser 就有 root 用户的权限,在 Linux 系统上能干任何事情。
显然,DAD 管理太过宽松,只要想办法在 Android 系统上获取到 root 权限就可以了。那么 SELinux 是怎么解决这个问题呢?在 DAC 之外,它设计了一种新的安全模型,叫 MAC(Mandatory Access Control),翻译为强制访问控制。
MAC 的理论也很简单,任何进程想在 SELinux 系统上干任何事情,都必须在《安全策略文件》中赋予权限,凡是没有出现在安全策略文件中的权限,就不行。
关于 DAC 和 MAC,可以总结几个知识点:
1. Linux 系统先做 DAC 检查。如果没有通过 DAC 权限检查,则操作直接失败。通过 DAC 检查之后,再做 MAC 权限检查
2. SELinux 有自己的一套规则来编写安全策略文件,这套规则被称之为 SELinux Policy 语言。
1.2 SEPolicy 语言
Linux中有两种东西,一种死的(Inactive),一种活的(Active)。死的东西就是文件(Linux哲学,万物皆文件。注意,万不可狭义解释为File),而活的东西就是进程。此处的 死 和 活 是一种比喻,映射到软件层面的意思是:进程能发起动作,例如它能打开文件并操作它。而文件只能被进程操作。
下面我们可以直接使用一个例子来看看效果:
首先我们弄一个插件工程:ShareUserIdPlugin
这个工程很简单,我们编译安装运行即可。
在弄一个宿主工程:ShareUserIdHost
这里有一个核心方法,我们首先通过插件工程的包名:cn.wjdiankong.shareuseridplugin;创建出一个Context对象。
这里看到第二参数有两个模式:
Context.CONTEXT_INCLUDE_CODE:这个标志是在我们需要执行插件中的某段代码需要加上的值。
CONTEXT_IGNORE_SECURITY:这个标志是必须的,是忽视安全性,如果没有这个值的话,那么我们访问什么都是失败的。
得到了Context变量之后,我们下面就可以通过反射来执行代码和获取资源了,这里需要注意的是,一定要先拿到Context对应的ClassLoader,然后才能加载对应的类,ClassLoader一定是Context的,是插件工程中的类加载器。
下面我们运行结果看看:
运行成功了啦~~是不是很简单呢。
下面如果我们把CONTEXT_INCLUDE_CODE去掉,在运行:
发现报错了,找不到指定的类。所以如果想运行代码的话,这个值一定要加上。
我们再把CONTEXT_IGNORE_SECURITY去掉,运行结果:
看到了,爆出了安全错误,所以要想构造成功Context出来,必须要加上这个值。
三、步入正题好了,到这里我们就介绍了如何通过包名构造一个Context变量出来,然后执行对应的代码和获取资源。那么这个我们看到工程中貌似没有用到shareUserId这个属性呢?那这个和我们今天要介绍的知识点有什么关系吗?其实没什么关系,上面的例子只能说是做一个简单的引子,那有些同学可能困惑了,为何都没有使用shareUserId属性,这两件事还可以做呢?那岂不是很不安全?其实我们在接触过逆向知识的时候会发现,关于Android中的一个App中的代码和资源说的直白点其实没有安全性可言,比如,我想获取一个一个app中的指定资源,可以使用反编译或者直接解压apk就可以得到,想看到app中的一段代码的含义或者执行结果,反编译也可以做到,所以说这个说的直白点关于代码和资源在Android中其实没什么安全性可说。有办法可以去搞定的。
当然我们在后面可以用这种构造Context的方式,去实现我们想要的一些功能,比如我们知道了一个app的资源名或者是方法名,想直接在我的工程中用,那么可以使用这种方式就可以啦,不过这个还是很不靠谱的,当然也是一种方式,比如A应用实现了一个很复杂的一个方法,我自己的应用和他没任何关系,但是也需要这个方法,那么可以直接使用这种方式去调用即可。但是前提是A应用安装了。当然正规公司的app都不会这么傻逼的去做的,其实我们在研究逆向app的时候可能会用到哦~~
那么说了这么多,shareUserId的属性的最大作用是什么呢?
前面都说了,Android中每个app都对应一个uid,每个uid都有自己的一个沙箱,这是基于安全考虑的,那么说到沙箱,我们会想到的是data/data/XXXX/目录下面的所有数据,因为我们知道这个目录下面的所有数据是一个应用私有的,一般情况下其他应用是没有权限访问的,当然root之后是另外情况,这里就不多说了。这里只看没有root的情况,下面我们在来看一个场景:
A应用和B应用都是一家公司的,现在想在A应用中能够拿到B引用存储的一些值,那么这时候该怎么办呢?
这时候就需要用到了shareUserId属性了,但是这里我们在介绍shareUserId属性前,我们先来看一个简单的例子:
还是使用上面的两个工程:
ShareUserIdPlugin中的MainActivity.java代码如下:
这里很简单,我们使用SharedPreferences来存储一个密码,注意模式是:Context.MODE_PRIVATE,关于这里,有很多种模式,后面会详细介绍。
下面在来看一下宿主工程中的代码,获取密码。
运行宿主工程结果:
我们看到运行结果打印出来了几个值,我先不管其他的,看到最后pwd的值是默认值,那说明我们宿主工程中获取插件工程中的密码失败了。
我们在去看看插件工程中那个shareperference的xml文件的权限:
这里使用root了之后查看的:-rw-rw----
关于这个值,不了解的同学可以网上去看一些资料:
Linux文件权限你分开三段来看:首位代表是目录还是文件,一般不用管,后面的三段每段3位,r代表可读,w代表可写,x代表可执行,第一段是代表文件所属的用户对它的权限,第二段是所属用户组的用户对它的权限,第三段是其他用户对它的权限。第一段:rw- ,所属用户(比如是root)对这个文件可读可写第二段:rw- ,所属用户组用户,对这个文件可读可写第三段:--- ,其他用户对这个文件什么都干不了
那么从上面的分析可以看出来,这个文件对于其他用户(不同uid的)访问是失败的。所以我们获取密码失败。
那么这个xml的权限在哪里设置的呢?其实就是在插件工程中的那个创建SharedPreferences的时候:
其实Context提供了几种模式:
1、Context.MODE_PRIVATE:为默认操作模式,代表该文件是私有数据,只能被应用本身访问,在该模式下,写入的内容会覆 盖原文件的内容,如果想把新写入的内容追加到原文件中。可以使用Context.MODE_APPEND2、Context.MODE_APPEND:模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件。3、Context.MODE_WORLD_READABLE和Context.MODE_WORLD_WRITEABLE用来控制其他应用是否有权限读写该文件。MODE_WORLD_READABLE:表示当前文件可以被其他应用读取;MODE_WORLD_WRITEABLE:表示当前文件可以被其他应用写入
我们可以查看源码ContextImpl.java:
这里获取一个SharedPreferencesImpl对象,这个对象是实现了SharedPreferences接口的。这里我们看到采用了缓存机制,将xml的名字和sp对象一一对应起来,所以我们可以得知,一个app中,最好简化xml的个数,尽量将值都定义到一个xml中,减少内存占用。
我们在看看SharedPreferencesImpl.java类源码:
有一个全局变量存储了mode值,再看看mMode在哪里用到了:
在writeToFile这个方法中用到了,这个方法其实后面会分析的,就是SP将内存中的值保存到磁盘中。
然后再看看ContextImpl的setFilePermissionsFromMode方法:
好了,到这里,我们可以看到,通过传递进来的mode值,来设置文件的权限。
那么代码看完了,下面我们在改一下插件工程中的那个创建sp的代码:
Context.MODE_WORLD_READABLE|Context.MODE_WORLD_WRITEABLE 为读写模式
再来测试一下:
看到这里取出来密码了,成功了,关于空指针后面会详细介绍的,这里先不管了。我们再来看一下sp的xml文件权限:
看到了,其他用户是可以进行读写操作的了,所以取出来的密码是成功的了。
到这里我们就弄清楚了Context提供的那几个创建sp文件的几种模式的区别,所以我们这里也可以看到,这个模式很重要,对于安全性来说,不过这个默认模式就是private的,也是挺好的。