Merge branch 'master' of github.com:whitechi73/OpenShamrock

This commit is contained in:
ikechan8370 2023-11-27 00:18:56 +08:00
commit 69bc80e9b3
28 changed files with 547 additions and 297 deletions

View File

@ -18,6 +18,8 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Shamrock" android:theme="@style/Theme.Shamrock"
android:zygotePreloadName="@string/app_name" android:zygotePreloadName="@string/app_name"
android:multiArch="true"
android:extractNativeLibs="true"
tools:targetApi="31"> tools:targetApi="31">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"

View File

@ -224,6 +224,16 @@ object ShamrockConfig {
preferences.edit().putBoolean("debug", v).apply() preferences.edit().putBoolean("debug", v).apply()
} }
fun isAntiTrace(ctx: Context): Boolean {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getBoolean("anti_qq_trace", true)
}
fun setAntiTrace(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putBoolean("anti_qq_trace", v).apply()
}
fun isInjectPacket(ctx: Context): Boolean { fun isInjectPacket(ctx: Context): Boolean {
val preferences = ctx.getSharedPreferences("config", 0) val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getBoolean("inject_packet", false) return preferences.getBoolean("inject_packet", false)
@ -293,6 +303,7 @@ object ShamrockConfig {
"ssl_pwd" to preferences.getString("ssl_pwd", ""), "ssl_pwd" to preferences.getString("ssl_pwd", ""),
"inject_packet" to preferences.getBoolean("inject_packet", false), "inject_packet" to preferences.getBoolean("inject_packet", false),
"debug" to preferences.getBoolean("debug", false), "debug" to preferences.getBoolean("debug", false),
"anti_qq_trace" to preferences.getBoolean("anti_qq_trace", true),
"auto_clear" to preferences.getBoolean("auto_clear", false), "auto_clear" to preferences.getBoolean("auto_clear", false),
"ssl_private_pwd" to preferences.getString("ssl_private_pwd", ""), "ssl_private_pwd" to preferences.getString("ssl_private_pwd", ""),
"key_store" to preferences.getString("key_store", ""), "key_store" to preferences.getString("key_store", ""),

View File

@ -1,7 +1,6 @@
package moe.fuqiuluo.shamrock.ui.fragment package moe.fuqiuluo.shamrock.ui.fragment
import android.content.Context import android.content.Context
import android.widget.Toast
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.absolutePadding import androidx.compose.foundation.layout.absolutePadding
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -85,6 +84,17 @@ fun LabFragment() {
ShamrockConfig.pushUpdate(ctx) ShamrockConfig.pushUpdate(ctx)
return@Function true return@Function true
} }
Function(
title = "防止调用栈检测",
desc = "防止QQ进行堆栈跟踪检测需要重新启动QQ。",
descColor = it,
isSwitch = ShamrockConfig.isAntiTrace(ctx)
) {
ShamrockConfig.setAntiTrace(ctx, it)
ShamrockConfig.pushUpdate(ctx)
return@Function true
}
} }
} }

View File

@ -33,7 +33,7 @@ public class MMKV implements SharedPreferences, SharedPreferences.Editor {
return null; return null;
} }
public SharedPreferences.Editor putBoolean(String str, boolean z) { public SharedPreferences.Editor putBoolean(String s, boolean z) {
return this; return this;
} }

View File

