Skip to content

Commit

Permalink
feat: encrypted message
Browse files Browse the repository at this point in the history
  • Loading branch information
fuqiuluo committed Jul 21, 2024
1 parent 7e31a9a commit da544d8
Show file tree
Hide file tree
Showing 15 changed files with 923 additions and 68 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ QwQ 是基于 LSPosed 实现的类似于QAuxiliary的通用性QQ增强LSPosed模
> 如有违反法律,请联系删除。
> 请勿在任何平台宣传,宣扬,转发本项目,请勿恶意修改企业安装包造成相关企业产生损失,如有违背,必将追责到底。
>
> 本项目在多处拥有唯一的特征,如若今后在任何闭源模块发现,将会给予警告及公示!
> 本项目代码仅提供学习与交流,不提供私有化,闭源分发!
>
> [Discord社区](https://discord.gg/MKR2wz863h)
## 兼容说明
Expand Down
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ fun getVersionName(): String {
}

dependencies {
implementation("androidx.exifinterface:exifinterface:1.3.7")
compileOnly("de.robv.android.xposed:api:82")
compileOnly(project(":qqinterface")) // oicq common interface
implementation(project(":processor")) // pre build
Expand Down
Binary file added app/src/main/assets/random_face.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ 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)
Expand Down
73 changes: 73 additions & 0 deletions app/src/main/java/moe/qwq/miko/internals/entries/EcTextElem.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package moe.qwq.miko.internals.entries

import com.tencent.qqnt.kernel.nativeinterface.LinkInfo
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import com.tencent.qqnt.kernel.nativeinterface.TextElement
import kotlinx.serialization.Serializable

@Serializable
class EcTextElem(
var atChannelId: Long? = null,
var atNtUid: String? = null,
var atRoleColor: Int? = null,
var atRoleId: Long? = null,
var atRoleName: String? = null,
var atTinyId: Long = 0,
var atType: Int = 0,
var atUid: Long = 0,
var content: String? = null,
var linkInfo: EcLinkInfo? = null,
var needNotify: Int? = null,
var subElementType: Int? = null
) {
companion object {
fun from(text: TextElement): EcTextElem {
return EcTextElem(
atChannelId = text.atChannelId,
atNtUid = text.atNtUid,
atRoleColor = text.atRoleColor,
atRoleId = text.atRoleId,
atRoleName = text.atRoleName,
atTinyId = text.atTinyId,
atType = text.atType,
atUid = text.atUid,
content = text.content,
linkInfo = text.linkInfo?.let { EcLinkInfo(it.icon, it.tencentDocType, it.title) },
needNotify = text.needNotify,
subElementType = text.subElementType
)
}
}

fun toTextElement(): TextElement {
return TextElement().apply {
atChannelId = this@EcTextElem.atChannelId
atNtUid = this@EcTextElem.atNtUid
atRoleColor = this@EcTextElem.atRoleColor
atRoleId = this@EcTextElem.atRoleId
atRoleName = this@EcTextElem.atRoleName
atTinyId = this@EcTextElem.atTinyId
atType = this@EcTextElem.atType
atUid = this@EcTextElem.atUid
content = this@EcTextElem.content
linkInfo = this@EcTextElem.linkInfo?.let { LinkInfo(it.title, it.icon, it.tencentDocType) }
needNotify = this@EcTextElem.needNotify
subElementType = this@EcTextElem.subElementType
}
}

fun toMsgElement(): MsgElement {
return MsgElement().apply {
elementType = MsgConstant.KELEMTYPETEXT
textElement = toTextElement()
}
}
}

@Serializable
class EcLinkInfo(
var icon: String? = null,
var tencentDocType: Int? = null,
var title: String? = null,
)
176 changes: 176 additions & 0 deletions app/src/main/java/moe/qwq/miko/internals/helper/MessageCrypt.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
@file:OptIn(ExperimentalSerializationApi::class)

package moe.qwq.miko.internals.helper

import android.graphics.BitmapFactory
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import com.tencent.qqnt.kernel.nativeinterface.PicElement
import com.tencent.qqnt.kernel.nativeinterface.QQNTWrapperUtil
import com.tencent.qqnt.kernel.nativeinterface.RichMediaFilePathInfo
import kotlinx.io.core.BytePacketBuilder
import kotlinx.io.core.readBytes
import kotlinx.io.core.use
import kotlinx.io.core.writeFully
import kotlinx.io.streams.inputStream
import kotlinx.serialization.encodeToByteArray
import kotlinx.serialization.protobuf.ProtoBuf
import moe.fuqiuluo.xposed.loader.LuoClassloader
import moe.qwq.miko.internals.entries.EcTextElem
import moe.qwq.miko.internals.setting.QwQSetting
import moe.qwq.miko.tools.FileUtils
import java.security.MessageDigest
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import androidx.exifinterface.media.ExifInterface
import de.robv.android.xposed.XposedBridge
import kotlinx.io.core.ByteReadPacket
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.decodeFromByteArray

object MessageCrypt {
private val IV_AES by lazy { "5201314fuqiuluo!".toByteArray() }
private val randomPicDir by lazy {
QwQSetting.dataDir.resolve("randomFaces").also {
if (it.isFile) it.delete()
if (!it.exists()) {
it.mkdirs()
val randomFaceFile = it.resolve("base.gif")
randomFaceFile.outputStream().use {
LuoClassloader.moduleLoader.getResourceAsStream("assets/random_face.gif").use { origin ->
origin.copyTo(it)
}
}
}
}
}
private val tmpPicDir by lazy {
QwQSetting.dataDir.resolve("tmp").also {
if (it.isFile) it.delete()
if (!it.exists()) it.mkdirs()
}
}

fun decrypt(data: ByteArray, keyStr: String): Result<ArrayList<MsgElement>> {
val aesKey = md5(keyStr)
val decrypt = aesDecrypt(data, aesKey)
val reader = ByteReadPacket(decrypt)
val result = ArrayList<MsgElement>()
repeat(reader.readInt()) {
when (val elementType = reader.readInt()) {
MsgConstant.KELEMTYPETEXT -> {
val text = ProtoBuf.decodeFromByteArray<EcTextElem>(reader.readBytes(reader.readInt()))
result.add(text.toMsgElement())
}
else -> return Result.failure(RuntimeException("Unsupported msg type: $elementType"))
}
}
return Result.success(result)
}

fun encrypt(msgs: ArrayList<MsgElement>, uin: String, keyStr: String): Result<MsgElement> {
// 加上uin作为Hash判断,防止别人转发加密后的消息后被解密解密
// 这种转发的加密消息不该被解密!
val keyHash = (keyStr + uin).hashCode()
val msgBuilder = BytePacketBuilder()
msgBuilder.writeInt(msgs.size)
msgs.forEach { msg ->
msgBuilder.writeInt(msg.elementType)
when (msg.elementType) {
MsgConstant.KELEMTYPETEXT -> {
val encrypt = ProtoBuf.encodeToByteArray(EcTextElem.from(msg.textElement))
msgBuilder.writeInt(encrypt.size)
msgBuilder.writeFully(encrypt)
}

// TODO support more msg
else -> return Result.failure(RuntimeException("Unsupported msg type: ${msg.elementType}"))
}
}

val data = msgBuilder.build().readBytes()
val aesKey = md5(keyStr)

val builder = BytePacketBuilder()
val encrypt = aesEncrypt(data, aesKey)
builder.writeFully(encrypt)
builder.writeInt(encrypt.size)
builder.writeInt(keyHash)
builder.writeInt(0x114514)

val tmpFile = tmpPicDir.resolve("${System.currentTimeMillis()}.tmp")
tmpFile.outputStream().use { out ->
(randomPicDir
.listFiles { f -> f.isFile }
?.random() ?: return Result.failure(RuntimeException("No random face found"))).inputStream().use {
it.copyTo(out)
}
builder.build().inputStream().use { it.copyTo(out) }
}

val elem = MsgElement()
elem.elementType = MsgConstant.KELEMTYPEPIC
val pic = PicElement()
pic.md5HexStr = QQNTWrapperUtil.CppProxy.genFileMd5Hex(tmpFile.absolutePath)

val msgService = NTServiceFetcher.kernelService.msgService!!
val originalPath = msgService.getRichMediaFilePathForMobileQQSend(RichMediaFilePathInfo(2, 0, pic.md5HexStr, tmpFile.name, 1, 0, null, "", true))

//XposedBridge.log("${pic.md5HexStr} encrypt: $originalPath")

if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize(originalPath) != tmpFile.length()) {
val thumbPath = msgService.getRichMediaFilePathForMobileQQSend(RichMediaFilePathInfo(2, 0, pic.md5HexStr, tmpFile.name, 2, 720, null, "", true))
QQNTWrapperUtil.CppProxy.copyFile(tmpFile.absolutePath, originalPath)
QQNTWrapperUtil.CppProxy.copyFile(tmpFile.absolutePath, thumbPath)
}

val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(originalPath, options)
val exifInterface = ExifInterface(originalPath!!)
val orientation = exifInterface.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_UNDEFINED
)
if (orientation != ExifInterface.ORIENTATION_ROTATE_90 && orientation != ExifInterface.ORIENTATION_ROTATE_270) {
pic.picWidth = options.outWidth
pic.picHeight = options.outHeight
} else {
pic.picWidth = options.outHeight
pic.picHeight = options.outWidth
}
pic.sourcePath = originalPath
pic.fileSize = QQNTWrapperUtil.CppProxy.getFileSize(originalPath)
pic.original = true
pic.picType = FileUtils.getPicType(tmpFile)
pic.picSubType = 0
pic.isFlashPic = false

elem.picElement = pic
tmpFile.delete()
return Result.success(elem)
}

