diff --git a/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/core/reflex/parse/ApkFile.kt b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/core/reflex/parse/ApkFile.kt new file mode 100755 index 00000000..e1cb338d --- /dev/null +++ b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/core/reflex/parse/ApkFile.kt @@ -0,0 +1,115 @@ +/* + * YukiHookAPI - An efficient Kotlin version of the Xposed Hook API. + * Copyright (C) 2019-2022 HighCapable + * https://github.com/fankes/YukiHookAPI + * + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * This file is Created by fankes on 2022/5/24. + */ +@file:Suppress("unused") + +package com.highcapable.yukihookapi.hook.core.reflex.parse + +import android.content.pm.ApplicationInfo +import com.highcapable.yukihookapi.hook.utils.parallelForEach +import java.io.Closeable +import java.io.File +import java.nio.ByteBuffer +import java.util.zip.ZipEntry +import java.util.zip.ZipFile + +/** + * 封装对 APK 文件的解析操作 + * + * 参考了 dongliu 的 [apk-parser](https://github.com/hsiafan/apk-parser) 项目 + * + * Contributed from [conan](https://github.com/meowool-catnip/conan) + * @param apkFile APK 文件 + */ +internal class ApkFile private constructor(apkFile: File) : Closeable { + + internal companion object { + + private const val DEX_FILE = "classes.dex" + private const val DEX_ADDITIONAL = "classes%d.dex" + + /** + * 获取 [ApkFile] 实例 + * @param appInfo 当前 APP 的 [ApplicationInfo] + * @return [ApkFile] or null + */ + internal fun from(appInfo: ApplicationInfo?) = runCatching { ApkFile(File(appInfo!!.sourceDir)) }.getOrNull() + } + + override fun close() = zipFile.close() + + /** + * 当前 APK 文件 + * @return [ZipFile] + */ + private val zipFile = ZipFile(apkFile) + + /** + * 读取 Entry + * @param entry 压缩文件 Entry + * @return [ByteArray] + */ + private fun readEntry(entry: ZipEntry) = zipFile.getInputStream(entry).use { it.readBytes() } + + /** + * 读取 DEX 文件路径 + * @param idx 字节序号 + * @return [String] + */ + private fun readDexFilePath(idx: Int) = if (idx == 1) DEX_FILE else String.format(DEX_ADDITIONAL, idx) + + /** + * DEX 文件是否存在 + * @param idx 字节序号 + * @return [Boolean] + */ + private fun isDexFileExist(idx: Int) = zipFile.getEntry(readDexFilePath(idx)) != null + + /** + * 读取 DEX 文件字节流 + * @param idx 字节序号 + * @return [ByteArray] + */ + private fun readDexFile(idx: Int) = readEntry(zipFile.getEntry(readDexFilePath(idx))) + + /** + * 获取当前 DEX 的 package 结构实例 + * @return [ClassTrie] + */ + internal val classTypes by lazy { + var end = 2 + while (isDexFileExist(end)) end++ + val ret = ClassTrie() + (1 until end).parallelForEach { idx -> + val data = readDexFile(idx) + val buffer = ByteBuffer.wrap(data) + val parser = DexParser(buffer) + ret += parser.parseClassTypes() + } + return@lazy ret.apply { mutable = false } + } +} \ No newline at end of file diff --git a/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/core/reflex/parse/ClassTrie.kt b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/core/reflex/parse/ClassTrie.kt new file mode 100755 index 00000000..992dce52 --- /dev/null +++ b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/core/reflex/parse/ClassTrie.kt @@ -0,0 +1,169 @@ +/* + * YukiHookAPI - An efficient Kotlin version of the Xposed Hook API. + * Copyright (C) 2019-2022 HighCapable + * https://github.com/fankes/YukiHookAPI + * + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * This file is Created by fankes on 2022/5/24. + */ +@file:Suppress("unused") + +package com.highcapable.yukihookapi.hook.core.reflex.parse + +import java.util.concurrent.ConcurrentHashMap + +/** + * 用来储存一个 APK 的 package 结构 + * + * 出于性能考虑 - 这个类不支持读线程和写线程同时操作 - 但支持同类型的线程同时操作 + * + * Contributed from [conan](https://github.com/meowool-catnip/conan) + */ +internal class ClassTrie internal constructor() { + + private companion object { + + /** + * 用来将 JVM 格式的类型标识符转换为类名 + * + * Example: String 的类型标识符为 "Ljava/lang/String;" + * + * [Refer](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3) + * @return [String] + */ + private fun convertJVMTypeToClassName(type: String) = type.substring(1, type.length - 1).replace(oldChar = '/', newChar = '.') + } + + /** + * 读写开关 - 用于增强线程间的安全性 + * + * 只有开关设为 true 的时候 - 写操作才会被执行 + * + * 只有开关设为 false 的时候 - 读操作才会返回有效数据 + */ + @Volatile + internal var mutable = true + + /** + * package 结构的根结点 + * @return [TrieNode] + */ + private val root = TrieNode() + + /** + * 插入一个单独的 JVM 格式的类型标识符 + * @param type 类型 + */ + internal operator fun plusAssign(type: String) { + if (mutable) root.add(convertJVMTypeToClassName(type)) + } + + /** + * 插入一组 JVM 格式的类型标识符 + * @param types 类型数组 + */ + internal operator fun plusAssign(types: Array) = types.forEach { this += it } + + /** + * 查找指定包里指定深度的所有类 + * + * 出于性能方面的考虑 - 只有深度相等的类才会被返回 - 比如查找深度为 0 的时候 - 就只返回这个包自己拥有的类 - 不包括它里面其他包拥有的类 + * @param packageName 包名 - 默认为根包名 + * @param depth 深度 + * @return [List] 查找到的类名数组 + */ + internal fun search(packageName: String = "root", depth: Int): List { + if (mutable) return emptyList() + if (packageName == "root") return root.classes + return root.search(packageName, depth) + } + + /** + * 私有的节点结构 + */ + inner class TrieNode { + + /** 当前的 Class 实例数组 */ + internal val classes: MutableList = ArrayList(50) + + /** 当前的 Class 子实例数组 */ + private val children: MutableMap = ConcurrentHashMap() + + /** + * 添加节点 + * @param className 类名 + */ + internal fun add(className: String) = add(className, pos = 0) + + /** + * 获取节点下的类名数组 + * @param depth 深度 + * @return [List] + */ + internal fun get(depth: Int = 0): List { + if (depth == 0) return classes + return children.flatMap { it.value.get(depth - 1) } + } + + /** + * 查找当前包里指定深度的所有类 + * @param packageName 包名 - 默认为根包名 + * @param depth 深度 + * @return [List] 查找到的类名数组 + */ + internal fun search(packageName: String, depth: Int) = search(packageName, depth, pos = 0) + + /** + * 添加节点 + * @param className 类名 + * @param pos 下标 + */ + private fun add(className: String, pos: Int) { + val delimiterAt = className.indexOf(char = '.', pos) + if (delimiterAt == -1) { + synchronized(this) { classes.add(className) } + return + } + val pkg = className.substring(pos, delimiterAt) + if (pkg !in children) children[pkg] = TrieNode() + children[pkg]?.add(className, pos = delimiterAt + 1) + } + + /** + * 查找当前包里指定深度的所有类 + * @param packageName 包名 - 默认为根包名 + * @param depth 深度 + * @param pos 下标 + * @return [List] 查找到的类名数组 + */ + private fun search(packageName: String, depth: Int, pos: Int): List { + val delimiterAt = packageName.indexOf(char = '.', pos) + if (delimiterAt == -1) { + val pkg = packageName.substring(pos) + return children[pkg]?.get(depth) ?: emptyList() + } + val pkg = packageName.substring(pos, delimiterAt) + val next = children[pkg] ?: return emptyList() + return next.search(packageName, depth, pos = delimiterAt + 1) + } + } +} \ No newline at end of file diff --git a/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/core/reflex/parse/DexHeader.kt b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/core/reflex/parse/DexHeader.kt new file mode 100755 index 00000000..90ffb695 --- /dev/null +++ b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/core/reflex/parse/DexHeader.kt @@ -0,0 +1,70 @@ +/* + * YukiHookAPI - An efficient Kotlin version of the Xposed Hook API. + * Copyright (C) 2019-2022 HighCapable + * https://github.com/fankes/YukiHookAPI + * + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * This file is Created by fankes on 2022/5/24. + */ +@file:Suppress("unused") + +package com.highcapable.yukihookapi.hook.core.reflex.parse + +/** + * DEX 格式的文件头 + * + * 参考来源 [Dex Format](https://source.android.com/devices/tech/dalvik/dex-format) + * + * Contributed from [conan](https://github.com/meowool-catnip/conan) + */ +internal class DexHeader internal constructor() { + + internal var version = 0 + internal var checksum = 0u + internal var signature = ByteArray(kSHA1DigestLen) + internal var fileSize = 0u + internal var headerSize = 0u + internal var endianTag = 0u + internal var linkSize = 0u + internal var linkOff = 0u + internal var mapOff = 0u + internal var stringIdsSize = 0 + internal var stringIdsOff = 0u + internal var typeIdsSize = 0 + internal var typeIdsOff = 0u + internal var protoIdsSize = 0 + internal var protoIdsOff = 0u + internal var fieldIdsSize = 0 + internal var fieldIdsOff = 0u + internal var methodIdsSize = 0 + internal var methodIdsOff = 0u + internal var classDefsSize = 0 + internal var classDefsOff = 0u + internal var dataSize = 0 + internal var dataOff = 0u + + internal companion object { + + internal const val kSHA1DigestLen = 20 + internal const val kSHA1DigestOutputLen = kSHA1DigestLen * 2 + 1 + } +} \ No newline at end of file diff --git a/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/core/reflex/parse/DexParser.kt b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/core/reflex/parse/DexParser.kt new file mode 100644 index 00000000..eee8e97d --- /dev/null +++ b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/core/reflex/parse/DexParser.kt @@ -0,0 +1,226 @@ +/* + * YukiHookAPI - An efficient Kotlin version of the Xposed Hook API. + * Copyright (C) 2019-2022 HighCapable + * https://github.com/fankes/YukiHookAPI + * + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * This file is Created by fankes on 2022/5/24. + */ +@file:Suppress("unused") + +package com.highcapable.yukihookapi.hook.core.reflex.parse + +import java.io.UTFDataFormatException +import java.nio.Buffer +import java.nio.ByteBuffer +import java.nio.ByteOrder + +/** + * 封装对 DEX 格式数据的解析操作 + * + * 参考了 dongliu 的 [apk-parser](https://github.com/hsiafan/apk-parser) 项目 + * + * Contributed from [conan](https://github.com/meowool-catnip/conan) + * @param buffer DEX 字节流 + */ +internal class DexParser internal constructor(buffer: ByteBuffer) { + + /** + * 当前字节流 + * @return [ByteBuffer] + */ + private val buffer = buffer.duplicate().apply { order(ByteOrder.LITTLE_ENDIAN) } + + /** + * 读取 Bytes + * @param size 大小 + * @return [ByteArray] + */ + private fun ByteBuffer.readBytes(size: Int) = ByteArray(size).also { get(it) } + + /** + * 获取当前 package 下全部的类名 + * @return [Array] + */ + internal fun parseClassTypes(): Array { + // read magic + val magic = String(buffer.readBytes(8)) + if (magic.startsWith(prefix = "dex\n").not()) return arrayOf() + val version = magic.substring(4, 7).toInt() + // now the version is 035 + if (version < 35) { + // version 009 was used for the M3 releases of the Android platform (November–December 2007), + // and version 013 was used for the M5 releases of the Android platform (February–March 2008) + error("Dex file version: $version is not supported") + } + // read header + val header = readDexHeader() + header.version = version + // read string offsets + val stringOffsets = readStringOffsets(header.stringIdsOff, header.stringIdsSize) + // read type ids + val typeIds = readTypeIds(header.typeIdsOff, header.typeIdsSize) + // read class ids + val classIds = readClassIds(header.classDefsOff, header.classDefsSize) + // read class types + return Array(classIds.size) { i -> + val classId = classIds[i] + val typeId = typeIds[classId] + val offset = stringOffsets[typeId] + readStringAtOffset(offset) + } + } + + /** + * 获取 DEX 文件头 + * @return [DexHeader] + */ + private fun readDexHeader() = DexHeader().apply { + checksum = buffer.int.toUInt() + buffer.get(signature) + fileSize = buffer.int.toUInt() + headerSize = buffer.int.toUInt() + endianTag = buffer.int.toUInt() + linkSize = buffer.int.toUInt() + linkOff = buffer.int.toUInt() + mapOff = buffer.int.toUInt() + stringIdsSize = buffer.int + stringIdsOff = buffer.int.toUInt() + typeIdsSize = buffer.int + typeIdsOff = buffer.int.toUInt() + protoIdsSize = buffer.int + protoIdsOff = buffer.int.toUInt() + fieldIdsSize = buffer.int + fieldIdsOff = buffer.int.toUInt() + methodIdsSize = buffer.int + methodIdsOff = buffer.int.toUInt() + classDefsSize = buffer.int + classDefsOff = buffer.int.toUInt() + dataSize = buffer.int + dataOff = buffer.int.toUInt() + } + + /** + * 读取字符串偏移量 + * @param stringIdsOff 偏移量 + * @param stringIdsSize 偏移大小 + * @return [IntArray] + */ + private fun readStringOffsets(stringIdsOff: UInt, stringIdsSize: Int): IntArray { + (buffer as Buffer).position(stringIdsOff.toInt()) + return IntArray(stringIdsSize) { buffer.int } + } + + /** + * 读取 Ids 类型偏移量 + * @param typeIdsOff 偏移量 + * @param typeIdsSize 偏移大小 + * @return [IntArray] + */ + private fun readTypeIds(typeIdsOff: UInt, typeIdsSize: Int): IntArray { + (buffer as Buffer).position(typeIdsOff.toInt()) + return IntArray(typeIdsSize) { buffer.int } + } + + /** + * 读取 Class Ids 偏移量 + * @param classDefsOff 偏移量 + * @param classDefsSize 偏移大小 + * @return [Array] + */ + private fun readClassIds(classDefsOff: UInt, classDefsSize: Int): Array { + (buffer as Buffer).position(classDefsOff.toInt()) + return Array(classDefsSize) { + val classIdx = buffer.int + // access_flags, skip + buffer.int + // superclass_idx, skip + buffer.int + // interfaces_off, skip + buffer.int + // source_file_idx, skip + buffer.int + // annotations_off, skip + buffer.int + // class_data_off, skip + buffer.int + // static_values_off, skip + buffer.int + classIdx + } + } + + /** + * 读取偏移量的字符串 + * @param offset 偏移量 + * @return [String] + */ + private fun readStringAtOffset(offset: Int): String { + (buffer as Buffer).position(offset) + return readString(readULEB128Int()) + } + + /** + * 读取 ULEB128 整型 + * @return [Int] + */ + private fun readULEB128Int(): Int { + var ret = 0 + var count = 0 + var byte: Int + do { + if (count > 4) error("read varints error.") + byte = buffer.get().toInt() + ret = ret or (byte and 0x7f shl count * 7) + count++ + } while (byte and 0x80 != 0) + return ret + } + + /** + * 读取字符串 + * @param len 长度 + * @return [String] + */ + private fun readString(len: Int): String { + val chars = CharArray(len) + for (i in 0 until len) { + val byte = buffer.get().toInt() + when { + // ascii char + byte and 0x80 == 0 -> chars[i] = byte.toChar() + // read one more + byte and 0xe0 == 0xc0 -> { + val b = buffer.get().toInt() + chars[i] = (byte and 0x1F shl 6 or (b and 0x3F)).toChar() + } + byte and 0xf0 == 0xe0 -> { + val b = buffer.get().toInt() + val c = buffer.get().toInt() + chars[i] = (byte and 0x0F shl 12 or (b and 0x3F shl 6) or (c and 0x3F)).toChar() + } + else -> throw UTFDataFormatException() + } + } + return String(chars) + } +} \ No newline at end of file diff --git a/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/utils/UtilsFactory.kt b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/utils/UtilsFactory.kt index 0aebc3e6..e8e7a5dc 100644 --- a/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/utils/UtilsFactory.kt +++ b/yukihookapi/src/api/kotlin/com/highcapable/yukihookapi/hook/utils/UtilsFactory.kt @@ -29,6 +29,25 @@ package com.highcapable.yukihookapi.hook.utils import android.os.Build import com.highcapable.yukihookapi.annotation.YukiPrivateApi +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +/** + * 进行一次并行计算的 ForEach 操作 + * @param action 回调内容方法体 + */ +@YukiPrivateApi +inline fun Iterable.parallelForEach(crossinline action: (T) -> Unit) { + Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()).apply { + val iterator = iterator() + while (iterator.hasNext()) { + val item = iterator.next() + execute { runCatching { action(item) } } + } + shutdown() + awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS) + } +} /** * 获取数组内容依次列出的字符串表示