@ -96,5 +96,11 @@ dependencies {
//ksp("androidx.room:room-compiler:$roomVersion") //ksp("androidx.room:room-compiler:$roomVersion")
// optional - Kotlin Extensions and Coroutines support for Room // optional - Kotlin Extensions and Coroutines support for Room
implementation("androidx.room:room-ktx:$roomVersion") implementation("androidx.room:room-ktx:$roomVersion")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation(platform("androidx.compose:compose-bom:2023.06.01"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
} }

View File

@ -0,0 +1 @@
libclover.so

View File

@ -6,11 +6,14 @@
# Sets the minimum CMake version required for this project. # Sets the minimum CMake version required for this project.
cmake_minimum_required(VERSION 3.22.1) cmake_minimum_required(VERSION 3.22.1)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Declares the project name. The project name can be accessed via ${ PROJECT_NAME}, # Declares the project name. The project name can be accessed via ${ PROJECT_NAME},
# Since this is the top level CMakeLists.txt, the project name is also accessible # Since this is the top level CMakeLists.txt, the project name is also accessible
# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level # with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level
# build script scope). # build script scope).
project("xposed") project("clover")
# Creates and names a library, sets it as either STATIC # Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code. # or SHARED, and provides the relative paths to its source code.
@ -27,7 +30,7 @@ project("xposed")
# used in the AndroidManifest.xml file. # used in the AndroidManifest.xml file.
add_library(${CMAKE_PROJECT_NAME} SHARED add_library(${CMAKE_PROJECT_NAME} SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt. # List C/C++ source files with relative paths to this CMakeLists.txt.
xposed.cpp) clover.cpp)
# Specifies libraries CMake should link to your target library. You # Specifies libraries CMake should link to your target library. You
# can link libraries from various origins, such as libraries defined in this # can link libraries from various origins, such as libraries defined in this

View File

@ -0,0 +1,116 @@
#include <jni.h>
#include <string>
#include <utility>
#include <initializer_list>
#include <vector>
#include <sys/auxv.h>
#include <android/log.h>
#include <dlfcn.h>
#include <string_view>
#include "lsposed.h"
#include "jnihelper.h"
static HookFunType hook_function = nullptr;
static std::vector<std::string> qemu_detect_props = {
"init.svc.qemu-props", "qemu.hw.mainkeys", "qemu.sf.fake_camera", "ro.kernel.android.qemud",
"qemu.sf.lcd_density", "init.svc.qemud", "ro.kernel.qemu",
"libc.debug.malloc"
};
int (*backup_system_property_get)(const char *name, char *value);
int fake_system_property_get(const char *name, char *value) {
for (auto &prop: qemu_detect_props) {
if (strstr(name, prop.c_str())) {
LOGI("[Shamrock] bypass qemu detection");
value[0] = 0;
return 0;
}
}
if (strstr(name, "ro.debuggable")
|| strstr(name, "ro.kernel.qemu.gles")
|| strstr(name, "debug.atrace.tags.enableflags")) {
strcpy(value, "0");
return 1;
}
if (strstr(name, "ro.product.cpu.abilist")) {
int len = backup_system_property_get(name, value);
if (len > 0) {
if (strstr(value, "x86")) {
strcpy(value, "arm64-v8a,armeabi-v7a,armeabi");
return 29;
}
}
return len;
}
if (strstr(name, "ro.hardware")) {
int len = backup_system_property_get(name, value);
if (len > 0) {
if (strstr(value, "generic")
|| strstr(value, "unknown")
|| strstr(value, "emulator")
|| strstr(value, "vbox")
|| strstr(value, "genymotion")
|| strstr(value, "goldfish")) {
strcpy(value, "qcom");
return 4;
}
}
return len;
}
//LOGI("[Shamrock] fake_system_property_get(%s)", name);
return backup_system_property_get(name, value);
}
void on_library_loaded(const char *name, void *handle) {
auto libraryName = std::string(name);
if (libraryName.ends_with("libc.so") || libraryName.ends_with("libfekit.so")) {
void *target = dlsym(handle, "__system_property_get");
if (target != nullptr) {
hook_function(target, (void *)fake_system_property_get, (void **) &backup_system_property_get);
} else {
LOGE("[Shamrock] failed to hook __system_property_get");
}
}
}
extern "C" [[gnu::visibility("default")]] [[gnu::used]]
jint JNI_OnLoad(JavaVM *jvm, void*) {
JNIHelper::initJavaVM(jvm);
int attach = 0;
JNIEnv *env = JNIHelper::getJNIEnv(&attach);
// do something
LOGI("[Shamrock] JNI_OnLoad NativeModule Init: %p", env);
if (attach == 1) {
JNIHelper::delJNIEnv();
}
//hook_function((void *)env->functions->FindClass, (void *)fake_FindClass, (void **)&backup_FindClass);
return JNI_VERSION_1_6;
}
extern "C" [[gnu::visibility("default")]] [[gnu::used]]
NativeOnModuleLoaded native_init(const NativeAPIEntries *entries) {
hook_function = entries->hook_func;
LOGI("[Shamrock] LSPosed NativeModule Init: %p", hook_function);
return on_library_loaded;
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_moe_fuqiuluo_shamrock_xposed_XposedEntry_00024Companion_injected(JNIEnv *env, jobject thiz) {
return hook_function != nullptr;
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_moe_fuqiuluo_shamrock_xposed_XposedEntry_00024Companion_hasEnv(JNIEnv *env, jobject thiz) {
return JNIHelper::global_jvm != nullptr;
}

View File

@ -0,0 +1,40 @@
#ifndef SHAMROCK_JNIHELPER_H
#define SHAMROCK_JNIHELPER_H
#include "android/log.h"
namespace JNIHelper {
static JavaVM *global_jvm = nullptr;
void initJavaVM(JavaVM *jvm) {
global_jvm = jvm;
}
JNIEnv *getJNIEnv(int *attach) {
if (global_jvm == NULL) return NULL;
*attach = 0;
JNIEnv *jni_env = NULL;
int status = global_jvm->GetEnv((void **)&jni_env, JNI_VERSION_1_6);
if (status == JNI_EDETACHED || jni_env == NULL) {
status = global_jvm->AttachCurrentThread(&jni_env, NULL);
if (status < 0) {
jni_env = NULL;
} else {
*attach = 1;
}
}
return jni_env;
}
jint delJNIEnv() {
if (global_jvm == nullptr) return 0;
return global_jvm->DetachCurrentThread();
}
}
#endif //SHAMROCK_JNIHELPER_H

View File

@ -0,0 +1,27 @@
#ifndef SHAMROCK_LSPOSED_H
#define SHAMROCK_LSPOSED_H
#include "stdint.h"
#define TAG "LSPosed-Bridge"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG ,__VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG ,__VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG ,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG ,__VA_ARGS__)
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,TAG ,__VA_ARGS__)
typedef int (*HookFunType)(void *func, void *replace, void **backup);
typedef int (*UnhookFunType)(void *func);
typedef void (*NativeOnModuleLoaded)(const char *name, void *handle);
typedef struct {
uint32_t version;
HookFunType hook_func;
UnhookFunType unhook_func;
} NativeAPIEntries;
typedef NativeOnModuleLoaded (*NativeInit)(const NativeAPIEntries *entries);
#endif //SHAMROCK_LSPOSED_H

View File

@ -1,5 +0,0 @@
#include <jni.h>
#include <string>
#include <utility>
#include <sys/auxv.h>

View File

@ -318,7 +318,7 @@ internal object GroupSvc: BaseSvc() {
fun getOwner(groupId: String): Long { fun getOwner(groupId: String): Long {
val groupInfo = getGroupInfo(groupId) val groupInfo = getGroupInfo(groupId)
return groupInfo.troopowneruin.toLong() return groupInfo.troopowneruin?.toLong() ?: 0
} }
fun isOwner(groupId: String): Boolean { fun isOwner(groupId: String): Boolean {

View File

@ -96,9 +96,21 @@ internal object MessageMaker {
"touch" to MessageMaker::createTouchElem, "touch" to MessageMaker::createTouchElem,
"weather" to MessageMaker::createWeatherElem, "weather" to MessageMaker::createWeatherElem,
"json" to MessageMaker::createJsonElem, "json" to MessageMaker::createJsonElem,
//"node" to MessageMaker::createNodeElem,
//"multi_msg" to MessageMaker::createLongMsgStruct, //"multi_msg" to MessageMaker::createLongMsgStruct,
) )
// private suspend fun createNodeElem(
// chatType: Int,
// msgId: Long,
// peerId: String,
// data: JsonObject
// ): Result<MsgElement> {
// data.checkAndThrow("data")
// SendForwardMessage(MsgConstant.KCHATTYPEC2C, TicketSvc.getUin(), data["content"].asJsonArray)
//
// }
private suspend fun createJsonElem( private suspend fun createJsonElem(
chatType: Int, chatType: Int,
msgId: Long, msgId: Long,

View File

@ -51,8 +51,29 @@ internal object LogCenter {
private val format = SimpleDateFormat("[HH:mm:ss] ") private val format = SimpleDateFormat("[HH:mm:ss] ")
fun log(string: String, level: Level = Level.INFO, toast: Boolean = false) = fun log(string: String, level: Level = Level.INFO, toast: Boolean = false) {
log({ string }, level, toast) if (!ShamrockConfig.isDebug() && level == Level.DEBUG) {
return
}
if (toast) {
MobileQQ.getContext().toast(string)
}
// 把日志广播到主进程
GlobalScope.launch(Dispatchers.Default) {
DataRequester.request("send_message", bodyBuilder = {
put("string", string)
put("level", level.id)
})
}
if (!LogFile.exists()) {
LogFile.createNewFile()
}
val format = "%s%s %s\n".format(format.format(Date()), level.name, string)
LogFile.appendText(format)
}
fun log( fun log(
string: () -> String, string: () -> String,

View File

@ -180,9 +180,9 @@ internal object MessageHelper {
var hasActionMsg = false var hasActionMsg = false
messageList.forEach { messageList.forEach {
val msg = it.jsonObject val msg = it.jsonObject
try {
val maker = MessageMaker[msg["type"].asString] val maker = MessageMaker[msg["type"].asString]
if (maker != null) { if (maker != null) {
try {
val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject
maker(chatType, msgId, targetUin, data).onSuccess { msgElem -> maker(chatType, msgId, targetUin, data).onSuccess { msgElem ->
msgList.add(msgElem) msgList.add(msgElem)
@ -193,12 +193,13 @@ internal object MessageHelper {
hasActionMsg = true hasActionMsg = true
} }
} }
} else {
LogCenter.log("不支持的消息类型: ${msg["type"].asString}", Level.ERROR)
}
} catch (e: Throwable) { } catch (e: Throwable) {
LogCenter.log(e.stackTraceToString(), Level.ERROR) LogCenter.log(e.stackTraceToString(), Level.ERROR)
} }
} else {
LogCenter.log("不支持的消息类型: ${msg["type"].asString}", Level.ERROR)
return false to arrayListOf()
}
} }
return hasActionMsg to msgList return hasActionMsg to msgList
} }

View File

@ -34,8 +34,8 @@ internal object ActionManager {
GetGroupSystemMsg, GetProhibitedMemberList, GetEssenceMessageList, GetGroupNotice, SendGroupNotice, GetGroupSystemMsg, GetProhibitedMemberList, GetEssenceMessageList, GetGroupNotice, SendGroupNotice,
// MSG ACTIONS // MSG ACTIONS
SendMessage, DeleteMessage, GetMsg, GetForwardMsg, SendGroupForwardMsg, SendGroupMessage, SendPrivateMessage, SendMessage, DeleteMessage, GetMsg, GetForwardMsg, SendPrivateForwardMessage, SendGroupMessage, SendPrivateMessage,
ClearMsgs, GetHistoryMsg, GetGroupMsgHistory, SendPrivateForwardMsg, ClearMsgs, GetHistoryMsg, GetGroupMsgHistory, SendGroupForwardMessage,
// RESOURCE ACTION // RESOURCE ACTION
GetRecord, GetImage, UploadGroupFile, CreateGroupFileFolder, DeleteGroupFolder, GetRecord, GetImage, UploadGroupFile, CreateGroupFileFolder, DeleteGroupFolder,

View File

@ -0,0 +1,169 @@
package moe.fuqiuluo.shamrock.remote.action.handlers
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MultiMsgInfo
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
import moe.fuqiuluo.qqinterface.servlet.msg.convert.toSegments
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.helper.ParamsException
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.remote.service.data.ForwardMessageResult
import moe.fuqiuluo.shamrock.tools.*
import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
sealed interface ForwardMsgNode {
class MessageIdNode(
val id: Int
) : ForwardMsgNode
open class MessageNode(
val name: String,
val content: JsonElement?
) : ForwardMsgNode
object EmptyNode : MessageNode("", null)
}
internal object SendForwardMessage : IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val detailType = session.getStringOrNull("detail_type") ?: session.getStringOrNull("message_type")
try {
val chatType = detailType?.let {
MessageHelper.obtainMessageTypeByDetailType(it)
} ?: run {
if (session.has("user_id")) {
MsgConstant.KCHATTYPEC2C
} else if (session.has("group_id")) {
MsgConstant.KCHATTYPEGROUP
} else {
return noParam("detail_type/message_type", session.echo)
}
}
val peerId = when (chatType) {
MsgConstant.KCHATTYPEGROUP -> session.getStringOrNull("group_id") ?: return noParam(
"group_id",
session.echo
)
MsgConstant.KCHATTYPEC2C -> session.getStringOrNull("user_id") ?: return noParam(
"user_id",
session.echo
)
else -> error("unknown chat type: $chatType")
}
if (session.isArray("messages")) {
val messages = session.getArray("messages")
invoke(chatType, peerId, messages, echo = session.echo)
}
return logic("未知格式合并转发消息", session.echo)
} catch (e: ParamsException) {
return noParam(e.message!!, session.echo)
} catch (e: Throwable) {
return logic(e.message ?: e.toString(), session.echo)
}
}
suspend operator fun invoke(
chatType: Int,
peerId: String,
message: JsonArray,
echo: JsonElement = EmptyJsonString
): String {
kotlin.runCatching {
val kernelService = NTServiceFetcher.kernelService
val sessionService = kernelService.wrapperSession
val msgService = sessionService.msgService
val selfUin = TicketSvc.getUin()
val msgs = message.map {
if (it.asJsonObject["type"].asStringOrNull != "node") return@map ForwardMsgNode.EmptyNode // 过滤非node类型消息段
it.asJsonObject["data"].asJsonObject.let { data ->
if (data.containsKey("content")) {
data["content"].asJsonArray.forEach { msg ->
if (msg.asJsonObject["type"].asStringOrNull == "node") {
LogCenter.log("合并转发消息不支持嵌套", Level.ERROR)
return@map ForwardMsgNode.EmptyNode
}
}
ForwardMsgNode.MessageNode(
name = data["name"].asStringOrNull ?: "",
content = data["content"]
)
}
else ForwardMsgNode.MessageIdNode(data["id"].asInt)
}
}.map {
if (it is ForwardMsgNode.MessageIdNode) {
val recordResult = MsgSvc.getMsg(it.id)
if (!recordResult.isFailure) {
ForwardMsgNode.EmptyNode
} else {
val record = recordResult.getOrThrow()
ForwardMsgNode.MessageNode(
name = record.sendMemberName
.ifBlank { record.sendNickName }
.ifBlank { record.sendRemarkName }
.ifBlank { record.peerName },
content = record.toSegments().map { segment ->
segment.toJson()
}.json
)
}
} else {
it as ForwardMsgNode.MessageNode
}
}.filter {
it.content != null
}
val multiNodes = msgs.map { node ->
suspendCoroutine {
GlobalScope.launch {
var msgId: Long = 0
msgId = MessageHelper.sendMessageWithMsgId(MsgConstant.KCHATTYPEC2C, selfUin, node.content!!.let { msg ->
if (msg is JsonArray) msg else MessageHelper.decodeCQCode(msg.asString)
},
{ code, why ->
if (code != 0) {
error("合并转发消息节点消息发送失败:$code($why)")
}
it.resume(node.name to msgId)
}).first
}
}
}
val from = MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, selfUin)
val to = MessageHelper.generateContact(chatType, peerId)
val uniseq = MessageHelper.generateMsgId(chatType)
msgService.multiForwardMsg(ArrayList<MultiMsgInfo>().apply {
multiNodes.forEach { add(MultiMsgInfo(it.second, it.first)) }
}.also { it.reverse() }, from, to, MsgSvc.MessageCallback(peerId, uniseq.first))
return ok(
ForwardMessageResult(
msgId = uniseq.first,
forwardId = ""
), echo = echo)
}.onFailure {
return error("error: $it", echo)
}
return logic("合并转发消息失败(unknown error)", echo)
}
override val requiredParams: Array<String> = arrayOf("message")
override fun path(): String = "send_forward_msg"
}

View File

@ -1,233 +0,0 @@
@file:OptIn(DelicateCoroutinesApi::class)
package moe.fuqiuluo.shamrock.remote.action.handlers
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MultiMsgInfo
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
import moe.fuqiuluo.qqinterface.servlet.msg.convert.toSegments
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.tools.EmptyJsonObject
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
import moe.fuqiuluo.shamrock.tools.asInt
import moe.fuqiuluo.shamrock.tools.asJsonObject
import moe.fuqiuluo.shamrock.tools.asString
import moe.fuqiuluo.shamrock.tools.asStringOrNull
import moe.fuqiuluo.shamrock.tools.json
import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
/**
* 合并转发消息节点数据类
*/
sealed interface ForwardMsgNode {
class MessageIdNode(
val id: Int
): ForwardMsgNode
open class MessageNode(
val name: String,
val content: JsonElement?
): ForwardMsgNode
object EmptyNode: MessageNode("", null)
}
/**
* 私聊合并转发
*/
internal object SendPrivateForwardMsg: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val groupId = session.getString("user_id")
if (session.isArray("messages")) {
val messages = session.getArray("messages")
return invoke(messages, groupId, session.echo)
}
return logic("未知格式合并转发消息", session.echo)
}
suspend operator fun invoke(
message: JsonArray,
userId: String,
echo: JsonElement = EmptyJsonString
): String {
kotlin.runCatching {
val kernelService = NTServiceFetcher.kernelService
val sessionService = kernelService.wrapperSession
val msgService = sessionService.msgService
val selfUin = TicketSvc.getUin()
val msgs = message.map {
if (it.asJsonObject["type"].asStringOrNull != "node") return@map ForwardMsgNode.EmptyNode // 过滤非node类型消息段
it.asJsonObject["data"].asJsonObject.let { data ->
if (data.containsKey("content"))
ForwardMsgNode.MessageNode(
name = data["name"].asStringOrNull ?: "",
content = data["content"]
)
else ForwardMsgNode.MessageIdNode(data["id"].asInt)
}
}.map {
if (it is ForwardMsgNode.MessageIdNode) {
val recordResult = MsgSvc.getMsg(it.id)
if (recordResult.isFailure) {
ForwardMsgNode.EmptyNode
} else {
val record = recordResult.getOrThrow()
ForwardMsgNode.MessageNode(
name = record.sendMemberName
.ifBlank { record.sendNickName }
.ifBlank { record.sendRemarkName }
.ifBlank { record.peerName },
content = record.toSegments().map { segment ->
segment.toJson()
}.json
)
}
} else {
it as ForwardMsgNode.MessageNode
}
}.filter {
it.content != null
}
val multiNodes = msgs.map { node ->
suspendCoroutine {
GlobalScope.launch {
var msgId: Long = 0
msgId = MessageHelper.sendMessageWithMsgId(MsgConstant.KCHATTYPEC2C, selfUin, node.content!!.let { msg ->
if (msg is JsonArray) msg else MessageHelper.decodeCQCode(msg.asString)
}, { code, why ->
if (code != 0) {
LogCenter.log("合并转发消息节点消息发送失败:$code($why)", Level.WARN)
}
it.resume(node.name to msgId)
}).first
}
}
}
val from = MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, selfUin)
val to = MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, userId)
msgService.multiForwardMsg(ArrayList<MultiMsgInfo>().apply {
multiNodes.forEach { add(MultiMsgInfo(it.second, it.first)) }
}.also { it.reverse() }, from, to) { code, why ->
if (code != 0)
LogCenter.log("合并转发消息:$code($why)", Level.WARN)
}
return ok(data = EmptyJsonObject, echo = echo)
}.onFailure {
return error("error: $it", echo)
}
return logic("合并转发消息失败(unknown error)", echo)
}
override val requiredParams: Array<String> = arrayOf("user_id")
override fun path(): String = "send_private_forward_msg"
}
/**
* 群聊合并转发
*/
internal object SendGroupForwardMsg: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val groupId = session.getString("group_id")
if (session.isArray("messages")) {
val messages = session.getArray("messages")
return invoke(messages, groupId, session.echo)
}
return logic("未知格式合并转发消息", session.echo)
}
suspend operator fun invoke(
message: JsonArray,
groupId: String,
echo: JsonElement = EmptyJsonString
): String {
kotlin.runCatching {
val kernelService = NTServiceFetcher.kernelService
val sessionService = kernelService.wrapperSession
val msgService = sessionService.msgService
val selfUin = TicketSvc.getUin()
val msgs = message.map {
if (it.asJsonObject["type"].asStringOrNull != "node") return@map ForwardMsgNode.EmptyNode // 过滤非node类型消息段
it.asJsonObject["data"].asJsonObject.let { data ->
if (data.containsKey("content"))
ForwardMsgNode.MessageNode(
name = data["name"].asStringOrNull ?: "",
content = data["content"]
)
else ForwardMsgNode.MessageIdNode(data["id"].asInt)
}
}.map {
if (it is ForwardMsgNode.MessageIdNode) {
val recordResult = MsgSvc.getMsg(it.id)
if (recordResult.isFailure) {
ForwardMsgNode.EmptyNode
} else {
val record = recordResult.getOrThrow()
ForwardMsgNode.MessageNode(
name = record.sendMemberName
.ifBlank { record.sendNickName }
.ifBlank { record.sendRemarkName }
.ifBlank { record.peerName },
content = record.toSegments().map { segment ->
segment.toJson()
}.json
)
}
} else {
it as ForwardMsgNode.MessageNode
}
}.filter {
it.content != null
}
val multiNodes = msgs.map { node ->
suspendCoroutine {
GlobalScope.launch {
var msgId: Long = 0
msgId = MessageHelper.sendMessageWithMsgId(MsgConstant.KCHATTYPEC2C, selfUin, node.content!!.let { msg ->
if (msg is JsonArray) msg else MessageHelper.decodeCQCode(msg.asString)
}, { code, why ->
if (code != 0) {
LogCenter.log("合并转发消息节点消息发送失败:$code($why)", Level.WARN)
}
it.resume(node.name to msgId)
}).first
}
}
}
val from = MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, selfUin)
val to = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, groupId)
msgService.multiForwardMsg(ArrayList<MultiMsgInfo>().apply {
multiNodes.forEach { add(MultiMsgInfo(it.second, it.first)) }
}.also { it.reverse() }, from, to) { code, why ->
if (code != 0)
LogCenter.log("合并转发消息:$code($why)", Level.WARN)
}
return ok(data = EmptyJsonObject, echo = echo)
}.onFailure {
return error("error: $it", echo)
}
return logic("合并转发消息失败(unknown error)", echo)
}
override val requiredParams: Array<String> = arrayOf("group_id")
override fun path(): String = "send_group_forward_msg"
}

