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方法將動態發送出去,代碼運行,發現帶圖片的朋友圈動態發表成功: