美文网首页
Android爬取第三方app推送消息

Android爬取第三方app推送消息

作者: FENGAO | 来源:发表于2020-06-10 14:37 被阅读0次

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。这里不再展开细说,因为这篇文章不是主要来讲这个问题的。
好了,现在按着以上说的

  1. 利用NotificationListenerService获取到想要劫持的通知的PenddingIntent
  2. 利用反射调用隐藏API获取Intent对象
  3. 将自己的应用打包,通过相应方式刷成系统App
  4. 运行,当你监听的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反序列化本地不存在的类

相关文章

网友评论

      本文标题:Android爬取第三方app推送消息

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