android apk 微信登入_Flutter Notes Android 借壳分享微信
第91次推文LZ-Says每个生命体的存在,其实本质都是一个复杂的过程。很多时候,无需追求完美的理想情况,毕竟,You are just you。免责声明为了避免收费的小哥哥干我,或者出现其它不好的情况,这里特意注明下:本文如同标题一样,只属于个人笔记,仅限技术分享~ 如出现其他情况,一概与本人无关~本文如同标题一样,只属于个人笔记,仅限技术分享~ 如出现其他情况,一概与本人无关~本文如...
第
91
次推文
LZ-Says
每个生命体的存在,其实本质都是一个复杂的过程。很多时候,无需追求完美的理想情况,毕竟,You are just you。

免责声明
为了避免收费的小哥哥干我,或者出现其它不好的情况,这里特意注明下:
本文如同标题一样,只属于个人笔记,仅限技术分享~ 如出现其他情况,一概与本人无关~
本文如同标题一样,只属于个人笔记,仅限技术分享~ 如出现其他情况,一概与本人无关~
本文如同标题一样,只属于个人笔记,仅限技术分享~ 如出现其他情况,一概与本人无关~
前言
前段时间,公司突然来一需求:
-
调研某款 App Android 版微信分享来源动态原理以及实现方式
第一时间,当然是看看网上有没有前辈开源,借鉴(CV 大法)一波。
查询结果真的是悲喜交加:
-
开森的是,有人研究过这个东西,也封装好了对应的 SDK。
-
悲剧的是收费,目前已了解的情况最低 100。
对于本身在帝都讨生活的落魄小 Android 而言,无疑是一笔巨款 (手动滑稽~勿喷~)。
都说穷人家的孩子早当家,不得已开始了逆向、分析之路 ???
相关代码已上传 GitHub,当然为了不给自己找事儿,本地命中库就不提供了,自己逆向去拿吧,地址如下:
-
https://github.com/HLQ-Struggle/share_wechat
效果图
空谈无用,来个实际效果图最棒,这里就以我梦想殿堂 App 为例进行测试咯。

准备工具
基于个人了解简单概述:
-
ApkTools:一般就是为了改包、回包,捎带脚拿个资源文件。
-
ClassyShark:一款贼方便分析 Apk 工具,一般用于看看大厂都玩啥。
-
dex2jar:将 .dex 文件转换为 .class 文件。
-
JD-GUI:主要是查看反编译后的源代码。
下面附上相关工具网盘链接:
-
链接:pan.baidu.com/s/1Ll5cTqMu… 密码:20fl
实战开搞
在正式开始前,先来见识下 ClassyShark 这个神器吧。
一、Hi,ClassyShark
首先进入你下载好的 ClassyShark.jar 目录中,随后执行如下命令即可:
java -jar ClassyShark.jar
示意图如下:

随后在打开的可视化工具中将想看的 Apk 直接拖进去即可:

拖进去之后点击包名,会有一个对当前 Apk 的简单概述:

点击 Methods count 可以查看当前 Apk 方法数:

当然你可以继续往下一层级查看,比如我点击 bilibili:

同样也可以导出文件,这里不作为本文重点阐述了,有兴趣的可以自己研究~
二、逆向分析走起
首先,网上下载目标 App,并将后缀名修改为 zip,随后解压进入该目录:

手动进入已下载完成的 dex-tools-2.1-SNAPSHOT 目录中,执行如下命令:
sh d2j-dex2jar.sh [目标 dex 文件地址]
例如:

完成之后,将会在 dex-tools-2.1-SNAPSHOT 目录中生成 classes-dex2jar.jar 文件,这里文件就是我们接下来逆向分析的靠山呐。
随后将生成的 jar 文件拖入 JD-GUI 中。

查看 AndroidManifest 获取到当前应用包名,有助于我们一步到位~
由于目标 App 是在文章的详情页中提供分享微信消息回话以及朋友圈,详情一般个人命名为 XxxDetailsActivity,根据这个思路去搜索。

有些尴尬啊,怎么搜索到了腾讯的 SDK 呢?
还是手动人工查找吧,???

在这块发现个比较有意思的东西,可能是我比较 low 吧。一般而言,我们都知道混淆实体类是肯定不能被混淆的,不然就会出现找不到的情况。那么奇怪了,昨天逆向 B 站 Apk,我竟然没发现实体类,难道他们的实体类有其他神操作?还是说分包太多我没找到?
终于找到你,文章详情页!!!

操作 App,发现是点击按钮弹出底部分享对话框,原版如下:

随后继续在代码中查看,果然:

这个就很好理解了,自定义一个底部对话框,点击传递分享的 Url 以及分享类型。现在我们去 ShareArticleDialog 这个类中验证一下猜想是否正确?

看,0 应该是代表分享微信消息会话,1 代表分享朋友圈。
经过一番排查,发现最终是通过调用如下方法进行分享微信:
public static int send(Context paramContext, String paramString1, String paramString2, String paramString3, Bundle paramBundle) { CURRENT_SHARE_CLIENT = null; if (paramContext == null || paramString1 == null || paramString1.length() == 0 || paramString2 == null || paramString2.length() == 0) { Log.w("MMessageAct", "send fail, invalid arguments"); return -1; } Intent intent = new Intent(); intent.setClassName(paramString1, paramString2); if (paramBundle != null) intent.putExtras(paramBundle); intent.putExtra("_mmessage_sdkVersion", 603979778); int i = getPackageSign(paramContext); if (i == -1) return -1; CURRENT_SHARE_CLIENT = shareClient.get(i); intent.putExtra("_mmessage_appPackage", "这里换成要借壳 App 包名"); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("weixin://sendreq?appid="); stringBuilder.append("这里换成要借壳 AppId"); intent.putExtra("_mmessage_content", stringBuilder.toString()); intent.putExtra("_mmessage_checksum", MMessageUtil.signatures(paramString3, paramContext.getPackageName())); intent.addFlags(268435456).addFlags(134217728); try { paramContext.startActivity(intent); StringBuilder stringBuilder1 = new StringBuilder(); this(); stringBuilder1.append("send mm message, intent="); stringBuilder1.append(intent); Log.d("MMessageAct", stringBuilder1.toString()); return i; } catch (Exception exception) { exception.printStackTrace(); Log.d("MMessageAct", "send fail, target ActivityNotFound"); return -1; } }
在查看微信 SDK 中也发现类似代码,由于掘金这个上传图片宽高我现在还不会调整,暂时防止目录位置,感兴趣的小伙伴自行查看:

其它细节就不一一分析了,直接上代码咯~
三、附上代码~
其实本质借壳分享,个人的理解如下:
-
第一步:绕过微信检测,例如包名、签名是否和微信开放平台绑定一致;
-
第二部:组装参数,直接直击深处,分享微信。
由于此次是 Flutter 项目,不得不的面对的是与原生 Android 的交互。由于我是刚刚入坑 Flutter 几周,内心真的是忐忑不安。
不过值得让人赞叹的是,Flutter 的生态,真的贼棒!尤其我鸡老大,神一般存在!默默的感谢我大哥~!
0. 简单聊下 Flutter 与交互
在 Flutter 中文社区中官网对此有这样的一段描述:
Flutter 使用了灵活的系统,它允许你调用相关平台的 API,无论是 Android 中的 Java 或 Kotlin 代码,还是 iOS 中的 Objective-C 或 Swift 代码。
Flutter 内置的平台特定 API 支持不依赖于任何生成代码,而是灵活的依赖于传递消息格式。或者,你也可以使用 Pigeon 这个
package,通过生成代码来发送结构化类型安全消息。
Google
应用程序中的 Flutter 部分通过平台通道向其宿主(应用程序中的 iOS 或 Android 部分)发送消息。
宿主监听平台通道并接收消息。然后,它使用原生编程语言来调用任意数量的相关平台 API,并将响应发送回客户端(即应用程序中的 Flutter 部分)。
Google
也就是说,Flutter 充分给予我们调用原生 Api 的权利,关键桥梁便是这个通道消息。
下面一起来看下官方的图:

消息和响应以异步的形式进行传递,以确保用户界面能够保持响应。
客户端做方法调用的时候 MethodChannel 会负责响应,从平台一侧来讲,Android 系统上使用 MethodChannelAndroid、 iOS 系统使用 MethodChanneliOS 来接收和返回来自 MethodChannel 的方法调用。
Google
1. 引入三方库
api 'com.tencent.mm.opensdk:wechat-sdk-android-without-mta:+'// 主要用于将分享的在线图片转换为 Bitmapimplementation 'com.github.bumptech.glide:glide:4.11.0'annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'implementation 'com.google.code.gson:gson:2.8.6'
2. 完善混淆文件
# 保护我方输出(保护实体类不被混淆)-keep public class com.Your Package Name.bean.**{*;}# Gson-keepattributes Signature# Gson specific classes-keep class sun.misc.Unsafe { *; }-keep class com.google.gson.** { *; }# Application classes that will be serialized/deserialized over Gson-keep class com.google.gson.examples.android.model.** { *; }
3. 编写原生 Android 工具类
这里具体还是需要结合实际项目需求而定,不过通用型的一些东西必须要有:
动态检测宿主,也可以理解为动态检测借壳目标是否存在;
而剩下的则是分享微信了,这里简单放置关键代码,详情可点击文章开始的 GitHub 地址。
package com.hlq.struggle.utilsimport android.content.Contextimport android.content.Intentimport android.graphics.Bitmapimport android.os.Bundleimport com.bumptech.glide.Glideimport com.bumptech.glide.load.DataSourceimport com.bumptech.glide.load.engine.GlideExceptionimport com.bumptech.glide.request.RequestListenerimport com.bumptech.glide.request.target.Targetimport com.google.gson.Gsonimport com.google.gson.reflect.TypeTokenimport com.hlq.struggle.app.appInfoJsonimport com.hlq.struggle.bean.AppInfoBeanimport com.tencent.mm.opensdk.modelmsg.SendMessageToWXimport com.tencent.mm.opensdk.modelmsg.WXMediaMessageimport com.tencent.mm.opensdk.modelmsg.WXMediaMessage.IMediaObjectimport com.tencent.mm.opensdk.modelmsg.WXWebpageObjectimport java.io.ByteArrayOutputStreamimport java.io.IOException/** * @author:HLQ_Struggle * @date:2020/6/27 * @desc: */@Suppress("SpellCheckingInspection")class ShareWeChatUtils { companion object { /** * 解析本地缓存 App 信息 */ private fun getLocalAppCache(): ArrayList { return Gson().fromJson( appInfoJson, object : TypeToken>() {}.type ) } /** * 检测用户设备安装 App 信息 */ fun checkAppInstalled(context: Context): Int { var tempCount = -1 // 获取本地宿主 App 信息 val appInfoList = getLocalAppCache() // 获取用户设备已安装 App 信息 val packageManager = context.packageManager val installPackageList = packageManager.getInstalledPackages(0) if (installPackageList.isEmpty()) { return 0 } for (packageInfo in installPackageList) { for (appInfo in appInfoList) { if (packageInfo.packageName == appInfo.packageName) { tempCount++ } } } return tempCount } /** * 命中已安装 App */ private fun hitInstalledApp(context: Context): AppInfoBean? { // 获取本地宿主 App 信息 val appInfoList = getLocalAppCache() // 获取用户设备已安装 App 信息 val packageManager = context.packageManager // 能进入方法说明本地已存在命中 App,使用时还需要预防 val installPackageList = packageManager.getInstalledPackages(0) for (packageInfo in installPackageList) { for (appInfo in appInfoList) { if (packageInfo.packageName == appInfo.packageName) { return appInfo } } } return null } /** * 分享微信 */ fun shareWeChat( context: Context, shareType: Int, url: String, title: String, text: String, paramString4: String?, umId: String? ) { Glide.with(context).asBitmap().load(paramString4) .listener(object : RequestListener { override fun onLoadFailed( param1GlideException: GlideException?, param1Object: Any, param1Target: Target, param1Boolean: Boolean ): Boolean { LogUtils.logE(" ---> Load Image Failed") return false } override fun onResourceReady( param1Bitmap: Bitmap?, param1Object: Any, param1Target: Target, param1DataSource: DataSource, param1Boolean: Boolean ): Boolean { LogUtils.logE(" ---> Load Image Ready") val i = send( context, shareType, url, title, text, param1Bitmap ) val stringBuilder = StringBuilder() stringBuilder.append("send index: ") stringBuilder.append(i) LogUtils.logE(" ---> Ready stringBuilder.toString() :$stringBuilder") return false } }).preload(200, 200) } private fun send( paramContext: Context, paramInt: Int, paramString1: String, paramString2: String, paramString3: String, paramBitmap: Bitmap? ): Int { val stringBuilder = StringBuilder() stringBuilder.append("share url: ") stringBuilder.append(paramString1) LogUtils.logE(" ---> send :$stringBuilder") val wXWebpageObject = WXWebpageObject() wXWebpageObject.webpageUrl = paramString1 val wXMediaMessage = WXMediaMessage(wXWebpageObject as IMediaObject) wXMediaMessage.title = paramString2 wXMediaMessage.description = paramString3 wXMediaMessage.thumbData = bmpToByteArray( paramContext, Bitmap.createScaledBitmap(paramBitmap!!, 150, 150, true), true ) val req = SendMessageToWX.Req() req.transaction = buildTransaction( "webpage" ) req.message = wXMediaMessage req.scene = paramInt val bundle = Bundle() req.toBundle(bundle) return sendToWx( paramContext, "weixin://sendreq?appid=wxd930ea5d5a258f4f", bundle ) } private fun buildTransaction(paramString: String): String { var paramString: String? = paramString paramString = if (paramString == null) { System.currentTimeMillis().toString() } else { val stringBuilder = StringBuilder() stringBuilder.append(paramString) stringBuilder.append(System.currentTimeMillis()) stringBuilder.toString() } return paramString } private fun bmpToByteArray( paramContext: Context?, paramBitmap: Bitmap, paramBoolean: Boolean ): ByteArray? { val byteArrayOutputStream = ByteArrayOutputStream() try { paramBitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream) if (paramBoolean) paramBitmap.recycle() val arrayOfByte = byteArrayOutputStream.toByteArray() byteArrayOutputStream.close() return arrayOfByte } catch (iOException: IOException) { iOException.printStackTrace() } return null } private fun sendToWx( paramContext: Context?, paramString: String?, paramBundle: Bundle? ): Int { return send( paramContext, "com.tencent.mm", "com.tencent.mm.plugin.base.stub.WXEntryActivity", paramString, paramBundle ) } private fun send( paramContext: Context?, packageName: String?, className: String?, paramString3: String?, paramBundle: Bundle? ): Int { if (paramContext == null || packageName == null || packageName.isEmpty() || className == null || className.isEmpty()) { LogUtils.logE(" ---> send fail, invalid arguments") return -1 } val appInfoBean = hitInstalledApp(paramContext) val intent = Intent() intent.setClassName(packageName, className) if (paramBundle != null) intent.putExtras(paramBundle) intent.putExtra("_mmessage_sdkVersion", 603979778) intent.putExtra("_mmessage_appPackage", appInfoBean?.packageName) val stringBuilder = StringBuilder() stringBuilder.append("weixin://sendreq?appid=") stringBuilder.append(appInfoBean?.packageSign) intent.putExtra("_mmessage_content", stringBuilder.toString()) intent.putExtra( "_mmessage_checksum", MMessageUtils.signatures(paramString3, paramContext.packageName) ) intent.addFlags(268435456).addFlags(134217728) return try { paramContext.startActivity(intent) val sb = StringBuilder() sb.append("send mm message, intent=") sb.append(intent) LogUtils.logE(" ---> sb :$sb") 0 } catch (exception: Exception) { exception.printStackTrace() LogUtils.logE(" ---> send fail, target ActivityNotFound") -1 } } }}
4. 对 Flutter 暴露通道
这块需要注意几点,现在你可以理解为你在编写一个 Flutter 的小型插件,那么你需要向外部暴露一些你规定的类型,或者说方法。这个不难理解吧。
好比你去调用某个 SDK,官方一定是告知了一些重要的特性。那么针对我们现在的这个小插件,它比较关键的特性又是什么?
关于这个特性,个人这里分为俩个部分来说:
内部特性:
-
本地命中宿主缓存 Json。这块主要是需要个人去维护,去抓去目前常用的一个 App 的相关信息,不断完善。
外部特性:
-
通道名称。这个理解起来比较容易,好比你拿着 A 小区的通行证进入 B 小区,那么 B 小区的保安大叔肯定会给你拦下来,而反之你进入 A 小区则畅行无阻。
-
对外暴露方法。比如说我现在对外暴露俩个方法,一个为检测命中宿主数量一个为实际的微信分享。
-
关键参数描述。例如微信分享类型,目前偷个懒,Flutter 调用时只需要传递 bool 类型即可,SDK 内部会自行匹配。
针对以上内容,这里提取配置类:
package com.hlq.struggle.app/** * @author:HLQ_Struggle * @date:2020/6/27 * @desc: *//** * 通道名称 */const val channelName = "HLQStruggle"/** * 检测命中数量 > 0 代表可采用命中宿主方案借壳分享 */const val checkAppInstalledChannel = "checkAppInstalled"/** * 分享微信 */const val shareWeChatChannel = "shareWeChat"/** * 分享微信消息会话 */const val shareWeChatSession = 0/** * 分享微信朋友圈 */const val shareWeChatLine = 1/** * 本地缓存 App 信息 */const val appInfoJson = "[{\"appName\":\"App Name\",\"downloadUrl\":\"\",\"optional\":1,\"packageName\":\"Package Name\",\"packageSign\":\"App WeChat ID\",\"type\":1}]"
下面则是本地工具类,拼接参数,发送微信:
package com.hlq.struggleimport com.hlq.struggle.app.*import com.hlq.struggle.utils.ShareWeChatUtils.Companion.checkAppInstalledimport com.hlq.struggle.utils.ShareWeChatUtils.Companion.shareWeChatimport io.flutter.embedding.android.FlutterActivityimport io.flutter.embedding.engine.FlutterEngineimport io.flutter.plugin.common.MethodCallimport io.flutter.plugin.common.MethodChannelimport io.flutter.plugins.GeneratedPluginRegistrantclass MainActivity: FlutterActivity() { override fun configureFlutterEngine(flutterEngine: FlutterEngine) { GeneratedPluginRegistrant.registerWith(flutterEngine) // 处理 Flutter 传递过来的消息 handleMethodChannel(flutterEngine) } private fun handleMethodChannel(flutterEngine: FlutterEngine) { MethodChannel(flutterEngine.dartExecutor, channelName).setMethodCallHandler { methodCall: MethodCall, result: MethodChannel.Result? -> when (methodCall.method) { checkAppInstalledChannel -> { // 获取命中 App 数量 result?.success(checkAppInstalled(activity)) } shareWeChatChannel -> { // 分享微信 val shareType = if (methodCall.argument("isScene")!!) { shareWeChatSession } else { shareWeChatLine } result?.success(shareWeChat( this, shareType, methodCall.argument("shareUrl")!!, methodCall.argument("shareTitle")!!, methodCall.argument("shareDesc")!!, methodCall.argument("shareThumbnail")!!, "")) } else -> { result?.notImplemented() } } } }}
5. Flutter 端调用
这里个人习惯,首先定义一个常量类,将 SDK 或者说 Android 端插件暴露参数定义一下,使用时统一调用,方便然后维护。
/// @date 2020-06-27/// @author HLQ_Struggle/// @desc 常量类/// 通道名称const String channelName = 'HLQStruggle';/// 检测命中数量 > 0 代表可采用命中宿主方案借壳分享const String checkAppInstalled = 'checkAppInstalled';/// 分享微信const String shareWeChat = 'shareWeChat';
而对于 Flutter 调用 Android 原生则比较 easy 了,相关注意的点已在代码中注释,这里直接附上对应的关键代码:
class _MyHomePageState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ GestureDetector( onTap: () async { _shareWeChatApp(true); }, child: Text( '点我分享微信消息会话', ), ), GestureDetector( onTap: () async { _shareWeChatApp(false); }, child: Padding( padding: EdgeInsets.only(top: 30), child: Text( '点我分享微信朋友圈', ), ), ) ], ), ), ); } /// 具体分享微信方式:true:消息会话 false:朋友圈 /// 提前调取通道验证采用官方 SDK 还是借壳方案 void _shareWeChatApp(bool isScene) async { /// 这里一定注意通道名称俩端一致 const platform = const MethodChannel(channelName); int tempHitNum = 0; try { tempHitNum = await platform.invokeMethod(checkAppInstalled); } catch (e) { print(e); } if (tempHitNum > 0) { // 当前设备存在目标宿主 - 开始执行分享 await platform.invokeMethod(shareWeChat, { 'isScene': isScene, 'shareTitle': '我是分享标题', 'shareDesc': '我是分享内容', 'shareUrl': 'https://juejin.im/post/5eb847e56fb9a0438e239243', /// 分享内容在线地址 'shareThumbnail': 'https://user-gold-cdn.xitu.io/2018/9/27/16618fef8bbf66fb?imageView2/1/w/180/h/180/q/85/format/webp/interlace/1' /// 分享图片在线地址 }); } else { // 当前设备不存在目前宿主 } }}
好了,整个一个流程完成了。我们看下最后实际分享的效果:
6. 查看效果
分享微信消息会话

