闲鱼自动发货机器人 使用以下代码Hook后,可以抓HTTP请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 try { findAndHookMethod("mtopsdk.mtop.global.SwitchConfig" , classLoader, "isGlobalSpdySwitchOpen" , new XC_MethodReplacement() { @Override protected Object replaceHookedMethod (MethodHookParam methodHookParam) throws Throwable { return false ; } }); }catch (Exception e){ log(Log.getStackTraceString(e)); }try { findAndHookMethod("mtopsdk.mtop.global.SwitchConfig" , classLoader, "isGlobalSpdySslSwitchOpen" , new XC_MethodReplacement() { @Override protected Object replaceHookedMethod (MethodHookParam methodHookParam) throws Throwable { return false ; } }); }catch (Exception e){ log(Log.getStackTraceString(e)); }
其中mtop.taobao.idlemessage.message.send/1.0/可以发送消息,mtop.taobao.idlemessage.region.sync/2.0/可以接收消息。使用这两个AP就可以实现消息的接收与回复。
但需要对请求签名,使用Xposed编写模块提供HTTP服务,调用App中的签名函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 public final HashMap getUnifiedSign (HashMap params, HashMap extendParas, String appKey, String authCode, boolean useWua, String requestId) { if (appKey == null ) { params.put("SG_ERROR_CODE" , "AppKey is null" ); TBSdkLog.e("mtopsdk.InnerSignImpl" , " [getUnifiedSign] AppKey is null." ); return null ; } if (params == null ) { TBSdkLog.e("mtopsdk.InnerSignImpl" , " [getUnifiedSign] params is null.appKey=" + appKey); return null ; } if (this .mUnifiedSign == null ) { params.put("SG_ERROR_CODE" , "unified is null" ); TBSdkLog.e("mtopsdk.InnerSignImpl" , " [getUnifiedSign]sg unified sign is null, please call ISign init()" ); return null ; } try { HashMap hashMap2 = new HashMap(); String s3 = (String)InnerSignImpl.convertInnerBaseStrMap(appKey, params, true ).get("INPUT" ); boolean z1 = StringUtils.isBlank(s3); if (z1) { TBSdkLog.e("mtopsdk.InnerSignImpl" , " [getUnifiedSign]get sign failed with sign data empty " , "appKeyIndex=" + this .mtopConfig.appKeyIndex + ",authCode=" + this .mtopConfig.authCode); return null ; } hashMap2.put("appkey" , appKey); hashMap2.put("data" , s3); hashMap2.put("useWua" , Boolean.valueOf(((boolean )(((int )useWua))))); hashMap2.put("env" , Integer.valueOf(this .getMiddleTierEnv())); hashMap2.put("authCode" , authCode); hashMap2.put("extendParas" , extendParas); hashMap2.put("requestId" , requestId); hashMap2.put("api" , params.get("api" )); hashMap2.put("mtopBusiness" , params.get("mtopBusiness" )); HashMap hashMap3 = this .mUnifiedSign.getSecurityFactors(hashMap2); if (hashMap3 != null && !hashMap3.isEmpty()) { return hashMap3; } TBSdkLog.e("mtopsdk.InnerSignImpl" , " [getUnifiedSign]get sign failed with no output " , "appKeyIndex=" + this .mtopConfig.appKeyIndex + ",authCode=" + this .mtopConfig.authCode); } catch (SecException secException0) { TBSdkLog.e("mtopsdk.InnerSignImpl" , " [getUnifiedSign]get sign failed and SecException errorCode " + secException0.getErrorCode() + ",appKeyIndex=" + this .mtopConfig.appKeyIndex + ",authCode=" + this .mtopConfig.authCode, secException0); } catch (Throwable throwable0) { TBSdkLog.e("mtopsdk.InnerSignImpl" , " [getUnifiedSign]get sign failed exception ,appKeyIndex=" + this .mtopConfig.appKeyIndex + ",authCode=" + this .mtopConfig.authCode, throwable0); } return null ; }
至此机器人也算可以使用了。但收消息依赖轮询,即不停的访问读取消息的接口。若请求较慢,消息的延迟会比较高,若请求太快,可能对闲鱼服务器造成压力,甚至可能被封号。
于是尝试寻找App中接收消息的位置,以实现被动接收消息的目的。
闲鱼的IM核心都是flutter写的,代码都在libapp.so中,即使使用了blutter ,也难以逆向。
我尝试hook java.lang.String.toString(),匹配我发送的内容,并打印堆栈,发现了flutter层调用数据库读写函数。
java.lang.Exception at java.lang.String.toString(Native Method) at android.database.sqlite.SQLiteConnection.bindArguments(Unknown Source:66) at android.database.sqlite.SQLiteConnection.executeForChangedRowCount(Unknown Source:20) at android.database.sqlite.SQLiteSession.executeForChangedRowCount(Unknown Source:15) at android.database.sqlite.SQLiteStatement.executeUpdateDelete(Unknown Source:20) at android.database.sqlite.SQLiteDatabase.executeSql(Unknown Source:44) at android.database.sqlite.SQLiteDatabase.execSQL(Unknown Source:2) at com.tekartik.sqflite.SqflitePlugin.executeOrError(SqflitePlugin.java:36) at com.tekartik.sqflite.SqflitePlugin.access$300(SqflitePlugin.java:4) at com.tekartik.sqflite.SqflitePlugin$4.run(SqflitePlugin.java:14) at android.os.Handler.handleCallback(Unknown Source:2) at android.os.Handler.dispatchMessage(Unknown Source:4) at android.os.Looper.loop(Unknown Source:251) at android.os.HandlerThread.run(Unknown Source:28)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 private void onInsertCall (MethodCall methodCall0, Result methodChannel$Result0, boolean z) { Database database0 = SqflitePlugin.getDatabaseOrError(methodCall0, methodChannel$Result0); if (database0 == null ) { return ; } BgResult sqflitePlugin$BgResult0 = new BgResult(methodChannel$Result0); if (z) { com.tekartik.sqflite.SqflitePlugin.3 sqflitePlugin$30 = new Runnable() { @Override public final void run () { MethodCallOperation methodCallOperation0 = new MethodCallOperation(this .val$call, this ); SqflitePlugin.access$300 (methodCall0, sqflitePlugin$BgResult0, methodCallOperation0); } }; this .mExecutors.execute(sqflitePlugin$30 ); return ; } SqflitePlugin.handler.post(new Runnable() { @Override public final void run () { MethodCallOperation methodCallOperation0 = new MethodCallOperation(this .val$call, this ); SqflitePlugin.access$300 (methodCall0, sqflitePlugin$BgResult0, methodCallOperation0); } }); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 package io.flutter.plugin.common;import androidx.annotation.NonNull;import androidx.annotation.Nullable;import java.util.Map;import org.json.JSONObject;public final class MethodCall { public final Object arguments; public final String method; public MethodCall (@NonNull String s, @Nullable Object object0) { this .method = s; this .arguments = object0; } @Nullable public Object argument (@NonNull String s) { Object object0 = this .arguments; if (object0 == null ) { return null ; } if ((object0 instanceof Map)) { return ((Map)object0).get(s); } if ((object0 instanceof JSONObject)) { return ((JSONObject)object0).opt(s); } throw new ClassCastException(); } @Nullable public Object arguments () { return this .arguments; } public boolean hasArgument (@NonNull String s) { Object object0 = this .arguments; if (object0 == null ) { return false ; } if ((object0 instanceof Map)) { return ((Map)object0).containsKey(s); } if ((object0 instanceof JSONObject)) { return ((JSONObject)object0).has(s); } throw new ClassCastException(); } }
最后Hook数据库写入函数,并转发消息到http服务的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 XposedHelpers.findAndHookMethod("com.tekartik.sqflite.SqflitePlugin" , classLoader, "onInsertCall" , "io.flutter.plugin.common.MethodCall" ,"io.flutter.plugin.common.MethodChannel$Result" ,boolean .class, new XC_MethodHook() { @Override protected void beforeHookedMethod (MethodHookParam param) throws Throwable { super .beforeHookedMethod(param); Object methodCall0 = param.args[0 ]; String sql = (String) callMethod(methodCall0, "argument" , "sql" ); if (is_open && url!=null && url.contains("http" ) && sql.contains("INSERT OR REPLACE INTO SessionInfo" )){ Object arguments = getObjectField(methodCall0, "arguments" ); String json_str = (String) callStaticMethod(JSON, "toJSONString" , arguments); log(json_str); String regex = "sid=\\d+&messageId=(\\w+)&" ; Pattern pattern = Pattern.compile(regex); Matcher matcher = pattern.matcher(json_str); if (matcher.find()){ String messageId = matcher.group(1 ); if (cache.get(messageId) == null ){ MyHttpClient.sendPostRequest(url, json_str); cache.put(messageId, 1 ); } } } } });