Skip to content

Commit

Permalink
support: message encrypt
Browse files Browse the repository at this point in the history
  • Loading branch information
fuqiuluo committed Jul 23, 2024
1 parent 3f995b7 commit 2d6898c
Show file tree
Hide file tree
Showing 16 changed files with 461 additions and 272 deletions.
19 changes: 18 additions & 1 deletion app/src/main/java/moe/fuqiuluo/entries/MessagePush.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,22 @@ data class MessageContentInfo(

@Serializable
data class MessageBody(
@ProtoNumber(2) val richMsg: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(1) val richMsg: NTRichText? = null,
@ProtoNumber(2) val msgContent: ByteArray? = EMPTY_BYTE_ARRAY,
)

@Serializable
data class NTRichText(
@ProtoNumber(2) val elems: ArrayList<Elem>? = null
)

@Serializable
data class Elem(
@ProtoNumber(1) val text: Text? = null,
)

@Serializable
data class Text(
@ProtoNumber(1) val text: String? = null,
@ProtoNumber(12) val resv: ByteArray? = null
)
10 changes: 10 additions & 0 deletions app/src/main/java/moe/fuqiuluo/entries/MsgELem.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package moe.fuqiuluo.entries

import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber


@Serializable
data class TextMsgExtPbResvAttr(
@ProtoNumber(1) val wording: ByteArray?,
)
18 changes: 18 additions & 0 deletions app/src/main/java/moe/fuqiuluo/entries/QQSecuritySign.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package moe.fuqiuluo.entries

import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber

/*
message SsoSecureInfo {
required bytes sec_sig = 1;
required bytes device_token = 2;
required bytes extra = 3;
}
*/
@Serializable
data class QQSsoSecureInfo(
@ProtoNumber(1) val secSig: ByteArray,
@ProtoNumber(2) val deviceToken: ByteArray,
@ProtoNumber(3) val extra: ByteArray
)
57 changes: 56 additions & 1 deletion app/src/main/java/moe/qwq/miko/actions/HookCodec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@ import android.content.Context
import de.robv.android.xposed.XposedBridge
import moe.fuqiuluo.processor.HookAction
import moe.fuqiuluo.xposed.loader.LuoClassloader
import moe.qwq.miko.ext.EMPTY_BYTE_ARRAY
import moe.qwq.miko.ext.beforeHook
import moe.qwq.miko.ext.hookMethod
import moe.qwq.miko.ext.toHexString
import moe.qwq.miko.ext.toInnerValuesString
import moe.qwq.miko.internals.hijackers.IHijacker
import moe.qwq.miko.utils.PlatformTools

@HookAction(desc = "HookWrapperCodec实现捕获抓包")
class HookCodec: AlwaysRunAction() {
Expand All @@ -15,9 +20,59 @@ class HookCodec: AlwaysRunAction() {
if (CodecWarpper == null) {
XposedBridge.log("[QwQ] 无法注入CodecWarpper!")
return
}
}/* else {
XposedBridge.log("[QwQ] 注入CodecWarpper成功!")
}*/
CodecWarpper.hookMethod("nativeEncodeRequest", beforeHook {
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// public static byte[] nativeEncodeRequest(int seq, String str, String str2, String str3, String str4, String cmd, byte[] bArr, int i2, int i3, String uin, byte b, byte b2, byte[] buffer, boolean z)
// public static byte[] nativeEncodeRequest(int seq, String str, String str2, String str3, String str4, String cmd, byte[] bArr, int i2, int i3, String uin, byte b, byte b2, byte[] bArr2, byte[] bArr3, byte[] buffer, boolean z)
// public static byte[] nativeEncodeRequest(int seq, String str, String str2, String str3, String str4, String cmd, byte[] bArr, int i2, int i3, String uin, byte b, byte b2, byte b3, byte[] bArr2, byte[] bArr3, byte[] buffer, boolean z)
//XposedBridge.log(it.toInnerValuesString())

val uin: String
val seq: Int
val buffer: ByteArray
val cmd: String
val bufferIndex: Int
val msgCookie: ByteArray?
when(it.args.size) {
14 -> {
seq = it.args[0] as Int
cmd = it.args[5] as String
buffer = it.args[12] as ByteArray
bufferIndex = 12
msgCookie = it.args[6] as? ByteArray
uin = it.args[9] as String
}
16 -> {
seq = it.args[0] as Int
cmd = it.args[5] as String
buffer = it.args[14] as ByteArray
bufferIndex = 14
msgCookie = it.args[6] as? ByteArray
uin = it.args[9] as String
}
17 -> {
seq = it.args[0] as Int
cmd = it.args[5] as String
buffer = it.args[15] as ByteArray
bufferIndex = 15
msgCookie = it.args[6] as? ByteArray
uin = it.args[9] as String
}
else -> throw RuntimeException("nativeEncodeRequest参数个数不匹配")
}
//XposedBridge.log("nativeEncodeRequest: $seq $cmd ${buffer.toHexString()}")
if (hijackers.firstOrNull { it.command == cmd }?.onHandle(it, uin, cmd, seq, buffer, bufferIndex) == true) {
it.result = EMPTY_BYTE_ARRAY
}
})
}

override val process: ActionProcess = ActionProcess.MSF

companion object {
val hijackers = arrayListOf<IHijacker>()
}
}
20 changes: 20 additions & 0 deletions app/src/main/java/moe/qwq/miko/ext/Kotlinx.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
package moe.qwq.miko.ext

