7 Commits

Author SHA1 Message Date
661680e60b Shamrock: 绕过资源上传检测
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-01 20:29:28 +08:00
54b7eb95a8 Shamrock: 修复真机反检测异常
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-01 20:29:13 +08:00
265fff3cd2 Merge pull request #271 from PisLuanyao/master
fix termination by PullConfig for Emu
2024-03-01 18:04:46 +08:00
8ca0a3815a Merge branch 'whitechi73:master' into master 2024-03-01 17:50:16 +09:00
da6d34c53e fix Crash for Emu
强制初始化配置
2024-03-01 16:48:49 +08:00
61ffb37951 Merge pull request #269 from PisLuanyao/master
fix NativeLoader for Emu
2024-03-01 15:40:44 +08:00
593f461ffe fix NativeLoader for Emu
发现 isEmu 里反射的有问题,会出现 java.lang.NoSuchFieldException: No field vmInstructionSet in class Ldalvik/system/VMRuntime; (declaration of 'dalvik.system.VMRuntime' appears in /system/framework/core-libart.jar)
2024-03-01 14:57:21 +08:00
20 changed files with 347 additions and 193 deletions

View File

@ -162,6 +162,17 @@ object ShamrockConfig {
pushUpdate(ctx)
}
fun getUploadResourceGroup(ctx: Context): String {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getString("up_res_group", "100000000")!!
}
fun setUploadResourceGroup(ctx: Context, v: String) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putString("up_res_group", v).apply()
pushUpdate(ctx)
}
fun getHttpPort(ctx: Context): Int {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getInt("port", 5700)
@ -354,6 +365,7 @@ object ShamrockConfig {
"disable_auto_sync_setting" to preferences.getBoolean("disable_auto_sync_setting", false),
"forbid_useless_process" to preferences.getBoolean("forbid_useless_process", false),
"enable_old_bdh" to preferences.getBoolean("enable_old_bdh", false),
"up_res_group" to preferences.getString("up_res_group", ""),
)
}

View File

