Android Activity Deeplink 启动来源获取源码分析
一、前言
目前有很多的业务模块提供了 Deeplink 服务,Deeplink 简单来说就是对外部应用提供入口。
针对不同的跳入类型,app 可能会选择提供不一致的服务,这个时候就需要对外部跳入的应用进行区分。一般来讲,我们会使用反射来调用 Acticity 中的 mReferrer 字段来获取跳转来源的包名。
具体代码如下;
但是 mReferrer 有没有被伪造的可能呢?
一旦 mReferrer 被伪造,轻则业务逻辑出错,重则造成经济损失,针对这种情况,有没有办法找到一种较为安全的来源获取方法呢?
这就需要对 mReferrer 的来源进行一次分析。下面我们来进行一次 mReferrer 来源的另类源码分析。之所以说另类,是因为这次会大量使用调试手段来逆向进行源码分析。
二、mReferrer 从哪里来
2.1 搜索 mReferrer,来源回溯
使用搜索功能来搜索 Activity 类中的 mReferrer;使用 Find Usages 功能来查找 mReferrer 字段。

在 Activity 的 Attach 方法中对 mReferrer 做了赋值。
2.2 使用断点调试跟踪调用栈
我们在 Attach 方法上添加断点,通过断点来跟踪 Attach 的调用;

红框中就是 Attach 的调用路径,该调用栈在主线程中执行;从调用栈中看出 Attach 是 ActivityThread.performLaunchActivity 调用的。

performLaunchActivity 调用 Attach 时传入的是 r 的 referrer 参数,r 是一个 ActivityClientRecord 对象。
我们进一步找到 ActivityClientRecord 中对 referrer 赋值的地方,就是 ActivityClientRecord 的构造函数。

在构造函数中添加断点,查看调用栈;

发现 ActivityClientRecord 在 LaunchActivityItem 的 execute 中被实例化,并且传入的是 LaunchActivityItem 的 mReferrer。
LaunchActivityItem 的 mReferrer 是在 setValues 方法中赋值的,我们需要通过调试来看 setValues 是被谁调用的。当我们使用常规方式断点查看 setValues 的调用方时,我们会发现这样一种情况。

说明 LaunchActivityItem 在本地进程中,是一个被序列化后反序列化生成的对象。
在 Activity 中,序列化对象传输通常是使用 binder 来完成的,而 binder 的服务端是在 System 进程中。这里实现了反序列化,那么在远端的 binder 服务中一定有序列化的过程。我们可以在 System 进程中调试这个断点,应该就是序列化的过程。
2.3 断点调试
对 System 进程调试的方式也比较简单;
step1:下载安装 Android 自带的 X86 模拟器(注意一定要安装 google api 版本,play 版本不支持调试 system 进程)。
step2:在调试的时候选择 System 进程。

通过调试,我们找到赋值堆栈(注意这里堆栈显示的进程已经是 Binder 进程了)。

我们根据这个堆栈的指示,一步一步的跟进,这里需要注意一下,我们在查看调试堆栈的时候,只需要关注类名和方法名就可以了,不用刻意去关注堆栈中的行号,因为行号不一定准确。如果调试过程中发现差异太大,可以尝试更换一个模拟器版本。
这里跟进到 ActivityStackSupervisor 的 realStartActivityLoacked 方法。

在 ActivityStackSupervisor 中,我们发现这个参数是由 r.LaunchedFromPackage 的来的,这个 r 是 ActivityRecord,查找 LaunchedFromPackage 的赋值的地方,最终找到 ActivityRecord 的初始化方法。
2.4 对象实例化过程
在初始化方法中添加断点进行堆栈调试;

跟着堆栈一步一步的看,到了 ActivityStarter 的 execute 方法里面,这里可以看到 package 的来源是 mRequest.callingPackage。

通过搜索 Request 的 callingPackage 对象对的 Vaule write,mRequest.callingPackage 的来源是 ActivityStarter 的 setCallingPackage 方法,一定是调用了 setCallingPackage 方法来实现了 callingPackage 内容的注入。

再看上一步骤中的堆栈,调用该方法的是 ActivityTaskManagerService 的 startActivity 方法;startActivity 在构建时使用 setCallingPackage 传入了 package。与我们之前的猜测是一致的。

分析到这里已经接近真相了。
2.5 远程服务 Binder 调用的分析
我们都知道 ActivityTaskManagerService 是一个远程服务,从它工作的进程就可以看出来,是一个 binder 进程。因为 ActivityTaskManagerService extends IActivityTaskManager.Stub,那我们就要去找 IActivityTaskManager.Stub 被远程调用的地方。
要想找他远程调用的地方,我们就要先找到 IActivityTaskManager.Stub 是如何被调用方拿到的。
全局搜索 IActivityTaskManager.Stub 或者搜索 IActivityTaskManager.Stub.asInterface,这里为了方便使用了在线的 Android 源码搜索平台。

我们在 ActivityTaskManager 中找到如下代码;
也就是说通过 ActivityTaskManager.getService()方法可以拿到 IActivityTaskManager.Stub 的远程调用句柄。
于是 ActivityTaskManagerService 的 startActivity 方法调用的写法应该是 ActivityTaskManager.getService().startActivity,下一步的计划是找到这个方法调用的地方 。
2.6 万能的搜索并不万能
按照正常的思路,我们会再来使用搜索功能在这个在线源码网站上搜索一下 ActivityTaskManager.getService().startActivity。
搜索不到?这里一定要注意,因为 startActivity 方法里面有很多参数,很可能代码被换行,一旦被换行,搜索 ActivityTaskManager.getService().startActivity 就不能搜到了。
搜索也不是万能的,我们还是考虑加断点试试。
那么断点应该加在哪里呢?我们是否可以将断点加在 ActivityTaskManagerService 的 startActivity 上呢?
答案是不行,如果你尝试去在一个 binder 进程调用(远程服务调用 )的方法上面添加断点。那么你只会得到如下调用栈。

很显然调用栈直接指向了 binder 远端,这不是我们想要的调用栈。我们知道,调用 startActivity 的源码一定是 ActivityTaskManager.getService().startActivity。
而这行代码一定是在 App 的进程中调用的,属于 binder 的客户端调用,因此我们试着在 getService()上面加一个断点试试。这里加了断点之后也要注意一下,因为这个时候的 startActivity 应该是攻击方调用的,也就是调起 Deeplink 的应用调用的。
所以。我们需要对 Deeplink 的发起方进行调试。我们可以写一个 Demo 来进行调试。

点击按钮来发起 Deeplink,然后进行断点,这个时候就能找到如下堆栈。

点击下一步断点(Step Over)刚好就是 ActivityTaskManager.getService().startActivity 的方法调用。

于是我们得到如下调用栈;
这边就找到了 可以看到,callingPackage 正是使用 getBasePackageName 方法来实现的。who 就是 context,也就是我们的 Activity。
到这里就可以确认 mReferrer 其实就是使用 context 的 getBasePackageName()来实现的。
三、如何避免包名被伪造
3.1 关注 PID 和 Uid
如何来防止 PackageName 被伪造呢?
在我们调试 ActivityRecord 的时候,我们发现 ActivityRecord 的属性中还有 PID 和 Uid;

只要拿到这个 Uid,我们就可以根据 Uid 调用 packageManager 的方法来获取对应 Uid 的报名。
3.2 调研 Uid 是否有伪造的可能性
下面就是要验证一下 Uid 是否有被伪造的可能了。调试查找 Uid 的来源,在 ActivityRecord 的初始化方法中断点查看 callingUid 的来源。

我们发现 这个 Uid 其实是在 ActivityStarter 里面使用 Binder.getCallingUid 得到的。Binder 进程可不是应用层面可以干涉的了,我们可以放心大胆的使用这个 Uid,不用担心被伪造,剩下的就是如何使用 Uid 获取 PackageName 了。
3.3 使用 Uid 置换 PackageName
我们检索代码,发现 ActivityTaskManagerService 恰好提供了获取 Uid 的方法。

所以我们需要拿到 ActivityTaskManagerService 引用,搜索 IActivityTaskManager.Stub。

ActivityTaskManager 是无法在 app 层引用的(是一个 hide 的类,但其实也是有办法的,大家可以自己去探索一下)。
我们继续查找;

最终发现 ActivityManager 提供了这么一个方法来获取 ActivityTaskManagerService,但是很不幸,getTaskService 是一个黑名单方法,被禁止调用。
最后我们发现 ActivityTaskManagerService 的 getLaunchedFromUid 方法其实是被 ActivityManageService 包装了一下的。
所以可以使用 ActivityManageService 来调用就可以了,代码如下(注意不同的系统的版本可能代码并不一样)。
使用 Uid 来置换 PackageName 是不是就万无一失了呢?这里面是否还有其他玄机?这里先卖个关子,小伙伴们可以在评论区讨论一下。
四、总结
mReferrer 很容易通过重写 context 的 getBasePackageName()被伪造,在使用时一定要小心。通过 ActivityManageService 获取的 Uid 是无法被伪造的,可以考虑使用 Uid 来转换 PackageName。
作者:vivo 互联网客户端团队-Chen Long
版权声明: 本文为 InfoQ 作者【vivo互联网技术】的原创文章。
原文链接:【http://xie.infoq.cn/article/a616b6f2a3b5aad6acf66b36a】。文章转载请联系作者。
评论