美文网首页
黑科技:提升进程优先级的一种思路

黑科技:提升进程优先级的一种思路

作者: 潇风寒月 | 来源:发表于2021-01-30 20:01 被阅读0次

    本文中的demo地址,文中源码为API 28

    一、前言

    前不久,看到维术大佬发表的一篇文章:另一种黑科技保活方法。文章内容主要是利用Android的2个bug(黑科技就是利用系统bug骚操作)来提升进程的优先级为前台进程,觉得挺有意思,于是决定找个时间研究一下。因为原文中大佬主要写的是思路,所以流程比较粗略,没有提供具体的demo实现。

    可能有些朋友不知道维术大佬,太极·虚拟框架就是他创作的。

    我就想着自己简单实现一下,搞个demo看看效果。结果不搞不知道啊,这玩意儿搞起来可太花时间了,太多知识盲区了。

    留下了没有技术含量的泪水.jpg

    本文将分析Android的这2个bug在哪里、如何才能触发、方案实施、Android修复bug方法、统计各厂商实际效果。

    展示一个正常的前台服务

    在日常的开发中,展示一个前台服务是经常使用到的一个功能,大致如下:

    //创建channel
    private void createChannel() {
        NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        NotificationChannel channel = new NotificationChannel(CHANNEL_ID, getString(R.string.app_name),
                NotificationManager.IMPORTANCE_HIGH);
        channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
        if (manager != null) {
            manager.createNotificationChannel(channel);
        }
    }
    
    //展示通知 成为前台服务
    private void showNormalNotify() {
        createChannel();
    
        Notification notification = new Notification.Builder(this, CHANNEL_ID)
                .setAutoCancel(false)
                .setContentTitle(getString(R.string.app_name))
                .setContentText("运行中...")
                .setWhen(System.currentTimeMillis())
                .setSmallIcon(R.mipmap.ic_launcher_round)
                .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
                .build();
        startForeground(1, notification);
    }
    

    二、方案1:创建前台服务时传递一个错误channel

    前置知识:startForeground流程

    首先来分析一下startForeground的流程,方便后续理解。咱们在代码里面使用startForeground()会来到Service#startForeground()中

    //Service.java
    private IActivityManager mActivityManager = null;
    public final void startForeground(int id, Notification notification) {
        try {
            mActivityManager.setServiceForeground(
                    new ComponentName(this, mClassName), mToken, id,
                    notification, 0);
        } catch (RemoteException ex) {
        }
    }
    

    这个方法里面实际上是调用的mActivityManager的setServiceForeground()来完成实际操作。而这个mActivityManager是一个IActivityManager接口,这个接口的实例是谁呢?我通过分析发现在Service#attach中有对其赋值

    //Service.java
    public final void attach(
            Context context,
            ActivityThread thread, String className, IBinder token,
            Application application, Object activityManager) {
        attachBaseContext(context);
        ......
        mActivityManager = (IActivityManager)activityManager;
    }
    

    因之前我写过一篇博客,刚好分析过Service的启动流程,里面见过这个方法。 看到这个熟悉的attach,我知道,肯定是在ActivityThread里面调用的这个方法了。

    //ActivityThread.java
    private void handleCreateService(CreateServiceData data) {
        //构建Service 利用反射取构建实例
        Service service = null;
        java.lang.ClassLoader cl = packageInfo.getClassLoader();
        service = packageInfo.getAppFactory()
                .instantiateService(cl, data.info.name, data.intent);
        
        //初始化ContextImpl
        ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
    
        Application app = packageInfo.makeApplication(false, mInstrumentation);
        //注意啦,在这里 传入的是ActivityManager.getService()
        service.attach(context, this, data.info.name, data.token, app,
                ActivityManager.getService());
        //接下来马上就会调用Service的onCreate方法
        service.onCreate();
        
        //mServices是用来存储已经启动的Service的
        mServices.put(data.token, service);
        ....
    }
    

    原来传入的是ActivityManager.getService(),就是ActivityManagerService的binder引用。所以,上面的startForeground逻辑来到了ActivityManagerService的setServiceForeground()。

    //ActivityManagerService.java
    @Override
    public void setServiceForeground(ComponentName className, IBinder token,
            int id, Notification notification, int flags) {
        synchronized(this) {
            //mServices中可以找到某个已经启动了的Service
            mServices.setServiceForegroundLocked(className, token, id, notification, flags);
        }
    }
    
    //ActiveServices.java
    public void setServiceForegroundLocked(ComponentName className, IBinder token,
            int id, Notification notification, int flags) {
        final int userId = UserHandle.getCallingUserId();
        final long origId = Binder.clearCallingIdentity();
        try {
            //根据className, token, userId找到需要创建前台服务的Service的ServiceRecord
            ServiceRecord r = findServiceLocked(className, token, userId);
            if (r != null) {
                setServiceForegroundInnerLocked(r, id, notification, flags);
            }
        } finally {
            Binder.restoreCallingIdentity(origId);
        }
    }
    
    /**
    * @param id Notification ID.  Zero === exit foreground state for the given service.
    */
    private void setServiceForegroundInnerLocked(final ServiceRecord r, int id,
            Notification notification, int flags) {
        if (id != 0) {
            if (notification == null) {
                throw new IllegalArgumentException("null notification");
            }
            // Instant apps 
            if (r.appInfo.isInstantApp()) {
                ......
            } else if (r.appInfo.targetSdkVersion >= Build.VERSION_CODES.P) {
                //Android P以上需要确认有权限
                mAm.enforcePermission(
                        android.Manifest.permission.FOREGROUND_SERVICE,
                        r.app.pid, r.appInfo.uid, "startForeground");
            }
            
            .....
            r.postNotification();
            if (r.app != null) {
                updateServiceForegroundLocked(r.app, true);
            }
            getServiceMapLocked(r.userId).ensureNotStartingBackgroundLocked(r);
            mAm.notifyPackageUse(r.serviceInfo.packageName,
                    PackageManager.NOTIFY_PACKAGE_USE_FOREGROUND_SERVICE);
        } else {
            ......
        }
    }
    

    ActivityManagerService转手就交给ActiveServices去处理,ActiveServices一顿操作来到ServiceRecord的postNotification,这里就比较重要的,仔细看一下

    //ServiceRecord.java
    public void postNotification() {
        final int appUid = appInfo.uid;
        final int appPid = app.pid;
        if (foregroundId != 0 && foregroundNoti != null) {
            // Do asynchronous communication with notification manager to
            // avoid deadlocks.
            final String localPackageName = packageName;
            final int localForegroundId = foregroundId;
            final Notification _foregroundNoti = foregroundNoti;
            ams.mHandler.post(new Runnable() {
                public void run() {
                    //NotificationManagerService
                    NotificationManagerInternal nm = LocalServices.getService(
                            NotificationManagerInternal.class);
                    if (nm == null) {
                        return;
                    }
                    Notification localForegroundNoti = _foregroundNoti;
                    try {
                        if (localForegroundNoti.getSmallIcon() == null) {
                            // It is not correct for the caller to not supply a notification
                            // icon, but this used to be able to slip through, so for
                            // those dirty apps we will create a notification clearly
                            // blaming the app.
                            Slog.v(TAG, "Attempted to start a foreground service ("
                                    + name
                                    + ") with a broken notification (no icon: "
                                    + localForegroundNoti
                                    + ")");
    
                            CharSequence appName = appInfo.loadLabel(
                                    ams.mContext.getPackageManager());
                            if (appName == null) {
                                appName = appInfo.packageName;
                            }
                            Context ctx = null;
                            try {
                                ctx = ams.mContext.createPackageContextAsUser(
                                        appInfo.packageName, 0, new UserHandle(userId));
    
                                Notification.Builder notiBuilder = new Notification.Builder(ctx,
                                        localForegroundNoti.getChannelId());
    
                                // it's ugly, but it clearly identifies the app
                                notiBuilder.setSmallIcon(appInfo.icon);
    
                                // mark as foreground
                                notiBuilder.setFlag(Notification.FLAG_FOREGROUND_SERVICE, true);
    
                                Intent runningIntent = new Intent(
                                        Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                                runningIntent.setData(Uri.fromParts("package",
                                        appInfo.packageName, null));
                                PendingIntent pi = PendingIntent.getActivityAsUser(ams.mContext, 0,
                                        runningIntent, PendingIntent.FLAG_UPDATE_CURRENT, null,
                                        UserHandle.of(userId));
                                notiBuilder.setColor(ams.mContext.getColor(
                                        com.android.internal
                                                .R.color.system_notification_accent_color));
                                notiBuilder.setContentTitle(
                                        ams.mContext.getString(
                                                com.android.internal.R.string
                                                        .app_running_notification_title,
                                                appName));
                                notiBuilder.setContentText(
                                        ams.mContext.getString(
                                                com.android.internal.R.string
                                                        .app_running_notification_text,
                                                appName));
                                notiBuilder.setContentIntent(pi);
    
                                localForegroundNoti = notiBuilder.build();
                            } catch (PackageManager.NameNotFoundException e) {
                            }
                        }
    
                        //注意了,如果是没有创建channel,则会抛出一个RuntimeException
                        if (nm.getNotificationChannel(localPackageName, appUid,
                                localForegroundNoti.getChannelId()) == null) {
                            int targetSdkVersion = Build.VERSION_CODES.O_MR1;
                            try {
                                final ApplicationInfo applicationInfo =
                                        ams.mContext.getPackageManager().getApplicationInfoAsUser(
                                                appInfo.packageName, 0, userId);
                                targetSdkVersion = applicationInfo.targetSdkVersion;
                            } catch (PackageManager.NameNotFoundException e) {
                            }
                            if (targetSdkVersion >= Build.VERSION_CODES.O_MR1) {
                                throw new RuntimeException(
                                        "invalid channel for service notification: "
                                                + foregroundNoti);
                            }
                        }
                        if (localForegroundNoti.getSmallIcon() == null) {
                            // Notifications whose icon is 0 are defined to not show
                            // a notification, silently ignoring it.  We don't want to
                            // just ignore it, we want to prevent the service from
                            // being foreground.
                            throw new RuntimeException("invalid service notification: "
                                    + foregroundNoti);
                        }
                        nm.enqueueNotification(localPackageName, localPackageName,
                                appUid, appPid, null, localForegroundId, localForegroundNoti,
                                userId);
    
                        foregroundNoti = localForegroundNoti; // save it for amending next time
                    } catch (RuntimeException e) {
                        //上面的Exception 在这里会被捕获  展示Notification失败了
                        Slog.w(TAG, "Error showing notification for service", e);
                        // If it gave us a garbage notification, it doesn't
                        // get to be foreground.
                        //给我一个垃圾Notification,还想成为前台服务?妄想
                        ams.setServiceForeground(name, ServiceRecord.this,
                                0, null, 0);
                        //调用AMS#crashApplication()
                        ams.crashApplication(appUid, appPid, localPackageName, -1,
                                "Bad notification for startForeground: " + e);
                    }
                }
            });
        }
    }
    

    这段代码的核心思想是构建Notification,然后告知NotificationManagerService需要展示通知。在展示通知之前,会先判断一下是否有为这个通知创建好channel,如果没有则抛出异常,然后方法末尾的catch会将抛出的异常给捕获住。

    捕获住异常之后,系统执行收尾清理工作。系统知道这个通知创建失败了,将该Service设置为非前台。然后调用AMS的crashApplication(),看着方法名看起来是想营造一个crash给app。咱跟下去,看看是啥情况

    @Override
    public void crashApplication(int uid, int initialPid, String packageName, int userId,
            String message) {
        synchronized(this) {
            //mAppErrors是AppErrors
            mAppErrors.scheduleAppCrashLocked(uid, initialPid, packageName, userId, message);
        }
    }
    
    //AppErrors.java
    /**
    * Induce a crash in the given app.
    */
    void scheduleAppCrashLocked(int uid, int initialPid, String packageName, int userId,
            String message) {
        ProcessRecord proc = null;
    
        // Figure out which process to kill.  We don't trust that initialPid
        // still has any relation to current pids, so must scan through the
        // list.
    
        synchronized (mService.mPidsSelfLocked) {
            for (int i=0; i<mService.mPidsSelfLocked.size(); i++) {
                ProcessRecord p = mService.mPidsSelfLocked.valueAt(i);
                if (uid >= 0 && p.uid != uid) {
                    continue;
                }
                if (p.pid == initialPid) {
                    proc = p;
                    break;
                }
                if (p.pkgList.containsKey(packageName)
                        && (userId < 0 || p.userId == userId)) {
                    proc = p;
                }
            }
        }
        proc.scheduleCrash(message);
    }
    

    好家伙,从AppErrors的scheduleAppCrashLocked()注释看,是让一个app崩溃。

    //ProcessRecord.java
    IApplicationThread thread; 
    void scheduleCrash(String message) {
        // Checking killedbyAm should keep it from showing the crash dialog if the process
        // was already dead for a good / normal reason.
        if (!killedByAm) {
            if (thread != null) {
                long ident = Binder.clearCallingIdentity();
                try {
                    //thread是IApplicationThread,实际上是ActivityThread中的ApplicationThread
                    thread.scheduleCrash(message);
                } catch (RemoteException e) {
                    // If it's already dead our work is done. If it's wedged just kill it.
                    // We won't get the crash dialog or the error reporting.
                    kill("scheduleCrash for '" + message + "' failed", true);
                } finally {
                    Binder.restoreCallingIdentity(ident);
                }
            }
        }
    }
    

    ProcessRecord的scheduleCrash()的核心代码是执行thread的scheduleCrash()。但是这个thread是什么,我们暂时不知道。

    这里的thread是IApplicationThread,IApplicationThread是一个接口并且继承自android.os.IInterface,它在源码中的存在形式是IApplicationThread.aidl (路径:frameworks/base/core/java/android/app/IApplicationThread.aidl),在线源码观看地址。 看起来是在跨进程通信,通信双方是AMS进程与app进程。app端接收消息的地方在ActivityThread的ApplicationThread

    public final class ActivityThread extends ClientTransactionHandler {
        private class ApplicationThread extends IApplicationThread.Stub {
            //ApplicationThread是ActivityThread的内部类
            //看这个标准的样子,就知道肯定和aidl有关
        }
    }
    
    

    于是上面的ProcessRecord的scheduleCrash()其实是想通知ApplicationThread执行scheduleCrash(),注意,这里是跨进程的。

    //ActivityThread#ApplicationThread
    public void scheduleCrash(String msg) {
        sendMessage(H.SCHEDULE_CRASH, msg);
    }
    
    void sendMessage(int what, Object obj) {
        sendMessage(what, obj, 0, 0, false);
    }
    
    private void sendMessage(int what, Object obj, int arg1, int arg2, boolean async) {
        Message msg = Message.obtain();
        msg.what = what;
        msg.obj = obj;
        msg.arg1 = arg1;
        msg.arg2 = arg2;
        if (async) {
            msg.setAsynchronous(true);
        }
        mH.sendMessage(msg);
    }
    

    而在ApplicationThread的scheduleCrash()方法中,看起来只是发了个消息给mH这个Handler。

    //ActivityThread.java
    final H mH = new H();
    
    class H extends Handler {
        public static final int SCHEDULE_CRASH          = 134;
    
        public void handleMessage(Message msg) {
            if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
            switch (msg.what) {
                ......
                case SCHEDULE_CRASH:
                    throw new RemoteServiceException((String)msg.obj);
            }
        }
    }
    

    H这个Handler我们再熟悉不过了,什么绑定Application、绑定Service、停止Service、Activity生命周期回调什么的,都得靠这个Handler。H这个Handler在接收到SCHEDULE_CRASH这个消息时,会抛出一个RemoteServiceException。

    到这里,startForeground()时传入一个不存在的channel的流程就走完了,系统会抛出一个异常导致app崩溃。正常情况下,这是没有什么问题的。

    这里的漏洞是什么?

    假设我把这个消息拦截下来,然后不抛出错误,那app岂不是正常继续运行咯。确实是这样。

    方案实施

    思路1 拦截消息

    系统让app这边抛出一个异常,自行结束生命。那我收到系统给的指示,然后不抛出异常,不就可以绕过了么?那么,怎么绕?

    可以hook这个ActivityThread的H,拦截其SCHEDULE_CRASH消息,然后做自己想做的事情。

    大体思路倒是有了,具体如何实现呢? 要hook这个H,那首先我们要拿到ActivityThread的实例(一个app进程对应着一个ActivityThread)。在搜寻ActivityThread的API过程中发现一个东西

    /** Reference to singleton {@link ActivityThread} */
    private static volatile ActivityThread sCurrentActivityThread;
    
    public static ActivityThread currentActivityThread() {
        return sCurrentActivityThread;
    }
    
    private void attach(boolean system, long startSeq) {
        sCurrentActivityThread = this;
        ......
    }
    
    public static void main(String[] args) {
        Looper.prepareMainLooper();
    
        ActivityThread thread = new ActivityThread();
        thread.attach(false, startSeq);
    
        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }
    
        Looper.loop();
    
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }
    

    我注意到有一个sCurrentActivityThread的东西,在main方法里面一开始就初始化好了,然后从它的注释也能看出它是一个全局单例,即它就是ActivityThread的实例了,拿到它就好办了。然后接着我发现一个currentActivityThread()的静态方法,妙啊,原来系统早就想好了,给我们提供了一个public静态方法方便获取ActivityThread实例。我兴致冲冲地跑去Activity里面使用时,却发现,我好像连ActivityThread这个类都无法访问。

    /**
     * {@hide}
     */
    public final class ActivityThread extends ClientTransactionHandler {}
    

    好家伙,加了{@hide},静态方法是用不起了。虽然静态方法是用不起了,但是我们可以反射拿到这个sCurrentActivityThread静态变量。

    //拿ActivityThread的class对象
    Class<?> activityThreadClazz = Class.forName("android.app.ActivityThread");
    Field sCurrentActivityThread = activityThreadClazz.getDeclaredField("sCurrentActivityThread");
    sCurrentActivityThread.setAccessible(true);
    Object activityThread = sCurrentActivityThread.get(activityThreadClazz);
    

    ActivityThread实例倒是拿到了,接下来我们需要拦截里面的H这个Handler的消息。自己写一个Handler然后把原来的H这个Handler替换掉?不行,里面那么多逻辑,我们自己搞风险太大了,而且不现实。但是,我们可以给这个Handler设置一个mCallback。回忆一下:

    //Handler.java
    /**
     * Handle system messages here.
     */
    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }
    

    Handler在分发消息时,发现mCallback不为空,则先交给mCallback处理,如果mCallback处理结果返回false,再交给handleMessage进行处理。

    基于这个,咱思路有了,hook那个Handler的mCallback,然后只处理SCHEDULE_CRASH这个消息,其他的不管,还是交给原来的Handler的handleMessage进行处理。因为我们只处理SCHEDULE_CRASH这个消息,所以把风险降到了最低。

    思路有了,show me the code:

    //拿到SCHEDULE_CRASH的int值,源码里面写的是134,为了防止官方后面修改了这个值,这个134不直接写死
    Class<?> HClass = Class.forName("android.app.ActivityThread$H");
    Field scheduleCrashField = HClass.getDeclaredField("SCHEDULE_CRASH");
    scheduleCrashField.setAccessible(true);
    final int whatForScheduleCrash = scheduleCrashField.getInt(HClass);
    
    //拿mH实例
    Field mHField = activityThreadClazz.getDeclaredField("mH");
    mHField.setAccessible(true);
    Handler mH = (Handler) mHField.get(activityThread); 
    
    //给mH设置一个mCallback
    Class<?> handlerClass = Class.forName("android.os.Handler");
    Field mCallbackField = handlerClass.getDeclaredField("mCallback");
    mCallbackField.setAccessible(true);
    mCallbackField.set(mH, new Handler.Callback() {
        @Override
        public boolean handleMessage(@NonNull Message msg) {
            if (msg.what == whatForScheduleCrash) {
                Log.d("xfhy_hook", "收到一杯罚酒,我干了,你随意");
                return true;
            }
            return false;
        }
    });
    

    好了,到这里,我们已经hook成功了。现在去启动前台服务,用一个没有创建channel的通知看起来也不会崩溃了(也不一定,厂商可能修改了这部分逻辑,后面有验证结果)。这种办法启动的前台服务是不会展示任何通知在状态栏上的,用户无感知。

    思路2 Handle the exception in main loop

    大家先看看下面这段代码,就这么一小段代码即可达到与思路1同样的效果。

    new Handler(Looper.getMainLooper()).post(new Runnable() {
        @Override
        public void run() {
            while (true) {
                try {
                    Looper.loop();
                } catch (Throwable e) {
                    e.printStackTrace();
                }
            }
        }
    });
    

    给主线程的Looper发送了一个消息,这个消息的callback是上面的这个Runnable,实际执行逻辑是一段看起来像死循环一样的代码。

    分析一下,我们知道,在主线程中维护了Handler的消息机制,在应用启动的时候就做好了Looper的创建和初始化,然后开始使用Looper.loop()循环处理消息。

    //ActivityThread.java
    public static void main(String[] args) {
        //准备主线的MainLooper
        Looper.prepareMainLooper();
    
        ActivityThread thread = new ActivityThread();
        thread.attach(false, startSeq);
    
        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }
    
        //开始loop循环
        Looper.loop();
    
        //loop循环是不能结束的,否则app就会异常退出咯
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }
    

    我们在使用app的过程中,用户的所有操作事件、Activity生命周期回调、列表滑动等等,都是通过Looper的loop循环中完成处理的,其本质是将消息加入MessageQueue队列,然后循环从这个队列中取出消息并处理。如果没有消息可以处理的时候,会依靠Linux的epoll机制暂时挂起等待唤醒。下面是loop的核心代码:

    public static void loop() {
        final Looper me = myLooper();
        final MessageQueue queue = me.mQueue;
        for (;;) {
            Message msg = queue.next(); 
            msg.target.dispatchMessage(msg);
        }
    }
    

    死循环,不断取消息,没有消息的话就暂时挂起。我们上面那段短小精炼的代码,通过Handler往主线程发送了一个Runnable任务,然后在里面执行了一个死循环,死循环地执行Looper的loop方法读取消息。只要Looper的loop方法执行到了咱这个Message的callback,那么后面所有的主线程消息都会走到我们这个loop方法中进行处理。一旦发生了主线程崩溃,那么这里就可以进行异常捕获。然后又是死循环,捕获到异常之后,又开始继续执行Looper的loop方法,这样主线程就可以一直正常读取消息,刷新UI啥的都是正常的,不会有影响。

    这样的话,不管在ActivityThread#mH的handleMessage()中抛出什么异常都没事了。

    Android是如何修复的

    还好,谷歌在2020.8就修复了这个问题。

    //ServiceRecord.java
    @@ -798,6 +798,7 @@
                 final String localPackageName = packageName;
                 final int localForegroundId = foregroundId;
                 final Notification _foregroundNoti = foregroundNoti;
    +            final ServiceRecord record = this;
                 ams.mHandler.post(new Runnable() {
                     public void run() {
                         NotificationManagerInternal nm = LocalServices.getService(
    @@ -896,10 +897,8 @@
                             Slog.w(TAG, "Error showing notification for service", e);
                             // If it gave us a garbage notification, it doesn't
                             // get to be foreground.
    -                        ams.setServiceForeground(instanceName, ServiceRecord.this,
    -                                0, null, 0, 0);
    -                        ams.crashApplication(appUid, appPid, localPackageName, -1,
    -                                "Bad notification for startForeground: " + e);
    +                        ams.mServices.killMisbehavingService(record,
    +                                appUid, appPid, localPackageName);
                         }
                     }
                 });
    
    //ActiveServices.java
    +    void killMisbehavingService(ServiceRecord r,
    +            int appUid, int appPid, String localPackageName) {
    +        synchronized (mAm) {
    +            stopServiceLocked(r);
    +            mAm.crashApplication(appUid, appPid, localPackageName, -1,
    +                    "Bad notification for startForeground", true /*force*/);
    +        }
    +    }
    +
    
    //AppErrors.java
    //如果force是true,则5秒之后把app干死
    if (force) {
        // If the app is responsive, the scheduled crash will happen as expected
        // and then the delayed summary kill will be a no-op.
        final ProcessRecord p = proc;
        mService.mHandler.postDelayed(
                () -> killAppImmediateLocked(p, "forced", "killed for invalid state"),
                5000L);
    }
    
    

    postNotification()的时候,如果发现是前台服务,那么将调用停掉该前台服务,当然crashApplication()还是得调的。

    三、方案2:创建前台服务时搞一个错误布局

    系统如何处理创建前台服务时的错误布局

    这里就不带大家分析了,创建前台服务时遇到错误布局,最后会来到onNotificationError()。

    @Override
    public void onNotificationError(int callingUid, int callingPid, String pkg, String tag, int id,
            int uid, int initialPid, String message, int userId) {
        cancelNotification(callingUid, callingPid, pkg, tag, id, 0, 0, false, userId,
                REASON_ERROR, null);
    }
    

    可以看到,连崩溃都没有,只是简单取消一下通知就完了。

    这里的漏洞是什么?

    既然没有崩溃,那开发者就可以传递一个错误的布局id过来,然后只是通知被取消了,前台服务还是被创建成功了,而且还是没有展示通知的。

    方案实施

    只需要在开启前台服务的时候,自定义布局那里传递一个不存在的布局id即可。

    private void showErrorLayoutNotify() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            return;
        }
    
        createChannel();
    
        RemoteViews remoteViewTemplate = new RemoteViews(getPackageName(), /*R.layout.layout_test*/4);
    
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID);
        builder.setOngoing(true);
        builder.setContent(remoteViewTemplate);
        builder.setTicker("fuck");
        builder.setPriority(NotificationCompat.PRIORITY_LOW);
        builder.setSmallIcon(R.mipmap.ic_launcher_round);
        try {
            startForeground(1, builder.build());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    

    Android是如何修复的

    同样也是在2020.8修复了该问题,crashApplication方法这次传递的force是true,必须死。

    //NotificationManagerService.java
    @Override
    public void onNotificationError(int callingUid, int callingPid, String pkg, String tag,
            int id, int uid, int initialPid, String message, int userId) {
        final boolean fgService;
        synchronized (mNotificationLock) {
            NotificationRecord r = findNotificationLocked(pkg, tag, id, userId);
            fgService = r != null && (r.getNotification().flags & FLAG_FOREGROUND_SERVICE) != 0;
        }
        cancelNotification(callingUid, callingPid, pkg, tag, id, 0, 0, false, userId,
                REASON_ERROR, null);
        if (fgService) {
            // Still crash for foreground services, preventing the not-crash behaviour abused
            // by apps to give us a garbage notification and silently start a fg service.
            Binder.withCleanCallingIdentity(
                    () -> mAm.crashApplication(uid, initialPid, pkg, -1,
                        "Bad notification(tag=" + tag + ", id=" + id + ") posted from package "
                            + pkg + ", crashing app(uid=" + uid + ", pid=" + initialPid + "): "
                            + message, true /* force */));
        }
    }
    

    四、实际效果

    测试方式:测试时App在前台弹出Service,然后按home键回到桌面,此时看app的adj(进程优先级,0表示在前台,越小则表示优先级越高)状态。

    先使用命令行adb shell ps -A | grep xfhy找到我的demo进程,查看进程号

    u0_a85       10502  1796 1445700 104284 0                   0 S com.xfhy.frontservicedemo
    

    上面的10502即是进程号,然后通过adb shell cat proc/10502/oom_adj查看该进程adj值。这种方式只适用于部分手机,一些手机上会提示你没有权限,这时只能使用adb shell dumpsys activity processes,然后找到你的进程。如下面的日志中,有一个oom: max=1001 curRaw=0 setRaw=0 cur=0 set=0的值,勉强可以看出点东西,这些值也是越小则优先级越高。

    *APP* UID 10085 ProcessRecord{587081b 10502:com.xfhy.frontservicedemo/u0a85}
        user #0 uid=10085 gids={50085, 20085, 9997}
        requiredAbi=x86 instructionSet=null
        dir=/data/app/com.xfhy.frontservicedemo-rujs7qyX7dkx9BoO0UwMug==/base.apk publicDir=/data/app/com.xfhy.frontservicedemo-rujs7qyX7dkx9BoO0UwMug==/base.apk data=/data/user/0/com.xfhy.frontservicedemo
        packageList={com.xfhy.frontservicedemo}
        compat={320dpi}
        thread=android.app.IApplicationThread$Stub$Proxy@ed8ddb8
        pid=10502 starting=false
        lastActivityTime=-2m14s174ms lastPssTime=-36s962ms pssStatType=0 nextPssTime=+53s9ms
        adjSeq=17211 lruSeq=0 lastPss=32MB lastSwapPss=0.00 lastCachedPss=0.00 lastCachedSwapPss=0.00
        procStateMemTracker: best=1 (1=1 2.25x)
        cached=false empty=false
        oom: max=1001 curRaw=0 setRaw=0 cur=0 set=0
        curSchedGroup=3 setSchedGroup=3 systemNoUi=false trimMemoryLevel=0
        curProcState=2 repProcState=2 pssProcState=2 setProcState=2 lastStateTime=-2m14s174ms
        hasShownUi=true pendingUiClean=true hasAboveClient=false treatLikeActivity=false
        reportedInteraction=true time=-2m14s178ms
        hasClientActivities=false foregroundActivities=true (rep=true)
        startSeq=86
        lastRequestedGc=-2m14s211ms lastLowMemory=-2m14s211ms reportLowMemory=false
        Activities:
          - ActivityRecord{ee9b8e4 u0 com.xfhy.frontservicedemo/.MainActivity t17}
        Recent Tasks:
          - TaskRecord{a34a791 #17 A=com.xfhy.frontservicedemo U=0 StackId=12 sz=1}
        Connected Providers:
          - 6e7baff/com.android.providers.settings/.SettingsProvider->10502:com.xfhy.frontservicedemo/u0a85 s1/1 u0/0 +2m12s890ms
    
    • 方案A : 正常展示通知的方式启动Service,作为对比
    • 方案B : 展示通知时,使用一个没有注册的channel
    • 方案C : 展示通知时,使用一个错误的布局
    手机 Android版本 ROM 版本 补丁版本 方案A 方案B 方案C
    荣耀6x 8.0 8.0 2020.9 oom cur=200 oom cur=200 oom cur=200
    小米8 10 12 2020.9 oom cur=50 5秒后崩溃 5秒后崩溃
    小米6 8 10 oom cur=200 oom cur=200 oom cur=200
    vivo nex 10 9.2 adj=0 adj=7 adj=0
    三星 Galaxy A60 10 2020.12 崩溃 崩溃
    原生 9 2019.8 adj=3 adj=11 adj=3

    从实际效果来看,大部分情况下表现良好,但是部分手机上可能导致app崩溃,这是不能接受的。比较有趣的是,部分手机打了补丁依然能正常运行,没有杀死app。

    五、题外话

    • 系统有提示升级就尽快升级,里面可能修复了大量漏洞之类的,让手机更安全,买手机尽量选择更新系统比较频繁的。
    • 2021年了,保活基本上是不太可能了,各大厂商招揽顶尖人才搞出稳定的Android系统,和系统抗衡是不可能的。还是好好做好产品,让用户爱上产品才是真正的保活
    • 严正声明:本文相关技术仅限于技术研究使用,不能用于非法目的,否则后果自负

    六、资料

    相关文章

      网友评论

          本文标题:黑科技:提升进程优先级的一种思路

          本文链接:https://www.haomeiwen.com/subject/hcmktltx.html