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

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®;

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();

学习福利

【Android 详细知识点思维脑图(技能树)】

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。

这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

由于篇幅有限,这里以图片的形式给大家展示一小部分。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
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();

学习福利

【Android 详细知识点思维脑图(技能树)】

[外链图片转存中…(img-GQ3RFMtL-1715481685709)]

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。

这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

由于篇幅有限,这里以图片的形式给大家展示一小部分。

[外链图片转存中…(img-Fb7F7AwA-1715481685710)]

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值