前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android 无需权限显示悬浮窗, 兼谈逆向分析 App

Android 无需权限显示悬浮窗, 兼谈逆向分析 App

作者头像
非著名程序员
发布2018-02-02 17:26:17
3.1K0
发布2018-02-02 17:26:17
举报
文章被收录于专栏:非著名程序员

前言

最近 UC 浏览器中文版出了一个快速搜索的功能, 在使用其他 app 的时候, 如果复制了一些内容, 屏幕顶部会弹一个窗口, 提示一些操作, 点击后跳转到 UC, 显示这个悬浮窗不需要申请android.permission.SYSTEM_ALERT_WINDOW权限.

如下图, 截图是在使用 Chrome 时截的, 但是屏幕顶部却有 UC 的 view 浮在屏幕上. 我使用的是小米, 我并没有给 UC 授悬浮窗权限, 所以我看到这个悬浮窗时是很震惊的.

悬浮窗原理

做过悬浮窗功能的人都知道, 要想显示悬浮窗, 要有一个服务运行在后台, 通过getSystemService(Context.WINDOW_SERVICE)拿到WindowManager, 然后向其中addView, addView第二个参数是一个WindowManager.LayoutParams, WindowManager.LayoutParams中有一个成员type, 有各种值, 一般设置成TYPE_PHONE就可以悬浮在很多 view 的上方了, 但是调用这个方法需要申请android.permission.SYSTEM_ALERT_WINDOW权限, 在很多机型上, 这个权限的名字叫悬浮窗, 比如小米手机上默认是禁用这个权限的, 有些恶意 app 会用这个权限弹广告, 而且很难追查是哪个应用弹的. 如果这个权限被禁用, 那么结果就是悬浮窗无法展示, 比如有道词典复制查词功能, 在小米手机上经常没用, 其实是用户没有授权, 而且应用也没有引导用户给它打开授权.

现在 UC 能突破这个限制, 我很好奇它是怎么做到的.

研究实现

Android 开发有点蛋疼的地方就是太容易被反编译, 但有时这也成为我们研究别人 app 的一种手段.

反编译

使用* apktool* 可以很轻松的反编译 UC.

找代码

逆向别人的 app, 比较关键的地方是怎么找代码, 因为代码基本上都是混淆的, 直接看肯定是看不懂的, 只能去找, 突破口一般在字符资源上, 比如我们看到上图中的快速搜索是 UC 的字符, 那么我们到res/values/strings.xml去找快速搜索, 就可以找到下面的内容

<string name="dark_search_banner_search">快速搜索</string>

这里我们拿到了快速搜索对应的名字dark_search_banner_search, Android 在编译时会给每个资源分配一个 id, 我们 grep 一下这个字符资源的名字就能知道 id 是多少, 一般在R.java, res/values/public.xml中有定义, 我直接到 public.xml 中找到了它的 id

<public type="string" name="dark_search_banner_search" id="0x7f070049" />

有了字符资源的 id 0x7f070049, 我们再在代码里面 grep 一下这个 id, 就能知道哪几个文件使用了这个字符资源.

之所以这么确定是在代码里, 是因为 UC 在我们复制的内容不同时, 悬浮窗标题会不一样, 一定是在代码里控制的, 结果如下

./com/uc/browser/b/f.smali

结果可能和大家不一样, 但是一定会找到一个被混淆的 smali 文件

看代码

这一部应该是最恶心的. smali 代码和 java 代码的关系, 就像汇编代码和 C++ 代码, 但是 smali 比汇编代码要容易理解的多, 不然也不会有那么多公司故意将代码写在 C++ 层了.

虽然代码都被混淆了, 而且以我们不熟悉的方式出现, 但我们可以根据一些蛛丝马迹来判断代码的执行, 比如 Framework 的类和 API 是不能被混淆的, 这也是我们能看懂 smali 的原因之一, 我们可以结合这些面包屑来还原整个 app 代码, 当然这需要我们对 smali 很熟悉, 如果不熟悉 smali, 至少要对 Android 的 API 熟悉. 因为有时实在看不懂, 我们要靠猜来还原一段代码的逻辑.

首先在代码里面找到0x7f070049, 发现了如下代码

这是0x7f070049出现之后的一部分代码, 一路看下来, 其实都是在取值赋值, 就拿0x7f070049来说:

其实就是我们常用的getResources().getString其实如果一直这么看下去, 会发现毫无头绪, 剩下的代码一直在干差不多的事情, 所以我只截取了这部分, 注意最后一行

goto/16 :goto_0

也就是说, 有可能代码转到goto_0那儿去了, 那么看看goto_0那里又写了些什么

其实看到const-string v1, "window", 我们就应该有所警惕了, 这可能是关键代码了. 为什么这么说? 因为悬浮窗的实现里面, 需要获取WindowManager, 从而需要调用Context.getSystemService(Context.WINDOW_SERVICE), 而官方文档写了Context.WINDOW_SERVICE就是常量window. 而后我们看到代码中构造了WindowManager.LayoutParams, 最终在addView时传入.

看到这里, 我也觉得很奇怪, 我在悬浮窗原理中写的是我知道的实现悬浮窗的方法, UC 的实现好像跟我调用的是相同的 API, 也没看到反射之类可能展示奇技淫巧的代码, 为什么 UC 就可以不需要权限直接显示悬浮窗呢?

猜测

我认为addView的第二个参数WindowManager.LayoutParams可能是关键, 所以我需要知道 UC 是如何构造这个WindowManager.LayoutParams的.

由于是系统的类, 无法混淆, 直接搜索LayoutParams就找到了下面的代码

iget-object v1, v10, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;

这句话就是把v10的值赋给v1, v10com/uc/browser/b/a的成员dpx, 那么打开com/uc/browser/b/a.smali看看dpx到底是怎么构造的.

这里的代码就很简单的, 我最先看的是下面这段

const/16 v1, 0x7d5 iput v1, v0, Landroid/view/WindowManager$LayoutParams;->type:I

这两句代码就是把WindowManager.LayoutParams.type字段设成 0x7d5, 官网上写了 0x000007d5 是WindowManager.LayoutParams.TYPE_TOAST的值.

验证

实际测试了一下, 将 type 设置成 TYPE_TOAST 果然有奇效, 不需要android.permission.SYSTEM_ALERT_WINDOW权限就能显示一个悬浮窗.

之前我一直以为调用了系统WindowManager.addView需要android.permission.SYSTEM_ALERT_WINDOW权限, 但实际上调用这个方法是不需要权限的, 在 Android 源码中有这么一段

可以猜到这个方法是往系统的WindowManageraddView的时候做权限检查用的, 那个type就是我们在构造WindowManager.LayoutParams时赋值的type, 可以看到, 除了TYPE_TOAST, 其他都是要权限的, 而且非常喜感的是, 代码中的注释还说他们现在对这种 type 毫无限制, 应该引入标记来限制开发者.

处理兼容性

在这篇文章刚刚公布的时候, 就有同学反馈悬浮窗无法接收事件, 刚开始我并没有特别在意, 在廖祜秋大神做了一个 demo 之后, 这篇文章阅读量又涨了不少, 随即收到更多反馈事件的问题, 我今天晚上借了台 MIUI V5 4.2.2 实测了一下, 这台机器上 UC 的快速搜索功能也无法正常使用.

在这个 ROM 上表现为:使用TYPE_PHONE这类需要权限的 type 时, 只有在 app 处于前台时能显示悬浮窗, 且能正常接受触摸事件. 如果在应用详情里面授悬浮窗权限, 则工作完全正常.(这里是 MIUI V5 对悬浮窗的特殊处理, 现在的 ROM, 包括 MIUI V6 上, 如果不授权, 无法显示任何悬浮窗)使用TYPE_TOAST这个不需要权限的 type 时, 悬浮窗正常显示, 但不能接受触摸事件.

我重新检查了一下 smali 代码, 发现 UC 是有分版本处理的, 不过因为 smali 代码的规则问题, 很难直接看出来, 我把分析过程写出来, 顺便解释一下 smali 的语法, 供大家以后逆向时拿来参考.

这次我是在 OS X 上反编译的, 所以变量名可能略有区别.

