Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -229,4 +229,6 @@ dependencies {

compileOnly(libs.lombok)
annotationProcessor(libs.lombok)

testImplementation(libs.junit)
}
293 changes: 235 additions & 58 deletions app/src/main/java/moe/ono/hooks/item/entertainment/FakeGroupOwner.kt
Original file line number Diff line number Diff line change
@@ -1,90 +1,267 @@
package moe.ono.hooks.item.entertainment

import android.content.Intent
import com.google.protobuf.ByteString
import com.google.protobuf.InvalidProtocolBufferException
import com.google.protobuf.UnknownFieldSet
import com.tencent.qphone.base.remote.ToServiceMsg
import de.robv.android.xposed.XC_MethodHook
import moe.ono.hooks._base.BaseSwitchFunctionHookItem
import moe.ono.hooks._core.annotation.HookItem
import moe.ono.hooks.base.api.QQMsfReqHandler
import moe.ono.hooks.protocol.sendPacket
import moe.ono.loader.hookapi.IMsfHandler
import moe.ono.util.FunProtoData
import org.json.JSONArray
import org.json.JSONObject
import moe.ono.service.QQInterfaces
import moe.ono.util.Logger
import java.nio.ByteBuffer

@HookItem(path = "娱乐功能/假群主", description = "* 随便发点消息,概率变成群主头衔\n* 管理员无效")
class FakeGroupOwner: BaseSwitchFunctionHookItem() {
class FakeGroupOwner : BaseSwitchFunctionHookItem() {
override fun entry(classLoader: ClassLoader) {
QQMsfReqHandler.handler.add(object : IMsfHandler {
override fun onHandle(
param: XC_MethodHook.MethodHookParam,
intent: Intent,
toServiceMsg: ToServiceMsg
) {
if (!this@FakeGroupOwner.isEnabled) return
if (toServiceMsg.getAttribute(RESEND_MARKER) == true) return

try {
if (!this@FakeGroupOwner.isEnabled) return
val original = toServiceMsg.wupBuffer ?: return
val patchedPayload = FakeGroupOwnerProto.patchWupBuffer(original) ?: return
val replacement = ToServiceMsg(
toServiceMsg.serviceName,
toServiceMsg.uin,
toServiceMsg.serviceCmd
).apply {
putWupBuffer(patchedPayload)
addAttribute("req_pb_protocol_flag", true)
addAttribute(RESEND_MARKER, true)
}

val pbData = FunProtoData.getUnpPackage(toServiceMsg.wupBuffer)
val json = FunProtoData().apply { fromBytes(pbData) }.toJSON()
QQInterfaces.app.sendToService(replacement)
param.result = null
} catch (t: Throwable) {
Logger.e("[FakeGroupOwner] Failed to resend MessageSvc.PbSendMsg", t)
}
}

json.optJSONObject("1")?.optJSONObject("2")?.opt("1") ?: return
override val cmd: String
get() = COMMAND
})
}

val obj3_1 = json.optJSONObject("3")?.optJSONObject("1") ?: return
val obj2 = obj3_1.opt("2") ?: return
private companion object {
const val COMMAND = "MessageSvc.PbSendMsg"
const val RESEND_MARKER = "ono_fake_group_owner_resend"
}
}

var isModified = false
internal object FakeGroupOwnerProto {
private const val LENGTH_HEADER_SIZE = 4
private const val OWNER_TITLE_VALUE = 300L

fun createTargetNode() = JSONObject().apply {
put("37", JSONObject().apply {
put("19", JSONObject().apply { put("4", 300) })
})
}
fun patchWupBuffer(buffer: ByteArray): ByteArray? {
val hasLengthHeader = hasLengthHeader(buffer)
val payload = if (hasLengthHeader) {
buffer.copyOfRange(LENGTH_HEADER_SIZE, buffer.size)
} else {
buffer
}
return patchPayload(payload)
}

fun updateExistingNode(targetNode: JSONObject) {
val inner37 = targetNode.getJSONObject("37")
val inner19 = inner37.optJSONObject("19") ?: JSONObject().also { inner37.put("19", it) }
inner19.put("4", 300)
}
fun patchPayload(payload: ByteArray): ByteArray? {
val request = parse(payload) ?: return null
if (!isGroupMessage(request)) return null

if (obj2 is JSONObject) {
if (obj2.has("37")) {
updateExistingNode(obj2)
isModified = true
} else {
obj3_1.put("2", JSONArray().apply {
put(obj2)
put(createTargetNode())
})
isModified = true
}
} else if (obj2 is JSONArray) {
var hasNode37 = false
for (i in 0 until obj2.length()) {
val item = obj2.optJSONObject(i)
if (item != null && item.has("37")) {
updateExistingNode(item)
hasNode37 = true
}
}
if (!hasNode37) {
obj2.put(createTargetNode())
}
isModified = true
}
return when (
val result = patchMessageField(request, 3, transform = ::patchMessageBody)
) {
is PatchResult.Modified -> result.value.toByteArray()
PatchResult.Malformed,
PatchResult.Unchanged -> null
}
}

if (isModified) {
finish(param, json)
}
} catch (_: Throwable) { }
private fun isGroupMessage(request: UnknownFieldSet): Boolean {
return request.getField(1).lengthDelimitedList.any { routingBytes ->
val routingHead = parse(routingBytes) ?: return@any false
routingHead.getField(2).lengthDelimitedList.any { groupBytes ->
parse(groupBytes)?.getField(1)?.varintList?.isNotEmpty() == true
}
}
}

private fun patchMessageBody(messageBody: UnknownFieldSet): PatchResult {
return patchMessageField(messageBody, 1, transform = ::patchRichText)
}

private fun patchRichText(richText: UnknownFieldSet): PatchResult {
val elementsField = richText.getField(2)
if (elementsField.lengthDelimitedList.isEmpty()) return PatchResult.Unchanged

private fun finish(param: XC_MethodHook.MethodHookParam, json: JSONObject) {
param.result = null
sendPacket("MessageSvc.PbSendMsg", json.toString())
var foundOwnerElement = false
val elements = ArrayList<ByteString>(elementsField.lengthDelimitedList.size + 1)

for (elementBytes in elementsField.lengthDelimitedList) {
val element = parse(elementBytes) ?: return PatchResult.Malformed
if (!element.hasField(37)) {
elements.add(elementBytes)
continue
}

override val cmd: String
get() = "MessageSvc.PbSendMsg"
})
foundOwnerElement = true
when (val result = patchOwnerElement(element)) {
is PatchResult.Modified -> elements.add(result.value.toByteString())
PatchResult.Malformed,
PatchResult.Unchanged -> return PatchResult.Malformed
}
}

if (!foundOwnerElement) {
elements.add(createOwnerElement().toByteString())
}

return PatchResult.Modified(replaceLengthDelimited(richText, 2, elements))
}