import de.robv.android.xposed.XposedBridge
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.newCoroutineContext
import java.util.Locale
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext

val EMPTY_BYTE_ARRAY = ByteArray(0)

Expand Down Expand Up @@ -62,3 +70,15 @@ fun String?.ifNullOrEmpty(defaultValue: () -> String?): String? {
}
return bs
}

fun CoroutineScope.launchWithCatch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
return launch(context, start) {
kotlin.runCatching {
block()
}
}
}
18 changes: 18 additions & 0 deletions app/src/main/java/moe/qwq/miko/ext/ProtoBuf.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package moe.qwq.miko.ext

import com.google.protobuf.UnknownFieldSet


fun UnknownFieldSet.getUnknownObject(number: Int): UnknownFieldSet {
return getField(number).groupList.firstOrNull() ?: getField(number).lengthDelimitedList.firstOrNull()?.let {
UnknownFieldSet.parseFrom(it)
} ?: throw RuntimeException("failed to fetch object")
}

fun UnknownFieldSet.getUnknownObjects(number: Int): List<UnknownFieldSet> {
return getField(number).groupList.ifEmpty {
getField(number).lengthDelimitedList.map {
UnknownFieldSet.parseFrom(it)
}
}
}
9 changes: 9 additions & 0 deletions app/src/main/java/moe/qwq/miko/ext/Xposed.kt
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,15 @@ internal fun Any.toInnerValuesString(): String {
}
builder.append("]")
}
is Array<*> -> {
builder.append("[\n\t")
v.forEach { value ->
builder.append("\t")
builder.append(value)
builder.append("\n")
}
builder.append("]")
}
else -> builder.append(v)
}
builder.append("\n")
Expand Down
162 changes: 159 additions & 3 deletions app/src/main/java/moe/qwq/miko/hooks/MessageEncrypt.kt
Original file line number Diff line number Diff line change
@@ -1,28 +1,184 @@
package moe.qwq.miko.hooks