接着上面com/uc/browser/b/a.smali中查看dpx的构造过程, 代码如下:

为了方便说明, 我遵循 smali 的规则, 它用.line XX, 我们就说这是第 XX 行的代码.

上面是我之前分析得到 UC 使用的是TYPE_TOAST的地方, 证据就是第 70 行的const/16 v1, 0x7d5, 但是要知道, smali 代码没有跳转的话, 就是从上往下执行, 我们看第 69 行的代码如下:

.line 69 if-eqz p2, :cond_0

这句话的意思是如果 p2 等于 0, 控制流跳转到 cond_0, 否则就是继续顺序往下执行. 也就是说 UC 只有在p2 != 0条件满足的时候才会使用TYPE_TOAST, 我们看看cond_0对应的代码.

这里很简单, 就是将0x7d2赋给了type, 官网写了0x000007d2TYPE_PHONE, 也就是说 UC 在某种情况下还是会用需要权限的老方法展示悬浮窗.

现在问题是条件是什么, 关键在p2, 在 smali 里面, 有两种寄存器命名规则, 一种叫 v 命名规则, 另一种是 p 命名规则, 当然只是命名规则而已, 在使用 apktool 时是可以选的. 这里是 p 命名规则.

我刚才分析的赋值过程, 所在的方法是下面这个, 我在刚才的代码片段中也保留了这个部分.

这就是com/uc/browser/b/a的构造方法, dpx就是在构造方法里初始化的, .locals 7告诉我们这个方法中将出现 7 个局部寄存器 (local register), 名字是 v0, v1...v6, 而这个方法的参数有 3 个, 隐式告诉我们这个方法中将出现 3 个参数寄存器 (parameter register), 名字分别是 p0, p1, p2.

我是怎么知道这个方法有 3 个参数的呢. smali 中非静态方法, 都隐含一个参数 p0, 指向自身, 和 Java 中的this是一个意思, 而方法的参数写在括号里, 也就是Landroid/content/Context;Z, 其中Landroid/content/Context;很明显就是 Android 中的Context, 值存储在 p1 里, 而Z对应的是 Android 中的boolean, p2 就是他了.

也就是说, type 是用TYPE_TOAST还是用TYPE_PHONE, 取决于这个构造方法的第二个参数, 那到底谁构造了com/uc/browser/b/a呢? 可以去代码里面搜形如new-instance ***, Lcom/uc/browser/b/a;的代码. 更保险的做法是搜Lcom/uc/browser/b/a然后一个一个的看.

我在com/uc/browser/b/f.smali里面找到了下面的代码:

这段代码首先是创建了com/uc/browser/b/a的实例, 存储在 v3 中, 从另一处拿到了一个Context存储在 v4 中, 然后拿到了当前系统的android.os.Build.VERSION.SDK_INT存储在 v5 中, 此时将 v6 的值设为0x13, 千万别粗心看成 13 了, 我好几次都觉得这是 13, 其实是十进制的 19, 接下来是一个条件分支, 如果 v5 的值小于 v6, 也就是说android.os.Build.VERSION.SDK_INT < 19, 直接跳转到 cond_0, 否则先将 v1 的值赋给 v0, 再顺序执行.

这句代码

invoke-direct {v3, v4, v0}, Lcom/uc/browser/b/a;-><init>(Landroid/content/Context;Z)V

就是调用 v3 的构造方法, 参数是 v4 和 v0, 分析一下上面这段代码的逻辑就是:如果当前系统 API level 小于 19, 那么第二个参数就是 0, 否则就是 1.

而这第二个参数的值就是之前我们分析的 p2 的值, UC 只有在p2 != 0条件满足的时候才会使用TYPE_TOAST, 把整个逻辑串起来就是:

UC 在 API level >= 19 的时候, 使用TYPE_TOAST, 其他情况使用TYPE_PHONE(需要权限).

可能是为了规避在低版本TYPE_TOAST不能接受事件的问题.

关于针对源代码的分析, 请看 Android 悬浮窗使用 TYPE_TOAST 的小结

实测效果

我之前写的一个 app 有悬浮窗播放功能, 支持拖动窗口和点击暂停, 关闭窗口等等, 在 4.4.4 上实测功能正常.

