diff --git a/app/src/main/java/com/highcapable/yukihookapi/demo/InjectTest.kt b/app/src/main/java/com/highcapable/yukihookapi/demo/InjectTest.kt new file mode 100644 index 00000000..8313ff03 --- /dev/null +++ b/app/src/main/java/com/highcapable/yukihookapi/demo/InjectTest.kt @@ -0,0 +1,35 @@ +/** + * MIT License + * + * Copyright (C) 2022 HighCapable + * + * This file is part of YukiHookAPI. + * + * 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/2/3. + */ +package com.highcapable.yukihookapi.demo + +// for test +class InjectTest(private val string: String) { + + // for test + fun getString() = string +} \ No newline at end of file diff --git a/app/src/main/java/com/highcapable/yukihookapi/demo/InjectTestName.kt b/app/src/main/java/com/highcapable/yukihookapi/demo/InjectTestName.kt new file mode 100644 index 00000000..ff266b7f --- /dev/null +++ b/app/src/main/java/com/highcapable/yukihookapi/demo/InjectTestName.kt @@ -0,0 +1,35 @@ +/** + * MIT License + * + * Copyright (C) 2022 HighCapable + * + * This file is part of YukiHookAPI. + * + * 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/2/3. + */ +package com.highcapable.yukihookapi.demo + +// for test +class InjectTestName(private val string: String) { + + // for test + fun getString() = string +} \ No newline at end of file diff --git a/app/src/main/java/com/highcapable/yukihookapi/demo/MainActivity.kt b/app/src/main/java/com/highcapable/yukihookapi/demo/MainActivity.kt index fdc4a13c..47883946 100644 --- a/app/src/main/java/com/highcapable/yukihookapi/demo/MainActivity.kt +++ b/app/src/main/java/com/highcapable/yukihookapi/demo/MainActivity.kt @@ -25,11 +25,14 @@ * * This file is Created by fankes on 2022/1/29. */ +@file:Suppress("SameParameterValue") + package com.highcapable.yukihookapi.demo import android.os.Bundle import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity +import com.highcapable.yukihookapi.hook.xposed.YukiHookModuleStatus class MainActivity : AppCompatActivity() { @@ -38,11 +41,30 @@ class MainActivity : AppCompatActivity() { setContentView(R.layout.activity_main) // for test AlertDialog.Builder(this) - .setMessage(hello()) - .setPositiveButton("OK", null) - .show() + .setTitle("Hook 方法返回值测试") + .setMessage(test() + "\n模块是否已激活:${YukiHookModuleStatus.isActive()}") + .setPositiveButton("下一个") { _, _ -> + AlertDialog.Builder(this) + .setTitle("Hook 方法参数测试") + .setMessage(test("这是没有更改的文字") + "\n模块是否已激活:${YukiHookModuleStatus.isActive()}") + .setPositiveButton("下一个") { _, _ -> + AlertDialog.Builder(this) + .setTitle("Hook 构造方法测试(stub)") + .setMessage(InjectTest("文字未更改").getString() + "\n模块是否已激活:${YukiHookModuleStatus.isActive()}") + .setPositiveButton("下一个") { _, _ -> + AlertDialog.Builder(this) + .setTitle("Hook 构造方法测试(名称)") + .setMessage(InjectTestName("文字没更改").getString() + "\n模块是否已激活:${YukiHookModuleStatus.isActive()}") + .setPositiveButton("完成", null) + .show() + }.show() + }.show() + }.show() } // for test - private fun hello() = "正常显示的一行文字" + private fun test() = "正常显示的一行文字" + + // for test + private fun test(string: String) = string } \ No newline at end of file diff --git a/app/src/main/java/com/highcapable/yukihookapi/demo/hook/HookMain.kt b/app/src/main/java/com/highcapable/yukihookapi/demo/hook/HookMain.kt index e882065f..dcb1ebd7 100644 --- a/app/src/main/java/com/highcapable/yukihookapi/demo/hook/HookMain.kt +++ b/app/src/main/java/com/highcapable/yukihookapi/demo/hook/HookMain.kt @@ -25,29 +25,68 @@ * * This file is Created by fankes on 2022/2/2. */ +@file:Suppress("unused") + package com.highcapable.yukihookapi.demo.hook -import android.app.Activity +import android.app.AlertDialog import android.widget.Toast +import com.highcapable.yukihookapi.annotation.xposed.InjectYukiHookWithXposed +import com.highcapable.yukihookapi.demo.InjectTest +import com.highcapable.yukihookapi.demo.MainActivity import com.highcapable.yukihookapi.hook.factory.encase import com.highcapable.yukihookapi.hook.proxy.YukiHookInitializeProxy import com.highcapable.yukihookapi.hook.type.ActivityClass import com.highcapable.yukihookapi.hook.type.BundleClass +import com.highcapable.yukihookapi.hook.type.StringType +@InjectYukiHookWithXposed class HookMain : YukiHookInitializeProxy { - override fun onHook() = encase { - loadApp(name = "com.highcapable.yukihookapi.demo") { - loadClass(name = "$packageName.MainActivity").hook { - grabMember = hookClass.loadMethod(name = "hello") - replaceTo(any = "这是一段 Hook 的文字内容") + private val moduleName = "com.highcapable.yukihookapi.demo" + + override fun onHook() = encase(moduleName) { + loadApp(name = moduleName) { + MainActivity::class.java.hook { + injectMethod { + name = "test" + returnType = StringType + replaceTo("这段文字已被 Hook 成功") + } + injectMethod { + name = "test" + param(StringType) + returnType = StringType + beforeHook { args().set("方法参数已被 Hook 成功") } + } + } + InjectTest::class.java.hook { + injectConstructor { + param(StringType) + beforeHook { args().set("构造方法已被 Hook 成功") } + } + } + findClass(name = "$packageName.InjectTestName").hook { + injectConstructor { + param(StringType) + beforeHook { args().set("构造方法已被 Hook 成功 [2]") } + } } } loadApp(name = "com.android.browser") { ActivityClass.hook { - grabMember = hookClass.loadMethod(name = "onCreate", BundleClass) - afterHook { - Toast.makeText(thisAny as Activity, "Hook Success", Toast.LENGTH_SHORT).show() + injectMethod { + name = "onCreate" + param(BundleClass) + afterHook { + AlertDialog.Builder(instance()) + .setCancelable(false) + .setTitle("测试 Hook") + .setMessage("Hook 已成功") + .setPositiveButton("OK") { _, _ -> + Toast.makeText(instance(), "Hook Success", Toast.LENGTH_SHORT).show() + }.show() + } } } } diff --git a/yukihookapi/src/main/java/com/highcapable/yukihookapi/YukiHook.kt b/yukihookapi/src/main/java/com/highcapable/yukihookapi/YukiHookAPI.kt similarity index 81% rename from yukihookapi/src/main/java/com/highcapable/yukihookapi/YukiHook.kt rename to yukihookapi/src/main/java/com/highcapable/yukihookapi/YukiHookAPI.kt index 8d4e258c..f89cd4c4 100644 --- a/yukihookapi/src/main/java/com/highcapable/yukihookapi/YukiHook.kt +++ b/yukihookapi/src/main/java/com/highcapable/yukihookapi/YukiHookAPI.kt @@ -30,17 +30,26 @@ package com.highcapable.yukihookapi import android.content.pm.ApplicationInfo -import com.highcapable.yukihookapi.YukiHook.encase +import com.highcapable.yukihookapi.YukiHookAPI.encase import com.highcapable.yukihookapi.annotation.DoNotUseField import com.highcapable.yukihookapi.param.CustomParam import com.highcapable.yukihookapi.param.PackageParam /** * YukiHook 的装载 API 调用类 + * * 可以实现作为模块装载和自定义 Hook 装载两种方式 + * * 模块装载方式已经自动对接 Xposed API - 可直接调用 [encase] 完成操作 */ -object YukiHook { +object YukiHookAPI { + + /** 全局标识 */ + const val TAG = "YukiHookAPI" + + /** Xposed Hook API 绑定的模块包名 */ + @DoNotUseField + internal var modulePackageName = "" /** Xposed Hook API 方法体回调 */ @DoNotUseField @@ -48,9 +57,11 @@ object YukiHook { /** * 作为模块装载调用入口方法 - Xposed API + * @param moduleName 模块包名 - 不填将无法实现监听模块激活状态 * @param initiate Hook 方法体 */ - fun encase(initiate: PackageParam.() -> Unit) { + fun encase(moduleName: String = "", initiate: PackageParam.() -> Unit) { + modulePackageName = moduleName packageParamCallback = initiate } @@ -66,5 +77,5 @@ object YukiHook { packageName: String, appInfo: ApplicationInfo, initiate: PackageParam.() -> Unit - ) = initiate.invoke(PackageParam(customInstance = CustomParam(classLoader, appInfo, packageName))) + ) = initiate.invoke(PackageParam(customParam = CustomParam(classLoader, appInfo, packageName))) } \ No newline at end of file diff --git a/yukihookapi/src/main/java/com/highcapable/yukihookapi/annotation/xposed/InjectYukiHookWithXposed.kt b/yukihookapi/src/main/java/com/highcapable/yukihookapi/annotation/xposed/InjectYukiHookWithXposed.kt new file mode 100644 index 00000000..ee8bee05 --- /dev/null +++ b/yukihookapi/src/main/java/com/highcapable/yukihookapi/annotation/xposed/InjectYukiHookWithXposed.kt @@ -0,0 +1,45 @@ +/** + * MIT License + * + * Copyright (C) 2022 HighCapable + * + * This file is part of YukiHookAPI. + * + * 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/2/3. + */ +package com.highcapable.yukihookapi.annotation.xposed + +import androidx.annotation.Keep +import com.highcapable.yukihookapi.hook.proxy.YukiHookInitializeProxy +import com.highcapable.yukihookapi.hook.xposed.YukiHookLoadPackage + +/** + * 标识注入 YukiHook 的类 + * + * 此类将使用 [YukiHookLoadPackage] 自动调用 XposedInit + * + * 你可以将被注释的类继承于 [YukiHookInitializeProxy] 接口实现 [YukiHookInitializeProxy.onHook] 方法 + * + * 只能拥有一个 Hook 入口 - 多个入口将以首个得到的入口为准 + */ +@Target(AnnotationTarget.CLASS) +@Keep +annotation class InjectYukiHookWithXposed diff --git a/yukihookapi/src/main/java/com/highcapable/yukihookapi/hook/core/YukiHookCreater.kt b/yukihookapi/src/main/java/com/highcapable/yukihookapi/hook/core/YukiHookCreater.kt index 8b459a81..46b43be5 100644 --- a/yukihookapi/src/main/java/com/highcapable/yukihookapi/hook/core/YukiHookCreater.kt +++ b/yukihookapi/src/main/java/com/highcapable/yukihookapi/hook/core/YukiHookCreater.kt @@ -25,11 +25,14 @@ * * This file is Created by fankes on 2022/2/2. */ -@file:Suppress("MemberVisibilityCanBePrivate", "unused") +@file:Suppress("MemberVisibilityCanBePrivate", "unused", "EXPERIMENTAL_API_USAGE") package com.highcapable.yukihookapi.hook.core +import android.util.Log +import com.highcapable.yukihookapi.YukiHookAPI import com.highcapable.yukihookapi.annotation.DoNotUseMethod +import com.highcapable.yukihookapi.hook.utils.ReflectionUtils import com.highcapable.yukihookapi.param.HookParam import com.highcapable.yukihookapi.param.PackageParam import de.robv.android.xposed.XC_MethodHook @@ -39,105 +42,33 @@ import java.lang.reflect.Member /** * YukiHook 核心类实现方法 + * * 这是一个 API 对接类 - 实现原生对接 [XposedBridge] - * @param instance 需要传入 [PackageParam] 实现方法调用 + * @param packageParam 需要传入 [PackageParam] 实现方法调用 * @param hookClass 要 Hook 的 [Class] */ -class YukiHookCreater(private val instance: PackageParam, val hookClass: Class<*>) { - - /** @call Base Field */ - private var beforeHookCallback: (HookParam.() -> Unit)? = null - - /** @call Base Field */ - private var afterHookCallback: (HookParam.() -> Unit)? = null - - /** @call Base Field */ - private var replaceHookCallback: (HookParam.() -> Any?)? = null - - /** 是否为替换模式 */ - private var isReplaceMode = false +class YukiHookCreater(private val packageParam: PackageParam, val hookClass: Class<*>) { /** 设置要 Hook 的方法、构造类 */ - var grabMember: Member? = null + private var hookMembers = HashMap() /** - * 在方法执行完成前 Hook - * 不可与 [replaceAny] [replaceVoid] [replaceTo] 同时使用 - * @param initiate [HookParam] 方法体 + * 注入要 Hook 的方法 + * @param initiate 方法体 */ - fun beforeHook(initiate: HookParam.() -> Unit) { - isReplaceMode = false - beforeHookCallback = initiate - } + fun injectMethod(initiate: MemberHookCreater.() -> Unit) = + MemberHookCreater(isConstructor = false).apply(initiate).apply { + hookMembers[toString()] = this + }.create() /** - * 在方法执行完成后 Hook - * 不可与 [replaceAny] [replaceVoid] [replaceTo] 同时使用 - * @param initiate [HookParam] 方法体 + * 注入要 Hook 的构造类 + * @param initiate 方法体 */ - fun afterHook(initiate: HookParam.() -> Unit) { - isReplaceMode = false - afterHookCallback = initiate - } - - /** - * 替换此方法内容 - 给出返回值 - * 不可与 [beforeHook] [afterHook] 同时使用 - * @param initiate [HookParam] 方法体 - */ - fun replaceAny(initiate: HookParam.() -> Any?) { - isReplaceMode = true - replaceHookCallback = initiate - } - - /** - * 替换此方法内容 - 没有返回值 (Void) - * 不可与 [beforeHook] [afterHook] 同时使用 - * @param initiate [HookParam] 方法体 - */ - fun replaceVoid(initiate: HookParam.() -> Unit) { - isReplaceMode = true - replaceHookCallback = initiate - } - - /** - * 替换方法返回值 - * 不可与 [beforeHook] [afterHook] 同时使用 - * @param any 要替换为的返回值对象 - */ - fun replaceTo(any: Any?) { - isReplaceMode = true - replaceHookCallback = { any } - } - - /** - * 替换方法返回值为 true - * 确保替换方法的返回对象为 [Boolean] - * 不可与 [beforeHook] [afterHook] 同时使用 - */ - fun replaceToTrue() { - isReplaceMode = true - replaceHookCallback = { true } - } - - /** - * 替换方法返回值为 false - * 确保替换方法的返回对象为 [Boolean] - * 不可与 [beforeHook] [afterHook] 同时使用 - */ - fun replaceToFalse() { - isReplaceMode = true - replaceHookCallback = { false } - } - - /** - * 拦截此方法 - * 不可与 [beforeHook] [afterHook] 同时使用 - */ - fun intercept() { - isReplaceMode = true - replaceHookCallback = { null } - } + fun injectConstructor(initiate: MemberHookCreater.() -> Unit) = + MemberHookCreater(isConstructor = true).apply(initiate).apply { + hookMembers[toString()] = this + }.create() /** * Hook 执行入口 - 不可在外部调用 @@ -145,25 +76,241 @@ class YukiHookCreater(private val instance: PackageParam, val hookClass: Class<* */ @DoNotUseMethod fun hook() { - if (grabMember == null) error("Target hook method cannot be null") - if (isReplaceMode) - XposedBridge.hookMethod(grabMember, object : XC_MethodReplacement() { - override fun replaceHookedMethod(param: MethodHookParam?): Any? { - if (param == null) return null - return replaceHookCallback?.invoke(HookParam(param)) - } - }) - else - XposedBridge.hookMethod(grabMember, object : XC_MethodHook() { - override fun beforeHookedMethod(param: MethodHookParam?) { - if (param == null) return - beforeHookCallback?.invoke(HookParam(param)) - } + if (hookMembers.isEmpty()) error("Hook Members is empty,hook aborted") + hookMembers.forEach { (_, hooker) -> hooker.findAndHook() } + } - override fun afterHookedMethod(param: MethodHookParam?) { - if (param == null) return - afterHookCallback?.invoke(HookParam(param)) - } - }) + /** + * 智能全局方法、构造类查找类实现方法 + * + * 处理需要 Hook 的方法 + * @param isConstructor 是否为构造方法 + */ + inner class MemberHookCreater(private val isConstructor: Boolean) { + + /** @call Base Field */ + private var beforeHookCallback: (HookParam.() -> Unit)? = null + + /** @call Base Field */ + private var afterHookCallback: (HookParam.() -> Unit)? = null + + /** @call Base Field */ + private var replaceHookCallback: (HookParam.() -> Any?)? = null + + /** @call Base Field */ + private var onFailureCallback: ((HookParam?, Throwable) -> Unit)? = null + + /** 是否为替换模式 */ + private var isReplaceMode = false + + /** 方法参数 */ + private var params: Array>? = null + + /** 方法名 */ + var name = "" + + /** 方法返回值 */ + var returnType: Class<*>? = null + + /** + * 方法参数 + * @param param 参数数组 + */ + fun param(vararg param: Class<*>) { + params = param + } + + /** + * 在方法执行完成前 Hook + * + * 不可与 [replaceAny]、[replaceUnit]、[replaceTo] 同时使用 + * @param initiate [HookParam] 方法体 + */ + fun beforeHook(initiate: HookParam.() -> Unit) { + isReplaceMode = false + beforeHookCallback = initiate + } + + /** + * 在方法执行完成后 Hook + * + * 不可与 [replaceAny]、[replaceUnit]、[replaceTo] 同时使用 + * @param initiate [HookParam] 方法体 + */ + fun afterHook(initiate: HookParam.() -> Unit) { + isReplaceMode = false + afterHookCallback = initiate + } + + /** + * 替换此方法内容 - 给出返回值 + * + * 不可与 [beforeHook]、[afterHook] 同时使用 + * @param initiate [HookParam] 方法体 + */ + fun replaceAny(initiate: HookParam.() -> Any?) { + isReplaceMode = true + replaceHookCallback = initiate + } + + /** + * 替换此方法内容 - 没有返回值 ([Unit]) + * + * 不可与 [beforeHook]、[afterHook] 同时使用 + * @param initiate [HookParam] 方法体 + */ + fun replaceUnit(initiate: HookParam.() -> Unit) { + isReplaceMode = true + replaceHookCallback = initiate + } + + /** + * 替换方法返回值 + * + * 不可与 [beforeHook]、[afterHook] 同时使用 + * @param any 要替换为的返回值对象 + */ + fun replaceTo(any: Any?) { + isReplaceMode = true + replaceHookCallback = { any } + } + + /** + * 替换方法返回值为 true + * + * 确保替换方法的返回对象为 [Boolean] + * + * 不可与 [beforeHook]、[afterHook] 同时使用 + */ + fun replaceToTrue() { + isReplaceMode = true + replaceHookCallback = { true } + } + + /** + * 替换方法返回值为 false + * + * 确保替换方法的返回对象为 [Boolean] + * + * 不可与 [beforeHook]、[afterHook] 同时使用 + */ + fun replaceToFalse() { + isReplaceMode = true + replaceHookCallback = { false } + } + + /** + * 拦截此方法 + * + * 这将会禁止此方法执行并返回 null + * + * 不可与 [beforeHook]、[afterHook] 同时使用 + */ + fun intercept() { + isReplaceMode = true + replaceHookCallback = { null } + } + + /** + * 得到需要 Hook 的方法 + * @return [Member] + * @throws NoSuchMethodError 如果找不到方法 + */ + private val hookMember: Member by lazy { + when { + name.isBlank() && !isConstructor -> error("Method name cannot be empty") + isConstructor -> + if (params != null) + ReflectionUtils.findConstructorExact(hookClass, *params!!) + else ReflectionUtils.findConstructorExact(hookClass) + else -> + if (params != null) + ReflectionUtils.findMethodBestMatch(hookClass, returnType, name, *params!!) + else ReflectionUtils.findMethodNoParam(hookClass, returnType, name) + } + } + + /** + * Hook 创建入口 - 不可在外部调用 + * @return [MemberHookResult] + */ + @DoNotUseMethod + fun create() = MemberHookResult() + + /** + * Hook 执行入口 - 不可在外部调用 + * @throws IllegalStateException 如果必要参数没有被设置 + */ + @DoNotUseMethod + fun findAndHook() = runCatching { + hookMember.also { member -> + if (isReplaceMode) + XposedBridge.hookMethod(member, object : XC_MethodReplacement() { + override fun replaceHookedMethod(baseParam: MethodHookParam?): Any? { + if (baseParam == null) return null + return HookParam(baseParam).let { param -> + try { + replaceHookCallback?.invoke(param) + } catch (e: Throwable) { + onFailureCallback?.invoke(param, e) ?: onHookFailure(e) + null + } + } + } + }) + else + XposedBridge.hookMethod(member, object : XC_MethodHook() { + override fun beforeHookedMethod(baseParam: MethodHookParam?) { + if (baseParam == null) return + HookParam(baseParam).also { param -> + runCatching { + beforeHookCallback?.invoke(param) + }.onFailure { + onFailureCallback?.invoke(param, it) ?: onHookFailure(it) + } + } + } + + override fun afterHookedMethod(baseParam: MethodHookParam?) { + if (baseParam == null) return + HookParam(baseParam).also { param -> + runCatching { + afterHookCallback?.invoke(param) + }.onFailure { + onFailureCallback?.invoke(param, it) ?: onHookFailure(it) + } + } + } + }) + } + }.onFailure { + onFailureCallback?.invoke(null, it) ?: onHookFailure(it) + } + + /** + * Hook 失败但未设置 [onFailureCallback] 将默认输出失败信息 + * @param throwable 异常信息 + */ + private fun onHookFailure(throwable: Throwable) { + Log.e(YukiHookAPI.TAG, "Try to hook $hookClass[$hookMember] got an Exception", throwable) + } + + override fun toString() = "$name$returnType$params$isConstructor$hookMember$hookClass#YukiHook" + + /** + * 监听 Hook 结果实现类 + * + * 可在这里处理失败事件 + */ + inner class MemberHookResult { + + /** + * 监听 Hook 过程发生错误的回调方法 + * @param initiate 回调错误 - ([HookParam] 当前 Hook 实例 or null,[Throwable] 异常) + */ + fun onFailure(initiate: (HookParam?, Throwable) -> Unit) { + onFailureCallback = initiate + } + } } } \ No newline at end of file diff --git a/yukihookapi/src/main/java/com/highcapable/yukihookapi/hook/factory/ReflectionFactory.kt b/yukihookapi/src/main/java/com/highcapable/yukihookapi/hook/factory/ReflectionFactory.kt new file mode 100644 index 00000000..2b7a5c24 --- /dev/null +++ b/yukihookapi/src/main/java/com/highcapable/yukihookapi/hook/factory/ReflectionFactory.kt @@ -0,0 +1,133 @@ +/** + * MIT License + * + * Copyright (C) 2022 HighCapable + * + * This file is part of YukiHookAPI. + * + * 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/2/2. + */ +@file:Suppress("unused") + +package com.highcapable.yukihookapi.hook.factory + +import java.lang.reflect.Constructor +import java.lang.reflect.Field +import java.lang.reflect.Method + +/** + * 字符串转换为实体类 + * @return [Class] + * @throws NoClassDefFoundError + */ +val String.clazz: Class<*> get() = Class.forName(this) + +/** + * 查找方法是否存在 + * @param name 名称 + * @param clazz params + * @return [Boolean] 是否存在 + */ +fun Class<*>.hasMethod(name: String, vararg clazz: Class<*>): Boolean = + try { + getDeclaredMethod(name, *clazz) + true + } catch (_: Throwable) { + false + } + +/** + * 查找静态 [Field] 的实例 + * @param name 名称 + * @return [Any] 实例对象 + * @throws NoSuchFieldError + */ +fun Class<*>.findStaticField(name: String): Any? = getDeclaredField(name).apply { isAccessible = true }[null] + +/** + * 查找 [Field] 的实例 - 不能是静态 + * @param any 对象 + * @param name 名称 + * @return [Any] 实例对象 + * @throws NoSuchFieldError + */ +fun Class<*>.findField(any: Any?, name: String): Any? = getDeclaredField(name).apply { isAccessible = true }[any] + +/** + * 设置 [Field] - 不能是静态 + * @param any 对象 + * @param name 名称 + * @param value 值 + * @throws NoSuchFieldError + */ +fun Class<*>.modifyField(any: Any?, name: String, value: Any?) { + getDeclaredField(name).apply { + isAccessible = true + set(any, value) + } +} + +/** + * 查找目标变量 + * @param name 方法名 + * @return [Field] + * @throws NoSuchFieldError 如果找不到变量会报错 + */ +fun Class<*>.findField(name: String): Field = + getDeclaredField(name).apply { isAccessible = true } + +/** + * 得到方法 + * @param name 方法名称 + * @param clazz params + * @return [Method] + * @throws NoSuchMethodError + */ +fun Class<*>.findMethod(name: String, vararg clazz: Class<*>): Method? = + getDeclaredMethod(name, *clazz).apply { isAccessible = true } + +/** + * 得到构造类 + * @param parameterTypes params + * @return [Constructor] + * @throws NoSuchMethodError + */ +fun Class<*>.findConstructor(vararg parameterTypes: Class<*>?): Constructor? = + getDeclaredConstructor(*parameterTypes).apply { isAccessible = true } + +/** + * 执行方法 - 静态 + * @param anys 方法参数 + * @return [T] + * @throws IllegalStateException 如果 [T] 类型错误 + */ +inline fun Method.invokeStatic(vararg anys: Any) = + invoke(null, anys) as? T? ?: error("Method ReturnType cannot cast to ${T::class.java}") + +/** + * 执行方法 - 非静态 + * @param any 目标对象 + * @param anys 方法参数 + * @return [T] + * @throws IllegalStateException 如果 [T] 类型错误 + */ +inline fun Method.invokeAny(any: Any?, vararg anys: Any) = + invoke(any, anys) as? T? ?: error("Method ReturnType cannot cast to ${T::class.java}") diff --git a/yukihookapi/src/main/java/com/highcapable/yukihookapi/hook/factory/YukiHookFactory.kt b/yukihookapi/src/main/java/com/highcapable/yukihookapi/hook/factory/YukiHookFactory.kt index e2a8dd90..dec7fd3f 100644 --- a/yukihookapi/src/main/java/com/highcapable/yukihookapi/hook/factory/YukiHookFactory.kt +++ b/yukihookapi/src/main/java/com/highcapable/yukihookapi/hook/factory/YukiHookFactory.kt @@ -29,12 +29,45 @@ package com.highcapable.yukihookapi.hook.factory -import com.highcapable.yukihookapi.YukiHook +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import com.highcapable.yukihookapi.YukiHookAPI import com.highcapable.yukihookapi.hook.proxy.YukiHookInitializeProxy import com.highcapable.yukihookapi.param.PackageParam /** - * 在 [YukiHookInitializeProxy] 中装载 [YukiHook] + * 在 [YukiHookInitializeProxy] 中装载 [YukiHookAPI] + * @param moduleName 模块包名 - 不填将无法实现监听模块激活状态 * @param initiate Hook 方法体 */ -fun YukiHookInitializeProxy.encase(initiate: PackageParam.() -> Unit) = YukiHook.encase(initiate) \ No newline at end of file +fun YukiHookInitializeProxy.encase(moduleName: String = "", initiate: PackageParam.() -> Unit) = + YukiHookAPI.encase(moduleName, initiate) + +/** + * 判断模块是否在太极、无极中激活 + * @return [Boolean] 是否激活 + */ +val Context.isTaichiModuleActive: Boolean + get() { + var isModuleActive = false + runCatching { + var result: Bundle? = null + Uri.parse("content://me.weishu.exposed.CP/").also { uri -> + runCatching { + result = contentResolver.call(uri, "active", null, null) + }.onFailure { + // TaiChi is killed, try invoke + runCatching { + startActivity(Intent("me.weishu.exp.ACTION_ACTIVE").apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }) + }.onFailure { return false } + } + if (result == null) + result = contentResolver.call(Uri.parse("content://me.weishu.exposed.CP/"), "active", null, null) + if (result == null) return false + } + isModuleActive = result?.getBoolean("active", false) == true + } + return isModuleActive + } \ No newline at end of file diff --git a/yukihookapi/src/main/java/com/highcapable/yukihookapi/hook/proxy/YukiHookInitializeProxy.kt b/yukihookapi/src/main/java/com/highcapable/yukihookapi/hook/proxy/YukiHookInitializeProxy.kt index 82deccfd..f1186501 100644 --- a/yukihookapi/src/main/java/com/highcapable/yukihookapi/hook/proxy/YukiHookInitializeProxy.kt +++ b/yukihookapi/src/main/java/com/highcapable/yukihookapi/hook/proxy/YukiHookInitializeProxy.kt @@ -30,19 +30,38 @@ package com.highcapable.yukihookapi.hook.proxy import androidx.annotation.Keep -import com.highcapable.yukihookapi.YukiHook +import com.highcapable.yukihookapi.YukiHookAPI +import com.highcapable.yukihookapi.hook.factory.encase /** * YukiHook 的 Xposed 装载 API 调用接口 - * 自动调用 [onHook] 完成 Hook 开始操作 + * + * Hook 开始时将自动调用 [onHook] 方法 + * + * 请在 [onHook] 中调用 [YukiHookAPI.encase] 或直接调用 [encase] + * + * 可写作如下形式: + * + * override fun onHook() = YukiHookAPI.encase(moduleName = "模块包名") { + * + * // Your code here. + * + * } + * + * 还可写作如下形式: + * + * override fun onHook() = encase(moduleName = "模块包名") { + * + * // Your code here. + * + * } + * + * 详情请参考 https://github.com/fankes/YukiHookAPI/wiki */ @Keep interface YukiHookInitializeProxy { - /** - * 作为模块装载调用入口方法 - Xposed API - * 请在此方法中调用 [YukiHook.encase] - */ + /** 模块装载调用入口方法 - Xposed API */ @Keep fun onHook() } \ No newline at end of file diff --git a/yukihookapi/src/main/java/com/highcapable/yukihookapi/hook/type/VariableTypeFactory.kt b/yukihookapi/src/main/java/com/highcapable/yukihookapi/hook/type/VariableTypeFactory.kt index 7f8e8a6f..131a5c43 100644 --- a/yukihookapi/src/main/java/com/highcapable/yukihookapi/hook/type/VariableTypeFactory.kt +++ b/yukihookapi/src/main/java/com/highcapable/yukihookapi/hook/type/VariableTypeFactory.kt @@ -37,6 +37,12 @@ import java.io.Serializable */ val AnyType get() = Any::class.java +/** + * 获得 [Unit] 类型 + * @return [Class] + */ +val UnitType get() = Unit::class.javaPrimitiveType + /** * 获得 [Boolean] 类型 * @return [Class] diff --git a/yukihookapi/src/main/java/com/highcapable/yukihookapi/hook/utils/ReflectionUtils.java b/yukihookapi/src/main/java/com/highcapable/yukihookapi/hook/utils/ReflectionUtils.java new file mode 100644 index 00000000..32daef60 --- /dev/null +++ b/yukihookapi/src/main/java/com/highcapable/yukihookapi/hook/utils/ReflectionUtils.java @@ -0,0 +1,243 @@ +/* + * MIT License + * + * Copyright (C) 2022 HighCapable + * + * This file is part of YukiHookAPI. + * + * 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 zpp0196 on 2019/1/24 0024. + * This file is Modified by fankes on 2022/2/2 2240. + */ +package com.highcapable.yukihookapi.hook.utils; + +import android.text.TextUtils; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.HashMap; + +import de.robv.android.xposed.XposedHelpers; + +@SuppressWarnings("ALL") +public class ReflectionUtils { + + private static final HashMap fieldCache = new HashMap<>(); + private static final HashMap methodCache = new HashMap<>(); + + private static String getParametersString(Class... clazzes) { + StringBuilder sb = new StringBuilder("("); + boolean first = true; + for (Class clazz : clazzes) { + if (first) + first = false; + else + sb.append(","); + + if (clazz != null) + sb.append(clazz.getCanonicalName()); + else + sb.append("null"); + } + sb.append(")"); + return sb.toString(); + } + + @Deprecated + public static T getStaticObjectIfExists(Class clazz, Class fieldType, String fieldName) { + return getObjectIfExists(clazz, fieldType, fieldName, null); + } + + public static T getObjectIfExists(Class clazz, Class fieldType, String fieldName, Object obj) { + return getObjectIfExists(clazz, fieldType.getName(), fieldName, obj); + } + + public static T getObjectIfExists(Class clazz, String typeName, String fieldName, Object obj) { + try { + Field field = findFieldIfExists(clazz, typeName, fieldName); + return field == null ? null : (T) field.get(obj); + } catch (Exception e) { + return null; + } + } + + @Deprecated + public static void setStaticObjectField(Class clazz, Class fieldType, String fieldName, Object value) + throws NoSuchFieldException, IllegalAccessException { + findFieldIfExists(clazz, fieldType, fieldName).set(null, value); + } + + @Deprecated + public static void setObjectField(Object obj, Class fieldType, String fieldName, Object value) + throws NoSuchFieldException, IllegalAccessException { + if (obj != null) { + Field field = findFieldIfExists(obj.getClass(), fieldType, fieldName); + if (field != null) { + field.set(obj, value); + } + } + } + + public static Field findFieldIfExists(Class clazz, Class fieldType, String fieldName) + throws NoSuchFieldException { + return findFieldIfExists(clazz, fieldType.getName(), fieldName); + } + + public static boolean isCallingFrom(String className) { + StackTraceElement[] stackTraceElements = Thread.currentThread() + .getStackTrace(); + for (StackTraceElement element : stackTraceElements) { + if (element.getClassName() + .contains(className)) { + return true; + } + } + return false; + } + + public static boolean isCallingFromEither(String... classname) { + StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace(); + for (StackTraceElement element : stackTraceElements) { + for (String name : classname) { + if (element.toString().contains(name)) { + return true; + } + } + } + return false; + } + + public static Field findField(Class clazz, String typeName, String fieldName) { + try { + return findFieldIfExists(clazz, typeName, fieldName); + } catch (NoSuchFieldException e) { + throw new IllegalArgumentException("Can't find field <" + clazz.getName() + "#" + typeName + "#" + fieldName + ">"); + } + } + + public static Field findFieldIfExists(Class clazz, String typeName, String fieldName) + throws NoSuchFieldException { + String fullFieldName = clazz.getName() + "#" + fieldName + "#" + typeName; + + if (!fieldCache.containsKey(fullFieldName)) { + if (clazz != null && !TextUtils.isEmpty(typeName) && !TextUtils.isEmpty(fieldName)) { + Class clz = clazz; + do { + for (Field field : clz.getDeclaredFields()) { + if (field.getType() + .getName() + .equals(typeName) && field.getName() + .equals(fieldName)) { + field.setAccessible(true); + fieldCache.put(fullFieldName, field); + return field; + } + } + } while ((clz = clz.getSuperclass()) != null); + throw new NoSuchFieldException(clazz.getName() + "#" + typeName + " " + fieldName); + } + return null; + } else { + Field field = fieldCache.get(fullFieldName); + if (field == null) + throw new NoSuchFieldError(fullFieldName); + return field; + } + } + + /** + * 适用于查找混淆类型的 abcd 方法 - 无 param + * + * @param clazz 方法所在类 + * @param returnType 返回类型 + * @param methodName 方法名 + */ + public static Method findMethodNoParam(Class clazz, Class returnType, String methodName) { + String fullMethodName = clazz.getName() + '#' + methodName + returnType + "#exact#NoParam"; + if (!methodCache.containsKey(fullMethodName)) { + Method method = findMethodIfExists(clazz, returnType, methodName); + methodCache.put(fullMethodName, method); + return method; + } else { + return methodCache.get(fullMethodName); + } + } + + /** + * 不区分 param 整个类搜索 - 适用于混淆方法 abcd + * + * @param clazz 方法所在类 + * @param returnType 返回类型 + * @param methodName 方法名 + * @param parameterTypes 方法参数类型数组 + */ + public static Method findMethodBestMatch(Class clazz, Class returnType, String methodName, Class... parameterTypes) { + String fullMethodName = clazz.getName() + '#' + methodName + returnType + getParametersString(parameterTypes) + "#exact"; + if (!methodCache.containsKey(fullMethodName)) { + Method method = findMethodIfExists(clazz, returnType, methodName, parameterTypes); + methodCache.put(fullMethodName, method); + return method; + } else { + return methodCache.get(fullMethodName); + } + } + + /** + * 查找构造方法 + * + * @param clazz 构造类所在类 + * @param parameterTypes 构造类方法参数类型数组 + */ + public static Constructor findConstructorExact(Class clazz, Class... parameterTypes) { + String fullConstructorName = clazz.getName() + getParametersString(parameterTypes) + "#exact"; + try { + Constructor constructor = clazz.getDeclaredConstructor(parameterTypes); + constructor.setAccessible(true); + return constructor; + } catch (NoSuchMethodException e) { + throw new NoSuchMethodError("Can't find constructor " + fullConstructorName); + } + } + + private static Method findMethodExact(Class clazz, String methodName, Class... parameterTypes) { + String fullMethodName = clazz.getName() + '#' + methodName + getParametersString(parameterTypes) + "#zYukiHook#exact"; + try { + Method method = clazz.getDeclaredMethod(methodName, parameterTypes); + method.setAccessible(true); + return method; + } catch (NoSuchMethodException e) { + throw new NoSuchMethodError("Can't find method " + fullMethodName); + } + } + + private static Method findMethodIfExists(Class clazz, Class returnType, String methodName, Class... parameterTypes) { + long l = System.currentTimeMillis(); + if (clazz != null && !TextUtils.isEmpty(methodName)) { + Class clz = clazz; + if (returnType == null) return findMethodExact(clazz, methodName, parameterTypes); + do { + Method[] methods = XposedHelpers.findMethodsByExactParameters(clazz, returnType, parameterTypes); + for (Method method : methods) if (method.getName().equals(methodName)) return method; + } while ((clz = clz.getSuperclass()) != null); + } + throw new IllegalArgumentException("Method not found in Class <" + clazz.getName() + ">"); + } +} diff --git a/yukihookapi/src/main/java/com/highcapable/yukihookapi/hook/xposed/YukiHookLoadPackage.kt b/yukihookapi/src/main/java/com/highcapable/yukihookapi/hook/xposed/YukiHookLoadPackage.kt index e87746a1..799c182c 100644 --- a/yukihookapi/src/main/java/com/highcapable/yukihookapi/hook/xposed/YukiHookLoadPackage.kt +++ b/yukihookapi/src/main/java/com/highcapable/yukihookapi/hook/xposed/YukiHookLoadPackage.kt @@ -31,35 +31,54 @@ package com.highcapable.yukihookapi.hook.xposed import android.util.Log import androidx.annotation.Keep -import com.highcapable.yukihookapi.YukiHook +import com.highcapable.yukihookapi.YukiHookAPI +import com.highcapable.yukihookapi.annotation.xposed.InjectYukiHookWithXposed +import com.highcapable.yukihookapi.hook.proxy.YukiHookInitializeProxy +import com.highcapable.yukihookapi.hook.type.BooleanType import com.highcapable.yukihookapi.param.PackageParam import de.robv.android.xposed.IXposedHookLoadPackage import de.robv.android.xposed.callbacks.XC_LoadPackage /** * 接管 Xposed 的 [IXposedHookLoadPackage] 入口 - * 你可以使用 [YukiHook.encase] 来监听模块开始装载 + * + * 你可以使用 [YukiHookAPI.encase] 或在 [YukiHookInitializeProxy] 中监听模块开始装载 + * + * 需要标识 Hook 入口的类 - 请声明注释 [InjectYukiHookWithXposed] */ @Keep class YukiHookLoadPackage : IXposedHookLoadPackage { override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam?) { if (lpparam == null) return - try { + runCatching { /** 执行入口方法 */ Class.forName(hookEntryClassName()).apply { getDeclaredMethod("onHook").apply { isAccessible = true } .invoke(getDeclaredConstructor().apply { isAccessible = true }.newInstance()) } - } catch (e: Throwable) { - Log.e("YukiHookAPI", "Try to load ${hookEntryClassName()} Failed", e) + }.onFailure { + Log.e(YukiHookAPI.TAG, "Try to load ${hookEntryClassName()} Failed", it) + } + /** 装载 APP Hook 实体类 */ + PackageParam(lpparam).apply { + /** Hook 模块激活状态 */ + loadApp(name = YukiHookAPI.modulePackageName) { + YukiHookModuleStatus::class.java.hook { + injectMethod { + name = "isActive" + returnType = BooleanType + replaceToTrue() + }.onFailure { _, t -> Log.e(YukiHookAPI.TAG, "Try to Hook ModuleStatus Failed", t) } + } + } + /** 设置装载回调 */ + YukiHookAPI.packageParamCallback?.invoke(this) } - /** 设置装载回调 */ - YukiHook.packageParamCallback?.invoke(PackageParam(lpparam)) } /** - * 获得目标装载类名 - AOP + * 获得目标装载类名 - 通过 APT 自动设置 TODO 待实现 * @return [String] 目标装载类名 */ @Keep diff --git a/yukihookapi/src/main/java/com/highcapable/yukihookapi/hook/xposed/YukiHookModuleStatus.kt b/yukihookapi/src/main/java/com/highcapable/yukihookapi/hook/xposed/YukiHookModuleStatus.kt new file mode 100644 index 00000000..0210933e --- /dev/null +++ b/yukihookapi/src/main/java/com/highcapable/yukihookapi/hook/xposed/YukiHookModuleStatus.kt @@ -0,0 +1,52 @@ +/** + * MIT License + * + * Copyright (C) 2022 HighCapable + * + * This file is part of YukiHookAPI. + * + * 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/2/3. + */ +package com.highcapable.yukihookapi.hook.xposed + +import android.util.Log +import androidx.annotation.Keep +import com.highcapable.yukihookapi.YukiHookAPI +import com.highcapable.yukihookapi.hook.xposed.YukiHookModuleStatus.isActive + +/** + * 这是一个 Xposed 模块 Hook 状态类 + * + * 我们需要监听自己的模块是否被激活 - 可直接调用这个类的 [isActive] 方法 + */ +@Keep +object YukiHookModuleStatus { + + /** + * 此方法经过 Hook 后返回 true 即模块已激活 + * @return [Boolean] + */ + @Keep + fun isActive(): Boolean { + Log.d(YukiHookAPI.TAG, "hook this method got active status") + return false + } +} \ No newline at end of file diff --git a/yukihookapi/src/main/java/com/highcapable/yukihookapi/param/CustomParam.kt b/yukihookapi/src/main/java/com/highcapable/yukihookapi/param/CustomParam.kt index cc04136b..412653a0 100644 --- a/yukihookapi/src/main/java/com/highcapable/yukihookapi/param/CustomParam.kt +++ b/yukihookapi/src/main/java/com/highcapable/yukihookapi/param/CustomParam.kt @@ -35,4 +35,8 @@ import android.content.pm.ApplicationInfo * @param appInfo APP [ApplicationInfo] * @param packageName 包名 */ -class CustomParam(var appClassLoader: ClassLoader, var appInfo: ApplicationInfo, var packageName: String) \ No newline at end of file +class CustomParam( + var appClassLoader: ClassLoader, + var appInfo: ApplicationInfo, + var packageName: String +) \ No newline at end of file diff --git a/yukihookapi/src/main/java/com/highcapable/yukihookapi/param/HookParam.kt b/yukihookapi/src/main/java/com/highcapable/yukihookapi/param/HookParam.kt index 3ecd686b..cfaf2122 100644 --- a/yukihookapi/src/main/java/com/highcapable/yukihookapi/param/HookParam.kt +++ b/yukihookapi/src/main/java/com/highcapable/yukihookapi/param/HookParam.kt @@ -35,86 +35,105 @@ import java.lang.reflect.Method /** * Hook 方法、构造类的目标对象实现类 - * @param instance 对接 Xposed API 的 [XC_MethodHook.MethodHookParam] + * @param baseParam 对接 Xposed API 的 [XC_MethodHook.MethodHookParam] */ -class HookParam(private val instance: XC_MethodHook.MethodHookParam) { +class HookParam(private val baseParam: XC_MethodHook.MethodHookParam) { /** - * 获取 Hook 方法的参数对象数组 + * 获取当前 [method] or [constructor] 的参数对象数组 * @return [Array] */ - val args get() = instance.args ?: arrayOf(0) + val args get() = baseParam.args ?: arrayOf(0) /** - * 获取 Hook 方法的参数对象数组第一位 + * 获取当前 [method] or [constructor] 的参数对象数组第一位 * @return [Array] * @throws IllegalStateException 如果数组为空或对象为空 */ val firstArgs get() = args[0] ?: error("HookParam args[0] with a non-null object") /** - * 获取 Hook 方法的参数对象数组最后一位 + * 获取当前 [method] or [constructor] 的参数对象数组最后一位 * @return [Array] * @throws IllegalStateException 如果数组为空或对象为空 */ val lastArgs get() = args[args.lastIndex] ?: error("HookParam args[lastIndex] with a non-null object") /** - * 获取 Hook 实例的 Class - * @return [Class] - */ - val thisClass get() = instance.thisObject.javaClass - - /** - * 获取 Hook 实例的对象 + * 获取当前 Hook 实例的对象 * @return [Any] * @throws IllegalStateException 如果对象为空 */ - val thisAny get() = instance.thisObject ?: error("HookParam must with a non-null object") + val instance get() = baseParam.thisObject ?: error("HookParam must with a non-null object") /** - * 获取 Hook 当前普通方法 + * 获取当前 Hook 实例的类对象 + * @return [Class] + */ + val instanceClass get() = instance.javaClass + + /** + * 获取当前 Hook 的方法 * @return [Method] * @throws IllegalStateException 如果方法为空或方法类型不是 [Method] */ - val method get() = instance.method as? Method? ?: error("Current hook method type is wrong or null") + val method get() = baseParam.method as? Method? ?: error("Current hook method type is wrong or null") /** - * 获取 Hook 当前构造方法 + * 获取当前 Hook 的构造方法 * @return [Constructor] * @throws IllegalStateException 如果方法为空或方法类型不是 [Constructor] */ - val constructor get() = instance.method as? Constructor<*>? ?: error("Current hook constructor type is wrong or null") + val constructor get() = baseParam.method as? Constructor<*>? ?: error("Current hook constructor type is wrong or null") /** - * 获取、设置 Hook 方法的返回值 + * 获取、设置当前 [method] or [constructor] 的返回值 * @return [Any] or null */ var result: Any? - get() = instance.result + get() = baseParam.result set(value) { - instance.result = value + baseParam.result = value } /** - * 获取 Hook 实例的对象 [T] - * @return [Any] + * 获取当前 Hook 实例的对象 [T] + * @return [T] * @throws IllegalStateException 如果对象为空或对象类型不是 [T] */ - inline fun thisAny() = thisAny as? T? ?: error("HookParam object cannot cast to ${T::class.java.name}") + inline fun instance() = instance as? T? ?: error("HookParam object cannot cast to ${T::class.java.name}") /** - * 获取 Hook 方法的参数实例化对象类 + * 获取当前 [method] or [constructor] 的参数实例化对象类 * @param index 参数对象数组下标 - 默认是 0 * @return [Array] */ fun args(index: Int = 0) = ArgsModifyer(index) /** - * 拦截整个方法体 + * 设置 [result] 返回值为 true + * + * 请确保返回值类型为 [Boolean] + */ + fun resultTrue() { + result = true + } + + /** + * 设置 [result] 返回值为 false + * + * 请确保返回值类型为 [Boolean] + */ + fun resultFalse() { + result = false + } + + /** + * 设置返回值为 null + * * 此方法将强制设置方法体的 [result] 为 null */ - fun intercept() { + fun resultNull() { result = null } @@ -132,24 +151,27 @@ class HookParam(private val instance: XC_MethodHook.MethodHookParam) { fun set(any: T?) { if (args.isEmpty()) error("HookParam method args is empty,mabe not has args") if (index > args.lastIndex) error("HookParam method args index out of bounds,max is ${args.lastIndex}") - instance.args[index] = any + baseParam.args[index] = any } /** * 设置方法参数的实例对象为 null + * * 此方法可以将任何被 Hook 的目标对象设置为空 */ fun setNull() = set(null) /** * 设置方法参数的实例对象为 true - * 请确保目标对象的类型是 [Boolean] 不然会出错 + * + * 请确保目标对象的类型是 [Boolean] 不然会发生意想不到的问题 */ fun setTrue() = set(true) /** * 设置方法参数的实例对象为 false - * 请确保目标对象的类型是 [Boolean] 不然会出错 + * + * 请确保目标对象的类型是 [Boolean] 不然会发生意想不到的问题 */ fun setFalse() = set(false) } diff --git a/yukihookapi/src/main/java/com/highcapable/yukihookapi/param/PackageParam.kt b/yukihookapi/src/main/java/com/highcapable/yukihookapi/param/PackageParam.kt index ae535e3f..9599e6f6 100644 --- a/yukihookapi/src/main/java/com/highcapable/yukihookapi/param/PackageParam.kt +++ b/yukihookapi/src/main/java/com/highcapable/yukihookapi/param/PackageParam.kt @@ -32,53 +32,57 @@ package com.highcapable.yukihookapi.param import android.content.pm.ApplicationInfo import com.highcapable.yukihookapi.hook.core.YukiHookCreater import de.robv.android.xposed.callbacks.XC_LoadPackage -import java.lang.reflect.Constructor -import java.lang.reflect.Method /** * 装载 Hook 的目标 APP 入口对象实现类 - * 如果是侵入式 Hook 自身 APP 可将参数 [instance] 置空获得当前类的 [ClassLoader] - * @param instance 对接 Xposed API 的 [XC_LoadPackage.LoadPackageParam] - 默认空 - * @param customInstance 自定义装载类 - 默认空 + * + * 如果你想将 YukiHook 作为 Hook API 使用 - 你可自定义 [customParam] + * + * ⚠️ 特别注意如果 [baseParam] 和 [customParam] 都为空将发生问题 + * @param baseParam 对接 Xposed API 的 [XC_LoadPackage.LoadPackageParam] - 默认空 + * @param customParam 自定义装载类 - 默认空 */ class PackageParam( - private val instance: XC_LoadPackage.LoadPackageParam? = null, - private val customInstance: CustomParam? = null + private val baseParam: XC_LoadPackage.LoadPackageParam? = null, + private val customParam: CustomParam? = null ) { /** * 获取当前 APP 的 [ClassLoader] * @return [ClassLoader] - * @throws IllegalStateException 如果 ClassLoader 是空的 + * @throws IllegalStateException 如果 [ClassLoader] 是空的 */ val appClassLoader - get() = instance?.classLoader ?: customInstance?.appClassLoader ?: javaClass.classLoader + get() = baseParam?.classLoader ?: customParam?.appClassLoader ?: javaClass.classLoader ?: error("PackageParam ClassLoader is null") /** * 获取当前 APP 的 [ApplicationInfo] * @return [ApplicationInfo] */ - val appInfo get() = instance?.appInfo ?: customInstance?.appInfo ?: ApplicationInfo() + val appInfo get() = baseParam?.appInfo ?: customParam?.appInfo ?: ApplicationInfo() /** * 获取当前 APP 的进程名称 - * 默认的进程名称是 [packageName] + * + * 默认的进程名称是 [packageName] 如果自定义了 [customParam] 将返回包名 * @return [String] */ - val processName get() = instance?.processName ?: "" + val processName get() = baseParam?.processName ?: customParam?.packageName ?: "" /** * 获取当前 APP 的包名 * @return [String] */ - val packageName get() = instance?.packageName ?: customInstance?.packageName ?: "" + val packageName get() = baseParam?.packageName ?: customParam?.packageName ?: "" /** * 获取当前 APP 是否为第一个 Application + * + * 若自定义了 [customParam] 将永远返回 true * @return [Boolean] */ - val isFirstApplication get() = instance?.isFirstApplication ?: true + val isFirstApplication get() = baseParam?.isFirstApplication ?: true /** * 装载并 Hook 指定包名的 APP @@ -90,36 +94,26 @@ class PackageParam( } /** - * 通过字符串装载 [Class] + * 将目标 [Class] 绑定到 [appClassLoader] + * + * ⚠️ 请注意未绑定到 [appClassLoader] 的 [Class] 不能被装载 - 调用 [hook] 方法会自动绑定 + * @return [Class] + * @throws NoClassDefFoundError 如果找不到类会报错 + */ + fun Class<*>.bind(): Class<*> = appClassLoader.loadClass(name) + + /** + * 通过 [appClassLoader] 查询并装载 [Class] * @param name 类名 * @return [Class] * @throws NoClassDefFoundError 如果找不到类会报错 */ - fun loadClass(name: String): Class<*> = appClassLoader.loadClass(name) - - /** - * 查找目标方法 - * @param name 方法名 - * @param params 方法参数 - 没有可空 - * @return [Method] - * @throws NoSuchMethodError 如果找不到方法会报错 - */ - fun Class<*>.loadMethod(name: String, vararg params: Class<*>): Method = - getDeclaredMethod(name, *params).apply { isAccessible = true } - - /** - * 查找目标构造类 - * @param params 方法参数 - 没有可空 - * @return [Constructor] - * @throws NoSuchMethodError 如果找不到方法会报错 - */ - fun Class<*>.loadConstructor(vararg params: Class<*>): Constructor<*> = - getDeclaredConstructor(*params).apply { isAccessible = true } + fun findClass(name: String): Class<*> = appClassLoader.loadClass(name) /** * Hook 方法、构造类 * @param initiate 方法体 */ fun Class<*>.hook(initiate: YukiHookCreater.() -> Unit) = - YukiHookCreater(instance = this@PackageParam, hookClass = this).apply(initiate).hook() + YukiHookCreater(packageParam = this@PackageParam, hookClass = bind()).apply(initiate).hook() } \ No newline at end of file