diff --git a/app/src/main/java/io/github/qauxv/util/dexkit/DexFlow.java b/app/src/main/java/io/github/qauxv/util/dexkit/DexFlow.java index 03f100bdc0..74f8339a76 100644 --- a/app/src/main/java/io/github/qauxv/util/dexkit/DexFlow.java +++ b/app/src/main/java/io/github/qauxv/util/dexkit/DexFlow.java @@ -28,9 +28,14 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Locale; +import java.util.Objects; public class DexFlow { + private DexFlow() { + throw new AssertionError("No instances"); + } + private static final byte[] OPCODE_LENGTH_TABLE = new byte[]{ 1, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 2, 2, 3, 5, 2, 2, 3, 2, 1, 1, 2, @@ -105,25 +110,13 @@ public static DexMethodDescriptor[] getDeclaredDexMethods(byte[] buf, String kla } @NonUiThread - @Deprecated - public static String guessNewInstanceType(byte[] buf, DexMethodDescriptor method, - DexFieldDescriptor field) throws NoSuchMethodException { - if (buf == null) { - throw new NullPointerException("dex == null"); - } - if (method == null) { - throw new NullPointerException("method == null"); - } - if (field == null) { - throw new NullPointerException("field == null"); - } + public static int getDexMethodOffset(@NonNull byte[] buf, @NonNull DexMethodDescriptor method) { int methodIdsSize = readLe32(buf, 0x58); int methodIdsOff = readLe32(buf, 0x5c); int classDefsSize = readLe32(buf, 0x60); int classDefsOff = readLe32(buf, 0x64); - int dexCodeOffset = -1; + int dexCodeOffset = 0; int[] p = new int[1]; - int[] ret = new int[1]; int[] co = new int[1]; main_loop: for (int cn = 0; cn < classDefsSize; cn++) { @@ -178,7 +171,17 @@ public static String guessNewInstanceType(byte[] buf, DexMethodDescriptor method } } } - if (dexCodeOffset == -1) { + return dexCodeOffset; + } + + @NonUiThread + @Deprecated + public static String guessNewInstanceType(byte[] buf, DexMethodDescriptor method, DexFieldDescriptor field) throws NoSuchMethodException { + Objects.requireNonNull(buf, "buf == null"); + Objects.requireNonNull(method, "method == null"); + Objects.requireNonNull(field, "field == null"); + int dexCodeOffset = getDexMethodOffset(buf, method); + if (dexCodeOffset == 0) { throw new NoSuchMethodException(method.toString()); } int registersSize = readLe16(buf, dexCodeOffset); @@ -187,7 +190,7 @@ public static String guessNewInstanceType(byte[] buf, DexMethodDescriptor method int triesSize = readLe16(buf, dexCodeOffset + 6); int insnsSize = readLe16(buf, dexCodeOffset + 12); int insnsOff = dexCodeOffset + 16; - //we only handle new-instance and iput-object + // we only handle new-instance and iput-object String[] regObjType = new String[insSize + outsSize]; for (int i = 0; i < insnsSize; ) { int opv = buf[insnsOff + 2 * i] & 0xff; @@ -215,6 +218,57 @@ public static String guessNewInstanceType(byte[] buf, DexMethodDescriptor method return null; } + @NonUiThread + public static ArrayList getViewSetIdP1Values(byte[] buf, DexMethodDescriptor method) throws NoSuchMethodException { + Objects.requireNonNull(buf, "buf == null"); + Objects.requireNonNull(method, "method == null"); + int dexCodeOffset = getDexMethodOffset(buf, method); + if (dexCodeOffset == 0) { + throw new NoSuchMethodException(method.toString()); + } + int registersSize = readLe16(buf, dexCodeOffset); + int insSize = readLe16(buf, dexCodeOffset + 2); + int outsSize = readLe16(buf, dexCodeOffset + 4); + int triesSize = readLe16(buf, dexCodeOffset + 6); + int insnsSize = readLe16(buf, dexCodeOffset + 12); + int insnsOff = dexCodeOffset + 16; + // we only handle const and invoke-virtual Landroid/widget/TextView;->setId(I)V + String targetMethodDesc = "Landroid/widget/TextView;->setId(I)V"; + Integer[] regObjType = new Integer[registersSize]; + ArrayList results = new ArrayList<>(); + int pc = 0; // program counter + while (pc < insnsSize) { + int opv = buf[insnsOff + 2 * pc] & 0xff; + int len = OPCODE_LENGTH_TABLE[opv]; + if (len == 0) { + throw new RuntimeException(String.format(Locale.ROOT, "Unrecognized opcode = 0x%02x", opv)); + } + if (opv == 0x14) { + // 14 31i const vAA, #+BBBBBBBB + int reg = buf[insnsOff + 2 * pc + 1] & 0xff; + int valLow16 = readLe16(buf, insnsOff + 2 * pc + 2); + int valHigh16 = readLe16(buf, insnsOff + 2 * pc + 4); + int value = valLow16 | (valHigh16 << 16) & 0xffff0000; + regObjType[reg] = value; + } else if (opv == 0x6e) { + // 6e 35c invoke-kind {vC, vD, vE, vF, vG}, meth@BBBB + // [A=2] op {vC, vD}, kind@BBBB + int methodIdx = readLe16(buf, insnsOff + 2 * pc + 2); + DexMethodDescriptor m = readMethod(buf, methodIdx); + if (m.getDescriptor().equals(targetMethodDesc)) { + // get p1/C reg index + int insOffset4 = readLe16(buf, insnsOff + 2 * pc + 4); + int regIndex = (insOffset4 >> 4) & 0xf; + if (regObjType[regIndex] != null) { + results.add(regObjType[regIndex]); + } + } + } + pc += len; + } + return results; + } + @NonUiThread public static DexFieldDescriptor guessFieldByNewInstance(byte[] buf, DexMethodDescriptor method, Class instanceClass) throws NoSuchMethodException { @@ -481,6 +535,15 @@ public static DexFieldDescriptor readField(byte[] buf, int idx) { return new DexFieldDescriptor(clz, name, type); } + public static DexMethodDescriptor readMethod(byte[] buf, int idx) { + int methodIdsOff = readLe32(buf, 0x5c); + int p = methodIdsOff + 8 * idx; + String clz = readType(buf, readLe16(buf, p)); + String sig = readProto(buf, readLe16(buf, p + 2)); + String name = readString(buf, readLe32(buf, p + 4)); + return new DexMethodDescriptor(clz, name, sig); + } + public static String readProto(byte[] buf, int idx) { int protoIdsOff = readLe32(buf, 0x4c); int returnTypeIdx = readLe32(buf, protoIdsOff + 12 * idx + 4); diff --git a/app/src/main/java/io/github/qauxv/util/dexkit/DexMethodDescriptor.java b/app/src/main/java/io/github/qauxv/util/dexkit/DexMethodDescriptor.java index a084f68443..a24e2c349e 100644 --- a/app/src/main/java/io/github/qauxv/util/dexkit/DexMethodDescriptor.java +++ b/app/src/main/java/io/github/qauxv/util/dexkit/DexMethodDescriptor.java @@ -21,6 +21,7 @@ */ package io.github.qauxv.util.dexkit; +import androidx.annotation.NonNull; import java.io.Serializable; import java.lang.reflect.Constructor; import java.lang.reflect.Method; @@ -158,6 +159,11 @@ public String toString() { return declaringClass + "->" + name + signature; } + @NonNull + public String getDescriptor() { + return declaringClass + "->" + name + signature; + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/app/src/main/java/io/github/qauxv/util/dexkit/HostMainDexHelper.java b/app/src/main/java/io/github/qauxv/util/dexkit/HostMainDexHelper.java new file mode 100644 index 0000000000..0ee034cc93 --- /dev/null +++ b/app/src/main/java/io/github/qauxv/util/dexkit/HostMainDexHelper.java @@ -0,0 +1,163 @@ +/* + * QAuxiliary - An Xposed module for QQ/TIM + * Copyright (C) 2019-2023 QAuxiliary developers + * https://github.com/cinit/QAuxiliary + * + * This software is non-free but opensource software: you can redistribute it + * and/or modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation; either + * version 3 of the License, or any later version and our eula as published + * by QAuxiliary contributors. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * and eula along with this software. If not, see + * + * . + */ + +package io.github.qauxv.util.dexkit; + +import android.app.Application; +import androidx.annotation.Nullable; +import io.github.qauxv.util.HostInfo; +import io.github.qauxv.util.IoUtils; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Objects; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +public class HostMainDexHelper { + + private HostMainDexHelper() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); + } + + private static HashMap> sCachedDex = new HashMap<>(32); + + @Nullable + private static byte[] extractDexFromHost(int index) { + Application app = HostInfo.getHostInfo().getApplication(); + // get path of base.apk + String apkPath = app.getApplicationInfo().sourceDir; + String dexName = getNameForIndex(index); + // FIXME 2023-07-25: on very old QQ/TIM versions, some dex is in assets + try (ZipFile zipFile = new ZipFile(apkPath)) { + ZipEntry entry = zipFile.getEntry(dexName); + if (entry == null) { + return null; + } + return IoUtils.readFully(zipFile.getInputStream(entry)); + } catch (IOException e) { + IoUtils.unsafeThrow(e); + // unreachable + return null; + } + } + + public static boolean hasDexIndex(int i) { + Application app = HostInfo.getHostInfo().getApplication(); + // get path of base.apk + String apkPath = app.getApplicationInfo().sourceDir; + String dexName = getNameForIndex(i); + // FIXME 2023-07-25: on very old QQ/TIM versions, some dex is in assets + try (ZipFile zipFile = new ZipFile(apkPath)) { + ZipEntry entry = zipFile.getEntry(dexName); + return entry != null; + } catch (IOException e) { + IoUtils.unsafeThrow(e); + // unreachable + return false; + } + } + + private static String getNameForIndex(int i) { + if (i <= 1) { + return "classes.dex"; + } else { + return "classes" + i + ".dex"; + } + } + + @Nullable + public static byte[] getHostDexIndex(int i) { + if (i <= 0) { + return null; + } + // load from cache + WeakReference weakReference = sCachedDex.get(i); + if (weakReference != null) { + byte[] bytes = weakReference.get(); + if (bytes != null) { + return bytes; + } + } + // load from host + byte[] bytes = extractDexFromHost(i); + if (bytes != null) { + sCachedDex.put(i, new WeakReference<>(bytes)); + } + return bytes; + } + + + public static class DexIterator implements Iterator { + + private int nextIndex = 1; + + private DexIterator() { + } + + public boolean hasNext() { + return HostMainDexHelper.hasDexIndex(this.nextIndex); + } + + public byte[] next() { + byte[] hostDexIndex = HostMainDexHelper.getHostDexIndex(this.nextIndex); + if (hostDexIndex != null) { + this.nextIndex++; + } + return hostDexIndex; + } + + public int getLastIndex() { + return this.nextIndex - 1; + } + + } + + public static Iterator getDexIterator() { + return new DexIterator(); + } + + public static Iterable asIterable() { + return DexIterator::new; + } + + @Nullable + public static byte[] findDexWithClass(String name) { + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("name == null || name.isEmpty()"); + } + for (byte[] dex : asIterable()) { + if (DexFlow.hasClassInDex(dex, name)) { + return dex; + } + } + return null; + } + + @Nullable + public static byte[] findDexWithClass(Class klass) { + Objects.requireNonNull(klass, "klass == null"); + return findDexWithClass(klass.getName()); + } + +} diff --git a/app/src/main/java/me/ketal/hook/ShowMsgAt.kt b/app/src/main/java/me/ketal/hook/ShowMsgAt.kt index 22f3eec4c3..376833e4dc 100644 --- a/app/src/main/java/me/ketal/hook/ShowMsgAt.kt +++ b/app/src/main/java/me/ketal/hook/ShowMsgAt.kt @@ -33,6 +33,7 @@ import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.core.view.forEach import cc.ioctl.hook.profile.OpenProfileCard +import cc.ioctl.util.HostInfo import cc.ioctl.util.ui.FaultyDialog import com.tencent.qqnt.kernel.nativeinterface.MsgRecord import com.tencent.qqnt.kernel.nativeinterface.TextElement @@ -40,14 +41,23 @@ import de.robv.android.xposed.XC_MethodHook import io.github.qauxv.base.annotation.UiItemAgentEntry import io.github.qauxv.bridge.ntapi.ChatTypeConstants import io.github.qauxv.bridge.ntapi.RelationNTUinAndUidApi +import io.github.qauxv.config.ConfigManager import io.github.qauxv.dsl.FunctionEntryRouter import io.github.qauxv.hook.CommonSwitchFunctionHook +import io.github.qauxv.step.Step import io.github.qauxv.ui.CommonContextWrapper +import io.github.qauxv.util.Initiator import io.github.qauxv.util.Log -import io.github.qauxv.util.QQVersion import io.github.qauxv.util.Toasts +import io.github.qauxv.util.dexkit.DexDeobfsProvider +import io.github.qauxv.util.dexkit.DexFlow +import io.github.qauxv.util.dexkit.DexKitFinder +import io.github.qauxv.util.dexkit.DexMethodDescriptor +import io.github.qauxv.util.dexkit.HostMainDexHelper +import io.github.qauxv.util.dexkit.impl.DexKitDeobfs +import io.github.qauxv.util.hostInfo import io.github.qauxv.util.isTim -import io.github.qauxv.util.requireMinQQVersion +import io.luckypray.dexkit.annotations.DexKitExperimentalApi import me.ketal.dispacher.BaseBubbleBuilderHook import me.ketal.dispacher.OnBubbleBuilder import me.singleneuron.data.MsgRecordData @@ -59,19 +69,25 @@ import xyz.nextalone.util.invoke import xyz.nextalone.util.method @UiItemAgentEntry -object ShowMsgAt : CommonSwitchFunctionHook(), OnBubbleBuilder { +object ShowMsgAt : CommonSwitchFunctionHook(), OnBubbleBuilder, DexKitFinder { override val name = "消息显示At对象" override val description = "可能导致聊天界面滑动掉帧" override val uiItemLocation = FunctionEntryRouter.Locations.Auxiliary.MESSAGE_CATEGORY override val extraSearchKeywords: Array = arrayOf("@", "艾特") - private val NAME_TEXTVIEW = if (requireMinQQVersion(QQVersion.QQ_8_9_70)) "ex1" - else if (requireMinQQVersion(QQVersion.QQ_8_9_68)) "ewl" - else "ewk" + private val mTextViewId: Int // 0 for unknown, -1 for not found + get() { + val cache = ConfigManager.getCache() + val lastVersion = cache.getIntOrDefault("ShowMsgAt_ex1_id_version_code", 0) + val id = cache.getIntOrDefault("ShowMsgAt_ex1_id_value", 0) + return if (HostInfo.getVersionCode() == lastVersion) { + id + } else 0 + } override fun initOnce(): Boolean { - return !isTim() && BaseBubbleBuilderHook.initialize() + return !isTim() && BaseBubbleBuilderHook.initialize() && mTextViewId > 0 } override fun onGetView( @@ -173,7 +189,10 @@ object ShowMsgAt : CommonSwitchFunctionHook(), OnBubbleBuilder { if (atElements.isEmpty()) { return } - val tv = rootView.findHostView(NAME_TEXTVIEW) ?: return + if (mTextViewId <= 0) { + return + } + val tv = rootView.findViewById(mTextViewId) ?: return // TODO 2023-07-19 更稳定查找TextView setAtSpanBySearch(tv, atElements, chatMessage.peerUin) } @@ -204,6 +223,88 @@ object ShowMsgAt : CommonSwitchFunctionHook(), OnBubbleBuilder { textView.text = spannableString textView.movementMethod = LinkMovementMethod.getInstance() } + + override val isNeedFind: Boolean + get() { + return mTextViewId == 0 + } + + @OptIn(DexKitExperimentalApi::class) + override fun doFind(): Boolean { + val fnSaveResult = { id: Int -> + val hostVersion = hostInfo.versionCode32 + val cache = ConfigManager.getCache() + cache.putInt("ShowMsgAt_ex1_id_version_code", hostVersion) + cache.putInt("ShowMsgAt_ex1_id_value", id) + } + // step 1 find target class + // "Lcom/tencent/mobileqq/aio/msglist/holder/component/text/util/TextContentViewUtil;" + val dexkitBridge = (DexDeobfsProvider.getCurrentBackend() as DexKitDeobfs).getDexKitBridge() + val result = dexkitBridge.findClassUsingAnnotation { + annotationUsingString = "Lcom/tencent/mobileqq/aio/msglist/holder/component/text/util/TextContentViewUtil;" + } + val klass: Class<*> + if (result.size == 1) { + klass = Initiator.loadClass(result[0].name) + } else { + val errMsg = "ShowMsgAt: cannot find class got ${result.size} results" + result.joinToString() + fnSaveResult(-1) + traceError(RuntimeException(errMsg)) + return false + } + // step 2 find specified method + val method = klass.declaredMethods.single { + val argt = it.parameterTypes + argt.size == 3 && argt[0] == Context::class.java && argt[1] == Integer.TYPE + } + val methodDesc = DexMethodDescriptor(method) + // step 3 load dex + val dex = HostMainDexHelper.findDexWithClass(klass) + if (dex == null) { + Log.e("ShowMsgAt: cannot find dex: $klass") + return false + } + // step 4 ??? + val idList = DexFlow.getViewSetIdP1Values(dex, methodDesc) + if (idList.size == 1) { + // good + fnSaveResult(idList[0]) + return true + } else { + // ??? + val errMsg = "ShowMsgAt: cannot find id got ${idList.size} results" + idList.joinToString() + traceError(RuntimeException(errMsg)) + fnSaveResult(-1) + return false + } + } + + private val mStep: Step = object : Step { + + override fun step(): Boolean { + return doFind() + } + + override fun isDone(): Boolean { + return mTextViewId != 0 + } + + override fun getPriority() = -99 + + override fun getDescription() = "ShowMsgAt: find id" + + } + + private val mSteps by lazy { + val steps = mutableListOf(mStep) + super.makePreparationSteps()?.let { + steps.addAll(it) + } + steps.toTypedArray() + } + + override fun makePreparationSteps(): Array = mSteps + } class ProfileCardSpan(val qq: Long) : ClickableSpan() {