Use modern hook API for internal hookers

This commit is contained in:
Nullptr 2023-08-16 11:56:41 +08:00 committed by Wang Han
parent e8e9105598
commit fc1adeac55
10 changed files with 239 additions and 152 deletions

View File

@ -34,34 +34,29 @@ import org.lsposed.lspd.hooker.HandleSystemServerProcessHooker;
import org.lsposed.lspd.hooker.LoadedApkCtorHooker;
import org.lsposed.lspd.hooker.OpenDexFileHooker;
import org.lsposed.lspd.impl.LSPosedContext;
import org.lsposed.lspd.impl.LSPosedHelper;
import org.lsposed.lspd.service.ILSPApplicationService;
import org.lsposed.lspd.util.Utils;
import dalvik.system.DexFile;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.XposedInit;
public class Startup {
@SuppressWarnings("deprecation")
private static void startBootstrapHook(boolean isSystem) {
Utils.logD("startBootstrapHook starts: isSystem = " + isSystem);
XposedHelpers.findAndHookMethod(Thread.class, "dispatchUncaughtException",
Throwable.class, new CrashDumpHooker());
LSPosedHelper.hookMethod(CrashDumpHooker.class, Thread.class, "dispatchUncaughtException", Throwable.class);
if (isSystem) {
XposedBridge.hookAllMethods(ZygoteInit.class,
"handleSystemServerProcess", new HandleSystemServerProcessHooker());
LSPosedHelper.hookAllMethods(HandleSystemServerProcessHooker.class, ZygoteInit.class, "handleSystemServerProcess");
} else {
var hooker = new OpenDexFileHooker();
XposedBridge.hookAllMethods(DexFile.class, "openDexFile", hooker);
XposedBridge.hookAllMethods(DexFile.class, "openInMemoryDexFile", hooker);
XposedBridge.hookAllMethods(DexFile.class, "openInMemoryDexFiles", hooker);
LSPosedHelper.hookAllMethods(OpenDexFileHooker.class, DexFile.class, "openDexFile");
LSPosedHelper.hookAllMethods(OpenDexFileHooker.class, DexFile.class, "openInMemoryDexFile");
LSPosedHelper.hookAllMethods(OpenDexFileHooker.class, DexFile.class, "openInMemoryDexFiles");
}
XposedHelpers.findAndHookConstructor(LoadedApk.class,
LSPosedHelper.hookConstructor(LoadedApkCtorHooker.class, LoadedApk.class,
ActivityThread.class, ApplicationInfo.class, CompatibilityInfo.class,
ClassLoader.class, boolean.class, boolean.class, boolean.class,
new LoadedApkCtorHooker());
XposedBridge.hookAllMethods(ActivityThread.class, "attach", new AttachHooker());
ClassLoader.class, boolean.class, boolean.class, boolean.class);
LSPosedHelper.hookAllMethods(AttachHooker.class, ActivityThread.class, "attach");
}
public static void bootstrapXposed() {

View File

@ -1,19 +1,17 @@
package org.lsposed.lspd.hooker;
import static org.lsposed.lspd.core.ApplicationServiceClient.serviceClient;
import android.app.ActivityThread;
import org.lsposed.lspd.impl.LSPosedContext;
import java.util.Optional;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedInit;
import io.github.libxposed.api.XposedInterface;
import io.github.libxposed.api.annotations.AfterInvocation;
import io.github.libxposed.api.annotations.XposedHooker;
public class AttachHooker extends XC_MethodHook {
@Override
protected void afterHookedMethod(MethodHookParam<?> param) throws Throwable {
XposedInit.loadModules((ActivityThread) param.thisObject);
@XposedHooker
public class AttachHooker implements XposedInterface.Hooker {
@AfterInvocation
public static void afterHookedMethod(XposedInterface.AfterHookCallback callback) {
XposedInit.loadModules((ActivityThread) callback.getThisObject());
}
}

View File

@ -2,15 +2,20 @@ package org.lsposed.lspd.hooker;
import android.util.Log;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import org.lsposed.lspd.impl.LSPosedBridge;
public class CrashDumpHooker extends XC_MethodHook {
@Override
protected void beforeHookedMethod(MethodHookParam<?> param) {
import io.github.libxposed.api.XposedInterface;
import io.github.libxposed.api.annotations.BeforeInvocation;
import io.github.libxposed.api.annotations.XposedHooker;
@XposedHooker
public class CrashDumpHooker implements XposedInterface.Hooker {
@BeforeInvocation
public static void beforeHookedMethod(XposedInterface.BeforeHookCallback callback) {
try {
var e = (Throwable) param.args[0];
XposedBridge.log("Crash unexpectedly: " + Log.getStackTraceString(e));
var e = (Throwable) callback.getArgs()[0];
LSPosedBridge.log("Crash unexpectedly: " + Log.getStackTraceString(e));
} catch (Throwable ignored) {
}
}

View File

@ -20,32 +20,35 @@
package org.lsposed.lspd.hooker;
import android.annotation.SuppressLint;
import org.lsposed.lspd.deopt.PrebuiltMethodsDeopter;
import org.lsposed.lspd.impl.LSPosedHelper;
import org.lsposed.lspd.util.Hookers;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import io.github.libxposed.api.XposedInterface;
import io.github.libxposed.api.annotations.AfterInvocation;
import io.github.libxposed.api.annotations.XposedHooker;
// system_server initialization
public class HandleSystemServerProcessHooker extends XC_MethodHook {
@XposedHooker
public class HandleSystemServerProcessHooker implements XposedInterface.Hooker {
public static volatile ClassLoader systemServerCL;
@Override
protected void afterHookedMethod(MethodHookParam<?> param) {
@SuppressLint("PrivateApi")
@AfterInvocation
public static void afterHookedMethod() {
Hookers.logD("ZygoteInit#handleSystemServerProcess() starts");
try {
// get system_server classLoader
systemServerCL = Thread.currentThread().getContextClassLoader();
// deopt methods in SYSTEMSERVERCLASSPATH
PrebuiltMethodsDeopter.deoptSystemServerMethods(systemServerCL);
XposedBridge.hookAllMethods(
XposedHelpers.findClass("com.android.server.SystemServer", systemServerCL),
"startBootstrapServices", new StartBootstrapServicesHooker());
var clazz = Class.forName("com.android.server.SystemServer", false, systemServerCL);
LSPosedHelper.hookAllMethods(StartBootstrapServicesHooker.class, clazz, "startBootstrapServices");
} catch (Throwable t) {
Hookers.logE("error when hooking systemMain", t);
}
}
}

View File

@ -26,19 +26,23 @@ import android.util.Log;
import org.lsposed.lspd.util.Hookers;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.XposedInit;
import io.github.libxposed.api.XposedInterface;
import io.github.libxposed.api.annotations.AfterInvocation;
import io.github.libxposed.api.annotations.XposedHooker;
// when a package is loaded for an existing process, trigger the callbacks as well
public class LoadedApkCtorHooker extends XC_MethodHook {
@XposedHooker
public class LoadedApkCtorHooker implements XposedInterface.Hooker {
@Override
protected void afterHookedMethod(MethodHookParam<?> param) {
@AfterInvocation
public static void afterHookedMethod(XposedInterface.AfterHookCallback callback) {
Hookers.logD("LoadedApk#<init> starts");
try {
LoadedApk loadedApk = (LoadedApk) param.thisObject;
LoadedApk loadedApk = (LoadedApk) callback.getThisObject();
assert loadedApk != null;
String packageName = loadedApk.getPackageName();
Object mAppDir = XposedHelpers.getObjectField(loadedApk, "mAppDir");
Hookers.logD("LoadedApk#<init> ends: " + mAppDir);

View File

@ -2,26 +2,30 @@ package org.lsposed.lspd.hooker;
import android.os.Build;
import org.lsposed.lspd.impl.LSPosedBridge;
import org.lsposed.lspd.nativebridge.HookBridge;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedHelpers;
import io.github.libxposed.api.XposedInterface;
import io.github.libxposed.api.annotations.AfterInvocation;
import io.github.libxposed.api.annotations.XposedHooker;
public class OpenDexFileHooker extends XC_MethodHook {
@Override
protected void afterHookedMethod(MethodHookParam<?> param) throws Throwable {
@XposedHooker
public class OpenDexFileHooker implements XposedInterface.Hooker {
@AfterInvocation
public static void afterHookedMethod(XposedInterface.AfterHookCallback callback) {
ClassLoader classLoader = null;
for (var arg : param.args) {
for (var arg : callback.getArgs()) {
if (arg instanceof ClassLoader) {
classLoader = (ClassLoader) arg;
}
}
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P && classLoader == null) {
classLoader = XposedHelpers.class.getClassLoader();
classLoader = LSPosedBridge.class.getClassLoader();
}
while (classLoader != null) {
if (classLoader == XposedHelpers.class.getClassLoader()) {
HookBridge.setTrusted(param.getResult());
if (classLoader == LSPosedBridge.class.getClassLoader()) {
HookBridge.setTrusted(callback.getResult());
return;
} else {
classLoader = classLoader.getParent();

View File

@ -27,16 +27,19 @@ import androidx.annotation.NonNull;
import org.lsposed.lspd.impl.LSPosedContext;
import org.lsposed.lspd.util.Hookers;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedInit;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
import io.github.libxposed.api.XposedInterface;
import io.github.libxposed.api.XposedModuleInterface;
import io.github.libxposed.api.annotations.BeforeInvocation;
import io.github.libxposed.api.annotations.XposedHooker;
public class StartBootstrapServicesHooker extends XC_MethodHook {
@XposedHooker
public class StartBootstrapServicesHooker implements XposedInterface.Hooker {
@Override
protected void beforeHookedMethod(MethodHookParam<?> param) {
@BeforeInvocation
public static void beforeHookedMethod() {
logD("SystemServer#startBootstrapServices() starts");
try {

View File

@ -2,6 +2,8 @@ package org.lsposed.lspd.impl;
import android.util.Log;
import androidx.annotation.NonNull;
import org.lsposed.lspd.nativebridge.HookBridge;
import java.lang.reflect.Executable;
@ -10,6 +12,11 @@ import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import de.robv.android.xposed.XposedBridge;
import io.github.libxposed.api.XposedInterface;
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;
public class LSPosedBridge {
@ -30,12 +37,19 @@ public class LSPosedBridge {
}
public static class HookerCallback {
Method beforeInvocation;
Method afterInvocation;
@NonNull
final Method beforeInvocation;
@NonNull
final Method afterInvocation;
public HookerCallback(Method beforeInvocation, Method afterInvocation) {
final int beforeParams;
final int afterParams;
public HookerCallback(@NonNull Method beforeInvocation, @NonNull Method afterInvocation) {
this.beforeInvocation = beforeInvocation;
this.afterInvocation = afterInvocation;
this.beforeParams = beforeInvocation.getParameterCount();
this.afterParams = afterInvocation.getParameterCount();
}
}
@ -111,7 +125,11 @@ public class LSPosedBridge {
for (beforeIdx = 0; beforeIdx < modernSnapshot.length; beforeIdx++) {
try {
var hooker = (HookerCallback) modernSnapshot[beforeIdx];
ctxArray[beforeIdx] = hooker.beforeInvocation.invoke(null, callback);
if (hooker.beforeParams == 0) {
ctxArray[beforeIdx] = hooker.beforeInvocation.invoke(null);
} else {
ctxArray[beforeIdx] = hooker.beforeInvocation.invoke(null, callback);
}
} catch (Throwable t) {
LSPosedBridge.log(t);
@ -150,12 +168,13 @@ public class LSPosedBridge {
Object lastResult = callback.getResult();
Throwable lastThrowable = callback.getThrowable();
var hooker = (HookerCallback) modernSnapshot[afterIdx];
var context = ctxArray[afterIdx];
try {
if (context == null) {
if (hooker.afterParams == 0) {
hooker.afterInvocation.invoke(null);
} else if (hooker.afterParams == 1) {
hooker.afterInvocation.invoke(null, callback);
} else {
hooker.afterInvocation.invoke(null, callback, context);
hooker.afterInvocation.invoke(null, callback, ctxArray[afterIdx]);
}
} catch (Throwable t) {
LSPosedBridge.log(t);
@ -186,4 +205,95 @@ public class LSPosedBridge {
}
}
}
public static void dummyCallback() {
}
public static <T extends Executable> XposedInterface.MethodUnhooker<T>
doHook(T hookMethod, int priority, Class<? extends XposedInterface.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");
} 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");
}
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 = (method.getModifiers() & modifiers) == modifiers;
var params = method.getParameterTypes();
if (params.length == 1) {
valid &= params[0].equals(XposedInterface.BeforeHookCallback.class);
} else if (params.length != 0) {
valid = false;
}
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 = (method.getModifiers() & modifiers) == modifiers;
valid &= method.getReturnType().equals(void.class);
var params = method.getParameterTypes();
if (params.length == 1 || params.length == 2) {
valid &= params[0].equals(XposedInterface.AfterHookCallback.class);
} else if (params.length != 0) {
valid = false;
}
if (!valid) {
throw new IllegalArgumentException("AfterInvocation method format is invalid");
}
afterInvocation = method;
}
}
if (beforeInvocation == null && afterInvocation == null) {
throw new IllegalArgumentException("No method annotated with @BeforeInvocation or @AfterInvocation");
}
try {
if (beforeInvocation == null) {
beforeInvocation = LSPosedBridge.class.getMethod("dummyCallback");
} else if (afterInvocation == null) {
afterInvocation = LSPosedBridge.class.getMethod("dummyCallback");
} else {
var ret = beforeInvocation.getReturnType();
var params = afterInvocation.getParameterTypes();
if (ret != void.class && params.length == 2 && !ret.equals(params[1])) {
throw new IllegalArgumentException("BeforeInvocation and AfterInvocation method format is invalid");
}
}
} catch (NoSuchMethodException e) {
throw new HookFailedError(e);
}
var callback = new LSPosedBridge.HookerCallback(beforeInvocation, afterInvocation);
if (HookBridge.hookMethod(true, hookMethod, LSPosedBridge.NativeHooker.class, priority, callback)) {
return new XposedInterface.MethodUnhooker<>() {
@NonNull
@Override
public T getOrigin() {
return hookMethod;
}
@Override
public void unhook() {
HookBridge.unhookMethod(true, hookMethod, callback);
}
};
}
throw new HookFailedError("Cannot hook " + hookMethod);
}
}

View File

@ -31,7 +31,6 @@ 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;
@ -39,10 +38,6 @@ import java.util.concurrent.ConcurrentHashMap;
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.errors.XposedFrameworkError;
import io.github.libxposed.api.utils.DexParser;
@ -169,104 +164,28 @@ public class LSPosedContext implements XposedInterface {
}
}
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");
} 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");
}
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[]{BeforeHookCallback.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 &= 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");
}
boolean valid;
var ctx = beforeInvocation.getReturnType();
if (ctx == void.class) {
valid = Arrays.equals(afterInvocation.getParameterTypes(), new Class[]{AfterHookCallback.class});
} else {
valid = Arrays.equals(afterInvocation.getParameterTypes(), new Class[]{AfterHookCallback.class, ctx});
}
if (!valid) {
throw new IllegalArgumentException("AfterInvocation method format is invalid");
}
var callback = new LSPosedBridge.HookerCallback(beforeInvocation, afterInvocation);
if (HookBridge.hookMethod(true, hookMethod, LSPosedBridge.NativeHooker.class, priority, callback)) {
return new MethodUnhooker<>() {
@NonNull
@Override
public T getOrigin() {
return hookMethod;
}
@Override
public void unhook() {
HookBridge.unhookMethod(true, hookMethod, callback);
}
};
}
throw new HookFailedError("Cannot hook " + hookMethod);
}
@Override
@NonNull
public MethodUnhooker<Method> hook(@NonNull Method origin, @NonNull Class<? extends Hooker> hooker) {
return doHook(origin, PRIORITY_DEFAULT, hooker);
return LSPosedBridge.doHook(origin, PRIORITY_DEFAULT, hooker);
}
@Override
@NonNull
public MethodUnhooker<Method> hook(@NonNull Method origin, int priority, @NonNull Class<? extends Hooker> hooker) {
return doHook(origin, priority, hooker);
return LSPosedBridge.doHook(origin, priority, hooker);
}
@Override
@NonNull
public <T> MethodUnhooker<Constructor<T>> hook(@NonNull Constructor<T> origin, @NonNull Class<? extends Hooker> hooker) {
return doHook(origin, PRIORITY_DEFAULT, hooker);
return LSPosedBridge.doHook(origin, PRIORITY_DEFAULT, hooker);
}
@Override
@NonNull
public <T> MethodUnhooker<Constructor<T>> hook(@NonNull Constructor<T> origin, int priority, @NonNull Class<? extends Hooker> hooker) {
return doHook(origin, priority, hooker);
return LSPosedBridge.doHook(origin, priority, hooker);
}
private static boolean doDeoptimize(@NonNull Executable method) {

View File

@ -0,0 +1,46 @@
package org.lsposed.lspd.impl;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;
import io.github.libxposed.api.XposedInterface;
import io.github.libxposed.api.errors.HookFailedError;
public class LSPosedHelper {
@SuppressWarnings("UnusedReturnValue")
public static <T> XposedInterface.MethodUnhooker<Method>
hookMethod(Class<? extends XposedInterface.Hooker> hooker, Class<T> clazz, String methodName, Class<?>... parameterTypes) {
try {
var method = clazz.getDeclaredMethod(methodName, parameterTypes);
return LSPosedBridge.doHook(method, XposedInterface.PRIORITY_DEFAULT, hooker);
} catch (NoSuchMethodException e) {
throw new HookFailedError(e);
}
}
@SuppressWarnings("UnusedReturnValue")
public static <T> Set<XposedInterface.MethodUnhooker<Method>>
hookAllMethods(Class<? extends XposedInterface.Hooker> hooker, Class<T> clazz, String methodName) {
var unhooks = new HashSet<XposedInterface.MethodUnhooker<Method>>();
for (var method : clazz.getDeclaredMethods()) {
if (method.getName().equals(methodName)) {
unhooks.add(LSPosedBridge.doHook(method, XposedInterface.PRIORITY_DEFAULT, hooker));
}
}
return unhooks;
}
@SuppressWarnings("UnusedReturnValue")
public static <T> XposedInterface.MethodUnhooker<Constructor<T>>
hookConstructor(Class<? extends XposedInterface.Hooker> hooker, Class<T> clazz, Class<?>... parameterTypes) {
try {
var constructor = clazz.getDeclaredConstructor(parameterTypes);
return LSPosedBridge.doHook(constructor, XposedInterface.PRIORITY_DEFAULT, hooker);
} catch (NoSuchMethodException e) {
throw new HookFailedError(e);
}
}
}