diff --git a/library/jni/art/runtime/art_method.hpp b/library/jni/art/runtime/art_method.hpp index db4721c..8b079bb 100644 --- a/library/jni/art/runtime/art_method.hpp +++ b/library/jni/art/runtime/art_method.hpp @@ -40,6 +40,10 @@ public: return GetAccessFlags() & kAccStatic; } + bool IsNative() { + return GetAccessFlags() & kAccNative; + } + void CopyFrom(const ArtMethod *other) { memcpy(this, other, art_method_size); } diff --git a/library/jni/include/lsplant.hpp b/library/jni/include/lsplant.hpp index 83213e8..0ef7474 100644 --- a/library/jni/include/lsplant.hpp +++ b/library/jni/include/lsplant.hpp @@ -3,35 +3,124 @@ #include #include +/// \namespace namespace of LSPlant namespace lsplant { +inline namespace v1 { +/// \struct InitInfo struct InitInfo { + /// \typedef Type of inline hook function. + /// In \p std::function form so that user can use lambda expression with capture list.
+ /// \p target is the target function to be hooked.
+ /// \p hooker is the hooker function to replace the \p target function.
+ /// \p return is the backup function that points to the previous target function. + /// it should return null if hook fails and nonnull if successes. using InlineHookFunType = std::function; + /// \typedef Type of inline unhook function. + /// In \p std::function form so that user can use lambda expression with capture list.
+ /// \p func is the target function that is previously hooked.
+ /// \p return should indicate the status of unhooking.
using InlineUnhookFunType = std::function; + /// \typedef Type of symbol resolver to \p libart.so. + /// In \p std::function form so that user can use lambda expression with capture list.
+ /// \p symbol_name is the symbol name that needs to retrieve.
+ /// \p return is the absolute address in the memory that points to the target symbol. It should + /// be null if the symbol cannot be found.
+ /// \note It should be able to resolve symbols from both .dynsym and .symtab. using ArtSymbolResolver = std::function; + /// \brief The inline hooker function. Must not be null. InlineHookFunType inline_hooker; + /// \brief The inline unhooker function. Must not be null. InlineUnhookFunType inline_unhooker; + /// \brief The symbol resolver to \p libart.so. Must not be null. ArtSymbolResolver art_symbol_resolver; }; +/// \brief Initialize LSPlant for procceding hook. +/// It mainly prefetch needed symbols and hook some functions. +/// \param[in] env The Java environment. Must not be null. +/// \param[in] info The information for initialized. \ref InitInfo. +/// Basically, the info provides the inline hooker and unhooker together with a symbol resolver of +/// libart.so to hook and extract needed native functions of ART. +/// \return Indicate whether initialization succeed. Behavior is undefined if calling other +/// LSPlant interfaces before initialization or after a fail initialization. [[nodiscard]] [[maybe_unused]] [[gnu::visibility("default")]] bool Init(JNIEnv *env, const InitInfo &info); +/// \brief Hook a Java method by providing the \p target_method together with the context object +/// \p hooker_object and its callback \p callback_method. +/// \param[in] env The Java environment. Must not be null. +/// \param[in] target_method The method id to the method you want to hook. Must not be null. +/// \param[in] hooker_object The hooker object to store the context of the hook. +/// The most likely usage is to store the \b backup method into it so that when \b callback_method +/// is invoked, it can call the original method. Another scenario is that, for example, +/// in Xposed framework, multiple modules can hook the same Java method and the \b hooker_object +/// can be used to store all the callbacks to allow multiple modules work simultaneously without +/// conflict. +/// \param[in] callback_method The callback method to the \p hooker_object is used to replace the +/// \p target_method. Whenever the \p target_method is invoked, the \p callback_method will be +/// inked instead of the original \p target_method. The signature of the \p callback_method must +/// be:
+/// \code{.java} +/// Object callback_method(Object []args) +/// \endcode
+/// That is, the return type must be \p Object and the parameter type must be \b Object[]. Behavior +/// is undefined if the signature does not match the requirement. +/// Extra info can be provided by defining member variables of \p hooker_object. +/// This method must be a method to \p hooker_object. +/// \return The backup method. You can invoke it by reflection to invoke the original method. null +/// if fails. +/// \note This function thread safe (you can call it simultaneously from multiple thread) +/// but it's not atomic to the same \b target_method. That means \p UnHook or \p IsUnhook does +/// not guarantee to work properly on the same \p target_method before it returns. Also, +/// simultaneously call on this function with the same \p target_method does not guarantee only one +/// will success. If you call this with different \p hooker_object on the same target_method +/// simultaneously, the behavior is undefined. [[nodiscard]] [[maybe_unused]] [[gnu::visibility("default")]] jmethodID Hook(JNIEnv *env, jmethodID target_method, jobject hooker_object, jmethodID callback_method); +/// Unhook a Java function that is previously hooked. +/// \param[in] env The Java environment. +/// \param[in] target_method The target method that is previously hooked. +/// \return Indicate whether the unhook succeed. +/// \note please read Hook's note for more details. [[nodiscard]] [[maybe_unused]] [[gnu::visibility("default")]] bool UnHook(JNIEnv *env, jmethodID target_method); +/// Check if a Java function is hooked by LSPlant or not +/// \param[in] env The Java environment. +/// \param[in] method The method to check if it was hooked or not. +/// \return If \p method hooked, ture; otherwise, false. +/// \note please read Hook's note for more details. [[nodiscard]] [[maybe_unused]] [[gnu::visibility("default")]] bool IsHooked(JNIEnv *env, jmethodID method); +/// Deoptimize a method to avoid hooked callee not being called because of inline +/// \param[in] env The Java environment. +/// \param[in] method The method to deoptimize. By deoptimizing the method, the method will back all +/// callee without inlining. For example, if you hooked a short method B that is invoked by method +/// A, and you find that your callback to B is not invoked after hooking, then it may mean A has +/// inlined B inside its method body. To force A to call your hooked B, you can deoptimize A and then +/// your hook can take effect. Generally, you need to find all the callers of your hooked callee +/// and that can be hardly achieve. Use this function if you are sure the deoptimized callers +/// are all you need. Otherwise, it would be better to change the hook point or to deoptimize the +/// whole app manually (by simple reinstall the app without uninstalled). +/// \return Indicate whether the deoptimizing succeed or not. +/// \note It is safe to call deoptimizing on a hooked method because the deoptimization will +/// perform on the backup method instead. [[nodiscard]] [[maybe_unused]] [[gnu::visibility("default")]] bool Deoptimize(JNIEnv *env, jmethodID method); +/// Get the registered native function pointer of a native function. It helps user to hook native +/// methods directly by backing up the native function pointer this function returns and +/// env->registerNatives another native function pointer. +/// \param[in] env The Java environment. +/// \param[in] method The native method to get the native function pointer. +/// \return The native function pointer the \p method previously registered. If it has not been +/// registered or it is not a native method, null is returned instead. [[nodiscard]] [[maybe_unused]] [[gnu::visibility("default")]] void *GetNativeFunction(JNIEnv *env, jmethodID method); - +} } // namespace lsplant diff --git a/library/jni/lsplant.cc b/library/jni/lsplant.cc index 1101f9a..92876f6 100644 --- a/library/jni/lsplant.cc +++ b/library/jni/lsplant.cc @@ -418,6 +418,10 @@ void OnPending(art::ArtMethod *target, art::ArtMethod *hook, art::ArtMethod *bac } } +inline namespace v1 { + +using ::lsplant::IsHooked; + [[maybe_unused]] bool Init(JNIEnv *env, const InitInfo &info) { bool static kInit = InitJNI(env) && InitNative(env, info); @@ -489,14 +493,15 @@ Hook(JNIEnv *env, jmethodID target_method, jobject hooker_object, jmethodID call LOGE("Failed to decode target class"); return nullptr; } - auto class_def = miror_class->GetClassDef(); + const auto *class_def = miror_class->GetClassDef(); if (!class_def) { LOGE("Failed to get target class def"); return nullptr; } RecordPending(class_def, target, hook, backup); return backup_method; - } else if (DoHook(target, hook, backup)) { + } + if (DoHook(target, hook, backup)) { RecordHooked(target, JNI_NewGlobalRef(env, reflected_backup)); if (!is_proxy) [[likely]] RecordJitMovement(target, backup); return backup_method; @@ -551,6 +556,17 @@ bool IsHooked(JNIEnv *env, jmethodID method) { bool Deoptimize(JNIEnv *env, jmethodID method) { auto reflected = JNI_ToReflectedMethod(env, jclass{ nullptr }, method, false); auto *art_method = ArtMethod::FromReflectedMethod(env, reflected); + if (IsHooked(art_method)) { + std::shared_lock lk(hooked_methods_lock_); + auto it = hooked_methods_.find(art_method); + if (it != hooked_methods_.end()) { + auto *reflected_backup = it->second; + art_method = ArtMethod::FromReflectedMethod(env, reflected_backup); + } + } + if (!art_method) { + return false; + } return ClassLinker::SetEntryPointsToInterpreter(art_method); } @@ -558,9 +574,12 @@ bool Deoptimize(JNIEnv *env, jmethodID method) { void *GetNativeFunction(JNIEnv *env, jmethodID method) { auto reflected = JNI_ToReflectedMethod(env, jclass{ nullptr }, method, false); auto *art_method = ArtMethod::FromReflectedMethod(env, reflected); + if (!art_method->IsNative()) return nullptr; return art_method->GetData(); } +} // namespace v1 + } // namespace lsplant #pragma clang diagnostic pop