Android微信逆向--實現發朋友圈動態

0x0 前言

最近一直在研究Windows逆向的東西,想着快要把Android給遺忘了。所以就想利用工作之餘來研究Android相關的技術,來保持對Android熱情。調用微信代碼來發送朋友圈動態一直是自己想實現的東西,研究了一下,果然實現了,遂寫下本文當作記錄。本文主要分析發送純文字朋友圈動態和發送圖片朋友圈動態。

0x1 朋友圈動態類型分析

本文用到的工具如下:

  • PC
  • 一臺可以調試微信進程的Android手機
  • 微信7.0.11
  • ddms(用於跟蹤調用過程)
  • uiautomatorviewer(用於定位控件id)
  • jadx(用於對微信apk靜態分析)
  • frida(用於hook微信,獲得相關信息,發佈朋友圈)

在分析代碼之前,首先要定位到與之相近的地方,我們首先想到的肯定是發朋友圈那個界面,如何查看發朋友圈的界面是哪個Activity呢?很簡單,在手機上打開發送朋友圈的界面

把手機連接電腦,打開USB調試,在PC的cmd窗口中執行以下命令:

adb shell dumpsys activity top

可以看到發朋友圈的那個Activity就是com.tencent.mm.plugin.sns.ui.SnsUploadUI

雖然找到了Activity,但還是不能高興太早,想要通過Activity知道哪部分是發朋友的代碼還是比較費力的。於是我們就想到從“發表”按鈕入手,找出發表朋友圈的相關代碼。點擊“發表”按鈕會發生什麼?發表是一個動態的行爲,我們可以通過跟蹤點擊“發表”按鈕時的調用過程,來找到有用的信息。跟蹤調用過程,可以使用ddms工具來完成。打開ddms,選中微信進程,在手機中打開發表朋友圈界面,然後在ddms中點擊下圖圈出的圖標開始跟蹤:

將朋友圈動態發出,再點一次上圖圈出的圖標停止跟蹤。ddms會生成跟蹤結果,對於跟蹤結果,怎麼找到按鈕事件相關的信息呢,學過Android的朋友就會想到onClick方法,那我們就在ddms的搜索結果中搜索這個名稱:

成功的定位到了onClick的位置,但是比起這條onClick結果,更加令人引人注目的是它的上一條結果,因爲它包含了我們剛纔找到的Activity的類名:

知道這個方法被調用,我們去看看com.tencent.mm.plugin.sns.ui.SnsUploadUI類裏的OnMemuItemClick究竟是什麼。

用jadx打開微信apk,定位到com.tencent.mm.plugin.sns.ui.SnsUploadUI類,在類中搜索onMemuItemClick,結果不多,看起來比較像的就是這個onMemuItemClick了:

在onMemuItemClick方法中看到了:

String unused2 = SnsUploadUI.this.desc = SnsUploadUI.this.tQN.getText().toString();

這行代碼有什麼特別的呢,在我看來,有兩個特別的地方:

  • desc是描述(description)的英文單詞的縮寫
  • this.desc被賦予this.tQN.getText().toString()

我們發朋友圈動態時候,是要寫動態的描述的,所以這個desc可能就是發朋友圈動態的描述,如果是描述,我們就可以根據這個描述,順藤摸瓜找到發朋友圈動態的地方。而且this.desc的值又來自於this.tQN.getText().toString(),即this.tQN很有可能就是我們填寫動態描述的文本框。我們來看看this.tQN賦值的地方,它在onCreate方法被賦值:

可以知道它的id是d41,那麼d41是哪個控件?打開uiautomatorviewer,對發朋友圈界面截圖分析,點擊截圖中的文本框,uiautomatorviewer右側跳轉到了相應的位置,果然,d41就是發動態時填寫描述文本框的id

好了,現在知道this.desc就是發表朋友圈動態時的描述,跟上他應該就可以找到發朋友圈動態的地方。繼續往onMemuItemClick方法下部分析,可以看到this.desc被傳入了SnsUploadUI.this.tQO.a方法:

SnsUploadUI.this.tQO.a方法定義在接口com.tencent.mm.plugin.sns.ui.z中:

知道它定義在哪個接口並不能解決問題,畢竟接口沒有實質性代碼,要找還得找接口的的實現類,在com.tencent.mm.plugin.sns.ui.SnsUploadUI類中尋找this.tQO在哪裏會被賦值。最終,我們在com.tencent.mm.plugin.sns.ui.SnsUploadUI類的ag方法中看到了許多給this.tQO賦值的地方:

由此,可見this.tQO被賦予什麼值是根據this.tMY來決定的,this.tMY是一個int類型的數據,那我們hook com.tencent.mm.plugin.sns.ui.SnsUploadUI類的ag方法就可以知道this.tMY是什麼值。在這裏,我用frida來hook,frida的javascript部分代碼如下:

var SnsUploadUI= Java.use('com.tencent.mm.plugin.sns.ui.SnsUploadUI');
var ag = SnsUploadUI.ag.overload("android.os.Bundle");
//get sns type
ag.implementation=function(bundle){
    var ret = ag.call(this,bundle);
    send("sns type = " + this.tMY.value);
    return ret;
}

hook之後,每當我們在手機上打開發布朋友圈動態的界面,ag方法被調用,控制檯就會輸出相應的數字。經過我的測試,這個數字是發表朋友圈動態的類型。朋友圈類型和其對應類如下:

  • 0 帶圖片的動態,對應:com.tencent.mm.plugin.sns.ui.ai
  • 9 純文字動態,對應:com.tencent.mm.plugin.sns.ui.ae

這些類都直接或間接的實現了上面講到的com.tencent.mm.plugin.sns.ui.z接口。這樣一來,就知道this.tQO會根據朋友圈的動態類型進行初始化,那麼,上面的SnsUploadUI.this.tQO.a方法很有可能就是發朋友圈動態的方法。接下來,我們根據不同的朋友圈動態所對應的類來分別分析

0x2 文字動態分析

文字動態分析起來應該比圖片動態來說簡單一些,我們就先來分析它。上面講到,這類動態對應類是com.tencent.mm.plugin.sns.ui.ae,這個類裏我們主要看a方法,在看a方法之前,先看它傳入什麼參數,爲了看清楚,這就要回看上文onMenuItemClick方法調用a方法的地方:

public final boolean onMenuItemClick(MenuItem menuItem) {
    String unused2 = SnsUploadUI.this.desc = SnsUploadUI.this.tQN.getText().toString();
    int pasterLen = SnsUploadUI.this.tQN.getPasterLen();
    int privated = SnsUploadUI.this.tKm.getPrivated();
    int syncFlag2 = SnsUploadUI.this.tKm.getSyncFlag();
    ......
    PInt pInt = new PInt();
    if (SnsUploadUI.this.tQO instanceof a) {
        Bundle bundle = new Bundle();
        bundle.putInt("param_is_privated", privated);
        bundle.putString("param_description", SnsUploadUI.this.desc);
        bundle.putStringArrayList("param_with_list", new ArrayList(SnsUploadUI.this.uij.getAtList()));
        bundle.putInt("param_paste_len", pasterLen);
        try {
            bundle.putByteArray("param_localtion", SnsUploadUI.this.uik.getLocation().toByteArray());
        } catch (IOException e2) {
            ab.printErrStackTrace("MicroMsg.SnsUploadUI", e2, "parse location error", new Object[0]);
        }
        bundle.putBoolean("param_is_black_group", SnsUploadUI.this.tQS);
        bundle.putStringArrayList("param_group_user", SnsUploadUI.this.tQR);
        bundle.putInt("param_contact_tag_count", SnsUploadUI.this.tOk);
        bundle.putInt("param_temp_user_count", SnsUploadUI.this.tOl);
        pInt.value = ((a) SnsUploadUI.this.tQO).getContentType();
        z unused4 = SnsUploadUI.this.tQO;
    } else {
        SnsUploadUI.this.tQO.a(privated, syncFlag2, SnsUploadUI.this.tKm.getTwitterAccessToken(), SnsUploadUI.this.desc, SnsUploadUI.this.uij.getAtList(), SnsUploadUI.this.uik.getLocation(), (LinkedList<Long>) null, pasterLen, SnsUploadUI.this.tQS, SnsUploadUI.this.tQR, pInt, SnsUploadUI.this.tOj, SnsUploadUI.this.tOk, SnsUploadUI.this.tOl);
    }
}

