Android爬取第三方app推送消息
该篇文章是在公司的一个特别奇葩的需求之下产生的,我们公司的产品需要监听一些新闻app的推送消息,从而进行自己的消息推送。作为一个Android Coder第一反应就是直接用NotificationListenerService进行推送监听不就完了嘛,So Easy。说干就干,百度示例代码,ctrl c+ctrl v一气呵成,run!这不就拿到了吗。然后屁颠的去找产品经理了。产品经理看完头都不抬的说我要的是推送携带的详细参数,不是简单的title 和content这种文字信息。我的天,这个时候我在开始细细琢磨这个需求,这个需求的本质就是获取第三方app通知栏通知中写的详细参数——再直白点就是获取第三方App的通知栏通知携带的Intent。有点方啊,坦白说这种需求不用动脑子都知道Android肯定不会给你这个权限的,要是谁都能随便拿到那那行啊。但是说归说还是要尝试一下。我们都知道Notification所携带的参数都是由Intent封装的所以找到这个Intent就行,而在生成Notification时Intent有封装进了PendingIntent里面,而这个PendingIntent是我们通过NotificationListenerService可以获取道的。所以基本思路就出来了通过PendingIntent获取Intent进而获取Intent携带的参数。
好了,废话不多说,下面进入正文,文章主要会讲到以下几个问题
-
利用NotificationListenerService监听获取第三App的Notification
-
获取Notification中的Intent
-
获取第三方App Intent携带的信息
-
解析第三方App的Serializable对象 -
解析第三方App的Parcelable对象
利用NotificationListenerService监听获取第三App的Notification
这个知识点其实网上有很多教程的无非就是自己写一个Service继承NotificationListenerService,之后就可以重写NotificationListenerService的相关方法然后从中获取到第三方App的通知,这个点是比较简单的稍微贴一下代码就好不再详细说了,如果有什么问题请出门google
public class NotificationMonitorService extends NotificationListenerService {
// 在收到消息时触发
@Override
public void onNotificationPosted(StatusBarNotification sbn) {
// TODO Auto-generated method stub
Bundle extras = sbn.getNotification().extras;
// 获取接收消息APP的包名
String notificationPkg = sbn.getPackageName();
// 获取接收消息的抬头
String notificationTitle = extras.getString(Notification.EXTRA_TITLE);
// 获取接收消息的内容
String notificationText = extras.getString(Notification.EXTRA_TEXT);
Log.i("XSL_Test", "Notification posted " + notificationTitle + " & " + notificationText);
}
// 在删除消息时触发
@Override
public void onNotificationRemoved(StatusBarNotification sbn) {
// TODO Auto-generated method stub
Bundle extras = sbn.getNotification().extras;
// 获取接收消息APP的包名
String notificationPkg = sbn.getPackageName();
// 获取接收消息的抬头
String notificationTitle = extras.getString(Notification.EXTRA_TITLE);
// 获取接收消息的内容
String notificationText = extras.getString(Notification.EXTRA_TEXT);
Log.i("XSL_Test", "Notification removed " + notificationTitle + " & " + notificationText);
}
}
这是一个service,只要startService一下,然后在系统中给与相应权限就可以了,这里不再细
表。其中sbn.getNotification().extras这个api获取的只是通知携带的参数,如title,textContent等,和咱们要获取的不是一回事,这里说明一下。
获取Notification中的Intent
从这开始就是比较重要的点了,通过常规的方法肯定是获取不到PendingIntent里边携带的Intent的,第一个思路就是从源码入手看看Android是如何在从通知跳转到activity时利用PendingIntent获取到Intent的。然后照葫芦画瓢做。好吧说干就干。由于涉及到Intent以及打开activity那么肯定会涉及到AMS。Android studio看AMS的源码有点力不从心啊,在这推荐一个Android在线源码地址
好吧,现在就开始看源码,从PendingIntent和Intent产生关联的地方开始看吧。
public static PendingIntent getActivity(Context context, int requestCode,
Intent intent, @Flags int flags) {
return getActivity(context, requestCode, intent, flags, null);
}
跟着方法找下去,发现其实是AMS处理的。
public static PendingIntent getActivity(Context context, int requestCode,
@NonNull Intent intent, @Flags int flags, @Nullable Bundle options) {
String packageName = context.getPackageName();
String resolvedType = intent != null ? intent.resolveTypeIfNeeded(
context.getContentResolver()) : null;
try {
intent.migrateExtraStreamToClipData();
intent.prepareToLeaveProcess(context);
IIntentSender target =
ActivityManagerNative.getDefault().getIntentSender(
ActivityManager.INTENT_SENDER_ACTIVITY, packageName,
null, null, requestCode, new Intent[] { intent },
resolvedType != null ? new String[] { resolvedType } : null,
flags, options, UserHandle.myUserId());
return target != null ? new PendingIntent(target) : null;
} catch (RemoteException e) {
}
return null;
}
我们接着看AMS的源码
懒得翻源码了,经过我用百度一番google之后,发现答案其实就在PendingIntent里边,PendingIntent有个API getIntent()
/**
* @hide
* Return the Intent of this PendingIntent.
*/
public Intent getIntent() {
try {
return ActivityManager.getService()
.getIntentForIntentSender(mTarget);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
获取的就是PendingIntent携带的Intent,简单不,就是这么Easy。
由于打了hide的注解,是不能直接调用的,但是反射处理一下就可以了。处理完之后run起来发现这个api需要系统级的权限。想想也是只有系统才能这么随便的去窥探别人家App的数据。怎么给应用加系统级的权限有两种办法
- 自己做个ROM把自己的应用打包进去成为系统App
- 把手机root,然后把自己的App放在系统App的目录下
难易程度上来讲当然是第二个比较简单,而且有专门的工具,条件只是手机root。这里不再展开细说,因为这篇文章不是主要来讲这个问题的。
好了,现在按着以上说的
- 利用NotificationListenerService获取到想要劫持的通知的PenddingIntent
- 利用反射调用隐藏API获取Intent对象
- 将自己的应用打包,通过相应方式刷成系统App
- 运行,当你监听的App推送一条通知时,在你的代码里应该就能获取这个通知的Intent了。
OK,现在可能有的童靴会说了 Intent都拿到了,还有啥难得。
naiveヽ(ー_ー)ノ下面才正式开始本篇文章干货。
获取第三方App Intent携带的信息
回想一下,大家平时获取Intent携带的信息都是怎么做的
intent.getStringExtra("key")
看起来平平无奇,其实这其中是有个隐藏条件的,你要获取一个intent携带的信息,一个必要条件就是你得知道存储这个信息的key值,就是对于第三方App的Intent来说,咱们对他内部的数据信息是一无所知的,所以这就带来了第一问题:如何在不知道key的情况下获取intent内部的数据。
遇到这种问题,第一个思路就是考虑intent数据结构。根据API可以知道intent里边的数据是以键值对的形式进行存储的,方式类似HashMap。HashMap的数据其实存放在内部的哈希表中的,本质就是一个元素为链表的数组,即使咱们不知道key值,直接对这个链表数组进行遍历也是可以拿到HashMap里边所有信息的。Intent同理,我虽然不知道key,但是只要找到内部存储的数据,然后遍历一遍也可以拿到我想要的所有信息。
查看Intent 获取数据的源码
public String getStringExtra(String name) {
return mExtras == null ? null : mExtras.getString(name);
}
接着看Bundle的getString
public String getString(@Nullable String key) {
unparcel();
final Object o = mMap.get(key);
try {
return (String) o;
} catch (ClassCastException e) {
typeWarning(key, o, "String", e);
return null;
}
}
其实跟我们想的差不多,Intent的所有数据都是放在Intent内部的mExtras(Bundle)字段的mMap(ArrayMap<String, Object>)字段中。我们只要拿到mMap,遍历然后就可以了。unparcel方法是用来从native层读取数据用来填充mMap用的。
看到这大致的思路其实就有了,先获取Intent的mExtras对象,然后调用mExtras的 unparcel()方法,填充值后,直接遍历mMap,就可以拿到Intent携带的所有信息了。
这两个字段都没办法直接拿到,只能用反射,代码还是比较简单的
try {
Intent intent = getIntent();
Field declaredField = getIntent().getClass().getDeclaredField("mExtras");
declaredField.setAccessible(true);
Bundle mExtras = (Bundle) declaredField.get(intent);
Method unparcel = mExtras.getClass().getDeclaredMethod("unparcel");
unparcel.setAccessible(true);
unparcel.invoke(mExtras);
Field mMap = mExtras.getClass().getDeclaredField("mMap");
mMap.setAccessible(true);
ArrayMap o = (ArrayMap) mMap.get(mExtras);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
到这写个demo赶紧跑一下:起一个应用发一个通知然后用上边的代码,自己监听自己。一气呵成,赶紧run起来。
App A发出一个通知,App B果真正确解析了A的通知中携带的参数。
你以为这就完了?
too young!
当年我也是这么想的,当时我们业务上需要检测二十来个App。App装上之后需要等待被检测App发出通知,所以不能实时观测结果。结果第二天来查看收集结果的日志,发现一堆报错😒😒😒😒, 主要是ClassNotFound
java.lang.ClassNotFoundException: Bean
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:348)
at java.io.ObjectInputStream.resolveClass(ObjectInputStream.java:626)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1613)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1518)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1774)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371)
不过也有成功收集的,后来统计了一下大概有一半的App能成功收集,剩下的一半一次成功的收集都没有。并且能成功收集的不会报错,报错的一次也不会成功,这就说明这个问题是有迹可循的🤓好消息啊。
其实,看到这可能有的同学已经推断出来了错误的原因。那就是报错的App的Intent中携带了序列化的参数。当Intent中填充mMap这个字段时会把序列化的信息反序列化回来,反序列回来的时候会要求本地虚拟机加载做序列化的class文件,显而易见我本地是肯定没有第三方App的class的,所以就会出现ClassNotFound的错误。而Intent中不携带序列化参数的则会正常解析。问题原因找到了,接下来就是分析解决了,首先是要找到反序列化的具体时机,然后对症下药。这就只能从代码入手了。从Bundle的unparcel方法看起, unparcel其实只是做了个参数判空,然后调用了initializeFromParcelLocked方法,摘了下initializeFromParcelLocked中主要逻辑:
final int count = parcelledData.readInt();
if (count < 0) {
return;
}
....
....
try {
if (parcelledByNative) {
// If it was parcelled by native code, then the array map keys aren't sorted
// by their hash codes, so use the safe (slow) one.
parcelledData.readArrayMapSafelyInternal(map, count, mClassLoader);
} else {
// If parcelled by Java, we know the contents are sorted properly,
// so we can use ArrayMap.append().
parcelledData.readArrayMapInternal(map, count, mClassLoader);
}
} catch (BadParcelableException e) {
if (sShouldDefuse) {
Log.w(TAG, "Failed to parse Bundle, but defusing quietly", e);
map.erase();
} else {
throw e;
}
}
parcelledData是一个关键字段
/*
* If mParcelledData is non-null, then mMap will be null and the
* data are stored as a Parcel containing a Bundle. When the data
* are unparcelled, mParcelledData willbe set to null.
*/
Parcel mParcelledData = null;
其实Intent的数据都是由mParcelledData保存的,Intent的数据携带在native层。由mParcelledData也就是Parcel类做了封装读取。Parcel native层的数据类似于一个列表的数据结构,Parcel相应的readInt() readString() 等API就是从native的列表中读取相应的数据。不过这一类的API是没有参数的,也就是说既不能获取指定位置的数据,已不能获取指定key对应的数据。这是因为其实Parcel数据定位是用native层的一个游标做的定位,每read一个数据,游标+1。这也就意味着每个数据只能read一次,再去read就是下一个位置的数据了。这种设计就要就做数据解析时,必须知道哪个数据在什么位置,或者什么位置的数据有着什么样的含义
比如上文中的parcelledData.readInt();其实就是取出了mParcelledData native层列表的第一个数据(int类型)这个int值标识了mParcelledData中一个携带了多少个数据。
对于普通的Intent都会走Parcel的readArrayMapInternal方法
void readArrayMapInternal(ArrayMap outVal, int N,
ClassLoader loader) {
while (N > 0) {
if (DEBUG_ARRAY_MAP) startPos = dataPosition();
String key = readString();
Object value = readValue(loader);
outVal.append(key, value);
N--;
}
outVal.validate();
}
public final Object readValue(ClassLoader loader) {
int type = readInt();
switch (type) {
case VAL_NULL:
return null;
case VAL_STRING:
return readString();
case VAL_INTEGER:
return readInt();
case VAL_MAP:
return readHashMap(loader);
case VAL_PARCELABLE:
return readParcelable(loader);
......
case VAL_SERIALIZABLE:
return readSerializable(loader);
......
}
这其实就是最核心的填充步骤,从这也能看出来Parcel native层携带的的数据格式大致是这样的
数据结构.png
上述的错误ClassNotFound则是发生在readParcelable和readSerializable中反序列化的过程中。知道了原因及位置现在咱们就可以处理问题了。
现在要彻底解决这个问题是不可能的,因为我本地的确没有这个这个Class。所以现在首先要做的是暂时用try catch解决报错,从而避免影响其他数据的解析。
跟着这个思路,我打算手动解析Parcel中数据,因为他的解析代码其实挺简单的,自己手动跟着撸一遍,在处理序列化数据时try cath跳过就行了,有一些private API 反射调用就行。还有一种思路就是hook,自己继承Parcel这个类,在重写需要try cath处理的方法。然后在解析数据之前利用反射替换掉Bundle中原有的Parcel对象。不过这个思路我没有试过,不敢保证一定能实现。第一个思路到是能确定是可以的,之前做的时候是用第一个思路做的。这种手动解析的思路,代码需要频繁调用反射,有点繁琐没啥技术含量就不贴上来了。
通过以上的处理20个App终于能够尽可能多的手机通信中Intent的信息了,有一半能完美解析,另外一半由于缺失了序列化的数据,只能解析出部分来。
解析第三方App的Serializable对象
仅仅到达上面的效果其实是有点遗憾的,后来经过一系列的查阅资料,翻阅源码终于找到了处理”反序列化本地不存在的class类“思路,总结起来四个字:无中生有。
现在,这篇文章最硬核的部分开始了😏。
要解决这个问题,首先必须熟悉正常的反序列化过程。当然也是从源码看起。
算了反序列化单独写一篇文章吧,要不然这篇文章还得再我文章库里趟一段时间😓😓😓😓😓
已更新 Java反序列化本地不存在的类
网友评论