import android.content.Context
import com.google.protobuf.ByteString
import com.google.protobuf.UnknownFieldSet
import com.tencent.mobileqq.fe.FEKit
import com.tencent.mobileqq.msf.core.MsfCore
import com.tencent.mobileqq.sign.QQSecuritySign
import com.tencent.qphone.base.remote.ToServiceMsg
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XposedBridge
import kotlinx.io.core.BytePacketBuilder
import kotlinx.io.core.readBytes
import kotlinx.io.core.toByteArray
import kotlinx.io.core.writeFully
import kotlinx.serialization.encodeToByteArray
import kotlinx.serialization.protobuf.ProtoBuf
import moe.fuqiuluo.entries.QQSsoSecureInfo
import moe.fuqiuluo.entries.TextMsgExtPbResvAttr
import moe.fuqiuluo.processor.HookAction
import moe.qwq.miko.actions.ActionProcess
import moe.qwq.miko.actions.HookCodec
import moe.qwq.miko.actions.IAction
import moe.qwq.miko.actions.PatchMsfCore
import moe.qwq.miko.ext.EMPTY_BYTE_ARRAY
import moe.qwq.miko.ext.getUnknownObject
import moe.qwq.miko.ext.getUnknownObjects
import moe.qwq.miko.ext.toHexString
import moe.qwq.miko.ext.toInnerValuesString
import moe.qwq.miko.internals.QQInterfaces
import moe.qwq.miko.internals.hijackers.IHijacker
import moe.qwq.miko.internals.setting.QwQSetting
import moe.qwq.miko.utils.AesUtils.aesEncrypt
import moe.qwq.miko.utils.AesUtils.md5
import moe.qwq.miko.utils.PlatformTools
import tencent.im.msg.im_msg_body.MsgBody
import java.security.MessageDigest
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec

@HookAction(desc = "消息加密抄送")
class MessageEncrypt: IAction {
class MessageEncrypt: IAction, QQInterfaces() {
override fun onRun(ctx: Context) {
HookCodec.hijackers.add(object: IHijacker {
override fun onHandle(
param: XC_MethodHook.MethodHookParam,
uin: String,
cmd: String,
seq: Int,
buffer: ByteArray,
bufferIndex: Int
): Boolean {
this@MessageEncrypt.onHandle(param, uin, cmd, seq, buffer, bufferIndex)
return false
}
override val command: String = "MessageSvc.PbSendMsg"
})
}

private fun onHandle(param: XC_MethodHook.MethodHookParam, uin: String, cmd: String, seq: Int, buffer: ByteArray, bufferIndex: Int) {
if (buffer.size <= 4) return
val unknownFields = UnknownFieldSet.parseFrom(buffer.copyOfRange(4, buffer.size))
if (!unknownFields.hasField(1)) return
val routingHead = unknownFields.getUnknownObject(1)
if (routingHead.hasField(1)) return // 私聊消息不加密
val msgBody = unknownFields.getUnknownObject(3)

val builder = UnknownFieldSet.newBuilder(unknownFields)
builder.clearField(3) // 清除原消息体

val newMsgBody = generateEncryptedMsgBody(msgBody)
builder.addField(3, UnknownFieldSet.Field.newBuilder().also {
it.addLengthDelimited(newMsgBody.toByteString())
}.build())

val data = builder.build().toByteArray()

if (bufferIndex == 15 && param.args[13] != null) {
//PlatformTools.copyToClipboard(text = "Sign13: ${(param.args[13] as ByteArray).toHexString()}")
// 因为包体改变,重新签名
val qqSecurityHead = UnknownFieldSet.parseFrom(param.args[13] as ByteArray)
val qqSecurityHeadBuilder = UnknownFieldSet.newBuilder(qqSecurityHead)
//XposedBridge.log(qqSecurityHead.getField(24).toInnerValuesString())
qqSecurityHeadBuilder.clearField(24)
val sign = FEKit.getInstance().getSign(cmd, data, seq, uin)
//XposedBridge.log(sign.toInnerValuesString())
qqSecurityHeadBuilder.addField(24, UnknownFieldSet.Field.newBuilder().also {
it.addLengthDelimited(ByteString.copyFrom(ProtoBuf.encodeToByteArray(QQSsoSecureInfo(
secSig = sign.sign,
extra = sign.extra,
deviceToken = sign.token
))))
}.build())
param.args[13] = qqSecurityHeadBuilder.build().toByteArray()
}

if (bufferIndex == 15 && param.args[14] != null) {
//PlatformTools.copyToClipboard(text = "Sign14: ${(param.args[14] as ByteArray).toHexString()}")
val qqSecurityHead = UnknownFieldSet.parseFrom(param.args[14] as ByteArray)
val qqSecurityHeadBuilder = UnknownFieldSet.newBuilder(qqSecurityHead)
qqSecurityHeadBuilder.clearField(24)
val sign = FEKit.getInstance().getSign(cmd, data, seq, uin)
qqSecurityHeadBuilder.addField(24, UnknownFieldSet.Field.newBuilder().also {
it.addLengthDelimited(ByteString.copyFrom(ProtoBuf.encodeToByteArray(QQSsoSecureInfo(
secSig = sign.sign,
extra = sign.extra,
deviceToken = sign.token
))))
}.build())
param.args[14] = qqSecurityHeadBuilder.build().toByteArray()
}

//PlatformTools.copyToClipboard(text = "SendData: ${(data).toHexString()}")
param.args[bufferIndex] = BytePacketBuilder().also {
it.writeInt(data.size + 4)
it.writeFully(data)
}.build().readBytes()
//sendBuffer(cmd, true, data)
//param.result = EMPTY_BYTE_ARRAY
}

private fun generateEncryptedMsgBody(msgBody: UnknownFieldSet): UnknownFieldSet {
val encryptKey = QwQSetting.getSetting<String>(name).getValue(null, null)
if (encryptKey.isBlank()) {
// 未设置加密密钥
return msgBody
}

val elements = UnknownFieldSet.Field.newBuilder()
msgBody.getUnknownObject(1).let { richText ->
richText.getUnknownObjects(2).forEach { element ->
if (element.hasField(37) || element.hasField(9)) {
elements.addLengthDelimited(element.toByteString()) // 通用字段,不自己合成
}
}
}

val newMsgBody = UnknownFieldSet.newBuilder()
val richText = UnknownFieldSet.newBuilder()

/* elements.addLengthDelimited(UnknownFieldSet.newBuilder().also { builder ->
builder.addField(1, UnknownFieldSet.Field.newBuilder().also {
it.addLengthDelimited(UnknownFieldSet.newBuilder().also { textElement ->
textElement.addField(1, UnknownFieldSet.Field.newBuilder().also { content ->
content.addLengthDelimited(ByteString.copyFromUtf8("[爱你]"))
}.build())
textElement.addField(12, UnknownFieldSet.Field.newBuilder().also { content ->
content.addLengthDelimited(ByteString.copyFrom(ProtoBuf.encodeToByteArray(TextMsgExtPbResvAttr(
wording = BytePacketBuilder().also {
it.writeInt(0x114514)
it.writeInt(encryptKey.hashCode())
it.writeFully(aesEncrypt(msgBody.toByteArray(), md5(encryptKey)))
}.build().readBytes()
))))
}.build())
}.build().toByteString())
}.build())
}.build().toByteString()) // add text*/

elements.addLengthDelimited(DEFAULT_FACE) // add image

richText.addField(2, elements.build())

newMsgBody.addField(1, UnknownFieldSet.Field.newBuilder().also {
it.addLengthDelimited(richText.build().toByteString())
}.build())

return newMsgBody.build()
}

override fun canRun(): Boolean {
val setting = QwQSetting.getSetting<String>(name)
return setting.getValue(null, null).isNotBlank()
}

//override val process: ActionProcess = ActionProcess.MAIN
override val process: ActionProcess = ActionProcess.MSF

override val name: String = QwQSetting.MESSAGE_ENCRYPT
}

private val DEFAULT_FACE by lazy {
ByteString.fromHex("323d0a055b5177515d1002180122101a156dd3d4367c701aecfe157b16f7f728b5bf0e30033a1035636664613661666530633537383466480050c80158c801")
}
Loading

0 comments on commit 2d6898c

Please sign in to comment.