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);
}
前置知识: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学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!