private fun patchOwnerElement(element: UnknownFieldSet): PatchResult {
return patchMessageField(
element,
37,
requireValues = true,
transform = ::patchOwnerInfo
)
}

private fun patchOwnerInfo(ownerInfo: UnknownFieldSet): PatchResult {
if (!ownerInfo.hasField(19)) {
return PatchResult.Modified(
replaceLengthDelimited(
ownerInfo,
19,
listOf(setOwnerTitle(UnknownFieldSet.getDefaultInstance()).toByteString())
)
)
}

return patchMessageField(ownerInfo, 19, requireValues = true) { titleInfo ->
PatchResult.Modified(setOwnerTitle(titleInfo))
}
}

private fun setOwnerTitle(titleInfo: UnknownFieldSet): UnknownFieldSet {
val oldField = titleInfo.getField(4)
val newField = UnknownFieldSet.Field.newBuilder().apply {
addVarint(OWNER_TITLE_VALUE)
oldField.fixed32List.forEach(::addFixed32)
oldField.fixed64List.forEach(::addFixed64)
oldField.lengthDelimitedList.forEach(::addLengthDelimited)
oldField.groupList.forEach(::addGroup)
}.build()

return UnknownFieldSet.newBuilder(titleInfo)
.clearField(4)
.addField(4, newField)
.build()
}

private fun createOwnerElement(): UnknownFieldSet {
val ownerInfo = UnknownFieldSet.newBuilder()
.addField(
19,
lengthDelimitedField(setOwnerTitle(UnknownFieldSet.getDefaultInstance()))
)
.build()

return UnknownFieldSet.newBuilder()
.addField(37, lengthDelimitedField(ownerInfo))
.build()
}

private fun patchMessageField(
parent: UnknownFieldSet,
fieldNumber: Int,
requireValues: Boolean = false,
transform: (UnknownFieldSet) -> PatchResult
): PatchResult {
val field = parent.getField(fieldNumber)
if (field.lengthDelimitedList.isEmpty()) {
return if (requireValues) PatchResult.Malformed else PatchResult.Unchanged
}

var modified = false
val values = ArrayList<ByteString>(field.lengthDelimitedList.size)
for (bytes in field.lengthDelimitedList) {
val child = parse(bytes) ?: return PatchResult.Malformed
when (val result = transform(child)) {
is PatchResult.Modified -> {
values.add(result.value.toByteString())
modified = true
}
PatchResult.Malformed -> return PatchResult.Malformed
PatchResult.Unchanged -> values.add(bytes)
}
}

return if (modified) {
PatchResult.Modified(replaceLengthDelimited(parent, fieldNumber, values))
} else {
PatchResult.Unchanged
}
}

private fun replaceLengthDelimited(
parent: UnknownFieldSet,
fieldNumber: Int,
values: List<ByteString>
): UnknownFieldSet {
val oldField = parent.getField(fieldNumber)
val newField = UnknownFieldSet.Field.newBuilder().apply {
oldField.varintList.forEach(::addVarint)
oldField.fixed32List.forEach(::addFixed32)
oldField.fixed64List.forEach(::addFixed64)
values.forEach(::addLengthDelimited)
oldField.groupList.forEach(::addGroup)
}.build()

return UnknownFieldSet.newBuilder(parent)
.clearField(fieldNumber)
.addField(fieldNumber, newField)
.build()
}

private fun lengthDelimitedField(value: UnknownFieldSet): UnknownFieldSet.Field {
return UnknownFieldSet.Field.newBuilder()
.addLengthDelimited(value.toByteString())
.build()
}

private fun hasLengthHeader(buffer: ByteArray): Boolean {
if (buffer.size < LENGTH_HEADER_SIZE) return false
return ByteBuffer.wrap(buffer, 0, LENGTH_HEADER_SIZE).int == buffer.size
}

private fun parse(bytes: ByteArray): UnknownFieldSet? {
return try {
UnknownFieldSet.parseFrom(bytes)
} catch (_: InvalidProtocolBufferException) {
null
}
}

private fun parse(bytes: ByteString): UnknownFieldSet? {
return try {
UnknownFieldSet.parseFrom(bytes)
} catch (_: InvalidProtocolBufferException) {
null
}
}

private sealed interface PatchResult {
data object Unchanged : PatchResult
data object Malformed : PatchResult
data class Modified(val value: UnknownFieldSet) : PatchResult
}
}
}
Loading
Loading