private fun md5(str: String): ByteArray {
val md = MessageDigest.getInstance("MD5")
val bytes = md.digest(str.toByteArray())
return bytes
}

private fun aesEncrypt(data: ByteArray, key: ByteArray): ByteArray {
val mAlgorithmParameterSpec = IvParameterSpec(IV_AES)
val mSecretKeySpec = SecretKeySpec(key, "AES")
val mCipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
mCipher.init(Cipher.ENCRYPT_MODE, mSecretKeySpec, mAlgorithmParameterSpec)
return mCipher.doFinal(data)
}

private fun aesDecrypt(data: ByteArray, key: ByteArray): ByteArray {
val mAlgorithmParameterSpec = IvParameterSpec(IV_AES)
val mSecretKeySpec = SecretKeySpec(key, "AES")
val mCipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
mCipher.init(Cipher.DECRYPT_MODE, mSecretKeySpec, mAlgorithmParameterSpec)
return mCipher.doFinal(data)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ package moe.qwq.miko.internals.helper
import com.google.protobuf.UnknownFieldSet
import com.tencent.qqnt.kernel.api.IKernelService
import com.tencent.qqnt.kernel.api.impl.MsgService
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
import de.robv.android.xposed.XposedBridge
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.protobuf.ProtoBuf
import moe.fuqiuluo.entries.MessagePush
import moe.qwq.miko.ext.hookMethod
import moe.qwq.miko.internals.AioListener
import moe.qwq.miko.internals.hooks.MessageHook

internal object NTServiceFetcher {
private lateinit var iKernelService: IKernelService
private var curKernelHash = 0
private var isMsgListenerHookLoaded = false

fun onFetch(service: IKernelService) {
val msgService = service.msgService ?: return
Expand All @@ -33,6 +36,23 @@ internal object NTServiceFetcher {

private fun initNTKernel(msgService: MsgService) {
XposedBridge.log("[QwQ] Init NT Kernel.")

msgService.javaClass.hookMethod("addMsgListener").before {
val listener = it.args[0]
if (isMsgListenerHookLoaded) return@before
listener.javaClass.hookMethod("onRecvMsg").before {
val msgs = it.args[0] as ArrayList<MsgRecord>
msgs.forEach { msg ->
MessageHook.tryHandleMessageDecrypt(msg)
}
}

listener.javaClass.hookMethod("onAddSendMsg").before {
val record = it.args[0] as MsgRecord
MessageHook.tryHandleMessageDecrypt(record)
}
}

kernelService.wrapperSession.javaClass.hookMethod("onMsfPush").before {
runCatching {
val cmd = it.args[0] as String
Expand Down
47 changes: 46 additions & 1 deletion app/src/main/java/moe/qwq/miko/internals/hooks/MessageHook.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,57 @@ import com.tencent.mobileqq.qroute.QRoute
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
import com.tencent.qqnt.kernel.nativeinterface.RichMediaFilePathInfo
import com.tencent.qqnt.kernel.nativeinterface.TextElement
import com.tencent.qqnt.msg.api.IMsgService
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XposedBridge
import moe.fuqiuluo.processor.HookAction
import moe.qwq.miko.actions.ActionProcess
import moe.qwq.miko.actions.IAction
import moe.qwq.miko.internals.helper.MessageCrypt
import moe.qwq.miko.internals.helper.NTServiceFetcher
import moe.qwq.miko.internals.helper.msgService
import moe.qwq.miko.internals.setting.QwQSetting
import moe.qwq.miko.tools.PlatformTools
import mqq.app.MobileQQ
import java.io.File
import java.io.RandomAccessFile

@HookAction("发送消息预劫持")
class MessageHook: IAction {
companion object {
fun tryHandleMessageDecrypt(record: MsgRecord) {
val encrypt by QwQSetting.getSetting<String>(QwQSetting.MESSAGE_ENCRYPT)
if (encrypt.isBlank()) return
if (record.elements.size != 1 || record.elements.first().elementType != MsgConstant.KELEMTYPEPIC) return

val pic = record.elements.first().picElement
val msgService = NTServiceFetcher.kernelService.msgService!!
val originalPath = msgService.getRichMediaFilePathForMobileQQSend(
RichMediaFilePathInfo(2, 0, pic.md5HexStr, "", 1, 0, null, "", true)
) ?: return
val originalFile = RandomAccessFile(originalPath, "r")
val length = originalFile.length()
originalFile.seek(length - 12)
val dataSize = originalFile.readInt()
val hash = originalFile.readInt()
val magic = originalFile.readInt()
if (magic == 0x114514 && hash == (encrypt + record.senderUin).hashCode()) {
val data = ByteArray(dataSize)
originalFile.seek(length - 12 - dataSize)
originalFile.read(data)
originalFile.close()
MessageCrypt.decrypt(data, encrypt).onSuccess {
record.elements.clear()
record.elements.addAll(it)
}.onFailure {
XposedBridge.log("消息解密失败: ${it.stackTraceToString()}")
}
}
}
}

private fun handleMessageBody(msgs: ArrayList<MsgElement>) {
if (msgs.isActionMsg()) return
val tail by QwQSetting.getSetting<String>(name)
Expand All @@ -37,7 +77,12 @@ class MessageHook: IAction {
}

private fun handleMessageEncrypt(msgs: ArrayList<MsgElement>, encryptKey: String) {

MessageCrypt.encrypt(msgs, PlatformTools.app.currentAccountUin, encryptKey).onFailure {
XposedBridge.log("[QwQ] 消息加密失败: ${it.stackTraceToString()}")
}.onSuccess {
msgs.clear()
msgs.add(it)
}
}

override fun onRun(ctx: Context) {
Expand Down
Loading

0 comments on commit da544d8

Please sign in to comment.