无权限悬浮窗演示 gif

感谢微博上关注的大神廖祜秋, 他做了个 demo, 虽然交互和 UC 不同, 可以参考一下实现.

廖祜秋大神的 demo

关于这个, 他也写了一篇 Android 悬浮窗的小结

其他补充

评论区的浮海大虾同学有更多补充如下:

TYPE_TOAST 一直都可以显示, 但是用 TYPE_TOAST 显示出来的在 2.3 上无法接收点击事件, 因此还是无法随意使用.下面是我之前研究后台线程显示对话框的时候记得笔记, 大家可以看看我们项目中有需求需要在后台任务中显示 Dialog, 项目最初的做法是用 Activity 模拟 Dialog, 一个 Activity 已经承载了近 20 种 Dialog, 代码混乱至极. 后来我发现 Dialog 可以通过改变 Window Type 实现不依赖 Activity 显示, 然后就很兴奋的要在使用这种方式来作为新的实现方式.最初 WindowType 是 WindowManager.LayoutParams.TYPE_SYSTEM_ALERT, 可是这是悬浮窗了, MIUI 会默认禁止 (真他妈操蛋,也没有任何提示) 最终放弃. 后来试着换成了 WindowManager.LayoutParams.TYPE_TOAST, 起初效果很好,MIUI 也不禁止了, 哪里都能显示, 这下开心了. 可是后来又发现在 2.3 上不能接收点击事件, 也就是说 Dialog 上的按钮不能点击, 这他妈就很操蛋了, 又放弃了. 又试了试其他的 Type 都不能满足需求, 结果如下:TYPE_SEARCH_BAR: 未知TYPE_ACCESSIBILITY_OVERLAY: 拒绝使用TYPE_APPLICATION: 只能配合 Activity 在当前 APP 使用 TYPE_APPLICATION_ATTACHED_DIALOG: 只能配合 Activity 在当前 APP 使用TYPE_APPLICATION_MEDIA: 无法使用 (什么也不显示)TYPE_APPLICATION_PANEL: 只能配合 Activity 在当前 APP 使用 (PopupWindow 默认就是这个 Type)TYPE_APPLICATION_STARTING: 无法使用 (什么也不显示)TYPE_APPLICATION_SUB_PANEL: 只能配合 Activity 在当前 APP 使用 TYPE_BASE_APPLICATION: 无法使用 (什么也不显示)TYPE_CHANGED: 只能配合 Activity 在当前 APP 使用TYPE_INPUT_METHOD: 无法使用 (直接崩溃)TYPE_INPUT_METHOD_DIALOG: 无法使用 (直接崩溃)TYPE_KEYGUARD_DIALOG: 拒绝使用TYPE_PHONE: 属于悬浮窗 (并且给一个 Activity 的话按下 HOME 键会出现看不到桌面上的图标异常情况)TYPE_TOAST: 不属于悬浮窗, 但有悬浮窗的功能, 缺点是在 Android2.3 上无法接收点击事件TYPE_SYSTEM_ALERT: 属于悬浮窗, 但是会被禁止

尾声

现在我们都知道了如何在不申请权限的情况下显示悬浮窗, 我相信以中国 Android 开发者的脑洞, 一定会有很多有趣或恶心的功能被开发出来, 一方面我自己觉得这个东西很有用, 可以实现一些很神奇的功能, 另一方面又担心这个 API 被滥用, 最终不得不限制权限.

还有就是, 逆向分析仅用于学习, 不要干违法的事情.

本人技术有限, 如果文中有错误的欢迎指正, 以免误导他人

利益声明: 虽然我目前在 UC 实习, 但我并没有 UC 浏览器中文版的代码权限, 也不会将公司的代码分享给外人. 本文完全是靠我自己开发经验 + 逆向分析经验 + Google 完成, 在此之前没有看过 UC 浏览器的任何代码.

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2017-09-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 非著名程序员 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 悬浮窗原理
  • 研究实现
    • 反编译
      • 找代码
        • 看代码
          • 猜测
          • 验证
          • 处理兼容性
          • 实测效果
          • 其他补充
          • 尾声
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档