每一個生命體的存在,其實本質都是一個複雜的過程。不少時候,無需追求完美的理想狀況,畢竟,You are just you。java
爲了不收費的小哥哥幹我,或者出現其它很差的狀況,這裏特地註明下:android
本文如同標題同樣,只屬於我的筆記,僅限技術分享~ 如出現其餘狀況,一律與本人無關~git
本文如同標題同樣,只屬於我的筆記,僅限技術分享~ 如出現其餘狀況,一律與本人無關~github
本文如同標題同樣,只屬於我的筆記,僅限技術分享~ 如出現其餘狀況,一律與本人無關~web
前段時間,公司忽然來一需求:編程
第一時間,固然是看看網上有沒有前輩開源,借鑑(CV 大法)一波。c#
查詢結果然的是悲喜交加:api
對於自己在帝都討生活的落魄小 Android 而言,無疑是一筆鉅款 (手動滑稽~勿噴~)。緩存
都說窮人家的孩子早當家,不得已開始了逆向、分析之路 😂😂😂安全
相關代碼已上傳 GitHub,固然爲了避免給本身找事兒,本地命中庫就不提供了,本身逆向去拿吧,地址以下:
空談無用,來個實際效果圖最棒,這裏就以我夢想殿堂 App 爲例進行測試咯。
基於我的瞭解簡單概述:
下面附上相關工具網盤連接:
在正式開始前,先來見識下 ClassyShark 這個神器吧。
首先進入你下載好的 ClassyShark.jar 目錄中,隨後執行以下命令便可:
示意圖以下:
隨後在打開的可視化工具中將想看的 Apk 直接拖進去便可:
拖進去以後點擊包名,會有一個對當前 Apk 的簡單概述:
點擊 Methods count 能夠查看當前 Apk 方法數:
固然你能夠繼續往下一層級查看,好比我點擊 bilibili:
一樣也能夠導出文件,這裏不做爲本文重點闡述了,有興趣的能夠本身研究~
首先,網上下載目標 App,並將後綴名修改成 zip,隨後解壓進入該目錄:
手動進入已下載完成的 dex-tools-2.1-SNAPSHOT 目錄中,執行以下命令:
例如:
完成以後,將會在 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 的生態,真的賊棒!尤爲我雞老大,神通常存在!默默的感謝我大哥~!
在 Flutter 中文社區中官網對此有這樣的一段描述:
Flutter 使用了靈活的系統,它容許你調用相關平臺的 API,不管是 Android 中的 Java 或 Kotlin 代碼,仍是 iOS 中的 Objective-C 或 Swift 代碼。
Flutter 內置的平臺特定 API 支持不依賴於任何生成代碼,而是靈活的依賴於傳遞消息格式。或者,你也可使用 Pigeon 這個 > package,經過生成代碼來發送結構化類型安全消息。
應用程序中的 Flutter 部分經過平臺通道向其宿主(應用程序中的 iOS 或 Android 部分)發送消息。
宿主監聽平臺通道並接收消息。而後,它使用原生編程語言來調用任意數量的相關平臺 API,並將響應發送回客戶端(即應用程序中的 Flutter 部分)。
也就是說,Flutter 充分給予咱們調用原生 Api 的權利,關鍵橋樑即是這個通道消息。
下面一塊兒來看下官方的圖:
消息和響應以異步的形式進行傳遞,以確保用戶界面可以保持響應。
客戶端作方法調用的時候 MethodChannel 會負責響應,從平臺一側來說,Android 系統上使用 MethodChannelAndroid、 iOS 系統使用 MethodChanneliOS 來接收和返回來自 MethodChannel 的方法調用。
其實對於我一個新手而言,看這些真的似懂非懂,因此過多的等之後掌握了以後再來探討吧。這塊內容將在下面代碼部分着重說明。
api 'com.tencent.mm.opensdk:wechat-sdk-android-without-mta:+'
// 主要用於將分享的在線圖片轉換爲 Bitmap
implementation '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'
複製代碼
# 保護我方輸出(保護實體類不被混淆)
-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.** { *; }
複製代碼
這裏具體仍是須要結合實際項目需求而定,不過通用型的一些東西必需要有:
而剩下的則是分享微信了,這裏簡單放置關鍵代碼,詳情可點擊文章開始的 GitHub 地址。
package com.hlq.struggle.utils
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.os.Bundle
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.hlq.struggle.app.appInfoJson
import com.hlq.struggle.bean.AppInfoBean
import com.tencent.mm.opensdk.modelmsg.SendMessageToWX
import com.tencent.mm.opensdk.modelmsg.WXMediaMessage
import com.tencent.mm.opensdk.modelmsg.WXMediaMessage.IMediaObject
import com.tencent.mm.opensdk.modelmsg.WXWebpageObject
import java.io.ByteArrayOutputStream
import java.io.IOException
/**
* @author:HLQ_Struggle
* @date:2020/6/27
* @desc:
*/
@Suppress("SpellCheckingInspection")
class ShareWeChatUtils {
companion object {
/**
* 解析本地緩存 App 信息
*/
private fun getLocalAppCache(): ArrayList<AppInfoBean> {
return Gson().fromJson(
appInfoJson,
object : TypeToken<ArrayList<AppInfoBean>>() {}.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<Bitmap?> {
override fun onLoadFailed(
param1GlideException: GlideException?,
param1Object: Any,
param1Target: Target<Bitmap?>,
param1Boolean: Boolean
): Boolean {
LogUtils.logE(" ---> Load Image Failed")
return false
}
override fun onResourceReady(
param1Bitmap: Bitmap?,
param1Object: Any,
param1Target: Target<Bitmap?>,
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
}
}
}
}
複製代碼
這塊須要注意幾點,如今你能夠理解爲你在編寫一個 Flutter 的小型插件,那麼你須要向外部暴露一些你規定的類型,或者說方法。這個不難理解吧。
比如你去調用某個 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.struggle
import com.hlq.struggle.app.*
import com.hlq.struggle.utils.ShareWeChatUtils.Companion.checkAppInstalled
import com.hlq.struggle.utils.ShareWeChatUtils.Companion.shareWeChat
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugins.GeneratedPluginRegistrant
class 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<Boolean>("isScene")!!) {
shareWeChatSession
} else {
shareWeChatLine
}
result?.success(shareWeChat(
this, shareType,
methodCall.argument<String>("shareUrl")!!,
methodCall.argument<String>("shareTitle")!!,
methodCall.argument<String>("shareDesc")!!,
methodCall.argument<String>("shareThumbnail")!!, ""))
}
else -> {
result?.notImplemented()
}
}
}
}
}
複製代碼
這裏我的習慣,首先定義一個常量類,將 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<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
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 {
// 當前設備不存在目前宿主
}
}
}
複製代碼
好了,整個一個流程完成了。咱們看下最後實際分享的效果:
分享成功提示,重點在分享來源:
分享微信消息會話,來源成功變成了我夢想殿堂旗下的某個 App 了。
而分享朋友圈則比較簡單了:
說實話,這個東西不難。
可是磕磕巴巴搞了好幾天,也被各類催,甚至差點掏錢去買。
當我很開心的和雞老大去分享這個事兒整個過程,除了雞老大平常三連誇以外,老大默默說了個思路,問我是否是這樣子的。
默默聽完,蛋疼了半天,如出一轍!
平常吹雞老大,老大卻淡淡的回覆,很正常呀,巴拉巴拉~
老大,不愧是老大~
爲了不收費的小哥哥幹我,或者出現其它很差的狀況,這裏特地註明下:
本文如同標題同樣,只屬於我的筆記,僅限技術分享~ 如出現其餘狀況,一律與本人無關~
本文如同標題同樣,只屬於我的筆記,僅限技術分享~ 如出現其餘狀況,一律與本人無關~
本文如同標題同樣,只屬於我的筆記,僅限技術分享~ 如出現其餘狀況,一律與本人無關~