這就是a方法調用的地方,根據這段代碼和編寫hook a方法的代碼來推出它的參數。hook代碼如下:

var ae = Java.use('com.tencent.mm.plugin.sns.ui.ae');
var ae_a = ae.a.overload("int","int","org.b.d.i","java.lang.String","java.util.List","com.tencent.mm.protocal.protobuf.bdi","java.util.LinkedList","int","boolean","java.util.List","com.tencent.mm.pointers.PInt","java.lang.String","int","int");
ae_a.implementation = function(isPrivate,syncFlag2,twitterAccessToken,desc,atList,location,list1,pasterLen,bool1,list2,pint1,str1,num1,num2){
    var ret = ae_a.call(this,isPrivate,syncFlag2,twitterAccessToken,desc,atList,location,list1,pasterLen,bool1,list2,pint1,str1,num1,num2);
    console.log("************Basic Info************");
    console.log("isPrivate = " + isPrivate);
    console.log("syncFlag2 = " + syncFlag2);
    console.log("twitterAccessToken = " + twitterAccessToken);
    console.log("desc = " + "'" + desc + "'");
    if(atList.size()>0){
        for(var i=0;i<atList.size();i++){
            console.log("atList[" + i + "] = " + atList.get(0));
        }
    }
    if(location != null){
         
        if(location.yRD.value != null){
            console.log("location.yRD = " + location.yRD.value);
        }

        if(location.yRE.value != null){
            console.log("location.yRE = " + location.yRE.value);
        }

    }
    console.log("list1 = " + list1);
    console.log("pasterLen = " + pasterLen);
    console.log("bool1 = " + bool1);
    if(list2 != null){
        console.log("list2 = " + list2.size());
    }
    else{
        console.log("list2 = " + list2);
    }
    console.log("pint1 = " + pint1.value.value);
    console.log("str1 = " + str1);
    console.log("num1 = " + num1);
    console.log("num1 = " + num1);

    return ret;
}//ae.a

hook成功後,發一條純文字的朋友圈動態,打印出:

所以,可得:

- privated(int):動態是否私密:0公開,1私密
- desc(String):朋友圈的文本
- AtList(List<String>):艾特人的wxid
- Location(com.tencent.mm.protocal.protobuf.bdi):定位信息

好多參數我們不知道是什麼,不過問題不大,那些我們需要的參數已經能搞懂了。懂得a方法的參數,那能否嘗試直接調用它?先來看一下com.tencent.mm.plugin.sns.ui.ae類的構造函數能否調用:

構造函數有Activity類型的參數,Activity類型的參數是很難構造的,所以放棄構造com.tencent.mm.plugin.sns.ui.ae類來調用a方法。那我們直接去看a方法,看能不能找到有用的東西。由於是文字動態,所以我們着重關注傳入的文本,即com.tencent.mm.plugin.sns.ui.SnsUploadUI類的desc成員,在a方法中它是第4個參數:

可見this.desc在a方法中它是str,而且在a方法中,str只有一處引用:

str傳給了ayVar.adk()方法,找一下ayVar來自哪裏,它在a方法裏初始化,而且初始化方式很簡單:

只傳入一個數字就能初始化,我們初始化ay類的時候不用深究這個數字是什麼,和它傳入一樣的值即可。在a方法的尾部,還看到一個引人注目的commit方法:

猜測這就是發佈朋友圈的方法,寫個簡單的frida腳本來驗證一下:

