New hook API (#2644)

This commit is contained in:
Nullptr 2023-07-22 12:52:38 +08:00 committed by GitHub
parent bd7a9b91b2
commit bee7938002
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 147 additions and 171 deletions

View File

@ -42,9 +42,6 @@
getFrameworkVersion(...);
getFrameworkVersionCode(...);
getFrameworkPrivilege(...);
featuredMethod(...);
hookBefore(...);
hookAfter(...);
hook(...);
deoptimize(...);
invokeOrigin(...);

View File

@ -20,20 +20,12 @@
package de.robv.android.xposed;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.lsposed.lspd.nativebridge.HookBridge;
import java.lang.reflect.Executable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import de.robv.android.xposed.callbacks.IXUnhook;
import de.robv.android.xposed.callbacks.XCallback;
import io.github.libxposed.api.XposedInterface;
/**
* Callback class for method hooks.
@ -103,7 +95,7 @@ public abstract class XC_MethodHook extends XCallback {
/**
* Wraps information about the method call and allows to influence it.
*/
public static final class MethodHookParam <T extends Executable> extends XCallback.Param implements XposedInterface.BeforeHookCallback<T>, XposedInterface.AfterHookCallback<T> {
public static final class MethodHookParam<T extends Executable> extends XCallback.Param {
/**
* @hide
*/
@ -158,11 +150,6 @@ public abstract class XC_MethodHook extends XCallback {
return throwable;
}
@Override
public boolean isSkipped() {
return returnEarly;
}
/**
* Returns true if an exception was thrown by the method.
*/
@ -189,62 +176,6 @@ public abstract class XC_MethodHook extends XCallback {
throw throwable;
return result;
}
@NonNull
@Override
public T getOrigin() {
return (T) method;
}
@Nullable
@Override
public Object getThis() {
return thisObject;
}
@NonNull
@Override
public Object[] getArgs() {
return args;
}
@Nullable
@Override
public <U> U getArg(int index) {
return (U) args[index];
}
@Override
public <U> void setArg(int index, U value) {
args[index] = value;
}
@Override
public void returnAndSkip(@Nullable Object returnValue) {
setResult(returnValue);
}
@Override
public void throwAndSkip(@Nullable Throwable throwable) {
setThrowable(throwable);
}
@Nullable
@Override
public Object invokeOrigin() throws InvocationTargetException, IllegalAccessException {
return HookBridge.invokeOriginalMethod((Executable) method, thisObject, args);
}
@Nullable
@Override
public <U> U getExtra(@NonNull String key) {
return (U) extras.get(key);
}
@Override
public <U> void setExtra(@NonNull String key, @Nullable U value) throws ConcurrentModificationException {
extras.put(key, value);
}
}
/**

View File

@ -35,8 +35,10 @@ import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
@ -219,7 +221,7 @@ public final class XposedBridge {
throw new IllegalArgumentException("callback should not be null!");
}
if (!HookBridge.hookMethod((Executable) hookMethod, AdditionalHookInfo.class, callback.priority, callback)) {
if (!HookBridge.hookMethod(false, (Executable) hookMethod, AdditionalHookInfo.class, callback.priority, callback)) {
log("Failed to hook " + hookMethod);
return null;
}
@ -238,7 +240,7 @@ public final class XposedBridge {
@Deprecated
public static void unhookMethod(Member hookMethod, XC_MethodHook callback) {
if (hookMethod instanceof Executable) {
HookBridge.unhookMethod((Executable) hookMethod, callback);
HookBridge.unhookMethod(false, (Executable) hookMethod, callback);
}
}
@ -438,7 +440,7 @@ public final class XposedBridge {
}
}
Object[] callbacksSnapshot = HookBridge.callbackSnapshot(method);
Object[] callbacksSnapshot = HookBridge.callbackSnapshot(HookerCallback.class, method);
if (callbacksSnapshot == null || callbacksSnapshot.length == 0) {
try {
return HookBridge.invokeOriginalMethod(method, param.thisObject, param.args);
@ -446,6 +448,7 @@ public final class XposedBridge {
throw (Throwable) HookBridge.invokeOriginalMethod(getCause, ite);
}
}
Queue<Object> extras = new ArrayDeque<>(callbacksSnapshot.length);
// call "before method" callbacks
int beforeIdx = 0;
@ -454,8 +457,9 @@ public final class XposedBridge {
var cb = callbacksSnapshot[beforeIdx];
if (HookBridge.instanceOf(cb, XC_MethodHook.class)) {
((XC_MethodHook) cb).beforeHookedMethod(param);
} else if (HookBridge.instanceOf(cb, XposedInterface.BeforeHooker.class)) {
((XposedInterface.BeforeHooker<T>) cb).before(param);
} else if (HookBridge.instanceOf(cb, HookerCallback.class)) {
var hooker = (HookerCallback) cb;
extras.add(hooker.beforeInvocation.invoke(null, method, param.thisObject, param.args));
}
} catch (Throwable t) {
XposedBridge.log(t);
@ -492,8 +496,9 @@ public final class XposedBridge {
try {
if (HookBridge.instanceOf(cb, XC_MethodHook.class)) {
((XC_MethodHook) cb).afterHookedMethod(param);
} else if (HookBridge.instanceOf(cb, XposedInterface.AfterHooker.class)) {
((XposedInterface.AfterHooker<T>) cb).after(param);
} else if (HookBridge.instanceOf(cb, HookerCallback.class)) {
var hooker = (HookerCallback) cb;
hooker.afterInvocation.invoke(null, extras.poll(), lastResult);
}
} catch (Throwable t) {
XposedBridge.log(t);
@ -518,4 +523,14 @@ public final class XposedBridge {
}
}
}
public static class HookerCallback {
Method beforeInvocation;
Method afterInvocation;
public HookerCallback(Method beforeInvocation, Method afterInvocation) {
this.beforeInvocation = beforeInvocation;
this.afterInvocation = afterInvocation;
}
}
}

View File

@ -55,10 +55,12 @@ import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@ -66,8 +68,12 @@ import java.util.concurrent.ConcurrentHashMap;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import io.github.libxposed.api.XposedContext;
import io.github.libxposed.api.XposedInterface;
import io.github.libxposed.api.XposedModule;
import io.github.libxposed.api.XposedModuleInterface;
import io.github.libxposed.api.annotations.AfterInvocation;
import io.github.libxposed.api.annotations.BeforeInvocation;
import io.github.libxposed.api.annotations.XposedHooker;
import io.github.libxposed.api.errors.HookFailedError;
import io.github.libxposed.api.utils.DexParser;
@ -862,41 +868,68 @@ public class LSPosedContext extends XposedContext {
}
}
@Override
public Object featuredMethod(String name, Object... args) {
throw new UnsupportedOperationException();
}
private <T, U extends Executable> MethodUnhooker<T, U> doHook(U hookMethod, int priority, T callback) {
private <T extends Executable> MethodUnhooker<T> doHook(T hookMethod, int priority, Class<? extends Hooker> hooker) {
if (Modifier.isAbstract(hookMethod.getModifiers())) {
throw new IllegalArgumentException("Cannot hook abstract methods: " + hookMethod);
} else if (hookMethod.getDeclaringClass().getClassLoader() == LSPosedContext.class.getClassLoader()) {
throw new IllegalArgumentException("Do not allow hooking inner methods");
} else if (hookMethod.getDeclaringClass() == Method.class && hookMethod.getName().equals("invoke")) {
throw new IllegalArgumentException("Cannot hook Method.invoke");
}
if (callback == null) {
} else if (hooker == null) {
throw new IllegalArgumentException("hooker should not be null!");
} else if (hooker.getAnnotation(XposedHooker.class) == null) {
throw new IllegalArgumentException("Hooker should be annotated with @XposedHooker");
}
if (HookBridge.hookMethod(hookMethod, XposedBridge.AdditionalHookInfo.class, priority, callback)) {
Method beforeInvocation = null, afterInvocation = null;
var modifiers = Modifier.PUBLIC | Modifier.STATIC;
for (var method : hooker.getDeclaredMethods()) {
if (method.getAnnotation(BeforeInvocation.class) != null) {
if (beforeInvocation != null) {
throw new IllegalArgumentException("More than one method annotated with @BeforeInvocation");
}
boolean valid;
valid = (method.getModifiers() & modifiers) == modifiers;
valid &= Arrays.equals(method.getParameterTypes(), new Class[]{Member.class, Object.class, Object[].class});
valid &= method.getReturnType().equals(Hooker.class);
if (!valid) {
throw new IllegalArgumentException("BeforeInvocation method format is invalid");
}
beforeInvocation = method;
}
if (method.getAnnotation(AfterInvocation.class) != null) {
if (afterInvocation != null) {
throw new IllegalArgumentException("More than one method annotated with @AfterInvocation");
}
boolean valid;
valid = (method.getModifiers() & modifiers) == modifiers;
valid &= Arrays.equals(method.getParameterTypes(), new Class[]{Hooker.class, Object.class});
valid &= method.getReturnType().equals(void.class);
if (!valid) {
throw new IllegalArgumentException("AfterInvocation method format is invalid");
}
afterInvocation = method;
}
}
if (beforeInvocation == null) {
throw new IllegalArgumentException("No method annotated with @BeforeInvocation");
}
if (afterInvocation == null) {
throw new IllegalArgumentException("No method annotated with @AfterInvocation");
}
var callback = new XposedBridge.HookerCallback(beforeInvocation, afterInvocation);
if (HookBridge.hookMethod(true, hookMethod, XposedBridge.AdditionalHookInfo.class, priority, callback)) {
return new MethodUnhooker<>() {
@NonNull
@Override
public U getOrigin() {
public T getOrigin() {
return hookMethod;
}
@NonNull
@Override
public T getHooker() {
return callback;
}
@Override
public void unhook() {
HookBridge.unhookMethod(hookMethod, callback);
HookBridge.unhookMethod(true, hookMethod, callback);
}
};
}
@ -905,73 +938,25 @@ public class LSPosedContext extends XposedContext {
@Override
@NonNull
public MethodUnhooker<BeforeHooker<Method>, Method> hookBefore(@NonNull Method origin, @NonNull BeforeHooker<Method> hooker) {
public MethodUnhooker<Method> hook(@NonNull Method origin, @NonNull Class<? extends Hooker> hooker) {
return doHook(origin, PRIORITY_DEFAULT, hooker);
}
@Override
@NonNull
public MethodUnhooker<AfterHooker<Method>, Method> hookAfter(@NonNull Method origin, @NonNull AfterHooker<Method> hooker) {
return doHook(origin, PRIORITY_DEFAULT, hooker);
}
@Override
@NonNull
public MethodUnhooker<Hooker<Method>, Method> hook(@NonNull Method origin, @NonNull Hooker<Method> hooker) {
return doHook(origin, PRIORITY_DEFAULT, hooker);
}
@Override
@NonNull
public MethodUnhooker<BeforeHooker<Method>, Method> hookBefore(@NonNull Method origin, int priority, @NonNull BeforeHooker<Method> hooker) {
public MethodUnhooker<Method> hook(@NonNull Method origin, int priority, @NonNull Class<? extends Hooker> hooker) {
return doHook(origin, priority, hooker);
}
@Override
@NonNull
public MethodUnhooker<AfterHooker<Method>, Method> hookAfter(@NonNull Method origin, int priority, @NonNull AfterHooker<Method> hooker) {
return doHook(origin, priority, hooker);
}
@Override
@NonNull
public MethodUnhooker<Hooker<Method>, Method> hook(@NonNull Method origin, int priority, @NonNull Hooker<Method> hooker) {
return doHook(origin, priority, hooker);
}
@Override
@NonNull
public <T> MethodUnhooker<BeforeHooker<Constructor<T>>, Constructor<T>> hookBefore(@NonNull Constructor<T> origin, @NonNull BeforeHooker<Constructor<T>> hooker) {
public <T> MethodUnhooker<Constructor<T>> hook(@NonNull Constructor<T> origin, @NonNull Class<? extends Hooker> hooker) {
return doHook(origin, PRIORITY_DEFAULT, hooker);
}
@Override
@NonNull
public <T> MethodUnhooker<AfterHooker<Constructor<T>>, Constructor<T>> hookAfter(@NonNull Constructor<T> origin, @NonNull AfterHooker<Constructor<T>> hooker) {
return doHook(origin, PRIORITY_DEFAULT, hooker);
}
@Override
@NonNull
public <T> MethodUnhooker<Hooker<Constructor<T>>, Constructor<T>> hook(@NonNull Constructor<T> origin, @NonNull Hooker<Constructor<T>> hooker) {
return doHook(origin, PRIORITY_DEFAULT, hooker);
}
@Override
@NonNull
public <T> MethodUnhooker<BeforeHooker<Constructor<T>>, Constructor<T>> hookBefore(@NonNull Constructor<T> origin, int priority, @NonNull BeforeHooker<Constructor<T>> hooker) {
return doHook(origin, priority, hooker);
}
@Override
@NonNull
public <T> MethodUnhooker<AfterHooker<Constructor<T>>, Constructor<T>> hookAfter(@NonNull Constructor<T> origin, int priority, @NonNull AfterHooker<Constructor<T>> hooker) {
return doHook(origin, priority, hooker);
}
@Override
@NonNull
public <T> MethodUnhooker<Hooker<Constructor<T>>, Constructor<T>> hook(@NonNull Constructor<T> origin, int priority, @NonNull Hooker<Constructor<T>> hooker) {
public <T> MethodUnhooker<Constructor<T>> hook(@NonNull Constructor<T> origin, int priority, @NonNull Class<? extends Hooker> hooker) {
return doHook(origin, priority, hooker);
}

View File

@ -6,9 +6,9 @@ import java.lang.reflect.InvocationTargetException;
import dalvik.annotation.optimization.FastNative;
public class HookBridge {
public static native boolean hookMethod(Executable hookMethod, Class<?> hooker, int priority, Object callback);
public static native boolean hookMethod(boolean useModernApi, Executable hookMethod, Class<?> hooker, int priority, Object callback);
public static native boolean unhookMethod(Executable hookMethod, Object callback);
public static native boolean unhookMethod(boolean useModernApi, Executable hookMethod, Object callback);
public static native boolean deoptimizeMethod(Executable method);
@ -24,5 +24,5 @@ public class HookBridge {
@FastNative
public static native boolean setTrusted(Object cookie);
public static native Object[] callbackSnapshot(Executable method);
public static native Object[] callbackSnapshot(Class<?> hooker_callback, Executable method);
}

View File

@ -29,9 +29,19 @@
using namespace lsplant;
namespace {
struct CallbackType {
bool use_modern_api;
union {
jobject callback_object;
struct {
jmethodID before_method;
jmethodID after_method;
};
};
};
struct HookItem {
std::multimap<jint, jobject, std::greater<>> callbacks {};
std::multimap<jint, CallbackType, std::greater<>> callbacks;
private:
std::atomic<jobject> backup {nullptr};
static_assert(decltype(backup)::is_always_lock_free);
@ -57,10 +67,13 @@ std::shared_mutex hooked_lock;
absl::flat_hash_map<jmethodID, std::unique_ptr<HookItem>> hooked_methods;
jmethodID invoke = nullptr;
jmethodID callback_ctor = nullptr;
jfieldID before_method_field = nullptr;
jfieldID after_method_field = nullptr;
}
namespace lspd {
LSP_DEF_NATIVE_METHOD(jboolean, HookBridge, hookMethod, jobject hookMethod,
LSP_DEF_NATIVE_METHOD(jboolean, HookBridge, hookMethod, jboolean useModernApi, jobject hookMethod,
jclass hooker, jint priority, jobject callback) {
bool newHook = false;
#ifndef NDEBUG
@ -108,11 +121,32 @@ LSP_DEF_NATIVE_METHOD(jboolean, HookBridge, hookMethod, jobject hookMethod,
jobject backup = hook_item->GetBackup();
if (!backup) return JNI_FALSE;
JNIMonitor monitor(env, backup);
hook_item->callbacks.emplace(std::make_pair(priority, env->NewGlobalRef(callback)));
if (useModernApi) {
if (before_method_field == nullptr) {
auto callback_class = JNI_GetObjectClass(env, callback);
callback_ctor = JNI_GetMethodID(env, callback_class, "<init>", "(Ljava/lang/reflect/Method;Ljava/lang/reflect/Method;)V");
before_method_field = JNI_GetFieldID(env, callback_class, "beforeInvocation", "Ljava/lang/reflect/Method;");
after_method_field = JNI_GetFieldID(env, callback_class, "afterInvocation", "Ljava/lang/reflect/Method;");
}
auto before_method = JNI_GetObjectField(env, callback, before_method_field);
auto after_method = JNI_GetObjectField(env, callback, after_method_field);
auto callback_type = CallbackType {
.use_modern_api = true,
.before_method = env->FromReflectedMethod(before_method),
.after_method = env->FromReflectedMethod(after_method),
};
hook_item->callbacks.emplace(std::make_pair(priority, callback_type));
} else {
auto callback_type = CallbackType {
.use_modern_api = false,
.callback_object = env->NewGlobalRef(callback),
};
hook_item->callbacks.emplace(std::make_pair(priority, callback_type));
}
return JNI_TRUE;
}
LSP_DEF_NATIVE_METHOD(jboolean, HookBridge, unhookMethod, jobject hookMethod, jobject callback) {
LSP_DEF_NATIVE_METHOD(jboolean, HookBridge, unhookMethod, jboolean useModernApi, jobject hookMethod, jobject callback) {
auto target = env->FromReflectedMethod(hookMethod);
HookItem * hook_item = nullptr;
{
@ -125,10 +159,22 @@ LSP_DEF_NATIVE_METHOD(jboolean, HookBridge, unhookMethod, jobject hookMethod, jo
jobject backup = hook_item->GetBackup();
if (!backup) return JNI_FALSE;
JNIMonitor monitor(env, backup);
jmethodID before = nullptr;
if (useModernApi) {
auto before_method = JNI_GetObjectField(env, callback, before_method_field);
before = env->FromReflectedMethod(before_method);
}
for (auto i = hook_item->callbacks.begin(); i != hook_item->callbacks.end(); ++i) {
if (env->IsSameObject(i->second, callback)) {
hook_item->callbacks.erase(i);
return JNI_TRUE;
if (useModernApi) {
if (i->second.use_modern_api && before == i->second.before_method) {
hook_item->callbacks.erase(i);
return JNI_TRUE;
}
} else {
if (!i->second.use_modern_api && env->IsSameObject(i->second.callback_object, callback)) {
hook_item->callbacks.erase(i);
return JNI_TRUE;
}
}
}
return JNI_FALSE;
@ -153,7 +199,7 @@ LSP_DEF_NATIVE_METHOD(jobject, HookBridge, invokeOriginalMethod, jobject hookMet
}
LSP_DEF_NATIVE_METHOD(jobject, HookBridge, allocateObject, jclass cls) {
return env->AllocObject(clazz);
return env->AllocObject(cls);
}
LSP_DEF_NATIVE_METHOD(jobject, HookBridge, invokeSpecialMethod, jobject method, jcharArray shorty,
@ -269,7 +315,7 @@ LSP_DEF_NATIVE_METHOD(jboolean, HookBridge, setTrusted, jobject cookie) {
return lsplant::MakeDexFileTrusted(env, cookie);
}
LSP_DEF_NATIVE_METHOD(jobjectArray, HookBridge, callbackSnapshot, jobject method) {
LSP_DEF_NATIVE_METHOD(jobjectArray, HookBridge, callbackSnapshot, jclass callback_class, jobject method) {
auto target = env->FromReflectedMethod(method);
HookItem *hook_item = nullptr;
{
@ -284,21 +330,28 @@ LSP_DEF_NATIVE_METHOD(jobjectArray, HookBridge, callbackSnapshot, jobject method
JNIMonitor monitor(env, backup);
auto res = env->NewObjectArray((jsize) hook_item->callbacks.size(), env->FindClass("java/lang/Object"), nullptr);
for (jsize i = 0; auto callback: hook_item->callbacks) {
env->SetObjectArrayElement(res, i++, env->NewLocalRef(callback.second));
if (callback.second.use_modern_api) {
auto before_method = JNI_ToReflectedMethod(env, clazz, callback.second.before_method, JNI_TRUE);
auto after_method = JNI_ToReflectedMethod(env, clazz, callback.second.after_method, JNI_TRUE);
auto callback_object = JNI_NewObject(env, callback_class, callback_ctor, before_method, after_method);
env->SetObjectArrayElement(res, i++, env->NewLocalRef(callback_object));
} else {
env->SetObjectArrayElement(res, i++, env->NewLocalRef(callback.second.callback_object));
}
}
return res;
}
static JNINativeMethod gMethods[] = {
LSP_NATIVE_METHOD(HookBridge, hookMethod, "(Ljava/lang/reflect/Executable;Ljava/lang/Class;ILjava/lang/Object;)Z"),
LSP_NATIVE_METHOD(HookBridge, unhookMethod, "(Ljava/lang/reflect/Executable;Ljava/lang/Object;)Z"),
LSP_NATIVE_METHOD(HookBridge, hookMethod, "(ZLjava/lang/reflect/Executable;Ljava/lang/Class;ILjava/lang/Object;)Z"),
LSP_NATIVE_METHOD(HookBridge, unhookMethod, "(ZLjava/lang/reflect/Executable;Ljava/lang/Object;)Z"),
LSP_NATIVE_METHOD(HookBridge, deoptimizeMethod, "(Ljava/lang/reflect/Executable;)Z"),
LSP_NATIVE_METHOD(HookBridge, invokeOriginalMethod, "(Ljava/lang/reflect/Executable;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;"),
LSP_NATIVE_METHOD(HookBridge, invokeSpecialMethod, "(Ljava/lang/reflect/Executable;[CLjava/lang/Class;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;"),
LSP_NATIVE_METHOD(HookBridge, allocateObject, "(Ljava/lang/Class;)Ljava/lang/Object;"),
LSP_NATIVE_METHOD(HookBridge, instanceOf, "(Ljava/lang/Object;Ljava/lang/Class;)Z"),
LSP_NATIVE_METHOD(HookBridge, setTrusted, "(Ljava/lang/Object;)Z"),
LSP_NATIVE_METHOD(HookBridge, callbackSnapshot, "(Ljava/lang/reflect/Executable;)[Ljava/lang/Object;"),
LSP_NATIVE_METHOD(HookBridge, callbackSnapshot, "(Ljava/lang/Class;Ljava/lang/reflect/Executable;)[Ljava/lang/Object;"),
};
void RegisterHookBridge(JNIEnv *env) {

View File

@ -149,11 +149,6 @@ public class LSPModuleService extends IXposedService.Stub {
return IXposedService.FRAMEWORK_PRIVILEGE_ROOT;
}
@Override
public Bundle featuredMethod(String name, Bundle args) {
throw new UnsupportedOperationException();
}
@Override
public List<String> getScope() throws RemoteException {
ensureModule();