Android逆向之旅—Android中的sharedUserId属性详解

Android技术篇 尼古拉斯.赵四 8732℃ 0评论

一、前言

今天我们来看一下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标签。

 

二、问题延伸

我们今天先来看一个场景:Android中一个App如何能够访问到其他App的信息和资源?

这个可能很多人感觉是两个App之间的通信,其实不是,比如我们在早期遇到支付宝有一个快捷支付,那么我们会看到手机中会安装两个app,一个是支付宝app,一个是快捷支付app,那么在开启快捷支付的时候,就会调用快捷支付app等,大家可能会想到现在有一个比较流行的技术叫做插件开发,的确如此,这个我在之前的文章也有说过,不清楚的同学可以点击这里:Android中插件开发篇

但是我们今天不说这个插件怎么搞,今天就来看看如何在一个app中去访问另外一个app的代码和资源等信息?

在说这个知识点之前,我们需要了解的一个知识点,就是我们可以通过一个包名来得到对应的Context的全局变量,可以直接使用Context的一个静态方法:createPackageContext

关于这个方法其实很简单,他有两个参数:

第一个参数:需要构造出来Context的包名字符串

第二个参数:构造出来的Context的开启模式

下面我们可以直接使用一个例子来看看效果:

首先我们弄一个插件工程: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_APPEND
2、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的,也是挺好的。

补充:

第一:不需要root来查看sp文件的权限

前面我们看到我们是使用root之后查看文件的权限的,其实还有一种方式,不root也是可以的,那就是run-as命令,关于这个命令不熟的同学可以自行google了,这个命令的作用是:可以查看指定包名应用的data目录下面的数据,也就是只能查看data/data/XXX/目录下面的内容,而且他的局限性也很大,只有debug模式下才能起作用,下面我们来看看怎么使用:

run-as 需要查看内容的应用包名

是不是这里也是可以查看的,但是他只能在debug下面才能使用,比如我们现在用它去查看非debug的应用:

看到了吧,很蛋疼,非debug模式还不能用。好吧,不过这里只是做了一个知识点的补充,记住有这个命令,在debug环境下也是蛮有用的。

第二:关于上面日志中的异常是怎么回事?

我们回去看看宿主工程中,用反射去访问了SP内部的一些变量值。为什么访问这些呢?源于我之前调试一个bug,但是这里引出来了一些问题,下面就来分析一下。

为了分析,这里我们还是需要去看SharePreferencesImpl源码:

代码逻辑不是很复杂,首先创建备份文件,然后加载xml内容到内存的map对象,用于后面的getXXX方法直接获取值,提高效率,然后将解析之后的map赋值给全局的map对象,如果解析出来的map为空,那么就直接赋值一个空数据的map。最后一行代码很重要,就是需要唤醒其他所有的wait地方,看完这段代码我们就可以很好理解上面的异常崩溃了:

首先文件是可读的,所以进入到了if语句中,开始解析xml到内存中,但是这时候需要注意的是,解析工作实在子线程中工作的,但是我们去访问全局map是在主线程做的,那么这时候解析还没有完成,那么只能获取到null值了,所以抛出一个空指针,但是后面我们使用getString方法的时候,可以获取到正确值了

下面我们来看看getString的源码:

看看awaitLoadedLocked方法:

这个方法什么都没干,就是wait住了,等待唤醒,这个也就和上面的那个notifyAll方法对应起来了。

那么既然都分析到这里了,我们干脆再来看一下常用的commit和apply两个方法吧:

commit方法:

这里主要就连个方法,首先来看看commitToMemory方法,这个是整理提交前的map数据结构,用于写到文件前的操作准备

整理好了内存中的数据,开始写入到磁盘中了,其实commit从内存写文件是在当前调运线程中直接执行的。那我们再来看看这个写内存到磁盘方法中真正的写方法writeToFile:

分析完了commit方法,我们总结一下:

如果用commit()方法提交数据,其过程是先把数据更新到内存,然后在当前线程中写文件操作,提交完成返回提交状态

接下来继续看apply方法:

这里也是调用了enqueueDiskWrite方法:

其实这个方法是commit和apply公用的,主要用isFromSyncCommit来进行区分的,postWriteRunnalbe==null就是commit方式。如果不为null的话,就是apply方式。

总结一下apply方法:

如果用的是apply()方法提交数据,首先也是写到内存,接着在一个新线程中异步写文件,然后没有返回值。

其实这里算是分析完了SharePreferences的源码,我们可以总结如下:

1、SharedPreferences在实例化时首先会从sdcard异步读文件,然后缓存在内存中;接下来的读操作都是内存缓存操作而不是文件操作。
2、在SharedPreferences的Editor中如果用commit()方法提交数据,其过程是先把数据更新到内存,然后在当前线程中写文件操作,提交完成返回提交状态;如果用的是apply()方法提交数据,首先也是写到内存,接着在一个新线程中异步写文件,然后没有返回值。

3、由于上面分析了,在写操作commit时有三级锁操作,所以效率很低,所以当我们一次有多个修改写操作时等都批量put完了再一次提交确认,这样可以提高效率。

上面算是开了一个小差,顺道分析了一下SharePreferences的源码,下面来说正题了,我们在上面的例子已经知道了,通过设置Context的文件创建模式来设置安全性。那么现在如果我们想让A应用访问到B应用的数据,我们可以这么做:把B应用创建模式改成可读模式的,那么A应用就可以操作了,那么这就有一个问题,A应用可以访问了,其他应用也可以访问了,这样所有的应用都可以访问B应用的沙盒数据了,太危险了,所以要用另外的一种方式,那么这时候就要用到shareUserId属性了,我们只需要将B应用创建方式还是private的,然后A应用和B应用公用一个uid即可,我们下面就来修改一下代码,还是上面的那两个工程,修改他们的AndroidManifest.xml,添加shareUserId即可。