if(Java.available)
{
    Java.perform(function(){
        var ay_class = Java.use("com.tencent.mm.plugin.sns.model.ay");
        var desc = "To be, or not to be, that is a question.";
        var ayInstance = ay_class.$new(2);
        ayInstance.adk(desc);
        ayInstance.commit();
    });
}

文字動態的內容是:To be, or not to be, that is a question.。果不其然,腳本運行後,文字動態發表成功了

經過對com.tencent.mm.plugin.sns.ui.ae的a方法的分析,我們可以知道,a方法主要傳入一些發朋友圈動態所需要的通用的數據,比如動態是否私密,動態的文字描述,艾特的人,定位信息等,這些信息在其他類型的朋友圈動態中也會用得到。我們還知道發文字動態只需要文字描述就能發表成功。

0x3 帶圖片的朋友圈動態分析

帶圖片的的動態和文字動態差不多,只是多加了圖片的參數,我們在分析此類動態時多關注圖片在哪傳入即可。帶圖片的動態對應的類是com.tencent.mm.plugin.sns.ui.ai,有了上面的經驗,我們直接去看它的a方法(ai類有許多a方法,注意這裏說的a方法參數和com.tencent.mm.plugin.sns.ui.z接口裏的a方法參數一致)。在a方法的開頭,看到利用迭代器去遍歷一個列表,遍歷過程中組裝com.tencent.mm.plugin.sns.data.j類的數據,然後把j類放入鏈表linkedList2中:

在組裝數據的時候,我們看到j類構造時傳入一個字符串和數字,而這個字符串對應j類的path字段,這可能就是圖片的路徑:

那麼我們猜測j類就是存儲朋友圈的動態圖片信息的類,上面提到j類被放入鏈表linkedList2中,那麼來看linkedList2被哪裏引用了

看到醒目的字符串:commit pic size,這應該是日誌要打印的字符串,現在基本上可以確定j類就是存儲要發表的圖片的信息的類了,那麼linkedList2就是存儲所有將要發表的圖片信息,繼續往下尋找linkedList2還被哪裏引用了

可以看到linkedList2傳入兩個地方,一處傳入a方法:

另一處傳入com.tencent.mm.plugin.sns.ui.ai$a類構造函數:

linkedList2傳入a類後,又賦值給成員變量tPF,這個tPF成員變量只在a類的dU方法中被引用

而dU方法在哪裏調用呢?在a類的父類:com.tencent.mm.plugin.sns.model.h中,我們看到dU方法在u方法被調用:

而u方法在ai類的a方法中調用(可以回看前面的圖)。分析到這,上面的linkedList2傳出去之後都終有所屬了,即最終都傳入了com.tencent.mm.plugin.sns.model.ay類ey方法。知道圖片往哪傳了,就寫段frida代碼調用試試吧

if(Java.available)
{
    Java.perform(function(){
        var ay_class = Java.use("com.tencent.mm.plugin.sns.model.ay");
        var j_class = Java.use("com.tencent.mm.plugin.sns.data.j")
        var desc = "To be, or not to be, that is a question.";
        var likedList_class = Java.use("java.util.LinkedList");
        var linkedListInstance = likedList_class.$new();
        var ayInstance = ay_class.$new(1);
        var jInstance1 = j_class.$new("/storage/emulated/0/test1.jpg",2);
        var jInstance2 = j_class.$new("/storage/emulated/0/test2.jpg",2);
        var jInstance3 = j_class.$new("/storage/emulated/0/test3.jpg",2);
        
        linkedListInstance.add(jInstance1);
        linkedListInstance.add(jInstance2);
        linkedListInstance.add(jInstance3);
        ayInstance.ey(linkedListInstance);
        ayInstance.adk(desc);
        ayInstance.commit();
    });
}

上面的代碼在發送文本動態代碼的基礎上初始化三個j類,分別傳入三個本地圖片路徑,再將三個類實例添加到鏈表,再將鏈表傳入ay類的ey方法,最後調用ay類的commit方法將動態發送出去,代碼運行,發現帶圖片的朋友圈動態發表成功:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章