利用xposed自动获得微信个人收款二维码链接和收款记录
一、說明
? ? ? ??現在的App一般都會帶有支付功能,而現在比較流行的支付一般有支付寶、微信、銀行卡等,一般情況下,應用開發者會直接對接支付寶、微信或者第三方支付公司的Api,以完成支付,但是都需要收取不小的費率,于是,有的第三方支付平臺就想到了鉆空子的方法,利用一些特殊的手段獲得收款二維碼以及收款記錄,這樣就可以繞過支付平臺完成支付過程了,本篇文章的目的就是分析如何完成這樣一個流程,本文的意圖只有一個就是通過分析app學習更多的逆向技術,如果有人利用本文知識和技術進行非法操作進行牟利,帶來的任何法律責任都將由操作者本人承擔,和本文作者無任何關系,最終還是希望大家能夠秉著學習的心態閱讀此文,支付寶的相關文章可以參考:https://blog.csdn.net/xiao_nian/article/details/79881274,這篇文章是獲得支付寶的個人收款二維碼和賬單信息,而我們現在是要獲得微信的個人收款二維碼,和用戶的收款記錄。本篇文章只分析hook 部分的代碼。
二、問題分析
1、微信的二維碼收款
我用的微信版本是6.6.2
獲得個人收錢二維碼的流程如下:
打開微信主界面--》點擊右上角的收付款---》進入到收付款界面--》點擊二維碼收款--》進入二維碼收款頁面--》點擊設置金額--》進入設置金額界面--》設置金額和備注--》點擊確定--》返回二維碼收錢界面并刷新收錢二維碼
二維碼收款界面如下:
設置金額頁面如下:
點擊收款小賬本進入收款小賬本頁面,再點擊收款記錄即可進入收款記錄界面:
?
三、反編譯微信并分析
反編譯應用的方法可以參考:https://blog.csdn.net/xiao_nian/article/details/79391417,這篇文章介紹了如何反編譯微信的apk。
1、hook微信主界面注冊廣播
類似支付寶的過程,我們首先需要在微信主界面創建時注冊廣播并保存其實例,并在其銷毀時銷毀廣播并清空對其實例的引用
// hook 微信主界面的onCreate方法,獲得主界面對象并注冊廣播findAndHookMethod("com.tencent.mm.ui.LauncherUI", classLoader, "onCreate", Bundle.class, new XC_MethodHook() {@Overrideprotected void afterHookedMethod(MethodHookParam param) throws Throwable {XposedLogUtils.log("com.tencent.mm.ui.LauncherUI onCreated" + "\n");weiXinLauncherActivity = (Activity) param.thisObject;// 注冊廣播weiXinBroadcast = new WeiXinBroadcast();IntentFilter intentFilter = new IntentFilter();intentFilter.addAction(WeiXinBroadcast.WEIXIN_QR_CODE_URL_STRING_INTENT_FILTER_ACTION);intentFilter.addAction(WeiXinBroadcast.WEIXIN_BILLLIST_INTENT_FILTER_ACTION);weiXinLauncherActivity.registerReceiver(weiXinBroadcast, intentFilter);}});// hook設置金額和備注的onDestroy方法findAndHookMethod("com.tencent.mm.ui.LauncherUI", classLoader, "onDestroy", new XC_MethodHook() {@Overrideprotected void afterHookedMethod(MethodHookParam param) throws Throwable {XposedLogUtils.log("com.tencent.mm.ui.LauncherUI onDestroy" + "\n");weiXinLauncherActivity.unregisterReceiver(weiXinBroadcast);weiXinBroadcast = null;weiXinLauncherActivity = null;}});?
2、設置金額界面
?
用hierarchy view查看設置金額界面布局,如下:
可以看到設置金額界面對應的Activity為"com.tencent.mm.plugin.collect.ui.CollectCreateQRCodeUI",再次查看設置金額界面的布局,找到確認按鈕,如下:
可以看到"確認"按鈕的id為"ak_",在微信apk的public.xml(解壓apk后的res/values/public.xml文件,記錄資源id)中搜索"ak_",可以搜到如下代碼:
<public type="id" name="ak_" id="0x7f1006ec" />也就是說id"ak_"對應的值為"0x7f1006ec",將"0x7f1006ec"轉換為10進制,結果為"2131756780",在反編譯代碼中全局搜索"2131756780",可以搜到如下內容:
Searching 16831 files for "2131756780"E:\tools\xpose\classes.jar.src\com\tencent\mm\plugin\facedetect\a.java:61: public static final int cAg = 2131756780;E:\tools\xpose\classes.jar.src\com\tencent\mm\plugin\wxpay\a.java:292: public static final int cAg = 2131756780; E:\tools\xpose\classes.jar.src\com\tencent\mm\R.java:1975: public static final int cAg = 2131756780;3 matches across 3 files可以"2131756780"這個值在三個地方都有定義,但是名稱都為"cAg",打開CollectCreateQRCodeUI反編譯代碼,搜索"cAg"字段,結果如下:
((Button)findViewById(a.f.cAg)).setOnClickListener(new View.OnClickListener(){public final void onClick(View paramAnonymousView){double d = bh.getDouble(CollectCreateQRCodeUI.a(CollectCreateQRCodeUI.this).getText(), 0.0D);g.Dk();int i = ((Integer)g.Dj().CU().get(w.a.xrD, Integer.valueOf(0))).intValue();x.i("MicroMsg.CollectCreateQRCodeUI", "wallet region: %s", new Object[] { Integer.valueOf(i) });if (!CollectCreateQRCodeUI.a(CollectCreateQRCodeUI.this).XO()) {com.tencent.mm.ui.base.u.makeText(CollectCreateQRCodeUI.this.mController.xIM, a.i.uPA, 0).show();}for (;;){return;if (d < 0.01D) {com.tencent.mm.ui.base.u.makeText(CollectCreateQRCodeUI.this.mController.xIM, a.i.uMS, 0).show();} else if (i == 8) {CollectCreateQRCodeUI.this.r(new m(Math.round(d * 100.0D), CollectCreateQRCodeUI.b(CollectCreateQRCodeUI.this), q.FZ()));} else {CollectCreateQRCodeUI.this.l(new s(d, "1", CollectCreateQRCodeUI.b(CollectCreateQRCodeUI.this)));}}}});很明顯,這段代碼應該是在布局中找到確認按鈕,并給其設置監聽事件,經分析,發現下面代碼是發起獲得二維碼鏈接的請求:
CollectCreateQRCodeUI.this.l(new s(d, "1", CollectCreateQRCodeUI.b(CollectCreateQRCodeUI.this)));"s"應該是請求參數,定義如下:
public final class sextends i {public String desc;public String fpP;public String ljf = null;public double ljg;// 構造函數的三個參數類型,第一個為金額,第二個為二維碼類型,我們一直傳入"1"就可以了,第三個為備注信息public s(double paramDouble, String paramString1, String paramString2){HashMap localHashMap = new HashMap();try{StringBuilder localStringBuilder = new java/lang/StringBuilder;localStringBuilder.<init>();localHashMap.put("fee", Math.round(100.0D * paramDouble));localHashMap.put("fee_type", paramString1);localHashMap.put("desc", URLEncoder.encode(paramString2, "UTF-8"));this.ljg = paramDouble;this.fpP = paramString1;this.desc = paramString2;D(localHashMap);return;}catch (UnsupportedEncodingException localUnsupportedEncodingException){for (;;){x.printErrStackTrace("Micromsg.NetSceneTenpayRemittanceQuery", localUnsupportedEncodingException, "", new Object[0]);}}}... }再來看一下發起請求的"l"方法,定義如下:
public final void l(k paramk){cCe();this.zIY.a(paramk, true, 1);}現在我們只需要hook住CollectCreateQRCodeUI的onCreate方法,獲得其實例對象保存在靜態變量中,并在onDestory是設置為空,這樣就可以獲得CollectCreateQRCodeUI的實例了,然后在需要獲得二維碼鏈接時,調用其"l"方法發起請求即可,代碼如下:
// hook設置金額和備注的onCreate方法,自動填寫數據并點擊findAndHookMethod("com.tencent.mm.plugin.collect.ui.CollectCreateQRCodeUI", classLoader, "onCreate", Bundle.class, new XC_MethodHook() {@Overrideprotected void afterHookedMethod(MethodHookParam param) throws Throwable {XposedLogUtils.log("com.tencent.mm.plugin.collect.ui.CollectCreateQRCodeUI" + ":onCreated方法");mWeiXinCreateQRCodeActivity = (Activity) param.thisObject;Intent intent = ((Activity) param.thisObject).getIntent();String jinEr = intent.getStringExtra("qr_money");String beiZhu = intent.getStringExtra("beiZhu");// 連續生成二維碼鏈接時,是否需要關閉金額設置界面executeWeiXinGenerateQrCodeMethod(jinEr, beiZhu);}});// hook設置金額和備注的onDestroy方法findAndHookMethod("com.tencent.mm.plugin.collect.ui.CollectCreateQRCodeUI", classLoader, "onDestroy", new XC_MethodHook() {@Overrideprotected void afterHookedMethod(MethodHookParam param) throws Throwable {XposedLogUtils.log("com.tencent.mm.plugin.collect.ui.CollectCreateQRCodeUI onDestroy" + "\n");mWeiXinCreateQRCodeActivity = null;}});/*** 當設置金額界面已經打開的情況下,可以直接執行生成二維碼方法*/public static void executeWeiXinGenerateQrCodeMethod(String amount, String beiZhu) {if (mWeiXinCreateQRCodeActivity != null) {XposedLogUtils.log("executeWeiXinGenerateQrCodeMethod " + amount + " " + beiZhu);if (!TextUtils.isEmpty(amount)) {Object paramObject = null;Class paramObjectClass = findClass("com.tencent.mm.plugin.collect.b.s", mWeiXinClassLoader);try {Constructor paramObjectConstructor = paramObjectClass.getDeclaredConstructor(double.class, String.class, String.class);paramObject = paramObjectConstructor.newInstance(Double.valueOf(amount), "1", beiZhu);callMethod(mWeiXinCreateQRCodeActivity, "l", paramObject);} catch (Exception e) {e.printStackTrace();}}}}/*** 打開設置金額界面** @param qr_money* @param beiZhu*/public static void startWeiXinCreateQRCodeActivity(String qr_money, String beiZhu) {Class weiXinCreateQRCodeActivityClass;Intent launcherIntent;if (weiXinLauncherActivity != null) {XposedLogUtils.log("launcher WeiXinCreateQRCodeActivity");weiXinCreateQRCodeActivityClass = XposedHelpers.findClass("com.tencent.mm.plugin.collect.ui.CollectCreateQRCodeUI", mWeiXinClassLoader);launcherIntent = new Intent(weiXinLauncherActivity, weiXinCreateQRCodeActivityClass);launcherIntent.putExtra("qr_money", qr_money);launcherIntent.putExtra("beiZhu", beiZhu);weiXinLauncherActivity.startActivity(launcherIntent);return;}}在支付寶的廣播接收類中,定義如下:
public class WeiXinBroadcast extends BroadcastReceiver{public static String WEIXIN_QR_CODE_URL_STRING_INTENT_FILTER_ACTION = "com.hhly.pay.weixin.info.qrCodeUrl";@Overridepublic void onReceive(Context context, Intent intent) {if (intent.getAction().contentEquals(WEIXIN_QR_CODE_URL_STRING_INTENT_FILTER_ACTION)) {String qr_money = intent.getStringExtra("qr_money");String beiZhu = intent.getStringExtra("beiZhu");XposedLogUtils.log("WeiXinBroadcast onReceive " + qr_money + " " + beiZhu + "\n");if (!TextUtils.isEmpty(qr_money)) {if (Main.mWeiXinCreateQRCodeActivity != null) { // 如果已經有設置金額,則直接使用其實例,不需要重復啟動Main.executeWeiXinGenerateQrCodeMethod(qr_money, beiZhu);} else { // 生成二維碼鏈接Main.startWeiXinCreateQRCodeActivity(qr_money, beiZhu);}}}} }我們hook了設置金額界面的onCreate方法并保存了其實例,這樣就不用每次生成二維碼鏈接都打開設置金額界面了,下面再來看一看當服務端返回二維碼鏈接之后設置金額界面的回調方法,經觀察,回調方法定義如下:
public final boolean d(int paramInt1, int paramInt2, final String paramString, k paramk){boolean bool2 = false;boolean bool1;if ((paramk instanceof s)){bool1 = bool2;if (paramInt1 == 0){bool1 = bool2;if (paramInt2 == 0){paramString = (s)paramk;paramk = new Intent();paramk.putExtra("ftf_pay_url", paramString.ljf); // 獲得二維碼鏈接paramk.putExtra("ftf_fixed_fee", paramString.ljg); // 獲得金額paramk.putExtra("ftf_fixed_fee_type", paramString.fpP); // 獲得類型paramk.putExtra("ftf_fixed_desc", paramString.desc); // 獲得備注setResult(-1, paramk); // 將參數回傳給二維碼收款界面finish(); // 關閉當前界面bool1 = true;}}}...}分析上面的代碼,該方法會將二維碼鏈接等數據放入intent中并回傳給收款二維碼界面,最后再關閉設置金額界面,我們只需要hook住該方法,并替換其執行邏輯,就可以在獲得二維碼鏈接后不銷毀設置金額界面了。其中,最后一個參數"k paramk"中定義了二維碼鏈接信息,代碼如下:
findAndHookMethod("com.tencent.mm.plugin.collect.ui.CollectCreateQRCodeUI", classLoader, "d", int.class, int.class, String.class,findClass("com.tencent.mm.ae.k", classLoader), new XC_MethodReplacement() {@Overrideprotected Object replaceHookedMethod(MethodHookParam methodHookParam) throws Throwable {XposedLogUtils.log("com.tencent.mm.plugin.collect.ui.CollectCreateQRCodeUI" + ":d方法");XposedLogUtils.log(methodHookParam.args[0] + " " + methodHookParam.args[1] + " " + methodHookParam.args[2]);if (methodHookParam.args[3] != null) {WeiXinQrCode weiXinQrCode = new WeiXinQrCode();weiXinQrCode.ftf_pay_url = getStringField(methodHookParam.args[3], "ljf"); // 獲得二維碼鏈接weiXinQrCode.ftf_fixed_fee = getStringField(methodHookParam.args[3], "ljg"); // 獲得金額weiXinQrCode.ftf_fixed_fee_type = getStringField(methodHookParam.args[3], "fpP"); // 獲得類型weiXinQrCode.ftf_fixed_desc = getStringField(methodHookParam.args[3], "desc"); // 獲得備注XposedLogUtils.log(weiXinQrCode.toString() + "\n");// 發送廣播將二維碼鏈接信息傳給xposed插件Intent broadCastIntent = new Intent();broadCastIntent.putExtra("weiXinQrCode", weiXinQrCode);broadCastIntent.setAction(PluginBroadcast.WEIXIN_QR_CODE_URL_STRING_INTENT_FILTER_ACTION);Activity activity = (Activity) methodHookParam.thisObject;activity.sendBroadcast(broadCastIntent);}return true;}});其中WeiXinQrCode類是我們自己定義的類,用來保存二維碼信息,定義如下:
public static class WeiXinQrCode implements Parcelable {public String ftf_pay_url;public String ftf_fixed_fee;public String ftf_fixed_fee_type;public String ftf_fixed_desc;public WeiXinQrCode() {}protected WeiXinQrCode(Parcel in) {ftf_pay_url = in.readString();ftf_fixed_fee = in.readString();ftf_fixed_fee_type = in.readString();ftf_fixed_desc = in.readString();}public static final Creator<WeiXinQrCode> CREATOR = new Creator<WeiXinQrCode>() {@Overridepublic WeiXinQrCode createFromParcel(Parcel in) {return new WeiXinQrCode(in);}@Overridepublic WeiXinQrCode[] newArray(int size) {return new WeiXinQrCode[size];}};@Overridepublic int describeContents() {return 0;}@Overridepublic void writeToParcel(Parcel dest, int flags) {dest.writeString(ftf_pay_url);dest.writeString(ftf_fixed_fee);dest.writeString(ftf_fixed_fee_type);dest.writeString(ftf_fixed_desc);}@Overridepublic String toString() {return "ftf_pay_url:" + ftf_pay_url + "," +"ftf_fixed_fee:" + ftf_fixed_fee + "," +"ftf_fixed_fee_type:" + ftf_fixed_fee_type + "," +"ftf_fixed_desc:" + ftf_fixed_desc + "\n";}}然后在xposed插件的廣播接收類中接收二維碼鏈接信息,如下:
public class PluginBroadcast extends BroadcastReceiver{public static String WEIXIN_QR_CODE_URL_STRING_INTENT_FILTER_ACTION = "com.tencent.mm.info.qrCodeUrl";@Overridepublic void onReceive(Context context, Intent intent) {if (intent.getAction().contentEquals(WEIXIN_QR_CODE_URL_STRING_INTENT_FILTER_ACTION)) {App.dealWeiXinQrCodeUrlString(intent);}} }public static void dealWeiXinQrCodeUrlString(Intent intent) {Main.WeiXinQrCode weiXinQrCode = intent.getParcelableExtra("weiXinQrCode");Log.i("aaaaa:", weiXinQrCode.toString() + " " + weiXinTotalCount); }在xposed插件中發送獲得二維碼鏈接廣播,打印信息如下:
05-31 16:58:42.181 12445-12445/? I/aaa:: ftf_pay_url:wxp://f2f1dsW3BhPFHjfKRd2Royq4-jp0yy_JUxxe,ftf_fixed_fee:5.0,ftf_fixed_fee_type:1,ftf_fixed_desc:你好5可以看到二維碼鏈接已經打印出來了。
三、獲得賬單信息
首先,我們來看一下收款記錄對應的頁面,如下:
用hierarchy view查看其布局,如下:
可以看到收款記錄頁面所在Activity為"com.tencent.mm.plugin.appbrand.ui.AppBrandUI",而具體的收款信息顯示在"InnerWebView"中,也就是說收款記錄是在一個WebView中顯示的,那么想從WebView中獲得收款信息就比較復雜了,一般的,如果頁面顯示在WebView中,應該是通過http請求獲得h5頁面并加載,那么我們可以嘗試一下抓包來獲得收款信息,打開抓包Charles抓包工具,然后再打開收款記錄頁面,可以抓到如下請求:
可以看到成功抓取到了賬單數據,分析請求參數,總結如下:
url:https://payapp.weixin.qq.com/qrappzd/user/incomelist?sid=AAHE8SPddES5nFhpG3Cm-7zVE2XqeHLx3PzD78ZL33nzvA&v=3.4.3請求方式:post請求參數:{"v": "3.4.3", // 請求接口版本"start_time": 0, // 查詢收款數據開始時間"end_time": 1527760717, // 查詢收款結束時間"last_bill_id": null, // 請求這個賬單編號對應賬單之后的數據"page_size": 10, // 每次請求數據條數"sort": "desc", // 排序方式"is_first": true, // 是否是第一個請求"sid": "AAHE8SPddES5nFhpG3Cm-7zVE2XqeHLx3PzD78ZL33nzvA" // 用戶信息id}用postman模擬請求,發現是可以成功請求到數據的:
上面的請求信息中,最關鍵的部分為"sid",這個應該類似loginToken一樣的信息,用來驗證用戶信息的,也就是說,我們只需要找到獲得"sid"的辦法,就能夠獲得到收款記錄。然后,微信的收款記錄是用微信小程序實現的,我找了一天也沒有找到獲得"sid"的方法,后面有時間可以嘗試反編譯微信小程序的代碼,看能不能找到獲得"sid"的方法??磥磉@個辦法暫時行不通,那么有沒有其他的辦法獲得收款數據呢?在微信的交易記錄中我們也可以獲得收款信息,如下:
查看其布局,如下:
麻蛋,又是一個WebView頁面,同樣,抓包獲得了如下數據:
總結請求數據:
url:https://wx.tenpay.com/userroll/userrolllist?classify_type=0&count=20&exportkey=A%2F4YTLDXVQMB1IScuR4uDa8%3D&sort_type=1請求方式:getCookie:export_key=A/4YTLDXVQMB1IScuR4uDa8=; userroll_encryption=5Wyzpp5yYABVU2ZLQ9ueNA5LT466SEtjxY5Z1L0bhnnzcWwYEij0Q0T+ZeUfu0T4qFTAELJEZYvclmmtF39a/mW5syVtXHUYstaIYCrEVcOb0yfR6OkVpqb1xUE5p3rCXT3OF8YwcgoIDCS5PepNkg==;上面得參數中,export_key和userroll_encryption是關鍵參數,有了這兩個參數我們就可以獲得賬單數據,用postman模擬請求如下:
我在反編譯代碼中找了很久,同樣沒有找到獲得export_key和userroll_encryption的辦法,到現在為止,問題好像卡住了,然而,一次偶然的操作,我打開了微信交易記錄的原生頁面,也就是說,微信交易記錄的頁面是可以配置使用原生的頁面還是使用h5頁面的,原生的交易記錄頁面如下:
查看其布局,如下:
可以看到原生交易記錄頁面對應的類為"com.tencent.mm.plugin.order.ui.MallOrderRecordListUI",首先分析一下其布局結構:
可以看到,其賬單數據其實是在MMLoadMoreListView控件中顯示的,應該是一個可以支持加載更多的ListView控件,這個我們是能夠猜到。查看"MallOrderRecordListUI"類的代碼:
public class MallOrderRecordListUIextends WalletBaseUI {...public MMLoadMoreListView ldB; // 交易記錄頁面對應的ListViewpublic a pcp = null; // listview adapter引用public List<i> pcq = new ArrayList(); // 交易記錄數據public int wn = 0; // 記錄已經加載的數據條數protected String pcr = null; // 服務端返回的已經加載過的賬單數據的條數,會在請求參數中傳入該值...// 初始化布局protected final void initView(){// 這里有一個判斷,只有用對應的方法打開交易記錄頁面才會去加載數據,我們再xposed中打開交易記錄界面,這個條件是不成立的,也就是說打開交易記錄界面時,不會主動的去加載數據if ((com.tencent.mm.wallet_core.a.ag(this) instanceof com.tencent.mm.plugin.order.a.a)){this.acS = true;biJ(); // 加載數據}...this.ldB = ((MMLoadMoreListView)findViewById(a.f.uqt)); // 在布局中找到listviewthis.pcp = new a(); // 新建adpater實例this.ldB.setAdapter(this.pcp); // 給listview設置adapter// 設置listview的item點擊監聽this.ldB.setOnItemClickListener(new AdapterView.OnItemClickListener(){public final void onItemClick(AdapterView<?> paramAnonymousAdapterView, View paramAnonymousView, int paramAnonymousInt, long paramAnonymousLong){...}});// 設置listview的item長按監聽this.ldB.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener(){public final boolean onItemLongClick(AdapterView<?> paramAnonymousAdapterView, View paramAnonymousView, final int paramAnonymousInt, long paramAnonymousLong){...}});// 給listview綁定加載更多的監聽this.ldB.ybX = new MMLoadMoreListView.a(){public final void axW(){if (!MallOrderRecordListUI.this.acS){MallOrderRecordListUI.this.acS = true;MallOrderRecordListUI localMallOrderRecordListUI = MallOrderRecordListUI.this;localMallOrderRecordListUI.wn += 10; // 已經加載的數據條數MallOrderRecordListUI.this.biJ(); // 調用該方法加載數據}}};...}// 請求賬單數據方法public void biJ(){// 調用"l"方法請求賬單數據,傳入請求參數,其中this.wn為我們ListView中數據的條數,this.pcr為服務端返回給我們的已經加載的數據條數l(new com.tencent.mm.plugin.order.model.e(this.wn, this.pcr));}// 獲得賬單數據之后的回調方法public boolean d(int paramInt1, int paramInt2, String paramString, k paramk){boolean bool;if ((paramk instanceof com.tencent.mm.plugin.order.model.e)){if (this.mzP != null){this.mzP.dismiss();this.mzP = null;}paramString = (com.tencent.mm.plugin.order.model.e)paramk; // 賬單數據this.pcr = paramString.pbf; // 服務端返回的已經加載過的賬單數據條數,這個值bl(paramString.pbd); // 具體賬單數據在paramk.pbd中bm(paramString.pbe);this.mCount = this.pcq.size(); // 現在已經加載過的賬單數據if (paramString.liB > this.mCount) // paramString.liB返回的是用戶當前賬單數據總條數{...}}...}// 交易記錄列表對應的adapterprotected final class aextends BaseAdapter{protected a() {}private i uA(int paramInt){return (i)MallOrderRecordListUI.this.pcq.get(paramInt);}public final int getCount(){return MallOrderRecordListUI.this.pcq.size();}public final long getItemId(int paramInt){return paramInt;}public final View getView(int paramInt, View paramView, ViewGroup paramViewGroup){...}}}可以看到,微信最終是調用"l"方法來請求賬單數據的,其中參數"e"的定義如下:
package com.tencent.mm.plugin.order.model;import com.tencent.mm.sdk.platformtools.x; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject;public final class eextends com.tencent.mm.wallet_core.tenpay.model.i {public int liB;private int ocK;public List<i> pbd = null; // 賬單數據public List<d> pbe = null; // 賬單列表上的懸浮日期數據,我們不關心public String pbf;public e(int paramInt, String paramString){// 設置請求參數HashMap localHashMap = new HashMap();localHashMap.put("Limit", "10"); // 每次請求數據的條數,微信寫死了就是10條,我們可以通過其他方法修改這個值localHashMap.put("Offset", String.valueOf(paramInt)); // listview中數據的條數localHashMap.put("Extbuf", paramString); // 服務端返回的已經加載過的數據條數D(localHashMap); // 調用D方法構造請求參數}// 數據回來后的回調方法public final void a(int paramInt, String paramString, JSONObject paramJSONObject){int i = 0;x.d("MicroMsg.NetScenePatchQueryUserRoll", "errCode " + paramInt + " errMsg: " + paramString);this.pbd = new LinkedList();// 解析json數據,轉換為對象形式try{this.liB = paramJSONObject.getInt("TotalNum");this.ocK = paramJSONObject.getInt("RecNum");this.pbf = paramJSONObject.optString("Extbuf");JSONArray localJSONArray = paramJSONObject.getJSONArray("UserRollList");if (localJSONArray != null) {for (paramInt = 0; paramInt < localJSONArray.length(); paramInt++){paramString = new com/tencent/mm/plugin/order/model/i;paramString.<init>();JSONObject localJSONObject = localJSONArray.getJSONObject(paramInt);paramString.pbq = localJSONObject.optInt("PayType");paramString.pbi = localJSONObject.optString("Transid");paramString.pbj = localJSONObject.optDouble("TotalFee");paramString.pbk = localJSONObject.optString("GoodsName");paramString.pbl = localJSONObject.optInt("CreateTime");paramString.pbn = localJSONObject.optInt("ModifyTime");paramString.pbo = localJSONObject.optString("FeeType");paramString.pbt = localJSONObject.optString("AppThumbUrl");paramString.pbm = localJSONObject.optString("TradeStateName");paramString.pby = localJSONObject.optString("StatusColor");paramString.pbz = localJSONObject.optString("FeeColor");paramString.pbA = localJSONObject.optDouble("ActualPayFee");paramString.pbB = localJSONObject.optString("BillId");this.pbd.add(paramString);}}...return;}catch (JSONException paramString){x.e("MicroMsg.NetScenePatchQueryUserRoll", "Parse Json exp:" + paramString.getLocalizedMessage());}}... }微信將每次請求的數據條數寫死了,每次只能請求10條數數據,從上面的代碼中可以看到最終是通過調用"D(localHashMap);"來構造請求數據的,它是在"com.tencent.mm.plugin.order.model.e"的父類"com.tencent.mm.wallet_core.c.h"中定義的,其定義如下:
public final void D(Map<String, String> paramMap) {...}那么只要我們考慮hook住該方法,判斷如果當前類的類名是"com.tencent.mm.plugin.order.model.e",就修改參數paramMap中key值為"Limit"的value值,這樣不就修改了每次請求賬單數據的條數了嗎?
考慮另外一個問題,我們現在想屏蔽掉用戶對交易記錄頁面的操作,因為我們可以自己調用對應的方法來獲得賬單數據,而不需要讓賬單數據顯示在界面上,或者讓用戶滑動賬單頁面就加載更多數據了,通過上面的分析我們知道,微信是通過調用"biJ()"來加載數據的,我們只需要replace掉該方法,不就可以讓用戶操作界面無法加載數據了嗎?而賬單數據的顯示是在交易記錄界面賬單數據返回的回調方法中實現的,同樣我們可以replace掉該方法,就可以讓賬單數據不顯示在界面上了。
整個處理賬單頁面的代碼如下:
?
public static Activity weiXinMallOrderRecordListUI = null; // 微信賬單頁面public static List<WeiXinBillObject> weiXinBillObjectList = null;// hook交易記錄頁面的onCreate方法,保存其對象實例并調用獲得賬單數據方法findAndHookMethod("com.tencent.mm.plugin.order.ui.MallOrderRecordListUI", classLoader, "onCreate", Bundle.class, new XC_MethodHook() {@Overrideprotected void afterHookedMethod(MethodHookParam param) throws Throwable {XposedLogUtils.log("com.tencent.mm.g.a.st.MallOrderRecordListUI" + ":onCreate");weiXinMallOrderRecordListUI = (Activity) param.thisObject;startGetWeiXinBillList();}});// hook交易記錄頁面的onDestory方法,釋放對其對象實例的引用findAndHookMethod("com.tencent.mm.plugin.order.ui.MallOrderRecordListUI", classLoader, "onDestroy", new XC_MethodHook() {@Overrideprotected void afterHookedMethod(MethodHookParam param) throws Throwable {XposedLogUtils.log("com.tencent.mm.plugin.order.ui.MallOrderRecordListUI onDestroy" + "\n");weiXinMallOrderRecordListUI = null;}});// 替換掉交易記錄頁面的加載數據方法,讓用戶操作界面不能加載數據findAndHookMethod("com.tencent.mm.plugin.order.ui.MallOrderRecordListUI", classLoader, "biJ", new XC_MethodReplacement() {@Overrideprotected Object replaceHookedMethod(MethodHookParam methodHookParam) throws Throwable {XposedLogUtils.log("com.tencent.mm.plugin.wallet_core.model.ae" + ":bLH");return null;}});// hook D方法,修改每次請求賬單數據的條數findAndHookMethod("com.tencent.mm.wallet_core.c.h", classLoader, "D", Map.class, new XC_MethodHook() {@Overrideprotected void beforeHookedMethod(MethodHookParam param) throws Throwable {if (param.thisObject.getClass().getName().contentEquals("com.tencent.mm.plugin.order.model.e")) {if (param.args[0] != null) {Map<String, String> paramMap = (Map<String, String>)param.args[0];if (paramMap.containsKey("Limit")) {paramMap.put("Limit", "100"); // 修改每次請求賬單數據的條數,這里修改為100條}}}super.beforeHookedMethod(param);}});// hook交易記錄頁面數據返回的回調方法,替換其原來的執行邏輯,讓賬單數據不顯示到界面上,并且獲得賬單數據findAndHookMethod("com.tencent.mm.plugin.order.ui.MallOrderRecordListUI", classLoader, "d", int.class, int.class, String.class,findClass("com.tencent.mm.ae.k", classLoader), new XC_MethodReplacement() {@Overrideprotected Object replaceHookedMethod(MethodHookParam methodHookParam) throws Throwable {XposedLogUtils.log("com.tencent.mm.plugin.order.ui.MallOrderRecordListUI:" + "d" + "\n");XposedLogUtils.log(methodHookParam.args[0] + " " + methodHookParam.args[1] + " " + methodHookParam.args[2] + "\n");if (methodHookParam.args[3] != null) {Object resObject = methodHookParam.args[3];String pcr = getStringField(resObject, "pbf"); // 服務端返回的已經加載過的數據條數Integer totalCount = 0;if (getField(resObject, "liB") != null) {totalCount = (Integer) getField(resObject, "liB"); // 服務端返回的當前用戶賬單數據總條數}XposedLogUtils.log("pcr:" + pcr + " " + "totalCount:" + totalCount + "\n");// 獲得賬單數據Field billListFiled = XposedHelpers.findField(findClass("com.tencent.mm.plugin.order.model.e", classLoader), "pbd");final Object billList = billListFiled.get(resObject);// 轉換賬單數據到我們自己的對象中if (billList != null) {List<Object> bList_obj = (List) billList;for (Object object : bList_obj) {WeiXinBillObject weiXinBillObject = new WeiXinBillObject();weiXinBillObject.payType = getStringField(object, "pbq");weiXinBillObject.transid = getStringField(object, "pbi");weiXinBillObject.totalFee = getStringField(object, "pbj");weiXinBillObject.goodsName = getStringField(object, "pbk");weiXinBillObject.createTime = getStringField(object, "pbl");weiXinBillObject.modifyTime = getStringField(object, "pbn");weiXinBillObject.feeType = getStringField(object, "pbo");weiXinBillObject.appThumbUrl = getStringField(object, "pbt");weiXinBillObject.tradeStateName = getStringField(object, "pbm");weiXinBillObject.statusColor = getStringField(object, "pby");weiXinBillObject.feeColor = getStringField(object, "pbz");weiXinBillObject.actualPayFee = getStringField(object, "pbA");weiXinBillObject.billId = getStringField(object, "pbB");weiXinBillObjectList.add(weiXinBillObject);}// 連續加載1000條數據或者加載完成則不再繼續加載,在這里判斷是否加載到了所要的賬單數據,我這里設置的條件是加載超過了1000條數據if (weiXinBillObjectList.size() >= 1000 || bList_obj.size() == 0 || bList_obj.size() >= totalCount) {for (WeiXinBillObject weiXinBillObject : weiXinBillObjectList) {XposedLogUtils.log(weiXinBillObject.toString());}} else { // 繼續加載賬單數據getWeiXinBillList(weiXinBillObjectList.size(), pcr);}}}return true;}});/*** 加載賬單數據* @param offset* @param extbuf*/private static void getWeiXinBillList(int offset, String extbuf) {if (weiXinMallOrderRecordListUI != null) {Object paramObject = null;Class paramObjectClass = findClass("com.tencent.mm.plugin.order.model.e", mWeiXinClassLoader);try {Constructor paramObjectConstructor = paramObjectClass.getDeclaredConstructor(int.class, String.class);paramObject = paramObjectConstructor.newInstance(offset, extbuf);callMethod(weiXinMallOrderRecordListUI, "l", paramObject);} catch (Exception e) {e.printStackTrace();}}}/*** 啟動微信交易記錄頁面*/public static void startWeiXinBillListActivity() {Intent launcherIntent;if (weiXinLauncherActivity != null) {XposedLogUtils.log("launcher MallOrderRecordListUI");launcherIntent = new Intent(weiXinLauncherActivity, findClass("com.tencent.mm.plugin.order.ui.MallOrderRecordListUI", mWeiXinClassLoader));weiXinLauncherActivity.startActivity(launcherIntent);return;}}/*** 開始獲得賬單數據*/public static void startGetWeiXinBillList() {if (weiXinBillObjectList == null) {weiXinBillObjectList = new ArrayList<>();}weiXinBillObjectList.clear(); // 清空原來的數據getWeiXinBillList(0, null); // 加載賬單數據}private static String getStringField(final Object obj, final String fieldName) throws IllegalAccessException {Field sField = XposedHelpers.findField(obj.getClass(), fieldName);if (sField == null) {return null;}return String.valueOf(sField.get(obj));}private static Object getField(final Object obj, final String fieldName) throws IllegalAccessException {Field sField = XposedHelpers.findField(obj.getClass(), fieldName);if (sField == null) {return null;}return sField.get(obj);}/*** 微信賬單對象*/public static class WeiXinBillObject implements Parcelable {public String payType;public String transid;public String totalFee;public String goodsName;public String createTime;public String modifyTime;public String feeType;public String appThumbUrl;public String tradeStateName;public String statusColor;public String feeColor;public String actualPayFee;public String billId;public WeiXinBillObject() {}protected WeiXinBillObject(Parcel in) {payType = in.readString();transid = in.readString();totalFee = in.readString();goodsName = in.readString();createTime = in.readString();modifyTime = in.readString();feeType = in.readString();appThumbUrl = in.readString();tradeStateName = in.readString();statusColor = in.readString();feeColor = in.readString();actualPayFee = in.readString();billId = in.readString();}public static final Creator<WeiXinBillObject> CREATOR = new Creator<WeiXinBillObject>() {@Overridepublic WeiXinBillObject createFromParcel(Parcel in) {return new WeiXinBillObject(in);}@Overridepublic WeiXinBillObject[] newArray(int size) {return new WeiXinBillObject[size];}};@Overridepublic int describeContents() {return 0;}@Overridepublic void writeToParcel(Parcel dest, int flags) {dest.writeString(payType);dest.writeString(transid);dest.writeString(totalFee);dest.writeString(goodsName);dest.writeString(createTime);dest.writeString(modifyTime);dest.writeString(feeType);dest.writeString(appThumbUrl);dest.writeString(tradeStateName);dest.writeString(statusColor);dest.writeString(feeColor);dest.writeString(actualPayFee);dest.writeString(billId);}@Overridepublic String toString() {return "payType:" + payType + "," +"transid:" + transid + "," +"totalFee:" + totalFee + "," +"createTime:" + createTime + "," +"modifyTime:" + modifyTime + "," +"feeType:" + feeType + "," +"appThumbUrl:" + appThumbUrl + "," +"tradeStateName:" + tradeStateName + "," +"statusColor:" + statusColor + "," +"feeColor:" + feeColor + "," +"actualPayFee:" + actualPayFee + "," +"billId:" + billId + "," +"goodsName:" + goodsName + "\n";}}在廣播中定義如下:
public class WeiXinBroadcast extends BroadcastReceiver{public static String WEIXIN_BILLLIST_INTENT_FILTER_ACTION = "com.tencent.mm.info.billlist";@Overridepublic void onReceive(Context context, Intent intent) {if (intent.getAction().contentEquals(WEIXIN_BILLLIST_INTENT_FILTER_ACTION)) {if (Main.weiXinMallOrderRecordListUI != null) {Main.startGetWeiXinBillList();} else {Main.startWeiXinBillListActivity();}}}}?
最后在Xposed插件程序發送一個廣播,可以看到如下日志打印:
?
06-01 10:27:46.137 I/Xposed (10765): payType:0,transid:4200000119201806015247324551,totalFee:9800.0,createTime:1527783145,modifyTime:1527783151,feeType:CNY,appThumbUrl:,tradeStateName:支付成功,statusColor:#888888,feeColor:#000000,actualPayFee:9800.0,billId:e91e105b20a10700d55d1574,goodsName:京東-訂單編號76517324492 06-01 10:27:46.137 I/Xposed (10765): payType:5,transid:100005020118053100078332119850436866,totalFee:52000.0,createTime:1527774458,modifyTime:1527774458,feeType:1,appThumbUrl:,tradeStateName:等待朋友確認收錢,statusColor:#888888,feeColor:#000000,actualPayFee:52000.0,billId:fafc0f5b20a10700d55d1574,goodsName:微信轉賬 06-01 10:27:46.137 I/Xposed (10765): payType:2,transid:4200000144201805318564157226,totalFee:680.0,createTime:1527771799,modifyTime:1527771799,feeType:CNY,appThumbUrl:,tradeStateName:支付成功,statusColor:#888888,feeColor:#000000,actualPayFee:680.0,billId:97f20f5b20a10700d55d1574,goodsName:深圳市南山家家樂生活超市消費 06-01 10:27:46.137 I/Xposed (10765): payType:2,transid:4200000116201805312045134300,totalFee:1900.0,createTime:1527768245,modifyTime:1527768245,feeType:,appThumbUrl:,tradeStateName:支付成功,statusColor:#888888,feeColor:#000000,actualPayFee:1900.0,billId:b5e40f5b20a10700d55d1574,goodsName:萬連佳 收銀員:005-102 06-01 10:27:46.137 I/Xposed (10765): payType:6,transid:100005030118053100078332117578249866,totalFee:1800.0,createTime:1527768062,modifyTime:1527768062,feeType:1,appThumbUrl:,tradeStateName:已轉賬,statusColor:#888888,feeColor:#000000,actualPayFee:1800.0,billId:fee30f5b20a10700d55d1574,goodsName:二維碼收款 ...ok,問題解決。
利用賬單cookie來獲得賬單
上面說到過,微信賬單h5頁面請求賬單數據的接口總結如下:
url:https://wx.tenpay.com/userroll/userrolllist?classify_type=0&count=20&sort_type=1請求方式:getCookie:export_key=A/4YTLDXVQMB1IScuR4uDa8=; userroll_encryption=5Wyzpp5yYABVU2ZLQ9ueNA5LT466SEtjxY5Z1L0bhnnzcWwYEij0Q0T+ZeUfu0T4qFTAELJEZYvclmmtF39a/mW5syVtXHUYstaIYCrEVcOb0yfR6OkVpqb1xUE5p3rCXT3OF8YwcgoIDCS5PepNkg==;其中最關鍵的就是要獲得Cookie,只要得到Cookie,就可以利用接口查詢賬單,之前一直沒有找到獲得cookie的方式,現在我們仔細分析一下這個問題,微信賬單頁面是一個h5頁面,而h5頁面在Android中都是通過WebView加載的,而我們在開發中一般都會給WebView設置WebViewClient來監控頁面的加載情況,WebViewClient的onPageFinished是頁面加載完成的回調方法,我們可以在該方法中獲得請求頁面所需要的Cookie信息,這是一種獲得H5頁面的Cookie的通用方法。
經過上面的分析,我們知道可以Hook住微信賬單頁面的WebViewClient的onPageFinished方法,然后通過判斷加載頁面的url是否是微信賬單頁面的url來判斷加載的是否是微信賬單頁面,如果是,則獲得cookie并返回。這里還要解決一個問題就是如何自動打開賬單頁面,只有打開了賬單頁面,才會去加載賬單。具體分析過程就不說明了,這里只貼出代碼:
/*** 啟動微信交易記錄頁面*/public static void startWeiXinBillListActivity() {XposedLogUtils.log("startWeiXinBillListActivity" + weiXinLauncherActivity);if (weiXinLauncherActivity != null) {// 打開賬單頁面callStaticMethod(findClass(VersionParam.weiXinBillListUIFullClassName, mWeiXinClassLoader), VersionParam.weiXinBillListMethodName, weiXinLauncherActivity);}} findAndHookMethod(VersionParam.weiXinWebViewClientFullClassName, classLoader, VersionParam.weiXinWebViewClientLoadFinishMethodName,findClass(VersionParam.weiXinWebViewFullClassName, classLoader), String.class, new XC_MethodHook() {@Overrideprotected void afterHookedMethod(MethodHookParam param) throws Throwable {String url = (String) param.args[1];if (url != null && url.contains("wx.tenpay.com/userroll/readtemplate")) { // 交易記錄urlXposedLogUtils.log("onPageFinished:" + url);String cookie = (String) callMethod(callStaticMethod(findClass(VersionParam.weiXinWebViewGetCookieFullClassName, classLoader),VersionParam.weiXinWebViewGetCookieInstanceMethodName), VersionParam.weiXinWebViewGetCookieMethodName, url);XposedLogUtils.log("cookie:" + cookie);if (cookie != null) {if (param.thisObject != null) {Activity webViewUI = (Activity) getField(param.thisObject, VersionParam.weiXinWebViewObjectFeildName);if (webViewUI != null) {Intent broadCastIntent = new Intent();broadCastIntent.putExtra("cookieStr", cookie);broadCastIntent.setAction(WebSocketService.WEIXIN_BILLLIST_COOKIE_INTENT_FILTER_ACTION);webViewUI.sendBroadcast(broadCastIntent);webViewUI.finish();}}}}}});// 這個是為了確保在賬單頁面加載失敗的時候能夠自動關閉賬單頁面,頁面打開多個賬單頁面,影響性能findAndHookMethod(VersionParam.weiXinWebViewUIFullClassName, classLoader, "b",findClass("com.tencent.mm.plugin.webview.stub.c", classLoader), new XC_MethodHook() {@Overrideprotected void afterHookedMethod(MethodHookParam param) throws Throwable {XposedLogUtils.log("WeiXin WebViewUI b");Activity webViewUI = (Activity) param.thisObject;try {if (webViewUI != null && !webViewUI.isFinishing()) {View refresh_mask = (View) getField(webViewUI, "pYL");if (refresh_mask != null && refresh_mask.getVisibility() == View.VISIBLE) {XposedLogUtils.log("WeiXin WebViewUI refresh_mask visibile");webViewUI.finish();}}} catch (Exception e) {}}});?用的的一些全局靜態變量的定義
public static String WEIXIN_PACKAGE_NAME = "com.tencent.mm";public static String UNIONPAY_PACKAGE_NAME = "com.unionpay";public static String weiXinReturnParamFullClassName = "com.tencent.mm.ae.k";public static String weiXinBillStartRequestMethodName = "biJ";public static String weiXinCreateBillRequestParamFullClassName = "com.tencent.mm.wallet_core.c.h";public static String weiXinCreateBillRequestParamMethodName = "D";public static String ftf_pay_url_field_name = "ljf";public static String ftf_fixed_fee_field_name = "ljg";public static String ftf_fixed_fee_type_field_name = "fpP";public static String ftf_fixed_desc_field_name = "desc";public static String weiXinIsLoginFullClassName = "com.tencent.mm.kernel.a";public static String weiXinIsLoginMethodName = "Dz";public static String weiXinCreateQRCodeUIFullClassName = "com.tencent.mm.plugin.collect.ui.CollectCreateQRCodeUI";public static String weiXinWebViewUIFullClassName = "com.tencent.mm.plugin.webview.ui.tools.WebViewUI";public static String weiXinCreateQRCodeUICallBackMethodName = "d";public static String weiXinLauncherUIFullClassName = "com.tencent.mm.ui.LauncherUI";public static String weiXinBillListUIFullClassName = "com.tencent.mm.plugin.mall.ui.MallIndexBaseUI";public static String weiXinBillListMethodName = "u";public static String weiXinCreateQRCodeRequestParamFullClassName = "com.tencent.mm.plugin.collect.b.s";public static String weiXinWebViewClientFullClassName = "com.tencent.mm.plugin.webview.ui.tools.WebViewUI$i";public static String weiXinWebViewClientLoadFinishMethodName = "a";public static String weiXinWebViewFullClassName = "com.tencent.xweb.WebView";public static String weiXinWebViewGetCookieFullClassName = "com.tencent.xweb.b";public static String weiXinWebViewGetCookieInstanceMethodName = "cIi";public static String weiXinWebViewGetCookieMethodName = "getCookie";public static String weiXinWebViewObjectFeildName = "pZJ";// 微信當前版本中的登錄界面類全名public static String mWeiXinLoginActivityClassFullName = "com.tencent.mm.plugin.account.ui.LoginUI";public static String mWeiXinSQLiteDatabaseClassFullName = "com.tencent.wcdb.database.SQLiteDatabase";public static String mWeiXinSQLiteDatabaseInsertMethodName = "insertWithOnConflict";由于微信的WebView和WebViewClient都是自定義的,所有在onPageFinished方法中獲得Cookie是通過調用微信提供的方法獲得的,一般情況下,我們要在onPageFinished中獲得Cookie,只需要執行如下代碼即可:
CookieManager cookieManager = CookieManager.getInstance();String cookieStr = cookieManager.getCookie(url);獲得Cookie后,我們就可以將Cookie發送給服務端,服務端通過接口來請求微信賬單數據了,這個Cookie是有失效時長的,如果服務端發現Cookie過期,則重新向App請求Cookie,App會再次打開微信賬單頁面,刷新Cookie并返回給服務端。
四、總結
? ? ? 需要總結的不多,基本都是https://blog.csdn.net/xiao_nian/article/details/79881274里面使用的技巧,不得不說微信的混淆還是做得很好的,代碼里面基本都是adcd之類的。
?
嚴重聲明
本文的意圖只有一個就是通過分析app學習更多的逆向技術,如果有人利用本文知識和技術進行非法操作進行牟利,帶來的任何法律責任都將由操作者本人承擔,和本文作者無任何關系,最終還是希望大家能夠秉著學習的心態閱讀此文。
總結
以上是生活随笔為你收集整理的利用xposed自动获得微信个人收款二维码链接和收款记录的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: matlab傅里叶级数展开
- 下一篇: (手机拍照)4、修图软件