近期,发现在部分Android手机调起其他应用时,会弹出一个系统弹窗拦截调起,需要用户二次确认。经过内部众测,发现绝大多数是小米手机,而且跟Android版本没有直接关系,猜测是MIUI某次升级引入的功能。这篇文章,我们从MIUI系统源码分析下该弹窗。
我有一系列Android逆向相关的文章,这是我今年六月份关于反编译腾讯新闻去开屏广告的逆向实战文章,也欢迎大家阅读:
Android逆向实战 - 腾讯新闻去开屏广告_apktool去广告_一个玩游戏的程序猿的博客-CSDN博客文章浏览阅读2.3k次,点赞6次,收藏17次。上次反编译一个工具类app去广告失败,原因是使用了360加固,回编译后无法启动。一般来讲,大厂的app考虑到性能、兼容性、包体积等,通常不用加固。因此,本次我们选一个大一些的app-腾讯新闻。写在前面:本篇博客仅用来研究和学习。如有侵权,请联系我删除,谢谢。_apktool去广告https://blog.csdn.net/qq_21154101/article/details/130409016?spm=1001.2014.3001.5501
目录
一、背景
二、确定弹窗来源
三、反编译手机管家
四、framework代码分析
1、获取framework
2、miui-services.jar分析
3、miui-framework.jar分析
五、疑似拦截白名单
一、背景
最近,发现手机在调起淘宝或京东时,有时会弹窗让二次确认。尤其是双11期间,各大厂商app都在疯狂投放阿里、京东等电商app的广告。跟往年大家吐槽一不小心就跳到其他app不一样,今年我们发现很多手机会进行拦截,尤其是小米手机。在这里给小米点个赞,我也是忠实的小米用户。现象如下图:
二、确定弹窗来源
当弹出该弹窗时,通过mCurrentFocus去确定来源:
adb shell
dumpsys window|grep Focus
如下:
mCurrentFocus=Window{c16c09e u0 com.miui.securitycenter/com.miui.wakepath.ui.ConfirmStartActivity}mFocusedApp=ActivityRecord{83602ab u0 com.miui.securitycenter/com.miui.wakepath.ui.ConfirmStartActivity} t5345}mPreferredTopFocusableRootTask=Task{5d40ca1 #5345 type=standard A=1000:com.miui.wakepath.ui.ConfirmStartActivit U=0 visible=true visibleRequested=true mode=fullscreen translucent=true sz=1}mLastFocusedRootTask=Task{5d40ca1 #5345 type=standard A=1000:com.miui.wakepath.ui.ConfirmStartActivit U=0 visible=true visibleRequested=true mode=fullscreen translucent=true sz=1}mFocusedWindow=Window{c16c09e u0 com.miui.securitycenter/com.miui.wakepath.ui.ConfirmStartActivity}mTopFocusedDisplayId=0
可以发现,弹窗来源于com.miui.securitycenter/com.miui.wakepath.ui.ConfirmStartActivity,securitycenter也就是小米手机的手机管家。
三、反编译手机管家
下载手机管家app,反编译。在这里我用的最新的版本(v8.6.2.231116,也就是2023年11月16号的版本)。反编译的步骤在这不细说了,反编译后,搜索ConfirmStartActivity:
看到如下代码:
第一部分代码是从intent中获取调起app的pkgname和被调起app的pkgname以及UserId等其他信息,这些信息获取后,在r0这个判断中有用到。看下r0这个方法,就是拿上面拿到的包名和UserId等做了一系列的判断:
private boolean r0() {PackageInfo n10;PackageInfo n11;if (Build.VERSION.SDK_INT <= 33) {return false;}boolean z10 = false;for (UserHandle userHandle : ((UserManager) getSystemService("user")).getUserProfiles()) {if (userHandle.hashCode() == this.f18556g) {z10 = true;}}if (z10) {Object d10 = sf.e.d(f18552n, this, "getLaunchedFromUid", null, new Object[0]);return !(d10 == null || p1.b(((Integer) d10).intValue()) == this.f18557h) || (n10 = x0.n(this.mAppContext, this.f18554e)) == null || p1.b(n10.applicationInfo.uid) != p1.b(this.f18557h) || (n11 = x0.n(this.mAppContext, this.f18555f)) == null || p1.b(n11.applicationInfo.uid) != p1.b(this.f18558i) || this.f18553d == null;}return true;}
在AndroidManifest文件中看,该Activity的配置了一个ACTION:CHECK_ALLOW_START_ACTIVITY,顾名思义:检验是否允许启动Activity。
继续看,看到了弹窗的方法i0,而且其两个按钮文案的命名是"deny"和"accept",因此高度怀疑就是dp调起拦截弹窗代码:
但是该弹窗代码只是构建弹窗的部分UI,并没有在ConfirmStartActivity中看到其调用的地方,看下其父类,发现在父类AlertActivity的onCreate中调用了i0方法:
而l0方法中调用了dialog.show(),该方法是在子类ConfirmStartActivity中k0调用的,k0又是在父类如上的onCreate方法最后调用的:
也就是说,弹窗是在ConfirmStartActivity的onCreate中展现的。如果f10185c这个变量是true,那就return不出弹窗。f10185c这个变量在哪赋值的呢?又是在ConfirmStartActivity的j0里面赋值的,大家可以往上翻一下j0的代码。
四、framework代码分析
1、获取framework
通过公司内部众测我们发现,该弹窗只有在MIUI才有,目前没发现是MIUI的哪个版本有的,现在是猜测需要MIUI版本 > XXX并且手机管家版本大于XXX,二者缺一不可。
我们看下framework的代码,在这里把framework和system_ext下的framework代码一并pull下来:
adb pull /system/frameworkadb pull /system/system_ext/framework
MIUI系统,system_ext下的framework代码主要是小米自己魔改的framework,比较经典的就是miui-framework.jar和miui-services.jar:
2、miui-services.jar分析
直接反编译miui-services.jar,搜索ConfirmStartActivity:
打开ConfirmStartHelper类看下,这里并没有看到启动ConfirmStartActivity的入口代码,但是看到了在isAllowStartCurrentActivity方法中用到手机管家,ACTION和ConfirmStartActivity包名类名进行判断的逻辑:
isAllowStartCurrentActivity方法比较清晰:
- 如果Uid不是1000才需要进行相关的判断,否则直接放行。1000代表的是MIUI系统应用(也就是MIUI对自己的应用默认开后门)
- 如果不是系统应用,判断Action是CONFIRM_START_ACTIVITY_ACTION,return false,不放行。
- Intent的来源是手机管家并且Activity是ConfirmStartActivity,那就直接return false,不放行。
可以看到,在miui-services.jar包中,相关的逻辑也就到此为止了。再搜索isAllowStartCurrentActivity也看不到更多的信息。因此,更多的逻辑可能还是在其他的framework代码里。(注意:以下篇幅也是经常在miui-services.jar和miui-framework.jar来回折腾)
3、miui-framework.jar分析
反编译miui-framework.jar,搜索CHECK_ALLOW_START_ACTIVITY:
MiuiIntent中定义了Intent的一些Action,在这里不截图分析。看SecurityManager类中的调用,主要就在getCheckStartActivityIntent这个方法。这个方法比较长,第一部分是通过Action是PICK还是SEND来做挑选图库图片还是发送图片的判断,然后通过buildStartIntent方法去构建Intent:
第二部分则是我们调研的场景:
规则大致如下:
- 如果被调起的app已经启动,则return null,也就是不进行拦截。
- 如果调起者为系统app(uid<10000)并且 IFAAFaceManager.ERR_FACE_LOCKED!=0,放行。
- 如果调起者不为系统app(uid>=10000),但是被调起者为系统app并且 IFAAFaceManager.ERR_FACE_LOCKED!=0,放行。
- 如果调起者与被调起者同包名,放行。
- 否则,通过checkAllowStartActivity方法判断,判断通过放行。
mService是SecurityManager的一个变量,可以看到是ISecurityManager,我们看下这个接口,这接口里面有两个实现类Proxy和Stub(这里就是用的代理模式),二者都继承了Binder,并且实现了ISecurityManager接口:
我们知道Binder是用来跨进程通信的,在这里简单说下Binder通信机制:
(1)服务端创建对应Binder实例对象,接收来自客户端的请求,同时,将自身的Binder注册到ServiceManager。
(2)Binder驱动中创建对应mRemote对象。
(3)客户端想和服务端通信,通过ServiceManager查找到服务端的Binder,然后Binder驱动将对应的mRemote对象返回,至此已经建立完通信。
(4)在建立完毕通信之后,客户端通过调用mRemote对象的transact()方法,将数据发送到服务端,然后挂起自己等待回复。服务端收到数据后在onTransact()方法进行处理后将结果返回给客户端,客户端收到数据,重新拉起线程,至此进程间通信完毕。
继续看下去Proxy类中checkAllowStartActivity的实现:
看Stub类的实现:
上面我们也提到了,Stub继承了Binder,该调用是在onTransact方法中:
所以,以上代码简单整理一下:
(1)客户端在_data写入了调起者和被调起者的包名、uid等信息,这些信息通过mRemote对象的transact()方法传递给服务端
(2)服务端处理后返回给客户端,客户端通过_replay.readBoolean去获取判断结果,决定是否放行。
五、疑似拦截白名单
Stub是一个抽象类,其实现类SecurityManagerService在miui-services.jar中。这个类里面有个AccessController对象,看其作用,其中有几个方法例如filterIntentLocked,skipActivity等:
进一步看checkAccessControlPassLockedCore方法的调用链,可以看到是如下两个方法:
我们进一步看AccessController这个类,看到了疑似拦截的白名单代码。首先,有一套本地写死的白名单,如下:
其次,还提供了服务端更新白名单的逻辑:
看下mSkipList的使用,在filterIntentLocked方法中,主要就是判断packageName是否在白名单中:
咱们一开始其实是根据Intent的Action - CHECK_ALLOW_START_ACTIVITY来跟踪的,定位到了getCheckStartActivityIntent方法,其调用来源在这如下代码这。上面的这个拦截白名单的使用并没有跟我们的场景关联起来,而是在另一种场景,其调用的入口如下图的最后一行代码:
因此,该白名单大概率不是调起三方应用的那个场景,而是其他的场景。 试想一下,针对调起三方应用弹窗的场景:如果小米对支付宝加白放行,没对微信或抖音放行,那么不会引起公愤吗?所以,我认为这必然不是该场景。