这时候,我们发现把ShareUserIdPlugin中的模式改成private的,A应用任然可以访问数据了,其实也好理解,他们两个的uid都相同了,A的文件就是B的,B的就是A的了,他们两个没有沙盒的概念了,数据也是透明的了。

所以这里我们就看到了,使用shareUserId可以达到多个应用之间的数据透明性互相访问。

那么问题来了,假如现在我手机没有root,想访问某个应用的沙盒数据,我把自己的应用修改成和他一样的shareUserId即可。

注意:这里有一个误点,就是这里所有的修改的前提是这个应用的AndroidManifest.xml本身就定义了这个属性,然后我们可以反编译看到这个值,把我们自己的shareUserId改成他的就可以了,但是如果这个应用本身没有这个属性,那么这里就没有办法的,为什么呢,如果要添加,那就是另外一条路了,就是逆向,修改AndroidManifest.xml之后,还需要从新打包在验证,但是这时候没必要了,我们也知道有时候回编译还是很艰难的,如果都能回编译了,那都不需要这些工作了,所以这里需要注意的一个前提

那么修改之后是不是真的可以呢?

答案是肯定不可以的,如果可以的话,那google也太傻比了,其实Android系统中有一个限制,就是说如果多个应用的uid相同的话,那么他们的apk签名必须一致,不然是安装失败的,如下错误:

我们可以查看PackageManagerService.java源码:

看到了,这里会作比较的,不过这里我们在深入看一下这个方法的调用链:

在scanPackageLI方法中调用的verifySignaturesLP方法,那么scanPackageLI方法在哪调用的呢?继续跟踪:

在这里,这里其实是一个文件监听类AppDirObserver:

这里会监听/data/app目录,如果有新的文件增加,就会调用scanPackageLI方法,然后在调用verifySignaturesLP方法来进行验证apk文件信息。同时我们也发现了,系统的安装和卸载apk的广播也是在这里发送的。果然这里的知识点还是很多的。

通过上面的分析,我们就知道了,Android中是不允许相同的uid的不同签名的应用。

那么我们上面的猜想就是失败的。及时改成目标应用相同的shareUserId,也是安装不成功的。

 

四、知识梳理

1、我们知道如何通过包名来构建一个Context,同时需要注意两种模式:

Context.CONTEXT_INCLUDE_CODE和Context.CONTEXT_IGNORE_SECURITY

构造完成之后,我们可以访问资源和执行一些模块代码,这些其实不算是一个应用的沙盒概念了,所以不会牵扯到shareUserId的知识点。

2、我们在实验A应用去访问B应用的SharedPreferences中的值时,发现创建sp的xml有几种模式:

Context.MODE_PRIVATE:为默认操作模式,代表该文件是私有数据,只能被应用本身访问,在该模式下,写入的内容会覆盖原文件的内容,如果想把新写入的内容追加到原文件中。可以使用Context.MODE_APPEND
Context.MODE_APPEND:模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件。
Context.MODE_WORLD_READABLE和Context.MODE_WORLD_WRITEABLE用来控制其他应用是否有权限读写该文件。

这三种模式的区别,我们最保险的操作就是设置成private的,不过默认也是这种模式

3、我们通过分析SharedPreferences的源码,知道这三种模式对应的就是设置xml文件的访问权限,同时我们顺便分析了commit,apply,getXXX等方法的实现,也算是对SP的更深入的理解了。其实SharedPreferences内部为了高效率,会第一次加载xml内容到内存中的map中,每次getXXX数据的时候,都是直接从map中取,每次保存数据,是首先保存到内存的map中,调用commit和apply方法只有在将数据写入到磁盘中的区别。apply是异步的没有返回值,commit是同步的有返回值

4、我们再次实验使用shareUserId属性来做到多个应用之间的数据共享和透明性,同时我们也做了一个猜想就是把自己的shareUserId修改成和目标应用相同来访问目标应用的数据,但是这个猜想是错误的,因为我们通过分析PackageManagerService源码知道,Android中是不允许相同的shareUserId的应用有着不同的签名文件的,会出现安装失败的情况。

 

五、遗留的问题

关于文件创建还有一种模式:Context.MODE_MULTI_PROCESS,这个模式其实我们知道是用来多进程访问的,这里关于源码就不在分析了,在ContextImpl.java中的getSharedPreferences方法中会做一次多进程的数据刷新加载操作:

不过这个方法已经废弃了,google建议还是使用ContentProvider比较靠谱,同样,上面的Context.MODE_WORLD_READABLE和Context.MODE_WORLD_WRITEABLE这两种模式也是被废弃了,也算是google为了增强安全性考虑吧。

 

六、总结

这篇文章就介绍了使用sharedUserId属性,来实现我们想要的应用数据共享效果,但是引出来的知识点有点多,所以说的就有点多了,不过我们就记住一点:

在创建文件时,一定要设置成Context.MODE_PRIVATE或者是Context.MODE_APPEND模式,为了做到应用的数据共享可以考虑shareUserId属性。同时Android中是不允许相同的sharedUserId有着不同签名的应用的,会出现安装失败。

分析的好累呀~~,跪求点赞啦啦~~

《Android应用安全防护和逆向分析》

点击立即购买:京东  天猫

更多内容:点击这里

关注微信公众号,最新Android技术实时推送

转载请注明:尼古拉斯.赵四 » Android逆向之旅—Android中的sharedUserId属性详解

喜欢 (13)or分享 (0)
发表我的评论
取消评论

表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址