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

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

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


截图

悬浮窗原理

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

如今UC能突破这个限制, 我很好奇它是怎么作到的. 浏览器

研究实现

Android开发有点蛋疼的地方就是太容易被反编译, 但有时这也成为咱们研究别人app的一种手段. 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 this

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

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

之因此这么肯定是在代码里, 是由于UC在咱们复制的内容不一样时, 悬浮窗标题会不同, 必定是在代码里控制的, 结果以下 线程

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

结果可能和你们不同, 可是必定会找到一个被混淆的smali文件

看代码

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

虽然代码都被混淆了, 并且以咱们不熟悉的方式出现, 但咱们能够根据一些蛛丝马迹来判断代码的执行, 好比Framework的类和API是不能被混淆的, 这也是咱们能看懂smali的缘由之一, 咱们能够结合这些面包屑来还原整个app代码, 固然这须要咱们对smali很熟悉, 若是不熟悉smali, 至少要对Android的API熟悉. 由于有时实在看不懂, 咱们要靠猜来还原一段代码的逻辑.

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

(省略) const v3, 0x7f070049 invoke-virtual {v1, v3}, Landroid/content/res/Resources;->getString(I)Ljava/lang/String;

    move-result-object v1

    iput-object v1, v0, Lcom/uc/browser/b/a;->dpC:Ljava/lang/String;

    :cond_9

    (省略)

    invoke-virtual {v0, v1}, Lcom/uc/browser/b/a;->o(Landroid/graphics/drawable/Drawable;)V
    :try_end_2
    .catch Ljava/lang/Exception; {:try_start_2 .. :try_end_2} :catch_0 goto/16 :goto_0
    (省略)

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

#使v3寄存器的值为0x7f070049 const v3, 0x7f070049 #v1是Resources实例, 调用它的getString方法, 方法的参数是v3中的值 invoke-virtual {v1, v3}, Landroid/content/res/Resources;->getString(I)Ljava/lang/String; #将结果存入v1寄存器 move-result-object v1

其实就是咱们经常使用的getResources().getString
其实若是一直这么看下去, 会发现毫无头绪, 剩下的代码一直在干差很少的事情, 因此我只截取了这部分, 注意最后一行

goto/16 :goto_0

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

:goto_0
    (省略) const-string v1, "window" invoke-virtual {v0, v1}, Landroid/content/Context;->getSystemService(Ljava/lang/String;)Ljava/lang/Object;

    move-result-object v0

    check-cast v0, Landroid/view/WindowManager;

    invoke-interface {v0}, Landroid/view/WindowManager;->getDefaultDisplay()Landroid/view/Display;

    move-result-object v0

    invoke-virtual {v0}, Landroid/view/Display;->getWidth()I

    move-result v0

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

    iput v0, v1, Landroid/view/WindowManager$LayoutParams;->width:I

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

    invoke-virtual {v10}, Lcom/uc/browser/b/a;->getContext()Landroid/content/Context;

    move-result-object v1

    invoke-virtual {v1}, Landroid/content/Context;->getResources()Landroid/content/res/Resources;

    move-result-object v1 const v2, 0x7f0d0022 invoke-virtual {v1, v2}, Landroid/content/res/Resources;->getDimension(I)F

    move-result v1

    float-to-int v1, v1

    iput v1, v0, Landroid/view/WindowManager$LayoutParams;->height:I

    iget-object v0, v10, Lcom/uc/browser/b/a;->mWindowManager:Landroid/view/WindowManager;

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

    invoke-interface {v0, v10, v1}, Landroid/view/WindowManager;->addView(Landroid/view/View;Landroid/view/ViewGroup$LayoutParams;)V

其实看到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, v10是com/uc/browser/b/a的成员dpx, 那么打开com/uc/browser/b/a.smali看看dpx究竟是怎么构造的.

(省略)

.field dpx:Landroid/view/WindowManager$LayoutParams;

    (省略)
    .line 68 new-instance v0, Landroid/view/WindowManager$LayoutParams;

    invoke-direct {v0}, Landroid/view/WindowManager$LayoutParams;-><init>()V

    iput-object v0, p0, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams;

    .line 69 if-eqz p2, :cond_0

    .line 70 iget-object v0, p0, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams; const/16 v1, 0x7d5 iput v1, v0, Landroid/view/WindowManager$LayoutParams;->type:I

    .line 74 :goto_0
    iget-object v0, p0, Lcom/uc/browser/b/a;->dpx:Landroid/view/WindowManager$LayoutParams; const/4 v1, 0x1 iput v1, v0, Landroid/view/WindowManager$LayoutParams;->format:I
    (省略)

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

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源码中有这么一段

public int checkAddPermission(WindowManager.LayoutParams attrs) { int type = attrs.type; if (type < WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW
            || type > WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) { return WindowManagerImpl.ADD_OKAY;
    }
    String permission = null; switch (type) { case TYPE_TOAST: // XXX right now the app process has complete control over // this... should introduce a token to let the system // monitor/control what they are doing. break; case TYPE_INPUT_METHOD: case TYPE_WALLPAPER: // The window manager will check these. break; case TYPE_PHONE: case TYPE_PRIORITY_PHONE: case TYPE_SYSTEM_ALERT: case TYPE_SYSTEM_ERROR: case TYPE_SYSTEM_OVERLAY:
            permission = android.Manifest.permission.SYSTEM_ALERT_WINDOW; break; default:
            permission = android.Manifest.permission.INTERNAL_SYSTEM_WINDOW;
    } if (permission != null) { if (mContext.checkCallingOrSelfPermission(permission)
                != PackageManager.PERMISSION_GRANTED) { return WindowManagerImpl.ADD_PERMISSION_DENIED;
        }
    } return WindowManagerImpl.ADD_OKAY;
}

能够猜到这个方法是往系统的WindowManager里addView的时候作权限检查用的, 那个type就是咱们在构造WindowManager.LayoutParams时赋值的type, 能够看到, 除了TYPE_TOAST, 其余都是要权限的, 并且很是喜感的是, 代码中的注释还说他们如今对这种type毫无限制, 应该引入标记来限制开发者.

实测效果

看到有评论说这样的是不支持点击的. 我以前写的一个app有悬浮窗播放功能, 支持拖动窗口和点击暂停, 关闭窗口等等, 实测功能正常.


无权限悬浮窗演示gif

可是在2.3上不能接收点击事件.

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

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: 属于悬浮窗, 可是会被禁止

更多问题

关于UC如何处理2.3的问题, 我并无仔细看, 由于我确实是没有在2.3上测过使用TYPE_TOAST的状况, 但愿有机器的同窗能帮忙测一下UC这个功能在2.3上的具体表现. 另外我的的解决方案是在2.3上使用级别更高的type, 我记得刚开始用小米的时候, 是没有悬浮窗这个权限的管理的, 加上2.3的手机如今不少都没有维护了, 直接申请android.permission.SYSTEM_ALERT_WINDOW也无妨.

但仍是但愿能有同窗告知一下UC在2.3上是如何表现这个功能的.

尾声

如今咱们都知道了如何在不申请权限的状况下显示悬浮窗, 我相信以中国Android开发者的脑洞, 必定会有不少有趣或恶心的功能被开发出来, 一方面我本身以为这个东西颇有用, 能够实现一些很神奇的功能, 另外一方面又担忧这个API被滥用, 最终不得不限制权限.

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

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

相关文章
相关标签/搜索