微信逆向之朋友圈

8,256 阅读14分钟

前提


这篇文章我主要会讲我所掌握逆向的一些小技巧,及如何一步步的爬取到微信朋友圈的数据的过程。关于微信逆向的工作,可能很多小伙伴呢都干过这事,最让人头疼的就是如何快速定位一个Hook点。还有就是如何理清被混淆之后的代码。这两个点弄清楚,可能Hook你所要的东西,那就是很轻松了。我这里只是针对Android的微信7.0.3版本最新版本7.0.4

生活在这样的大数据年代,当然数据是最重要的,如果作为一个商户,你有N多个客户,那如何对这些商户进行分类,其次就是做到精准营销,然后那就是Money了。 那位对于朋友圈数据的分析会起到非常重要的作用。给每个商户打上一个Tag。我这只是一个简单的举例,不妨碍大家阅读下面的文章,那么废话不多说了。开始编码

如果你掌握了以下内容,你将可以获取好友的朋友圈图片,视频,评论,点赞及你之前所看到的内容,即使他已经关闭朋友圈,只要你看过,那就是存在数据库中,我们就可以拿得到

准备工作


以上除了手机root外,其他的工具我都附上了链接,大家直接去下载即可。对于如何root手机,作为一个Android 开发者,自行Google吧 推荐通过TWRP+SuperSu 的方式,很多机型都可以烧成功。网上很多案例。xpose 的使用这里我就不过多的赘述了,一些Api的调用而已。Jadx这个工具呢必不可少,当然你也可以使用其他的反编译工具(dex2jar,jeb,jd-gui....)用着顺手就行,我这里使用的是Jadx,啊真的很好用。

加载微信源码

以下的操作会让你加快Hook点定位

  1. 用Jadx 打开下载好的微信Apk,编译成功应该是如下页面

jadx.png

  1. 把编译好的项目另存为Gradle项目

  2. 用AS 打开刚刚另存的Gradle项目,AS 可以帮们建立快速索引,方便我们调试找Hook点。可能还有更好的操作,有同学知道的可以留言区交流以下

以上操作完成之后,准备工作已经大部分都完成了。

朋友圈数据库

这里我就简单介绍一下,网上也有这方面的资料,SnsMicroMsg.db这个就是保存我们朋友圈数据的数据库,SnsInfo这个表是具体内容,我们可以在/data/data/com.tencent.mm/MicroMsg/md5({mm+uin})账号目录下找到这个文件,而且也没有对数据库加密,可一直直接Navicat打开的,但是微信聊天记录数据库就是加密的,需要破解密码才能打开,密码一直没变过还是通过{IMEI+UIN}md5即可,回到主题,但是虽然你可以拿到朋友圈数据库,但是你发现并没有什么卵用,里面的数据全部加密了,完全摸不着头脑。这里大家可以看看这个数据库文件。除了能看到userName 和createTime 有点用,其他的字段真是看不到头绪

我们接下来注意观察第二张图片,content,attrBuf,这二个字段里面全是BLOB格式的,可以发现,小老弟把数据藏起来了啊。那难道我们就没有办法了吗!当然还是可以拿的到的,那就是通过微信源码查看了,我们接下来的工作就是,看微信是如何拿到这个BLOB,如何进行解析的。我们也通过他的方式进行解析不就得了吗。接下来进入主题。

Hook朋友圈Activity

我这里Hook点是个人朋友圈入口,通过点击通讯录,进入个人的朋友圈页面,然后找到这个Activity是哪一个Activity,然后查看这个页面的源码。目前到现在为止,还没有用到Xpose 这个工具,那么我们如何知道这个Activity是哪个呢,其实Android系统给我们提供了一个dumpsys的工具,根据包名可以获取到当前是哪个Activity,我们只需要adb shell 登进去

点击进入某个人的这个页面,然后调用一下代码就可以获取当前是哪个activity了

dumpsys activity |grep -i com.tencent.mm

打开微信个人朋友圈页面执行上述代码之后即可获取当前页面,这时候可以在terminal里面看到如下

com.tencent.mm/.plugin.sns.ui.SnsUserUI