分享成功提示,重点在分享来源:

分享微信消息会话,来源成功变成了我梦想殿堂旗下的某个 App 了。
而分享朋友圈则比较简单了:

番外 - 瞎叨叨
说实话,这个东西不难。
但是磕磕巴巴搞了好几天,也被各种催,甚至差点掏钱去买。
当我很开心的和鸡老大去分享这个事儿整个过程,除了鸡老大日常三连夸之外,老大默默说了个思路,问我是不是这样子的。
默默听完,蛋疼了半天,一模一样!
日常吹鸡老大,老大却淡淡的回复,很正常呀,巴拉巴拉~
老大,不愧是老大~
免责声明
为了避免收费的小哥哥干我,或者出现其它不好的情况,这里特意注明下:
本文如同标题一样,只属于个人笔记,仅限技术分享~ 如出现其他情况,一概与本人无关~
本文如同标题一样,只属于个人笔记,仅限技术分享~ 如出现其他情况,一概与本人无关~
本文如同标题一样,只属于个人笔记,仅限技术分享~ 如出现其他情况,一概与本人无关~
Thanks
-
ClassyShark
-
Shrinking Your Build With No Rules and do it with Class(yShark)
-
JD-GUI
-
dex2jar
-
Writing custom platform-specific code:撰写双端平台代码(插件编写实现)
-
Flutter Plugin调用Native APIs
欢迎各位关注
不定期发布
见证成长路


觉得不错,右下角点个好看呗~
更多推荐

所有评论(0)