@ -356,6 +356,36 @@ private fun FunctionCard(
return@Function true
}
run {
val uploadResourceGroup = remember { mutableStateOf(ShamrockConfig.getUploadResourceGroup(ctx)) }
Column(
modifier = Modifier
.absolutePadding(left = 8.dp, right = 8.dp, top = 12.dp, bottom = 0.dp)
) {
Text(
modifier = Modifier.padding(2.dp),
text = "用来上传资源的群聊,错误的资源上传终点可能导致封禁,请自建一个群聊并填写在下方。",
color = Color.Red,
fontSize = 11.sp
)
}
TextItem(
title = "接受资源群聊",
desc = "用来上传资源的群聊,请自建一个群聊并填写在下方。",
text = uploadResourceGroup,
hint = "请输入群号",
error = "群号不合法",
checker = {
it.isNotBlank() && it.toULongOrNull() != null
},
confirm = {
val groupId = uploadResourceGroup.value
ShamrockConfig.setUploadResourceGroup(ctx, groupId)
AppRuntime.log("设置接受资源群聊为[$groupId]。")
}
)
}
/*
Function(
title = "专业级接口",
@ -445,9 +475,7 @@ private fun InfoItem(
.fillMaxWidth()
.combinedClickable(onDoubleClick = {
doubleClick?.invoke(content)
}) {
true
}
}) { true }
,
verticalAlignment = Alignment.CenterVertically
) {

View File

@ -32,6 +32,11 @@ data class AdaptShareInfoReq(
@Serializable
data class Template(
@ProtoNumber(1) var templateId: UInt? = null,
@ProtoNumber(1) var templateId: ULong? = null,
@ProtoNumber(2) var templateData: ByteArray? = null,
)
@Serializable
data class AdaptShareInfoResp(
@ProtoNumber(2) var json: String? = null,
): Protobuf<AdaptShareInfoResp>

View File

@ -7,6 +7,8 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
import moe.fuqiuluo.symbols.Protobuf
const val DEFAULT_DEVICE_INFO = "i=&imsi=&mac=02:00:00:00:00:00&m=Shamrock&o=114514&a=1919810&sd=0&c64=1&sc=1&p=8000*8000&aid=123456789012345678901234567890abcdef&f=Tencent&mm=5610&cf=1726&cc=8&qimei=&qimei36=&sharpP=1&n=nether_world&support_xsj_live=false&client_mod=concise&timezone=America/La_Paz&material_sdk_version=&vh265=&refreshrate=10086&hwlevel=9&suphdr=1&is_teenager_mod=8&liveH265=&bmst=5&AV1=0"
@Serializable
data class QWebReq(
@ProtoNumber(1) val seq: Int = 0,

View File

@ -37,6 +37,7 @@ import protobuf.oidb.cmx0xf57.Oidb0xf57Req
import protobuf.oidb.cmx0xf57.Oidb0xf57Rsp
import protobuf.oidb.cmx0xf57.Oidb0xf57U1
import protobuf.oidb.cmx0xf57.Oidb0xf57U2
import protobuf.qweb.DEFAULT_DEVICE_INFO
import protobuf.qweb.QWebExtInfo
import protobuf.qweb.QWebReq
import protobuf.qweb.QWebRsp
@ -73,7 +74,7 @@ internal object GProSvc: BaseSvc() {
val buffer = sendBufferAW("QChannelSvr.trpc.qchannel.commreader.ComReader.GetGuildFeeds", true, QWebReq(
seq = 10,
qua = PlatformUtils.getQUA(),
deviceInfo = "i=&imsi=&mac=02:00:00:00:00:00&m=Shamrock&o=114514&a=1919810&sd=0&c64=1&sc=1&p=8000*8000&aid=123456789012345678901234567890abcdef&f=Tencent&mm=5610&cf=1726&cc=8&qimei=&qimei36=&sharpP=1&n=nether_world&support_xsj_live=false&client_mod=concise&timezone=America/La_Paz&material_sdk_version=&vh265=&refreshrate=10086&hwlevel=9&suphdr=1&is_teenager_mod=8&liveH265=&bmst=5&AV1=0",
deviceInfo = DEFAULT_DEVICE_INFO,
buffer = GetGuildFeedsReq(
count = 12,
from = startIndex,

View File

@ -338,13 +338,13 @@ internal object MsgSvc : BaseSvc() {
else -> MessageHelper.decodeCQCode(data["content"].asString)
}.onEach { element ->
val elementData = element.asJsonObject["data"].asJsonObject
if (element.asJsonObject["type"].asString == "forward")
if (element.asJsonObject["type"].asString == "forward") {
forwardMsg[elementData["filename"].asString] =
elementData["id"].asString
}
).getOrElse { throw Exception("消息合成失败: $it") }.let {
desc[++i] =
(data["name"].asStringOrNull ?: data["uin"].asStringOrNull
}
).getOrElse { error("消息合成失败: $it") }.let {
desc[++i] = (data["name"].asStringOrNull ?: data["uin"].asStringOrNull
?: TicketSvc.getNickname()) + ": " + it.first
it.second
}
@ -353,8 +353,9 @@ internal object MsgSvc : BaseSvc() {
} else {
error("消息节点缺少id或content字段")
}
}.onFailure {
LogCenter.log("消息节点解析失败:${it.stackTraceToString()}", Level.WARN)
}.getOrElse {
LogCenter.log("消息节点解析失败:$it", Level.WARN)
null
}
}.ifEmpty { return Result.failure(Exception("消息节点为空")) }

View File

@ -1,9 +1,45 @@
package moe.fuqiuluo.qqinterface.servlet.ark
import moe.fuqiuluo.qqinterface.servlet.BaseSvc
import moe.fuqiuluo.qqinterface.servlet.ark.data.ArkAppInfo
import moe.fuqiuluo.shamrock.utils.PlatformUtils
import moe.fuqiuluo.symbols.decodeProtobuf
import protobuf.auto.toByteArray
import protobuf.lightapp.AdaptShareInfoReq
import protobuf.lightapp.AdaptShareInfoResp
import protobuf.qweb.DEFAULT_DEVICE_INFO
import protobuf.qweb.QWebReq
import protobuf.qweb.QWebRsp
internal object LightAppSvc: BaseSvc() {
suspend fun adaptShare() {
suspend fun adaptShareJumpUrl(
arkAppInfo: ArkAppInfo,
coverUrl: String,
desc: String,
url: String
): Result<String> {
val rsp = sendBufferAW("LightAppSvc.mini_app_share.AdaptShareInfo", true, QWebReq(
seq = 10,
qua = PlatformUtils.getQUA(),
deviceInfo = DEFAULT_DEVICE_INFO,
buffer = AdaptShareInfoReq(
appid = arkAppInfo.miniAppId.toString(),
title = arkAppInfo.appName,
desc = desc,
time = (System.currentTimeMillis() * 0.001).toULong(),
scene = 3u,
templetType = 1u,
businessType = 0u,
picUrl = coverUrl,
jumpUrl = "pages",
verType = 3u,
withShareTicket = 0u,
webURL = url,
).toByteArray(),
traceId = app.account + "_0_0",
).toByteArray())?.decodeProtobuf<QWebRsp>()?.buffer?.decodeProtobuf<AdaptShareInfoResp>()
if (rsp == null || rsp.json.isNullOrEmpty())
return Result.failure(Exception("unable to adapt ShareInfo"))
return Result.success(rsp.json!!)
}
}

View File

@ -5,7 +5,8 @@ sealed class ArkAppInfo(
val version: String,
val packageName: String,
val signature: String,
val miniAppId: Long = 0
val miniAppId: Long = 0,
val appName: String = ""
) {
data object QQMusic: ArkAppInfo(
appId = 100497308,
@ -25,7 +26,8 @@ sealed class ArkAppInfo(
version = "0.0.0",
packageName = "tv.danmaku.bili",
signature = "7194d531cbe7960a22007b9f6bdaa38b",
miniAppId = 1109937557
miniAppId = 1109937557,
appName = "哔哩哔哩"
)
data object Docs: ArkAppInfo(

View File

@ -261,7 +261,7 @@ internal class ElemMaker {
resources = arrayListOf(file),
timeout = 30.seconds
).getOrThrow().first()
LogCenter.log(uploadRet.toString(), Level.DEBUG)
LogCenter.log({ uploadRet.toString() }, Level.DEBUG)
val elem = when (chatType) {
MsgConstant.KCHATTYPEGROUP -> Elem(

View File

@ -1,14 +1,11 @@
package moe.fuqiuluo.qqinterface.servlet.transfile
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.media.MediaMetadataRetriever
import androidx.exifinterface.media.ExifInterface
import com.tencent.mobileqq.qroute.QRoute
import com.tencent.qqnt.aio.adapter.api.IAIOPttApi
import com.tencent.qqnt.kernel.nativeinterface.CommonFileInfo
import com.tencent.qqnt.kernel.nativeinterface.Contact
import com.tencent.qqnt.kernel.nativeinterface.FileElement
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import com.tencent.qqnt.kernel.nativeinterface.PicElement
@ -16,28 +13,19 @@ import com.tencent.qqnt.kernel.nativeinterface.PttElement
import com.tencent.qqnt.kernel.nativeinterface.QQNTWrapperUtil
import com.tencent.qqnt.kernel.nativeinterface.RichMediaFilePathInfo
import com.tencent.qqnt.kernel.nativeinterface.VideoElement
import com.tencent.qqnt.msg.api.IMsgUtilApi
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import moe.fuqiuluo.qqinterface.servlet.BaseSvc
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
import moe.fuqiuluo.qqinterface.servlet.transfile.data.Private
import moe.fuqiuluo.qqinterface.servlet.transfile.data.Troop
import moe.fuqiuluo.qqinterface.servlet.transfile.data.TryUpPicData
import moe.fuqiuluo.qqinterface.servlet.transfile.data.VideoResource
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.helper.TransfileHelper
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
import moe.fuqiuluo.shamrock.tools.hex2ByteArray
import moe.fuqiuluo.shamrock.tools.slice
import moe.fuqiuluo.shamrock.utils.AudioUtils
import moe.fuqiuluo.shamrock.utils.FileUtils
import moe.fuqiuluo.shamrock.utils.MD5
import moe.fuqiuluo.shamrock.utils.MediaType
import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
import moe.fuqiuluo.shamrock.xposed.helper.msgService
@ -63,7 +51,6 @@ import protobuf.oidb.cmd0x388.Cmd0x388ReqBody
import protobuf.oidb.cmd0x388.Cmd0x388RspBody
import protobuf.oidb.cmd0x388.TryUpImgReq
import java.io.File
import java.io.FileOutputStream
import kotlin.coroutines.resume
import kotlin.math.roundToInt
import kotlin.random.Random
@ -72,14 +59,30 @@ import kotlin.random.nextULong
import kotlin.time.Duration
internal object NtV2RichMediaSvc: BaseSvc() {
private const val GROUP_PIC_UPLOAD_TO = "100000000"
private val requestIdSeq = atomic(2L)
private fun fetchGroupResUploadTo(): String {
return ShamrockConfig.getUpResGroup().ifEmpty { "100000000" }
}
suspend fun tryUploadResourceByNt(
chatType: Int,
elementType: Int,
resources: ArrayList<File>,
timeout: Duration,
retryCnt: Int = 5
): Result<MutableList<CommonFileInfo>> {
return internalTryUploadResourceByNt(chatType, elementType, resources, timeout).onFailure {
if (retryCnt > 0) {
return tryUploadResourceByNt(chatType, elementType, resources, timeout, retryCnt - 1)
}
}
}
/**
* 批量上传图片
*/
suspend fun tryUploadResourceByNt(
private suspend fun internalTryUploadResourceByNt(
chatType: Int,
elementType: Int,
resources: ArrayList<File>,
@ -276,8 +279,9 @@ internal object NtV2RichMediaSvc: BaseSvc() {
}
val contact = when(chatType) {
MsgConstant.KCHATTYPEC2C -> MessageHelper.generateContact(chatType, TicketSvc.getUin())
else -> Contact(chatType, GROUP_PIC_UPLOAD_TO, GROUP_PIC_UPLOAD_TO)
else -> Contact(chatType, fetchGroupResUploadTo(), null)
}
LogCenter.log(contact.toString())
val result = mutableListOf<CommonFileInfo>()
withTimeoutOrNull(timeout) {
suspendCancellableCoroutine {
@ -297,11 +301,13 @@ internal object NtV2RichMediaSvc: BaseSvc() {
message = ArrayList(messages),
uniseq = uniseq.qqMsgId
) { _, _ ->
if (contact.chatType == MsgConstant.KCHATTYPEGROUP && contact.peerUid == "100000000") {
val kernelService = NTServiceFetcher.kernelService
val sessionService = kernelService.wrapperSession
val msgService = sessionService.msgService
msgService.deleteMsg(contact, arrayListOf(uniseq.qqMsgId), null)
}
}
it.invokeOnCancellation {
RichMediaUploadHandler.removeListener(uniseq.qqMsgId)
}

View File

@ -0,0 +1,44 @@
package moe.fuqiuluo.shamrock.remote.action.handlers
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.qqinterface.servlet.ark.LightAppSvc
import moe.fuqiuluo.qqinterface.servlet.ark.data.ArkAppInfo
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
import moe.fuqiuluo.symbols.OneBotHandler
@OneBotHandler("adapt_share_json")
internal object AdaptShareJson: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
//val json = if(session.isString("json")) session.getString("json")
//else session.getJsonElement("json").toString()
val cover = session.getString("cover")
val desc = session.getString("desc")
val url = session.getStringOrNull("url") ?: ""
return invoke(cover, desc, url, session.echo)
}
suspend operator fun invoke(cover: String, desc: String, url: String, echo: JsonElement = EmptyJsonString): String {
/*
ArkMsgSvc.tryShareJsonMessage(json).onSuccess {
return ok(SignArkMessageResult(it), echo = echo)
}.onFailure {
return error(it.message ?: it.toString(), echo)
}*/
LightAppSvc.adaptShareJumpUrl(ArkAppInfo.DanMaKu, cover, desc, url).onSuccess {
return ok(AdaptShareInfo(it), echo = echo)
}.onFailure {
return error(it.message ?: it.toString(), echo)
}
return logic("logic error", echo)
}
override val requiredParams: Array<String> = arrayOf("cover", "desc")
@Serializable
data class AdaptShareInfo(
val result: String
)
}

View File

@ -1,33 +0,0 @@
package moe.fuqiuluo.shamrock.remote.action.handlers
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.qqinterface.servlet.ark.ArkMsgSvc
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
import moe.fuqiuluo.symbols.OneBotHandler
@OneBotHandler("sign_ark_message")
internal object SignArkMessage: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val json = if(session.isString("json")) session.getString("json")
else session.getJsonElement("json").toString()
return invoke(json, session.echo)
}
suspend operator fun invoke(json: String, echo: JsonElement = EmptyJsonString): String {
/*
ArkMsgSvc.tryShareJsonMessage(json).onSuccess {
return ok(SignArkMessageResult(it), echo = echo)
}.onFailure {
return error(it.message ?: it.toString(), echo)
}*/
return logic("logic error", echo)
}
@Serializable
data class SignArkMessageResult(
val result: String
)
}