View File

@ -0,0 +1,21 @@
package moe.fuqiuluo.shamrock.remote.action.handlers;
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
internal object SendGroupForwardMessage: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val groupId = session.getString("group_id")
return if (session.isArray("messages")) {
val messages = session.getArray("messages")
SendForwardMessage(MsgConstant.KCHATTYPEGROUP, groupId, messages, session.echo)
} else {
logic("未知格式合并转发消息", session.echo)
}
}
override val requiredParams: Array<String> = arrayOf("messages", "group_id")
override fun path(): String = "send_group_forward_msg"
}

View File

@ -0,0 +1,21 @@
package moe.fuqiuluo.shamrock.remote.action.handlers;
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
internal object SendPrivateForwardMessage : IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val userId = session.getString("user_id")
return if (session.isArray("messages")) {
val messages = session.getArray("messages")
SendForwardMessage(MsgConstant.KCHATTYPEC2C, userId, messages, session.echo)
} else {
logic("未知格式合并转发消息", session.echo)
}
}
override val requiredParams: Array<String> = arrayOf("messages", "user_id")
override fun path(): String = "send_private_forward_msg"
}

View File

@ -9,12 +9,12 @@ internal object SendPrivateMessage: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String { override suspend fun internalHandle(session: ActionSession): String {
val userId = session.getString("user_id") val userId = session.getString("user_id")
val groupId = session.getStringOrNull("group_id") val groupId = session.getStringOrNull("group_id")
val chatTYpe = if (groupId == null) MsgConstant.KCHATTYPEC2C else MsgConstant.KCHATTYPETEMPC2CFROMGROUP val chatType = if (groupId == null) MsgConstant.KCHATTYPEC2C else MsgConstant.KCHATTYPETEMPC2CFROMGROUP
return if (session.isString("message")) { return if (session.isString("message")) {
val autoEscape = session.getBooleanOrDefault("auto_escape", false) val autoEscape = session.getBooleanOrDefault("auto_escape", false)
val message = session.getString("message") val message = session.getString("message")
SendMessage.invoke( SendMessage.invoke(
chatType = chatTYpe, chatType = chatType,
peerId = userId, peerId = userId,
message = message, message = message,
autoEscape = autoEscape, autoEscape = autoEscape,
@ -24,7 +24,7 @@ internal object SendPrivateMessage: IActionHandler() {
} else if (session.isArray("message")) { } else if (session.isArray("message")) {
val message = session.getArray("message") val message = session.getArray("message")
SendMessage( SendMessage(
chatType = chatTYpe, chatType = chatType,
peerId = userId, peerId = userId,
message = message, message = message,
echo = session.echo, echo = session.echo,
@ -33,7 +33,7 @@ internal object SendPrivateMessage: IActionHandler() {
} else { } else {
val message = session.getObject("message") val message = session.getObject("message")
SendMessage( SendMessage(
chatType = chatTYpe, chatType = chatType,
peerId = userId, peerId = userId,
message = listOf( message ).jsonArray, message = listOf( message ).jsonArray,
echo = session.echo, echo = session.echo,

View File

@ -33,20 +33,22 @@ fun Routing.messageAction() {
post { post {
val groupId = fetchPostOrNull("group_id") val groupId = fetchPostOrNull("group_id")
val messages = fetchPostJsonArray("messages") val messages = fetchPostJsonArray("messages")
call.respondText(SendGroupForwardMsg(messages, groupId ?: ""), ContentType.Application.Json) call.respondText(SendForwardMessage(MsgConstant.KCHATTYPEGROUP, groupId ?: "", messages), ContentType.Application.Json)
} }
get { get {
respond(false, Status.InternalHandlerError, "Not support GET method") respond(false, Status.InternalHandlerError, "Not support GET method")
} }
} }
post("/send_group_forward_msg") {
} route("/send_private_forward_msg") {
post {
post("/send_private_forward_msg") {
val userId = fetchPostOrNull("user_id") val userId = fetchPostOrNull("user_id")
val messages = fetchPostJsonArray("messages") val messages = fetchPostJsonArray("messages")
call.respondText(SendPrivateForwardMsg(messages, userId ?: ""), ContentType.Application.Json) call.respondText(SendForwardMessage(MsgConstant.KCHATTYPEC2C, userId ?: "", messages), ContentType.Application.Json)
}
get {
respond(false, Status.InternalHandlerError, "Not support GET method")
}
} }
getOrPost("/get_forward_msg") { getOrPost("/get_forward_msg") {

View File

@ -7,7 +7,6 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import moe.fuqiuluo.shamrock.remote.service.api.WebSocketClientServlet import moe.fuqiuluo.shamrock.remote.service.api.WebSocketClientServlet
import moe.fuqiuluo.shamrock.remote.service.data.push.*
import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter
@ -35,7 +34,7 @@ internal class WebSocketClientService(
} }
}) })
submitFlowJob(GlobalScope.launch { submitFlowJob(GlobalScope.launch {
GlobalEventTransmitter.onRequestEvent() { event -> GlobalEventTransmitter.onRequestEvent { event ->
pushTo(event) pushTo(event)
} }
}) })

View File

@ -43,6 +43,7 @@ internal object ShamrockConfig {
putBoolean( "debug", intent.getBooleanExtra("debug", false)) // 调试模式 putBoolean( "debug", intent.getBooleanExtra("debug", false)) // 调试模式
Config.defaultToken = intent.getStringExtra("token") Config.defaultToken = intent.getStringExtra("token")
Config.antiTrace = intent.getBooleanExtra("anti_qq_trace", true)
val wsPort = intent.getIntExtra("ws_port", 5800) val wsPort = intent.getIntExtra("ws_port", 5800)
Config.activeWebSocket = if (Config.activeWebSocket == null) ConnectionConfig( Config.activeWebSocket = if (Config.activeWebSocket == null) ConnectionConfig(

View File

@ -9,6 +9,11 @@ internal data class MessageResult(
@SerialName("message_id") val msgId: Int, @SerialName("message_id") val msgId: Int,
@SerialName("time") val time: Long @SerialName("time") val time: Long
) )
@Serializable
internal data class ForwardMessageResult(
@SerialName("message_id") val msgId: Int,
@SerialName("forward_id") val forwardId: String
)
@Serializable @Serializable
internal data class MessageDetail( internal data class MessageDetail(

View File

@ -5,6 +5,8 @@ import de.robv.android.xposed.IXposedHookLoadPackage
import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.callbacks.XC_LoadPackage import de.robv.android.xposed.callbacks.XC_LoadPackage
import de.robv.android.xposed.XposedBridge.log import de.robv.android.xposed.XposedBridge.log
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.utils.MMKVFetcher import moe.fuqiuluo.shamrock.utils.MMKVFetcher
import moe.fuqiuluo.shamrock.xposed.loader.ActionLoader import moe.fuqiuluo.shamrock.xposed.loader.ActionLoader
import moe.fuqiuluo.shamrock.xposed.loader.FuckAMS import moe.fuqiuluo.shamrock.xposed.loader.FuckAMS
@ -12,6 +14,7 @@ import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader
import moe.fuqiuluo.shamrock.tools.FuzzySearchClass import moe.fuqiuluo.shamrock.tools.FuzzySearchClass
import moe.fuqiuluo.shamrock.tools.afterHook import moe.fuqiuluo.shamrock.tools.afterHook
import moe.fuqiuluo.shamrock.utils.PlatformUtils import moe.fuqiuluo.shamrock.utils.PlatformUtils
import moe.fuqiuluo.shamrock.xposed.loader.NativeLoader
import mqq.app.MobileQQ import mqq.app.MobileQQ
import java.lang.reflect.Field import java.lang.reflect.Field
import java.lang.reflect.Modifier import java.lang.reflect.Modifier
@ -26,6 +29,12 @@ internal class XposedEntry: IXposedHookLoadPackage {
companion object { companion object {
@JvmStatic @JvmStatic
var sec_static_stage_inited = false var sec_static_stage_inited = false
@JvmStatic
var sec_static_nativehook_inited = false
external fun injected(): Boolean
external fun hasEnv(): Boolean
} }
private var firstStageInit = false private var firstStageInit = false
@ -108,7 +117,6 @@ internal class XposedEntry: IXposedHookLoadPackage {
if (sec_static_stage_inited) return if (sec_static_stage_inited) return
val classLoader = ctx.classLoader.also { requireNotNull(it) } val classLoader = ctx.classLoader.also { requireNotNull(it) }
LuoClassloader.hostClassLoader = classLoader LuoClassloader.hostClassLoader = classLoader
if(injectClassloader(XposedEntry::class.java.classLoader)) { if(injectClassloader(XposedEntry::class.java.classLoader)) {
@ -116,12 +124,7 @@ internal class XposedEntry: IXposedHookLoadPackage {
System.setProperty("qxbot_flag", "1") System.setProperty("qxbot_flag", "1")
} else return } else return
log("Process Name = " + MobileQQ.getMobileQQ().qqProcessName.apply { log("Process Name = " + MobileQQ.getMobileQQ().qqProcessName)
// if (!contains("msf", ignoreCase = true)) return // 非MSF进程 退出
//if (contains("peak")) {
// PlatformUtils.killProcess(ctx, this)
//}
})
PlatformUtils.isTim() PlatformUtils.isTim()

View File

@ -5,7 +5,6 @@ import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.VersionedPackage import android.content.pm.VersionedPackage
import android.os.Build import android.os.Build
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XC_MethodReplacement import de.robv.android.xposed.XC_MethodReplacement
import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.XposedHelpers import de.robv.android.xposed.XposedHelpers
@ -13,8 +12,9 @@ import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
import moe.fuqiuluo.shamrock.tools.hookMethod import moe.fuqiuluo.shamrock.tools.hookMethod
import moe.fuqiuluo.shamrock.xposed.XposedEntry
import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader
import mqq.app.MobileQQ import moe.fuqiuluo.shamrock.xposed.loader.NativeLoader
/** /**
* 反检测 * 反检测
@ -22,6 +22,7 @@ import mqq.app.MobileQQ
class AntiDetection: IAction { class AntiDetection: IAction {
override fun invoke(ctx: Context) { override fun invoke(ctx: Context) {
antiFindPackage(ctx) antiFindPackage(ctx)
antiNativeDetection()
if (ShamrockConfig.isAntiTrace()) if (ShamrockConfig.isAntiTrace())
antiTrace() antiTrace()
antiMemoryWalking() antiMemoryWalking()
@ -38,6 +39,23 @@ class AntiDetection: IAction {
return false return false
} }
private fun antiNativeDetection() {
try {
//System.loadLibrary("clover")
NativeLoader.load("clover")
val env = XposedEntry.hasEnv()
val injected = XposedEntry.injected()
if (!env || !injected) {
LogCenter.log("[Shamrock] Shamrock反检测启动失败(env=$env, injected=$injected)", Level.ERROR)
} else {
XposedEntry.sec_static_nativehook_inited = true
LogCenter.log("[Shamrock] Shamrock反检测启动成功", Level.INFO)
}
} catch (e: Throwable) {
LogCenter.log("[Shamrock] Shamrock反检测启动失败请检查LSPosed版本使用大于100: ${e.message}", Level.ERROR)
}
}
private fun antiFindPackage(context: Context) { private fun antiFindPackage(context: Context) {
val packageManager = context.packageManager val packageManager = context.packageManager
val applicationInfo = packageManager.getApplicationInfo("moe.fuqiuluo.shamrock", 0) val applicationInfo = packageManager.getApplicationInfo("moe.fuqiuluo.shamrock", 0)

View File

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.XposedBridge
import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.xposed.XposedEntry
import mqq.app.MobileQQ import mqq.app.MobileQQ
import java.io.File import java.io.File
@ -16,15 +17,13 @@ internal object NativeLoader {
return externalLibPath.resolve("libffmpegkit.so").exists() return externalLibPath.resolve("libffmpegkit.so").exists()
} }
private var isInitShamrock = false
/** /**
* 使目标进程可以使用来自模块的库 * 使目标进程可以使用来自模块的库
*/ */
@SuppressLint("UnsafeDynamicallyLoadedCode") @SuppressLint("UnsafeDynamicallyLoadedCode")
fun load(name: String) { fun load(name: String) {
try { try {
if (name == "shamrock") { if (name == "shamrock" || name == "clover") {
val context = MobileQQ.getContext() val context = MobileQQ.getContext()
val packageManager = context.packageManager val packageManager = context.packageManager
val applicationInfo = packageManager.getApplicationInfo("moe.fuqiuluo.shamrock.hided", 0) val applicationInfo = packageManager.getApplicationInfo("moe.fuqiuluo.shamrock.hided", 0)