微信吊起app適配
0x1 背景
最近微信的一些變化導致從微信內調起app開始水土不服,過去我們可以通過scheme直接吊起三方app,現在不行了。通過介面調整後,每次吊起app都會新建一個棧,導致選單頁面出現多個相同app記錄,這是我們不希望看到的。
0x2 現狀分析
啟動一個Activity可以設定launchMode和FLAG,新版微信推測在吊起三方app是走的是類似於分享的處理,直接吊起了WXEntryActivity,並且給他添加了類似NEW_TASK, MULTI_TASK的flag。
判斷一個App當前有多少個Task,可以通過dumpsys檢視,清楚明瞭:
aven-mac-pro-2:~ aven$ adb shell dumpsys activity activities|grep TaskRecord|grep com.wuba|grep \* * TaskRecord{9fa5181 #5400 A=com.wuba U=0 StackId=1 sz=1}
也可以新增日誌觀察app的棧情況,比如可以統一新增activity啟動的日誌,輸出當前taskid
LOGGER.i("ActivityTrack", activity.getClass().getSimpleName() + " create, task id=" + activity.getTaskId());
多跑幾遍吊起流程,會發現id一致在變化中
WXEntryActivity create, task id=5438
0x3 解決方案
為了規避這個問題,需要處理管理activity棧,確保落地頁Activity所在的棧不會每次吊起都新建一個獨立的。
0x3.1 銷燬棧
可以在WXEntryActivity吊起落地頁的時候,將當前棧清空並退出,以便後續Activity能夠進入預設棧。
finishAndRemoveTask()
這個辦法有相容性問題,因為該API是21字後引入的。
0x3.2 指定affinity
第二個辦法,給WXEntryActivity配置一個獨立的affinity,如此,其他所有頁面都是預設值便能和WXEntryActivity進入不同棧。
<activity android:name="${applicationId}.wxapi.WXEntryActivity" android:configChanges="keyboardHidden|locale" android:exported="true" android:screenOrientation="portrait" android:taskAffinity="com.wuba.share" android:theme="@style/Theme.Translucent" />
0x4 採坑
0x4.1 正式包和Debug包
上面兩張方案在測試的時候會遇到一個問題,從微信內吊起三方app,如果首次失敗,那麼後續一致會失敗,原因是測試app的簽名不一致。 要解決,只能清除微信資料,解除安裝重灌微信。並且安裝release的app
0x4.2 測試流程複雜,不可除錯
這個問題其實和上一個本質相同,我們在得出上述解決方案之前,經歷了大量測試驗證,每次都需要釋出簽名是在太麻煩,筆者開發匯中即使改了一行程式碼需要重走流程到測試觀察日誌需要20分鐘以上。
為了解決測試耗時和不能除錯的問題,我們需要模擬微信吊起app。
0x5 模擬微信吊起app
通過dump當前的應用狀態,我們可以知道微信吊起確實是喚起了WXEntryActivity,清切intent攜帶了一些資料。 我們需要確定傳遞了什麼資料
LOGGER.d("WeChat", getIntent().getExtras().toString());
有了這些資料,我們便可以模擬吊起Activity,來規避微信錯誤提示。
0x5.1 ADB調起WXEntryActivity
嘗試通過adb啟動Activity,並逐一新增引數
adb shell am start-activity -n com.wuba/com.wuba.wxapi.WXEntryActivity --es _wxappextendobject_extInfo xxxx
很快頁面吊起來了,但是並沒有直達落地頁,而是停留在入口Activity上。 直覺反應肯定是引數沒有通過校驗,仔細核查所以引數,發現可以欄位_mmessage_checksum
從命名上可以推測他是某種訊息摘要,用於校驗傳遞的資料,防止篡改。
0x5.2 引數分析
如果這個摘要有時間戳相關維度判斷,那估計就沒戲了,根據關鍵字,我們可以查一下微信sdk內部校驗邏輯,關鍵程式碼如下:
String var3 = var1.getStringExtra("_mmessage_content"); int var4 = var1.getIntExtra("_mmessage_sdkVersion", 0); String var5; if ((var5 = var1.getStringExtra("_mmessage_appPackage")) == null || var5.length() == 0) { Log.e("MicroMsg.SDK.WXApiImplV10", "invalid argument"); return false; } byte[] var6 = var1.getByteArrayExtra("_mmessage_checksum"); byte[] var14 = b.a(var3, var4, var5); if (!this.checkSumConsistent(var6, var14)) { Log.e("MicroMsg.SDK.WXApiImplV10", "checksum fail"); return false; }
public final class b { public static final String e(byte[] var0) { char[] var1 = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; try { MessageDigest var2; (var2 = MessageDigest.getInstance("MD5")).update(var0); int var8; char[] var3 = new char[(var8 = (var0 = var2.digest()).length) * 2]; int var4 = 0; for(int var5 = 0; var5 < var8; ++var5) { byte var6 = var0[var5]; var3[var4++] = var1[var6 >>> 4 & 15]; var3[var4++] = var1[var6 & 15]; } return new String(var3); } catch (Exception var7) { return null; } } } private boolean checkSumConsistent(byte[] var1, byte[] var2) { if (var1 != null && var1.length != 0 && var2 != null && var2.length != 0) { if (var1.length != var2.length) { Log.e("MicroMsg.SDK.WXApiImplV10", "checkSumConsistent fail, length is different"); return false; } else { for(int var3 = 0; var3 < var1.length; ++var3) { if (var1[var3] != var2[var3]) { return false; } } return true; } } else { Log.e("MicroMsg.SDK.WXApiImplV10", "checkSumConsistent fail, invalid arguments"); return false; } }
分析上述程式碼,可知訊息校驗沒有時間戳,只是一個MD5的HEX字串判斷。
同時,結合日誌資訊,發現幾處問題:
- 引數型別錯誤
- byte[]陣列問題
02-13 17:39:09.888 12938 12938 D MicroMsg.SDK.MMessage: send mm message, intent=Intent { act=com.tencent.mm.plugin.openapi.Intent.ACTION_HANDLE_APP_REGISTER (has extras) }, perm=com.tencent.mm.permission.MM_MESSAGE 02-13 17:39:09.888 12938 12938 W Bundle: Key _mmessage_sdkVersion expected Integer but value was a java.lang.String.The default value 0 was returned. 02-13 17:39:09.889 12938 12938 W Bundle: Attempt to cast generated internal exception: 02-13 17:39:09.889 12938 12938 W Bundle: java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer 02-13 17:39:09.889 12938 12938 W Bundle: at android.os.BaseBundle.getInt(BaseBundle.java:1045) 02-13 17:39:09.889 12938 12938 W Bundle: at android.content.Intent.getIntExtra(Intent.java:7398) 02-13 17:39:09.889 12938 12938 W Bundle: at com.tencent.mm.opensdk.openapi.WXApiImplV10.handleIntent(Unknown Source:73) 02-13 17:39:09.889 12938 12938 W Bundle: at com.wuba.loginsdk.wxapi.WXCallbackEntryActivity.onCreate(WXCallbackEntryActivity.java:81) 02-13 17:39:09.889 12938 12938 W Bundle: at com.wuba.wxapi.WXEntryActivity.onCreate(WXEntryActivity.java:39) 02-13 17:39:09.889 12938 12938 W Bundle: at android.app.Activity.performCreate(Activity.java:7210) 02-13 17:39:09.889 12938 12938 W Bundle: at android.app.Activity.performCreate(Activity.java:7201) 02-13 17:39:09.889 12938 12938 W Bundle: at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1272) 02-13 17:39:09.889 12938 12938 W Bundle: at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2926) 02-13 17:39:09.889 12938 12938 W Bundle: at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3081) 02-13 17:39:09.889 12938 12938 W Bundle: at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78) 02-13 17:39:09.889 12938 12938 W Bundle: at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108) 02-13 17:39:09.889 12938 12938 W Bundle: at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68) 02-13 17:39:09.889 12938 12938 W Bundle: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1831) 02-13 17:39:09.889 12938 12938 W Bundle: at android.os.Handler.dispatchMessage(Handler.java:106) 02-13 17:39:09.889 12938 12938 W Bundle: at android.os.Looper.loop(Looper.java:201) 02-13 17:39:09.889 12938 12938 W Bundle: at android.app.ActivityThread.main(ActivityThread.java:6806) 02-13 17:39:09.889 12938 12938 W Bundle: at java.lang.reflect.Method.invoke(Native Method) 02-13 17:39:09.889 12938 12938 W Bundle: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547) 02-13 17:39:09.889 12938 12938 W Bundle: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:873) 02-13 17:39:09.889 12938 12938 W Bundle: Key _mmessage_checksum expected byte[] but value was a java.lang.String.The default value <null> was returned. 02-13 17:39:09.889 12938 12964 I ContentCatcher: Interceptor : Catcher list invalid for [email protected]@246059734 02-13 17:39:09.889 12938 12964 I ContentCatcher: Interceptor : Get featureInfo from config pick_mode 02-13 17:39:09.889 12938 12938 W Bundle: Attempt to cast generated internal exception: 02-13 17:39:09.889 12938 12938 W Bundle: java.lang.ClassCastException: java.lang.String cannot be cast to byte[] 02-13 17:39:09.889 12938 12938 W Bundle: at android.os.BaseBundle.getByteArray(BaseBundle.java:1357) 02-13 17:39:09.889 12938 12938 W Bundle: at android.os.Bundle.getByteArray(Bundle.java:1090) 02-13 17:39:09.889 12938 12938 W Bundle: at android.content.Intent.getByteArrayExtra(Intent.java:7607) 02-13 17:39:09.889 12938 12938 W Bundle: at com.tencent.mm.opensdk.openapi.WXApiImplV10.handleIntent(Unknown Source:105) 02-13 17:39:09.889 12938 12938 W Bundle: at com.wuba.loginsdk.wxapi.WXCallbackEntryActivity.onCreate(WXCallbackEntryActivity.java:81) 02-13 17:39:09.889 12938 12938 W Bundle: at com.wuba.wxapi.WXEntryActivity.onCreate(WXEntryActivity.java:39) 02-13 17:39:09.889 12938 12938 W Bundle: at android.app.Activity.performCreate(Activity.java:7210) 02-13 17:39:09.889 12938 12938 W Bundle: at android.app.Activity.performCreate(Activity.java:7201) 02-13 17:39:09.889 12938 12938 W Bundle: at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1272) 02-13 17:39:09.889 12938 12938 W Bundle: at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2926) 02-13 17:39:09.889 12938 12938 W Bundle: at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3081) 02-13 17:39:09.889 12938 12938 W Bundle: at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78) 02-13 17:39:09.889 12938 12938 W Bundle: at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108) 02-13 17:39:09.889 12938 12938 W Bundle: at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68) 02-13 17:39:09.889 12938 12938 W Bundle: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1831) 02-13 17:39:09.889 12938 12938 W Bundle: at android.os.Handler.dispatchMessage(Handler.java:106) 02-13 17:39:09.889 12938 12938 W Bundle: at android.os.Looper.loop(Looper.java:201) 02-13 17:39:09.889 12938 12938 W Bundle: at android.app.ActivityThread.main(ActivityThread.java:6806) 02-13 17:39:09.889 12938 12938 W Bundle: at java.lang.reflect.Method.invoke(Native Method) 02-13 17:39:09.889 12938 12938 W Bundle: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547) 02-13 17:39:09.889 12938 12938 W Bundle: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:873) 02-13 17:39:09.889 12938 12938 E MicroMsg.SDK.WXApiImplV10: checkSumConsistent fail, invalid arguments
由於adb不支援所以引數,比如序列化物件,位元組陣列等,因此通過adb啟動並不可行
0x5.3 App調起WXEntryActivity
既然adb有限制,就換成一個測試app來吊起入口類。
public void onClickOpen(View view) { Intent intent = new Intent(); intent.setClassName("com.wuba", "com.wuba.wxapi.WXEntryActivity"); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK); intent.putExtra("_wxappextendobject_extInfo", "wbmain://jump/core/common?params=%7B%22url%22%3A%22https%3A%2F%2Fdown.58.com%2Fh5%2F58person.html%22%2C%22title%22%3A%2258%E4%BA%BA%E7%89%A9%22%2C%22pagetype%22%3A%22common%22%7D"); intent.putExtra("_wxapi_basereq_transaction", "f3ac1733e4b2154d97dc3996bff55287"); intent.putExtra("_wxobject_sdkVer", 620953856); intent.putExtra("_mmessage_appPackage", "com.tencent.mm"); intent.putExtra("_wxobject_message_ext", "wbmain://jump/core/common?params=%7B%22url%22%3A%22https%3A%2F%2Fdown.58.com%2Fh5%2F58person.html%22%2C%22title%22%3A%2258%E4%BA%BA%E7%89%A9%22%2C%22pagetype%22%3A%22common%22%7D"); intent.putExtra("_wxapi_command_type", 4); intent.putExtra("_wxapi_basereq_openid", ""); intent.putExtra("_mmessage_checksum", "1c84447a3580a34a094a9aa00820631a".getBytes()); intent.putExtra("wx_token_key", "com.tencent.mm.openapi.token"); intent.putExtra("_mmessage_sdkVersion", 620953856); intent.putExtra("_wxapi_showmessage_req_country", "CN"); intent.putExtra("_wxobject_identifier_", "com.tencent.mm.sdk.openapi.WXAppExtendObject"); intent.putExtra("_wxapi_showmessage_req_lang", "zh_CN"); intent.putExtra("platformId", "wechat"); startActivity(intent); }
至此,我們可以繞開微信對app簽名的限制,通過一個測試app,無限制吊起目標app,而資料則全部源自正是呼叫的樣本。可以很方便的對app做除錯。