到此为止,我们已经知道了个人朋友圈页面在哪里了。接下来我们去刚刚AS打开的微信项目里面找到这个SnsUserUI Activity了,打开页面可以发现很多代码已经混淆的很难读懂了,什么a.a.a,b.c.da这样的包名已经方法名。但是我们这时候不要慌张。接下来打开手机,我们可以通过微信这个页面的UI看到,这是一个list,那肯定不是listview就是recyclerview了。这是后打开AS找到SnsUserUI这个类,然后倒开他的structure目录

snsuser.png

可以发现里面有个方法initView 是不是很熟悉,我们经常写代码也会这样写,那么我们这就点进去看一下, 这里就不粘贴很多代码。有兴趣可以自行反编译看看


 public final void initView() {
        this.qXD = (RelativeLayout) findViewById(f.sns_user_year_tip_layout);
        this.qXE = (TextView) findViewById(f.sns_user_year_tip);
        this.qXD.post(new Runnable() {
            public final void run() {
                LayoutParams layoutParams = new LayoutParams(-1, -2);
                layoutParams.topMargin = x.aj(SnsUserUI.this) + SnsUserUI.this.getResources().getDimensionPixelSize(i.d.ActionBarHeight);
                SnsUserUI.this.qXD.setLayoutParams(layoutParams);
            }
        });
        this.qXz = new as(this, new a() {
            public final void fc(int i, int i2) {
                super.fc(i, i2);
            }
        }, this.hMy, new as.c() {
        });
        this.qXA.naj.setAdapter(this.qXz);
        this.qXA.naj.setOnItemClickListener(new OnItemClickListener() {
            public final void onItemClick(AdapterView<?> adapterView, View view, int i, long j) {
            }
        });

打开观察之后,发现这里面有个setAdapter的地方,对的!肯定数据就在这个Adapter里面,因为我们平时写list 渲染的时候数据都是通过adapter 渲染到list 上的,我们继续点进去看看这个adapter 是如何写的。

朋友圈Adapter深入

我们通过上述的方法已经找到了Adapter了,我们点击(this.qXz)这个字段发现他的类名是as,点击进去发现,果然没错。

public final class as extends BaseAdapter {
    private Activity coM;
    boolean cog = false;
    private String country;
    List<n> list = new ArrayList();
    String lnO = "";
    private String ngt = "";
    Map<Integer, Integer> qBc = new HashMap();
    Map<Integer, Integer> qBd = new HashMap();
    int qBe = 0;
    int qBf = 0;
    String qHI = "";
    private bd qKW = null;
    private az qQo;
    Map<Integer, Integer> qQp = new HashMap();
    private f qQq;
    boolean qQr = false;
    at qQs;
    private c qQt;
    int qQu = BaseClientBuilder.API_PRIORITY_OTHER;
    int qQv = 0;
    private long qQw = 0;
    private long qQx = 0;
    int qQy = 0;
    protected OnClickListener qQz = new OnClickListener() {
        public final void onClick(View view) {
            if (view.getTag() instanceof TimeLineObject) {
                TimeLineObject timeLineObject = (TimeLineObject) view.getTag();
                if (as.Yp(timeLineObject.Id)) {
                    h.ptS.X(10231, "1");
                    com.tencent.mm.av.a.agc();
                } else {
                    h.ptS.X(10090, "1,0");
                    if (!(com.tencent.mm.q.a.bN(as.this.coM) || com.tencent.mm.q.a.bL(as.this.coM))) {
                        com.tencent.mm.av.e a = g.a(af.getAccPath(), timeLineObject, 8);
                        a.fuR = as.this.userName;
                        com.tencent.mm.av.a.b(a);
                    }
                }
                as.this.notifyDataSetChanged();
            }
            ........省略很多代码........
            

那我们接下来怎么找呢,Adapter已经拿到了,难道还是一点点翻吗?当然不是了,想想我们日常写listview的Adapter时候,是不是在getView的时候进行给view赋值的操作,这时候微信也一样,我们同样打开as这个类的structure目录

as.png
从图中可以发a这个方法嫌疑很大,里面有各种view


   private void a(int i, QFadeImageView qFadeImageView, TextView textView, TextView textView2, TextView textView3, TextView textView4, int i2, d dVar, int i3) {
        n nVar = (n) getItem(i);
        TimeLineObject cmi = nVar.cmi();
        bys q = aj.q(nVar);
        Object obj = null;
        if (q != null && (((q.vUS & 2) == 2 && q.wnx != null) || ((q.vUS & 4) == 4 && q.vTG != null))) {
            obj = 1;
        }
        if (!(!this.cog || q == null || obj == null || this.userName == null || !this.userName.equals(nVar.field_userName))) {
            textView3.setBackgroundResource(com.tencent.mm.plugin.sns.i.e.personactivity_sharephoto_icon);
            textView3.setVisibility(0);
        }
        
 ........省略很多代码........

我们点进去可以看到,果然如同我们所想一样,第一行就暴露了, n nVar = (n) getItem(i); 可以发现这个n类就是每个item的数据源,而且往下看一行,这个类名也很有嫌疑TimeLineObject,而且是通过n 调用cmi方法返回的,那会不会是数据库里面的那个BLOB字段呢。容我们点进去看看

 Map<String, TimeLineObject> qAb = new ConcurrentHashMap();
     ........省略很多代码........

 public final TimeLineObject cmi() {
        if (this.field_content == null) {
            return e.ahM();
        }
        TimeLineObject timeLineObject;
        if (this.qzT == null) {
            this.qzT = g.u(this.field_content) + g.u(this.field_attrBuf);
        }
        if (qAb.containsKey(this.qzT)) {
            timeLineObject = (TimeLineObject) qAb.get(this.qzT);
            if (timeLineObject != null) {
                return timeLineObject;
            }
        }
        try {
            timeLineObject = (TimeLineObject) new TimeLineObject().parseFrom(this.field_content);
            qAb.put(this.qzT, timeLineObject);
            return timeLineObject;
        } catch (Exception e) {
            ab.e("MicroMsg.SnsInfo", "error get snsinfo timeline!");
            return e.ahM();
        }
    }

这时候发现了不可思议的东西 field_contentfield_attrBuf 这两个字段,这个不就是数据库里面定义的content和attrbuf字段吗。原来是通过这样的方式转化的。继续往下看,可以发现如果 ' qAb.get(this.qzT) ' 拿不到TimeLineObject的话,就会自己new 一个然后存储到这个Map集合中,做缓存作用,那我们是不是可以通过这种方式呢拿到这个TimeLineObject,当然是可以的。

解码朋友圈BLOB字段

通过上面我们已经知道了如何解析朋友圈数据库content 字段了,通过TimeLineObject的parseFrom方法进行转化。但是当我们点到TimeLineObject这个类里面你会发现,卧槽,怎么这样。

public class TimeLineObject extends a {
    public String Id;
    public int dhE;
    public int eRm;
    public String hPC;
    public String jfn;
    public int ozl;
    public String qEy;
    public String qXr;
    public av qiN;
    public csw qiP;
    public String uzJ;
    public int vTa;
    public int wsA;
    public String wsB;
    public ccr wsC;
    public cqv wsD;
    public int wsE;
    public String wsu;
    public axc wsv;
    public du wsw;
    public ta wsx;
    public String wsy;
    public int wsz;

可以发现里面嵌套了很多的类。这是我时候我们怎么做呢。我也没有很好的办法,然后就通过一个类一个类的点开,然后打印它里面是String字段,这里应该来个表情捂脸,有好方法的同学们可以留言区交流,但是当我打印到了wsu这个字段的时候发现,可以拿得到我们发朋友圈时的标题了。瞬间感觉到了轻松。但是这只是一个很小的进步,我们要拿的可是朋友圈图片视频评论点赞所有内容啊。

获取朋友圈信息源具体内容

那么如何获取到图片视频等信息呢。那么我们还是要回到adapter 这个类里面。在他的a方法中有个构造参数QFadeImageView看到ImageView 感觉离获取图片地址不远了。那么我们继续观察这个方法。

 private void a(int i, QFadeImageView qFadeImageView, TextView textView, TextView textView2, TextView textView3, TextView textView4, int i2, d dVar, int i3) {
        n nVar = (n) getItem(i);
        TimeLineObject cmi = nVar.cmi();
        bys q = aj.q(nVar);
        Object obj = null;
        if (q != null && (((q.vUS & 2) == 2 && q.wnx != null) || ((q.vUS & 4) == 4 && q.vTG != null))) {
            obj = 1;
        }
        ........省略很多代码........
        if (cmi.wsx.vqt == 1) {
            qFadeImageView.setVisibility(0);
            af.cjr().a(cmi.wsx.vqu, (View) qFadeImageView, this.coM.hashCode(), com.tencent.mm.plugin.sns.model.g.a.IMG_SCENE_SNSSUSER, azVar);
        } else if (cmi.wsx.vqt == 2) {
            textView4.setText(bp.bb(cmi.wsx.Desc, ""));
            textView4.setVisibility(0);
        } else if (cmi.wsx.vqt == 21) {
            nVar.cmA();
            boolean z = true;
            if (this.cog) {
                z = true;
            } else if (m.a(nVar, q)) {
                z = false;
            }
            qFadeImageView.setVisibility(0);
            af.cjr().a(cmi.wsx.vqu, (View) qFadeImageView, this.coM.hashCode(), com.tencent.mm.plugin.sns.model.g.a.IMG_SCENE_SNSSUSER, azVar, z);
        }
        ........省略很多代码........

从上面的代码中我们可以看得到,微信这边调用了这个方法把 af.cjr().a(xxxxx)把Imageview 传入了进入,点击查看了其他的参数只有cmi.wsx.vqu这个参数有用。可以发现他调用了TimeLineObject里面ta这个类里面的集合vqu字段。

public final class ta extends a {
    public String Desc;
    public String Title;
    public String Url;
    public int vqt;
    public LinkedList<azc> vqu = new LinkedList();
    public int vqv;
    public String vqw;
    public ayd vqx;

先不管调用了imageview 这个方法干了什么,我们先把这个集合里面的东西给打印出来。

azc.toString{id:13055580620160049319 Desc:黄昏 Title: Url:http://mmsns.qpic.cn/mmsns/kEgG3ynkRxponsiaCyzhl9Gniaz7GdWLujZdSMV4kmlME6fia07m577MQ3OU9kMKibjAIiaMc7MWHKF0/0 ckO:null qDg: vSY:http://mmsns.qpic.cn/mmsns/kEgG3ynkRxponsiaCyzhl9Gniaz7GdWLujZdSMV4kmlME6fia07m577MQ3OU9kMKibjAIiaMc7MWHKF0/150 vTc: vTf: vTh: vTi: vTj: vTm:f814a6351db8cd3ef118e14e6ff70b80 vTn:WSEN6qDsKwV8A02w3onOGQYfxnkibdqSOkmHhZGNB4DFJ9qdBeATTF8UiaDA1go3GLryav2ukPJK06SOFjchiaqJA vTp:14604729124651202068 vTq:WSEN6qDsKwV8A02w3onOGQYfxnkibdqSOkmHhZGNB4DFJ9qdBeATTF8UiaDA1go3GLwHGCkbxWxDWW5dsMhLsBUg vTs:14604729124651202068 vTt:}

不出我们所料,里面果然有我们需要的东西,而且把Desc,url都给了我们,通过我的测试发现图片的url并不能打开。但是视频链接的url是可以打开的。是不是很happy。这时候我们已经拿到了视频源和分享的链接源,但是如何拿到图片源呢。那我们继续查看源码,发现点击刚刚给上述调用了这个集合的方法里面并没有什么东西,都是一些赋值的操作。没有获取具体图片源信息的位置。

这时候我是这样操作的,因为这个获取到的Url虽然我们解析不了,但是我们可以点击查看Url的引用,看哪里有着对这个Url字段的引用,微信内部是如何解析的 当你点击开的时候会发现并没有什么用处,见下图

url.png

引用的地方太多了,根本不知道如何下手。这时候又会一头雾水不知道如何进行下一步。那么我们只能回到起点,再次寻找下一个hook点。

进入朋友圈详情二次Hook

既然上一个页面我们拿不到图片的数据,那我们就深入进入朋友圈具体内容页面,然后进行二次Hook,获取到朋友圈具体图片内容。我们还是以同样的方法进行,hook,打开此页面然后通过 dumpsys 获取当前的activity,进入这个activity查看源码下手找到图片的具体位置。

通过代码查看到这是一个com.tencent.mm.plugin.sns.ui.SnsGalleryUI,顾名思义,朋友圈相册页面。那么我们点进去看看具体写了些什么东西

public class SnsGalleryUI extends SnsBaseGalleryUI implements a {
    private int qKn = 0;
    private String userName = "";

    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        getWindow().addFlags(128);
        wS(this.mController.xyi.getResources().getColor(c.dark_actionbar_color));
        LV(this.mController.xyi.getResources().getColor(c.dark_actionbar_color));
        initView();
    }
........省略很多代码........


并没有什么东西可看的。没有具体的内容,我们进入父类SnsBaseGalleryUI看看做了些什么进入看了一下,发现方法体也没有什么东西,但是一个字段引起了我的注意

public abstract class SnsBaseGalleryUI extends MMActivity implements a {
    private boolean jop = true;
    private LinearLayout qKf;
    r qKg;
    private LinearLayout qKh;
    s qKi;
    private boolean qKj = true;
    private TextView qKk = null;
    protected SnsInfoFlip qKl;
    protected Button qKm;
........省略很多代码........

我们从上面的字段可以猜到哪一个是引起了我的注意,很明显那就是SnsInfoFlip这个类。继续点击去看看里面写了些什么内容。据我猜测这应该也是个adapter,viewpager的adapter,通过structure目录可以发现里面有个getView的方法,但是反编译并没有完全把smail 内容转换成java 类型,看着比较费劲,那我们继续往下寻找,这时候你会有惊喜

sns.png

可以看到这个函数里面参数是azc记忆力好的小伙伴应该还记得,前面所说的,azc这个类里面包含着url,这时候找到了用的地方了。把代码贴上大家可以看一下


  private void a(azc azc, int i, String str) {
        String str2;
        long j = 0;
        if (this.qNm != null && (this.qNm instanceof MMGestureGallery)) {
            float f;
            float f2;
            float f3;
            if (azc.vTb != null) {
                f = azc.vTb.vTP;
                f2 = azc.vTb.vTO;
            } else {
                f = 0.0f;
                f2 = 0.0f;
            }
            if (f <= 0.0f || r5 <= 0.0f) {
                if (azc.Id.startsWith("Locall_path")) {
                    str2 = an.fQ(af.getAccSnsPath(), azc.Id) + com.tencent.mm.plugin.sns.data.i.m(azc);
                } else {
                    str2 = an.fQ(af.getAccSnsPath(), azc.Id) + com.tencent.mm.plugin.sns.data.i.d(azc);
                }
                Options akG = com.tencent.mm.sdk.platformtools.d.akG(str2);
                ........省略很多代码........

上面的字段local_path,博主感觉找到了那个点,开始hook这个method an.fQ(af.getAccSnsPath(), azc.Id) + com.tencent.mm.plugin.sns.data.i.m(azc);,surprise这个方法返回的值就是朋友圈图片的本地路径。打印下来是这样的路径

/storage/emulated/0/tencent/MicroMsg/cd1c67e86728a83c6079e2b48ia/sns/1/4/snst_13069507190820253764
//这个路径通过adb pull 出来,就是一张图片

总结

通过以上的分析,我们已经定位到了哪些类和哪些方法可以帮助我们获取到朋友圈的数据。

接下来我们要做的事爬取朋友圈数据,这里两种方式爬取数据

  1. xpose

这种方式是最简单粗暴的,hook微信的内部方法

首先通过sqlite拿到SnsMicroMsg.db
然后query SnsInfo这个表里面的数据content
通过XposedHelpers 实例化TimeLineObject
调用TimeLineObject的parseFrom方法(可获取朋友圈标题)
然后获取TimeLineObject里面的ta类,获取到集合azc(视频链接可直接通过这个类获取)
然后循环这个集合,通过SnsInfoFlip里面的a方法获取到thumb的地址。
  1. classloader

这种方式呢,也是可行的,只需要手机root即可不需要xpose框架

首先把当前微信目录下的SnsMicroMsg这个数据库导出到Sdcard中
然后一样通过sqlite打开数据库,query SnsInfo table
接着DexClassLoader把微信apkloader起来,同时把我们上诉介绍到的类都给load进去。
然后通过反射的方式执行第一种xpose 方式也行。

通过以上两种方式皆可完成爬朋友圈数据功能,下面抛个爬取数据成功的页面。

detail.png