Android无需权限显示悬浮窗, 兼谈逆向分析app
2015-10-17 12:35
302 查看
本文先在简书上发布, 获得许多反馈, 所以在CSDN也发一下, 与大家分享
如下图, 截图是在使用Chrome时截的, 但是屏幕顶部却有UC的view浮在屏幕上. 我使用的是小米, 我并没有给UC授悬浮窗权限, 所以我看到这个悬浮窗时是很震惊的.
现在UC能突破这个限制, 我很好奇它是怎么做到的.
这里我们拿到了
有了字符资源的id
之所以这么确定是在代码里, 是因为UC在我们复制的内容不同时, 悬浮窗标题会不一样, 一定是在代码里控制的, 结果如下
结果可能和大家不一样, 但是一定会找到一个被混淆的smali文件
虽然代码都被混淆了, 而且以我们不熟悉的方式出现, 但我们可以根据一些蛛丝马迹来判断代码的执行, 比如Framework的类和API是不能被混淆的, 这也是我们能看懂smali的原因之一, 我们可以结合这些面包屑来还原整个app代码, 当然这需要我们对smali很熟悉, 如果不熟悉smali, 至少要对Android的API熟悉. 因为有时实在看不懂, 我们要靠猜来还原一段代码的逻辑.
首先在代码里面找到
这是
其实就是我们常用的
其实如果一直这么看下去, 会发现毫无头绪, 剩下的代码一直在干差不多的事情, 所以我只截取了这部分, 注意最后一行
也就是说, 有可能代码转到
其实看到
看到这里, 我也觉得很奇怪, 我在悬浮窗原理中写的是我知道的实现悬浮窗的方法, UC的实现好像跟我调用的是相同的API, 也没看到反射之类可能展示奇技淫巧的代码, 为什么UC就可以不需要权限直接显示悬浮窗呢?
由于是系统的类, 无法混淆, 直接搜索
这句话就是把
这里的代码就很简单的, 我最先看的是下面这段
这两句代码就是把
之前我一直以为调用了系统
可以猜到这个方法是往系统的
但是在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: 属于悬浮窗, 但是会被禁止
(TODO: 补充演示GIF)
但还是希望能有同学告知一下UC在2.3上是如何表现这个功能的.
还有就是, 逆向分析仅用于学习, 不要干违法的事情.
本人技术有限, 如果文中有错误的欢迎指正, 以免误导他人
利益声明: 虽然我目前在UC实习, 但我并没有UC浏览器中文版的代码权限, 也不会将公司的代码分享给外人. 本文完全是靠我自己开发经验+逆向分析经验+Google完成, 在此之前没有看过UC浏览器的任何代码.
前言
最近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, 发现了如下代码
(省略) 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有悬浮窗播放功能, 支持拖动窗口和点击暂停, 关闭窗口等等, 实测功能正常, 今天下班匆匆忙忙写的这篇文章, 没有录制演示视频.但是在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: 属于悬浮窗, 但是会被禁止
(TODO: 补充演示GIF)
更多问题
关于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被滥用, 最终不得不限制权限.还有就是, 逆向分析仅用于学习, 不要干违法的事情.
本人技术有限, 如果文中有错误的欢迎指正, 以免误导他人
利益声明: 虽然我目前在UC实习, 但我并没有UC浏览器中文版的代码权限, 也不会将公司的代码分享给外人. 本文完全是靠我自己开发经验+逆向分析经验+Google完成, 在此之前没有看过UC浏览器的任何代码.
相关文章推荐
- 使用C++实现JNI接口需要注意的事项
- Android IPC进程间通讯机制
- Android Manifest 用法
- [转载]Activity中ConfigChanges属性的用法
- Android之获取手机上的图片和视频缩略图thumbnails
- Android之使用Http协议实现文件上传功能
- Android学习笔记(二九):嵌入浏览器
- android string.xml文件中的整型和string型代替
- i-jetty环境搭配与编译
- android之定时器AlarmManager
- android wifi 无线调试
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- android 代码实现控件之间的间距
- android FragmentPagerAdapter的“标准”配置
- Android"解决"onTouch和onClick的冲突问题
- android:installLocation简析
- android searchView的关闭事件
- SourceProvider.getJniDirectories