View File

@ -31,16 +31,21 @@ import moe.fuqiuluo.shamrock.tools.jsonArray
import moe.fuqiuluo.shamrock.tools.respond
fun Routing.messageAction() {
route("/sign_ark_message") {
route("/adapt_share_json") {
get {
val json = fetchGetOrThrow("json")
call.respondText(SignArkMessage(json), ContentType.Application.Json)
val cover = fetchGetOrThrow("cover")
val desc = fetchGetOrThrow("desc")
val url = fetchGetOrNull("url") ?: ""
call.respondText(AdaptShareJson(cover, desc, url), ContentType.Application.Json)
}
post {
val json = if (isJsonData() && (isJsonObject("json") || isJsonArray("json")))
fetchPostJsonElement("json").toString()
else fetchPostOrThrow("json")
call.respondText(SignArkMessage(json), ContentType.Application.Json)
//val json = if (isJsonData() && (isJsonObject("json") || isJsonArray("json")))
// fetchPostJsonElement("json").toString()
//else fetchPostOrThrow("json")
val cover = fetchPostOrThrow("cover")
val desc = fetchPostOrThrow("desc")
val url = fetchPostOrNull("url") ?: ""
call.respondText(AdaptShareJson(cover, desc, url), ContentType.Application.Json)
}
}
@ -97,6 +102,13 @@ fun Routing.messageAction() {
call.respondText(GetMsg(msgHash), ContentType.Application.Json)
}
getOrPost("/send_msg_by_resid") {
val resId = fetchOrThrow("res_id")
val peerId = fetchOrThrow("peer_Id")
val messageType = fetchOrThrow("message_type")
call.respondText(SendMsgByResid(peerId, resId, messageType))
}
route("/(send_msg|send_message)".toRegex()) {
get {
val msgType = fetchGetOrThrow("message_type")

View File

@ -24,13 +24,6 @@ fun Routing.testAction() {
return
}
getOrPost("/send_msg_by_resid") {
val resId = fetchOrThrow("res_id")
val peerId = fetchOrThrow("peer_Id")
val messageType = fetchOrThrow("message_type")
call.respondText(SendMsgByResid(peerId, resId, messageType))
}
getOrPost("/createUidFromTinyId") {
val selfId = fetchOrThrow("selfId").toLong()
val peerId = fetchOrThrow("peerId")

View File

@ -2,8 +2,10 @@ package moe.fuqiuluo.shamrock.remote.service.config
import android.content.Intent
import com.tencent.mmkv.MMKV
import de.robv.android.xposed.XposedBridge
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import moe.fuqiuluo.qqinterface.servlet.transfile.NtV2RichMediaSvc
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.tools.GlobalJson5
@ -17,7 +19,7 @@ internal object ShamrockConfig {
if (it.exists()) it.delete()
it.mkdirs()
}
private val Config = kotlin.runCatching {
private val config = kotlin.runCatching {
GlobalJson5.decodeFromString<ServiceConfig>(ConfigDir.resolve("config.json").also {
if (!it.exists()) it.writeText("{}")
}.readText())
@ -32,40 +34,21 @@ internal object ShamrockConfig {
return mmkv.getBoolean("isInit", false)
}
private fun updateConfig(config: ServiceConfig = Config) {
private fun updateConfig(config: ServiceConfig = this.config) {
ConfigDir.resolve("config.json").writeText(GlobalJson5.encodeToString(config))
}
fun updateConfig(intent: Intent) {
val mmkv = MMKVFetcher.mmkvWithId("shamrock_config")
mmkv.apply {
if (!intent.getBooleanExtra("disable_auto_sync_setting", false)) {
putBoolean(
"tablet",
intent.getBooleanExtra("tablet", false)
) // 强制平板模式
putBoolean("tablet", intent.getBooleanExtra("tablet", false)) // 强制平板模式
putInt("port", intent.getIntExtra("port", 5700)) // 主动HTTP端口
putBoolean("ws", intent.getBooleanExtra("ws", false)) // 主动WS开关
putBoolean(
"http",
intent.getBooleanExtra("http", false)
) // HTTP回调开关
putString(
"http_addr",
intent.getStringExtra("http_addr")
) // WebHook回调地址
putBoolean(
"ws_client",
intent.getBooleanExtra("ws_client", false)
) // 被动WS开关
putBoolean(
"use_cqcode",
intent.getBooleanExtra("use_cqcode", false)
) // 使用CQ码
putBoolean(
"inject_packet",
intent.getBooleanExtra("inject_packet", false)
) // 拦截无用包
putBoolean("http", intent.getBooleanExtra("http", false)) // HTTP回调开关
putString("http_addr", intent.getStringExtra("http_addr")) // WebHook回调地址
putBoolean("ws_client", intent.getBooleanExtra("ws_client", false)) // 被动WS开关
putBoolean("use_cqcode", intent.getBooleanExtra("use_cqcode", false)) // 使用CQ码
putBoolean("inject_packet", intent.getBooleanExtra("inject_packet", false)) // 拦截无用包
putBoolean("debug", intent.getBooleanExtra("debug", false)) // 调试模式
putString( "key_store", intent.getStringExtra("key_store")) // 证书路径
putString( "ssl_pwd", intent.getStringExtra("ssl_pwd")) // 证书密码
@ -78,22 +61,24 @@ internal object ShamrockConfig {
putBoolean("enable_sync_msg_as_sent_msg", intent.getBooleanExtra("enable_sync_msg_as_sent_msg", false)) // 推送同步消息
putBoolean("forbid_useless_process", intent.getBooleanExtra("forbid_useless_process", false)) // 禁用QQ生成无用进程
putBoolean("enable_old_bdh", intent.getBooleanExtra("enable_old_bdh", false)) // 启用旧版BDH
intent.getStringExtra("up_res_group")?.let { putString("up_res_group", it) }
} else {
XposedBridge.log("[Shamrock] 已禁用自动同步配置")
}
Config.defaultToken = intent.getStringExtra("token")
Config.antiTrace = intent.getBooleanExtra("anti_qq_trace", true)
config.defaultToken = intent.getStringExtra("token")
config.antiTrace = intent.getBooleanExtra("anti_qq_trace", true)
val wsPort = intent.getIntExtra("ws_port", 5800)
Config.activeWebSocket = if (Config.activeWebSocket == null) ConnectionConfig(
config.activeWebSocket = if (config.activeWebSocket == null) ConnectionConfig(
address = "0.0.0.0",
port = wsPort,
) else Config.activeWebSocket?.also {
) else config.activeWebSocket?.also {
it.port = wsPort
}
Config.passiveWebSocket = intent.getStringExtra("ws_addr")?.split(",", "|", "")?.filter { address ->
config.passiveWebSocket = intent.getStringExtra("ws_addr")?.split(",", "|", "")?.filter { address ->
address.isNotBlank() && (address.startsWith("ws://") || address.startsWith("wss://"))
}?.map {
ConnectionConfig(address = it)
}?.toMutableList()
putBoolean("isInit", true)
}
if (!intent.getBooleanExtra("disable_auto_sync_setting", false)) {
@ -101,23 +86,77 @@ internal object ShamrockConfig {
}
}
fun putDefaultSettings() {
val mmkv = MMKVFetcher.mmkvWithId("shamrock_config")
if ((!isInit()) && (!mmkv.getBoolean("isEmergencyInitBefore", false))){
mmkv.apply {
putBoolean("tablet", false) // 强制平板模式
putInt("port", 5700) // 主动HTTP端口
putBoolean("ws", false) // 主动WS开关
putBoolean("http", false) // HTTP回调开关
putString("http_addr", null) // WebHook回调地址
putBoolean("ws_client", false) // 被动WS开关
putBoolean("use_cqcode", false) // 使用CQ码
putBoolean("inject_packet", false) // 拦截无用包
putBoolean("debug", false) // 调试模式
putString("key_store", null) // 证书路径
putString("ssl_pwd", null) // 证书密码
putString("ssl_private_pwd", null) // 证书私钥密码
putString("ssl_alias", null) // 证书别名
putInt("ssl_port", 5701) // 主动HTTP端口
putBoolean("alive_reply", true) // 自回复测试
putBoolean("enable_self_msg", false) // 推送自己发的消息
putBoolean("shell", false) // 开启Shell接口
putBoolean("enable_sync_msg_as_sent_msg", true) // 推送同步消息
putBoolean("forbid_useless_process", false) // 禁用QQ生成无用进程
putBoolean("enable_old_bdh", false) // 启用旧版BDH
putBoolean("antiTrace", false)
putBoolean("super_anti", true)
putString("up_res_group", "")
config.defaultToken = null
// config.antiTrace = true
val wsPort = 5800
config.activeWebSocket =
if (config.activeWebSocket == null) ConnectionConfig(
address = "0.0.0.0",
port = wsPort,
) else config.activeWebSocket?.also {
it.port = wsPort
}
config.passiveWebSocket = null
putBoolean("isInit", true)
putBoolean("isEmergencyInitBefore", true)
}
XposedBridge.log("[Shamrock] 强制初始化配置完成,请重新打开QQ")
} else {
XposedBridge.log("[Shamrock] 程序逻辑错误或错误地多次强制初始化")
mmkv.putBoolean("isEmergencyInitBefore", false)
XposedBridge.log("[Shamrock] 如果执意要再次强制初始化,请重新执行程序")
}
}
private val mmkv: MMKV
get() = MMKVFetcher.mmkvWithId("shamrock_config")
fun getUpResGroup(): String {
return mmkv.getString("up_res_group", "") ?: ""
}
fun aliveReply(): Boolean {
return mmkv.getBoolean("alive_reply", false)
}
fun allowTempSession(): Boolean {
return Config.allowTempSession
return config.allowTempSession
}
fun getGroupMsgRule(): GroupRule? {
return Config.rules?.groupRule
return config.rules?.groupRule
}
fun getPrivateRule(): PrivateRule? {
return Config.rules?.privateRule
return config.rules?.privateRule
}
fun enableSyncMsgAsSentMsg(): Boolean {
@ -137,7 +176,7 @@ internal object ShamrockConfig {
}
fun getWebSocketClientAddress(): List<ConnectionConfig> {
return Config.passiveWebSocket ?: emptyList()
return config.passiveWebSocket ?: emptyList()
}
fun openWebSocket(): Boolean {
@ -145,11 +184,11 @@ internal object ShamrockConfig {
}
fun getActiveWebSocketConfig(): ConnectionConfig? {
return Config.activeWebSocket
return config.activeWebSocket
}
fun getToken(): String {
return Config.defaultToken ?: ""
return config.defaultToken ?: ""
}
fun useCQ(): Boolean {
@ -236,7 +275,7 @@ internal object ShamrockConfig {
}
fun isAntiTrace(): Boolean {
return Config.antiTrace
return config.antiTrace
}
fun allowShell(): Boolean {

View File

@ -81,7 +81,6 @@ class AntiDetection: IAction {
} else if (!pref.getBoolean("super_anti", false)) {
return
}
//System.loadLibrary("clover")
NativeLoader.load("clover")
val env = XposedEntry.hasEnv()
val injected = XposedEntry.injected()

View File

@ -70,6 +70,7 @@ class PullConfig: IAction {
DataRequester.request("init", onFailure = {
if (!ShamrockConfig.isInit()) {
ctx.toast("请启动Shamrock主进程以初始化服务进程将退出。")
ShamrockConfig.putDefaultSettings()
thread {
Thread.sleep(3000)
exitProcess(1)

View File

@ -1,9 +1,11 @@
package moe.fuqiuluo.shamrock.xposed.loader
object LuoClassloader: ClassLoader() {
internal object LuoClassloader: ClassLoader() {
lateinit var hostClassLoader: ClassLoader
lateinit var ctxClassLoader: ClassLoader
internal val moduleLoader: ClassLoader = LuoClassloader::class.java.classLoader!!
fun load(name: String): Class<*>? {
return kotlin.runCatching {
loadClass(name)

View File

@ -1,12 +1,11 @@
package moe.fuqiuluo.shamrock.xposed.loader
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import de.robv.android.xposed.XposedBridge
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.xposed.loader.tmpnativehelper.moduleClassLoader
import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader.moduleLoader
import mqq.app.MobileQQ
import oicq.wlogin_sdk.tools.MD5
import java.io.File
@ -20,28 +19,24 @@ internal object NativeLoader {
return externalLibPath.resolve("libffmpegkit.so").exists()
}
private val isEmu: Boolean
get() {
private val isEmu: Boolean = runCatching {
if (Build.SUPPORTED_ABIS.any { it.contains("x86") }) {
XposedBridge.log("[Shamrock] 通过SUPPORTED_ABIS检测到 Android x86")
return true
return@runCatching true
}
return try {
val clazz = Class.forName("dalvik.system.VMRuntime")
val method = clazz.getDeclaredMethod("getRuntime")
val runtime = method.invoke(null)
val field = clazz.getDeclaredField("vmInstructionSet")
field.isAccessible = true
val instructionSet = field.get(runtime) as String
if ( instructionSet.contains("x86") ) {
if (instructionSet.contains("x86") ) {
XposedBridge.log("[Shamrock] 反射检测到 Android x86")
true
} else false
} catch (e: Exception) {
XposedBridge.log("[Shamrock] $e")
false
}
}
}.onFailure {
XposedBridge.log("[Shamrock] ${it.stackTraceToString()}")
}.getOrElse { false }
private fun getLibFilePath(name: String): String {
return if (isEmu) "lib/x86_64/lib${name}.so" else "lib/arm64-v8a/lib$name.so"
@ -50,48 +45,61 @@ internal object NativeLoader {
/**
* 使目标进程可以使用来自模块的库
*/
@SuppressLint("UnsafeDynamicallyLoadedCode")
fun load(name: String) {
try {
if (name == "shamrock" || name == "clover") {
onLoadByCopiedLibrary(name, getCtx())
} else {
val sourceFile = externalLibPath.resolve("lib$name.so")
val soFile = MobileQQ.getContext().filesDir.parentFile!!.resolve("txlib").resolve("lib$name.so")
if (!soFile.exists()) {
if (!sourceFile.exists()) {
LogCenter.log("LoadExternalLibrary(name = $name) failed, file not exists.", level = Level.ERROR)
return
} else {
sourceFile.copyTo(soFile)
Runtime.getRuntime().exec("chmod 755 ${soFile.absolutePath}").waitFor()
}
}
LogCenter.log("LoadExternalLibrary(name = $name)")
System.load(soFile.absolutePath)
if (name == "shamrock"
|| (name == "clover" && isEmu)
) {
onLoadByResource(name)
} else if (!onLoadByAbsolutePath(name)) {
onLoadByExternalFile(name)
}
} catch (e: Throwable) {
XposedBridge.log(e)
}
}
private inline fun getCtx() = MobileQQ.getContext()
private fun onLoadByAbsolutePath(name: String): Boolean {
val context = MobileQQ.getContext()
val packageManager = context.packageManager
val applicationInfo = packageManager.getApplicationInfo("moe.fuqiuluo.shamrock.hided", 0)
val file = File(applicationInfo.nativeLibraryDir)
LogCenter.log("LoadLibrary(name = $name)")
loadLibrary(file.resolve("lib$name.so").also {
if (!it.exists()) {
LogCenter.log("LoadLibrary(name = $name) failed, file not exists.", level = Level.ERROR)
return false
}
}.absolutePath, false)
return true
}
@SuppressLint("UnsafeDynamicallyLoadedCode")
private fun onLoadByCopiedLibrary(name: String, context: Context) {
val soDir = File(context.filesDir, "SM_LIBS")
if (soDir.isFile) {
soDir.delete()
private fun onLoadByExternalFile(name: String) {
val sourceFile = externalLibPath.resolve("lib$name.so")
val soFile = MobileQQ.getContext().filesDir.parentFile!!
.resolve("txlib").resolve("lib$name.so")
if (!soFile.exists()) {
if (!sourceFile.exists()) {
LogCenter.log("LoadExternalLibrary(name = $name) failed, file not exists.", level = Level.ERROR)
return
} else {
sourceFile.copyTo(soFile)
}
if (!soDir.exists()) {
soDir.mkdirs()
}
LogCenter.log("LoadExternalLibrary(name = $name)")
loadLibrary(soFile.absolutePath)
}
private fun onLoadByResource(name: String) {
val soDir = File(MobileQQ.getContext().filesDir, "SM_LIBS")
if (soDir.isFile) soDir.delete()
if (!soDir.exists()) soDir.mkdirs()
val soPath = getLibFilePath(name)
val soFile = File(soDir, name)
fun reloadSo(tmp: File? = null) {
LogCenter.log("SO文件大小不一致或不存在正在重新加载", Level.INFO)
soFile.delete()
if (tmp == null) moduleClassLoader.getResourceAsStream(soPath).use { origin ->
if (tmp == null) moduleLoader.getResourceAsStream(soPath).use { origin ->
soFile.outputStream().use { origin.copyTo(it) }
} else tmp.renameTo(soFile)
}
@ -99,10 +107,10 @@ internal object NativeLoader {
if (!soFile.exists()) {
reloadSo()
} else {
val tmpSoFile = soFile.resolve("$name.tmp").also { file ->
val tmpSoFile = File(soDir, "$name.tmp").also { file ->
if (file.exists()) file.delete()
file.outputStream().use {
moduleClassLoader.getResourceAsStream(soPath).use { origin ->
moduleLoader.getResourceAsStream(soPath).use { origin ->
origin.copyTo(it)
}
}
@ -111,18 +119,20 @@ internal object NativeLoader {
it != MD5.getFileMD5(tmpSoFile)
}) {
reloadSo(tmpSoFile)
} else { tmpSoFile.delete() }
}
}
try {
System.load(soFile.absolutePath)
LogCenter.log("加载SO文件成功 -> ${soFile.path}", Level.INFO)
} catch (e: Throwable) {
LogCenter.log(e.toString(), Level.WARN)
throw e
}
loadLibrary(soFile.absolutePath)
} catch (e: Exception) {
LogCenter.log(e.toString(), Level.WARN)
LogCenter.log(e.toString(), Level.ERROR)
throw e
}
}
@SuppressLint("UnsafeDynamicallyLoadedCode")
private fun loadLibrary(path: String, autoChmod: Boolean = true) {
if (autoChmod) Runtime.getRuntime()
.exec("chmod 755 $path").waitFor()
System.load(path)
LogCenter.log({ "加载SO文件成功 -> $path" }, Level.DEBUG)
}
}

View File

@ -1,6 +0,0 @@
@file:JvmName("ModuleInfo")
package moe.fuqiuluo.shamrock.xposed.loader.tmpnativehelper
import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader
val moduleClassLoader: ClassLoader = LuoClassloader::class.java.classLoader!!