2018-12-29 回答
主要思路
从ui获取文本信息是最为简单的方法,于是应该优先逆向ui代码部分。
逆向微信apk
首先解包微信apk,用dex2jar反编译classes.dex,然后用jd-gui查看jar源码。当然,能看到的源码都是经过高度混淆的。但是,继承自安卓重要组件(如activity、service等)的类名无法被混淆,于是还是能从中看到点东西。
首先定位到微信app package。我们知道这个是 com.tencent.mm。
在 com.tencent.mm
中,我们找到一个 ui
包,有点意思。
展开 com.tencent.mm.ui
,发现多个未被混淆的类,其中发现 mmbaseactivity直接继承自 activity
, mmfragmentactivity
继承自 actionbaractivity
, mmactivity
继承自 mmfragmentactivity
,并且 mmactivity
是微信中大多数activity的父类:
public class mmfragmentactivity
extends actionbaractivity
implements swipebacklayout.a, b.a {
...
}
public abstract class mmactivity
extends mmfragmentactivity {
...
}
public class mmbaseactivity
extends activity {
...
}
现在需要找出朋友圈的activity,为此要用xposed hook mmactivity。
创建一个xposed模块
参考 [tutorial]xposed module devlopment,创建一个xposed项目。
简单xposed模块的基本思想是:hook某个app中的某个方法,从而达到读写数据的目的。
小编尝试hook com.tencent.mm.ui.mmactivity.setcontentview这个方法,并打印出这个activity下的全部textview内容。那么首先需要遍历这个activity下的所有textview,遍历viewgroup的方法参考了so的以下代码:
private void getalltextviews(final view v) {if (v instanceof viewgroup) {
viewgroup vg = (viewgroup) v;
for (int i = 0; i < vg.getchildcount(); i++) {view child = vg.getchildat(i);
getalltextviews(child);
}
} else if (v instanceof textview ) {
dealwithtextview((textview)v); //dealwithtextview(textview tv)方法:打印textview中的显示文本}
}
hook mmactivity.setcontentview
的关键代码如下:
findandhookmethod("com.tencent.mm.ui.mmactivity", lpparam.classloader, "setcontentview", view.class, new xc_methodhook() {...
});
在findandhookmethod方法中,第一个参数为完整类名,第三个参数为需要hook的方法名,其后若干个参数分别对应该方法的各形参类型。在这里, activity.setcontentview(view view)方法只有一个类型为 view
的形参,因此传入一个 view.class
。
现在,期望的结果是运行时可以从log中读取到每个activity中的所有的textview的显示内容。
但是,因为view中的数据并不一定在 setcontentview()时就加载完毕,因此小编的实验结果是,log中啥都没有。
意外的收获
当切换到朋友圈页面时,xposed模块报了一个异常,异常源从 com.tencent.mm.plugin.sns.ui.snstimelineui这个类捕捉到。从类名上看,这个很有可能是朋友圈首页的ui类。展开这个类,发现更多有趣的东西:
这个类下有个子类 a
(被混淆过的类名),该子类下有个名为 gyo的 listview
类的实例。我们知道, listview
是显示列表类的ui组件,有可能就是用来展示朋友圈的列表。
顺藤摸瓜
那么,我们先要获得一个 snstimelineui.a.gyo的实例。但是在这之前,要先获得一个 com.tencent.mm.plugin.sns.ui.snstimelineui.a的实例。继续搜索,发现 com.tencent.mm.plugin.sns.ui.snstimelineui有一个名为 glz
的 snstimelineui.a
实例,那么我们先取得这个实例。
经过测试, com.tencent.mm.plugin.sns.ui.snstimelineui.a(boolean, boolean, string, boolean)这个方法在每次初始化微信界面的时候都会被调用。因此我们将hook这个方法,并从中取得 glz。
findandhookmethod("com.tencent.mm.plugin.sns.ui.snstimelineui", lpparam.classloader, "a", boolean.class, boolean.class, string.class, boolean.class, new xc_methodhook() {@override
protected void afterhookedmethod(methodhookparam param) throws throwable {xposedbridge.log("hooked. ");
object currentobject = param.thisobject;
for (field field : currentobject.getclass().getdeclaredfields()) { //遍历类成员field.setaccessible(true);
object value = field.get(currentobject);
if (field.getname().equals("glz")) {
xposedbridge.log("child a found.");
childa = value;
//这里获得了glz
...
}
}
}
});
现在取得了 snstimelineui.a
的一个实例 glz
,需要取得这个类下的 listview
类型的 gyo
属性。
private void dealwitha() throws throwable{if (childa == null) {
return;
}
for (field field : childa.getclass().getdeclaredfields()) { //遍历属性field.setaccessible(true);
object value = field.get(childa);
if (field.getname().equals("gyo")) { //取得了gyoviewgroup vg = (listview)value;
for (int i = 0; i < vg.getchildcount(); i++) { //遍历这个listview的每一个子view...
view child = vg.getchildat(i);
getalltextviews(child); //这里调用上文的getalltextviews()方法,每一个子view里的所有textview的文本...
}
}
}
}
现在已经可以将朋友圈页面中的全部文字信息打印出来了。我们需要根据textview的子类名判断这些文字是朋友圈内容、好友昵称、点赞或评论等。
private void dealwithtextview(textview v) {string classname = v.getclass().getname();string text = ((textview)v).gettext().tostring().trim().replaceall("\n", " ");if (!v.isshown())
return;
if (text.equals(""))
return;
if (classname.equals("com.tencent.mm.plugin.sns.ui.asynctextview")) {//好友昵称
...
}
else if (classname.equals("com.tencent.mm.plugin.sns.ui.snstextview")) {//朋友圈文字内容
...
}
else if (classname.equals("com.tencent.mm.plugin.sns.ui.masktextview")) {if (!text.contains(":")) {
//点赞
...
} else {
//评论
...
}
}
}
自此,我们已经从微信app里取得了朋友圈数据。当然,这部分抓取代码需要定时执行。因为从 listview中抓到的数据只有当前显示在屏幕上的可见部分,为此需要每隔很短一段时间再次执行,让用户在下滑加载的过程中抓取更多数据。
剩下的就是数据分类处理和格式化输出到文件,受本文篇幅所限不再赘述,详细实现可参考作者github上的源码。