Compare commits
No commits in common. "43ffcee9c80112730a0d2f50e1defa16f10ef94a" and "211bd5f1158f2fed9db8471b1422bf764bfc9d62" have entirely different histories.
43ffcee9c8
...
211bd5f115
|
|
@ -49,13 +49,6 @@ jobs:
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@v5
|
uses: gradle/actions/setup-gradle@v5
|
||||||
|
|
||||||
- name: Configure Gradle properties
|
|
||||||
run: |
|
|
||||||
echo 'android.native.buildOutput=verbose' >> ~/.gradle/gradle.properties
|
|
||||||
echo 'org.gradle.parallel=true' >> ~/.gradle/gradle.properties
|
|
||||||
echo 'org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -XX:+UseParallelGC' >> ~/.gradle/gradle.properties
|
|
||||||
echo 'android.native.buildOutput=verbose' >> ~/.gradle/gradle.properties
|
|
||||||
|
|
||||||
- name: Setup ninja
|
- name: Setup ninja
|
||||||
uses: seanmiddleditch/gha-setup-ninja@v6
|
uses: seanmiddleditch/gha-setup-ninja@v6
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ val verCode by extra(commitCount)
|
||||||
val verName by extra(latestTag)
|
val verName by extra(latestTag)
|
||||||
val androidTargetSdkVersion by extra(36)
|
val androidTargetSdkVersion by extra(36)
|
||||||
val androidMinSdkVersion by extra(27)
|
val androidMinSdkVersion by extra(27)
|
||||||
val androidBuildToolsVersion by extra("36.1.0")
|
val androidBuildToolsVersion by extra("36.0.0")
|
||||||
val androidCompileSdkVersion by extra(36)
|
val androidCompileSdkVersion by extra(36)
|
||||||
val androidCompileNdkVersion by extra("29.0.13113456")
|
val androidCompileNdkVersion by extra("29.0.13113456")
|
||||||
val androidSourceCompatibility by extra(JavaVersion.VERSION_21)
|
val androidSourceCompatibility by extra(JavaVersion.VERSION_21)
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,9 @@
|
||||||
-keepclassmembers class org.lsposed.lspd.impl.LSPosedHookCallback {
|
-keepclassmembers class org.lsposed.lspd.impl.LSPosedHookCallback {
|
||||||
public <methods>;
|
public <methods>;
|
||||||
}
|
}
|
||||||
-keepclassmembers,allowoptimization class ** implements io.github.libxposed.api.XposedInterface$Hooker {
|
-keep,allowoptimization,allowobfuscation @io.github.libxposed.api.annotations.* class * {
|
||||||
public static *** before();
|
@io.github.libxposed.api.annotations.BeforeInvocation <methods>;
|
||||||
public static *** before(io.github.libxposed.api.XposedInterface$BeforeHookCallback);
|
@io.github.libxposed.api.annotations.AfterInvocation <methods>;
|
||||||
public static void after();
|
|
||||||
public static void after(io.github.libxposed.api.XposedInterface$AfterHookCallback);
|
|
||||||
public static void after(io.github.libxposed.api.XposedInterface$AfterHookCallback, ***);
|
|
||||||
}
|
}
|
||||||
-assumenosideeffects class android.util.Log {
|
-assumenosideeffects class android.util.Log {
|
||||||
public static *** v(...);
|
public static *** v(...);
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ import android.app.ActivityThread;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.LogPrinter;
|
|
||||||
|
|
||||||
import org.lsposed.lspd.impl.LSPosedBridge;
|
import org.lsposed.lspd.impl.LSPosedBridge;
|
||||||
import org.lsposed.lspd.impl.LSPosedHookCallback;
|
import org.lsposed.lspd.impl.LSPosedHookCallback;
|
||||||
|
|
@ -38,9 +37,7 @@ import java.lang.reflect.Member;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.lang.reflect.Modifier;
|
import java.lang.reflect.Modifier;
|
||||||
import java.lang.reflect.Proxy;
|
import java.lang.reflect.Proxy;
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Date;
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CopyOnWriteArraySet;
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
|
|
@ -75,8 +72,6 @@ public final class XposedBridge {
|
||||||
|
|
||||||
private static final Object[] EMPTY_ARRAY = new Object[0];
|
private static final Object[] EMPTY_ARRAY = new Object[0];
|
||||||
|
|
||||||
private static final SimpleDateFormat format = new SimpleDateFormat("'['yyyy-MM-dd'T'HH:mm:ss.SSS");
|
|
||||||
|
|
||||||
// built-in handlers
|
// built-in handlers
|
||||||
public static final CopyOnWriteArraySet<XC_LoadPackage> sLoadedPackageCallbacks = new CopyOnWriteArraySet<>();
|
public static final CopyOnWriteArraySet<XC_LoadPackage> sLoadedPackageCallbacks = new CopyOnWriteArraySet<>();
|
||||||
/*package*/ static final CopyOnWriteArraySet<XC_InitPackageResources> sInitPackageResourcesCallbacks = new CopyOnWriteArraySet<>();
|
/*package*/ static final CopyOnWriteArraySet<XC_InitPackageResources> sInitPackageResourcesCallbacks = new CopyOnWriteArraySet<>();
|
||||||
|
|
@ -86,12 +81,6 @@ public final class XposedBridge {
|
||||||
|
|
||||||
public static volatile ClassLoader dummyClassLoader = null;
|
public static volatile ClassLoader dummyClassLoader = null;
|
||||||
|
|
||||||
private static LogPrinter printer;
|
|
||||||
|
|
||||||
public static void setLogPrinter(LogPrinter printer){
|
|
||||||
XposedBridge.printer = printer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void initXResources() {
|
public static void initXResources() {
|
||||||
if (dummyClassLoader != null) {
|
if (dummyClassLoader != null) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -157,9 +146,6 @@ public final class XposedBridge {
|
||||||
*/
|
*/
|
||||||
public synchronized static void log(String text) {
|
public synchronized static void log(String text) {
|
||||||
Log.i(TAG, text);
|
Log.i(TAG, text);
|
||||||
if (printer != null){
|
|
||||||
printer.println(format.format(new Date()) + " " + ActivityThread.currentProcessName() + ";" + Thread.currentThread().getName() + "]" + text);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -173,9 +159,6 @@ public final class XposedBridge {
|
||||||
public synchronized static void log(Throwable t) {
|
public synchronized static void log(Throwable t) {
|
||||||
String logStr = Log.getStackTraceString(t);
|
String logStr = Log.getStackTraceString(t);
|
||||||
Log.e(TAG, logStr);
|
Log.e(TAG, logStr);
|
||||||
if (printer != null){
|
|
||||||
printer.println(format.format(new Date()) + " " + ActivityThread.currentProcessName() + ";" + Thread.currentThread().getName() + "]" + logStr);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,14 @@ import android.app.ActivityThread;
|
||||||
|
|
||||||
import de.robv.android.xposed.XposedInit;
|
import de.robv.android.xposed.XposedInit;
|
||||||
import io.github.libxposed.api.XposedInterface;
|
import io.github.libxposed.api.XposedInterface;
|
||||||
|
import io.github.libxposed.api.annotations.AfterInvocation;
|
||||||
|
import io.github.libxposed.api.annotations.XposedHooker;
|
||||||
|
|
||||||
|
@XposedHooker
|
||||||
public class AttachHooker implements XposedInterface.Hooker {
|
public class AttachHooker implements XposedInterface.Hooker {
|
||||||
|
|
||||||
public static void after(XposedInterface.AfterHookCallback callback) {
|
@AfterInvocation
|
||||||
|
public static void afterHookedMethod(XposedInterface.AfterHookCallback callback) {
|
||||||
XposedInit.loadModules((ActivityThread) callback.getThisObject());
|
XposedInit.loadModules((ActivityThread) callback.getThisObject());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,14 @@ import org.lsposed.lspd.impl.LSPosedBridge;
|
||||||
import org.lsposed.lspd.util.Utils.Log;
|
import org.lsposed.lspd.util.Utils.Log;
|
||||||
|
|
||||||
import io.github.libxposed.api.XposedInterface;
|
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 {
|
public class CrashDumpHooker implements XposedInterface.Hooker {
|
||||||
|
|
||||||
public static void before(XposedInterface.BeforeHookCallback callback) {
|
@BeforeInvocation
|
||||||
|
public static void beforeHookedMethod(XposedInterface.BeforeHookCallback callback) {
|
||||||
try {
|
try {
|
||||||
var e = (Throwable) callback.getArgs()[0];
|
var e = (Throwable) callback.getArgs()[0];
|
||||||
LSPosedBridge.log("Crash unexpectedly: " + Log.getStackTraceString(e));
|
LSPosedBridge.log("Crash unexpectedly: " + Log.getStackTraceString(e));
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,11 @@ import org.lsposed.lspd.impl.LSPosedHelper;
|
||||||
import org.lsposed.lspd.util.Hookers;
|
import org.lsposed.lspd.util.Hookers;
|
||||||
|
|
||||||
import io.github.libxposed.api.XposedInterface;
|
import io.github.libxposed.api.XposedInterface;
|
||||||
|
import io.github.libxposed.api.annotations.AfterInvocation;
|
||||||
|
import io.github.libxposed.api.annotations.XposedHooker;
|
||||||
|
|
||||||
// system_server initialization
|
// system_server initialization
|
||||||
|
@XposedHooker
|
||||||
public class HandleSystemServerProcessHooker implements XposedInterface.Hooker {
|
public class HandleSystemServerProcessHooker implements XposedInterface.Hooker {
|
||||||
|
|
||||||
public interface Callback {
|
public interface Callback {
|
||||||
|
|
@ -39,7 +42,8 @@ public class HandleSystemServerProcessHooker implements XposedInterface.Hooker {
|
||||||
public static volatile Callback callback = null;
|
public static volatile Callback callback = null;
|
||||||
|
|
||||||
@SuppressLint("PrivateApi")
|
@SuppressLint("PrivateApi")
|
||||||
public static void after() {
|
@AfterInvocation
|
||||||
|
public static void afterHookedMethod() {
|
||||||
Hookers.logD("ZygoteInit#handleSystemServerProcess() starts");
|
Hookers.logD("ZygoteInit#handleSystemServerProcess() starts");
|
||||||
try {
|
try {
|
||||||
// get system_server classLoader
|
// get system_server classLoader
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,11 @@ import de.robv.android.xposed.XposedInit;
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage;
|
import de.robv.android.xposed.callbacks.XC_LoadPackage;
|
||||||
import io.github.libxposed.api.XposedInterface;
|
import io.github.libxposed.api.XposedInterface;
|
||||||
import io.github.libxposed.api.XposedModuleInterface;
|
import io.github.libxposed.api.XposedModuleInterface;
|
||||||
|
import io.github.libxposed.api.annotations.AfterInvocation;
|
||||||
|
import io.github.libxposed.api.annotations.XposedHooker;
|
||||||
|
|
||||||
@SuppressLint("BlockedPrivateApi")
|
@SuppressLint("BlockedPrivateApi")
|
||||||
|
@XposedHooker
|
||||||
public class LoadedApkCreateCLHooker implements XposedInterface.Hooker {
|
public class LoadedApkCreateCLHooker implements XposedInterface.Hooker {
|
||||||
private final static Field defaultClassLoaderField;
|
private final static Field defaultClassLoaderField;
|
||||||
|
|
||||||
|
|
@ -74,7 +77,8 @@ public class LoadedApkCreateCLHooker implements XposedInterface.Hooker {
|
||||||
loadedApks.add(loadedApk);
|
loadedApks.add(loadedApk);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void after(XposedInterface.AfterHookCallback callback) {
|
@AfterInvocation
|
||||||
|
public static void afterHookedMethod(XposedInterface.AfterHookCallback callback) {
|
||||||
LoadedApk loadedApk = (LoadedApk) callback.getThisObject();
|
LoadedApk loadedApk = (LoadedApk) callback.getThisObject();
|
||||||
|
|
||||||
if (callback.getArgs()[0] != null || !loadedApks.contains(loadedApk)) {
|
if (callback.getArgs()[0] != null || !loadedApks.contains(loadedApk)) {
|
||||||
|
|
@ -160,31 +164,6 @@ public class LoadedApkCreateCLHooker implements XposedInterface.Hooker {
|
||||||
return isFirstPackage;
|
return isFirstPackage;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ClassLoader defaultClassLoaderForCompat = classLoader;
|
|
||||||
if (defaultClassLoaderField != null) {
|
|
||||||
try {
|
|
||||||
var defaultCl = (ClassLoader) defaultClassLoaderField.get(loadedApk);
|
|
||||||
if (defaultCl != null) {
|
|
||||||
defaultClassLoaderForCompat = defaultCl;
|
|
||||||
}
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Object appComponentFactory = null;
|
|
||||||
try {
|
|
||||||
appComponentFactory = XposedHelpers.getObjectField(loadedApk, "mAppComponentFactory");
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
}
|
|
||||||
LSPosedContext.callOnPackageReady(
|
|
||||||
loadedApk.getPackageName(),
|
|
||||||
loadedApk.getApplicationInfo(),
|
|
||||||
isFirstPackage,
|
|
||||||
defaultClassLoaderForCompat,
|
|
||||||
classLoader,
|
|
||||||
appComponentFactory
|
|
||||||
);
|
|
||||||
Hookers.logD("callOnPackageReady via LoadedApkCreateCLHooker: " + loadedApk.getPackageName());
|
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
Hookers.logE("error when hooking LoadedApk#createClassLoader", t);
|
Hookers.logE("error when hooking LoadedApk#createClassLoader", t);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -29,11 +29,15 @@ import org.lsposed.lspd.util.Utils.Log;
|
||||||
import de.robv.android.xposed.XposedHelpers;
|
import de.robv.android.xposed.XposedHelpers;
|
||||||
import de.robv.android.xposed.XposedInit;
|
import de.robv.android.xposed.XposedInit;
|
||||||
import io.github.libxposed.api.XposedInterface;
|
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
|
// when a package is loaded for an existing process, trigger the callbacks as well
|
||||||
|
@XposedHooker
|
||||||
public class LoadedApkCtorHooker implements XposedInterface.Hooker {
|
public class LoadedApkCtorHooker implements XposedInterface.Hooker {
|
||||||
|
|
||||||
public static void after(XposedInterface.AfterHookCallback callback) {
|
@AfterInvocation
|
||||||
|
public static void afterHookedMethod(XposedInterface.AfterHookCallback callback) {
|
||||||
Hookers.logD("LoadedApk#<init> starts");
|
Hookers.logD("LoadedApk#<init> starts");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,14 @@ import org.lsposed.lspd.impl.LSPosedBridge;
|
||||||
import org.lsposed.lspd.nativebridge.HookBridge;
|
import org.lsposed.lspd.nativebridge.HookBridge;
|
||||||
|
|
||||||
import io.github.libxposed.api.XposedInterface;
|
import io.github.libxposed.api.XposedInterface;
|
||||||
|
import io.github.libxposed.api.annotations.AfterInvocation;
|
||||||
|
import io.github.libxposed.api.annotations.XposedHooker;
|
||||||
|
|
||||||
|
@XposedHooker
|
||||||
public class OpenDexFileHooker implements XposedInterface.Hooker {
|
public class OpenDexFileHooker implements XposedInterface.Hooker {
|
||||||
|
|
||||||
public static void after(XposedInterface.AfterHookCallback callback) {
|
@AfterInvocation
|
||||||
|
public static void afterHookedMethod(XposedInterface.AfterHookCallback callback) {
|
||||||
ClassLoader classLoader = null;
|
ClassLoader classLoader = null;
|
||||||
for (var arg : callback.getArgs()) {
|
for (var arg : callback.getArgs()) {
|
||||||
if (arg instanceof ClassLoader) {
|
if (arg instanceof ClassLoader) {
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,14 @@ import de.robv.android.xposed.XposedInit;
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage;
|
import de.robv.android.xposed.callbacks.XC_LoadPackage;
|
||||||
import io.github.libxposed.api.XposedInterface;
|
import io.github.libxposed.api.XposedInterface;
|
||||||
import io.github.libxposed.api.XposedModuleInterface;
|
import io.github.libxposed.api.XposedModuleInterface;
|
||||||
|
import io.github.libxposed.api.annotations.BeforeInvocation;
|
||||||
|
import io.github.libxposed.api.annotations.XposedHooker;
|
||||||
|
|
||||||
|
@XposedHooker
|
||||||
public class StartBootstrapServicesHooker implements XposedInterface.Hooker {
|
public class StartBootstrapServicesHooker implements XposedInterface.Hooker {
|
||||||
|
|
||||||
public static void before() {
|
@BeforeInvocation
|
||||||
|
public static void beforeHookedMethod() {
|
||||||
logD("SystemServer#startBootstrapServices() starts");
|
logD("SystemServer#startBootstrapServices() starts");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,9 @@ import java.lang.reflect.Modifier;
|
||||||
|
|
||||||
import de.robv.android.xposed.XposedBridge;
|
import de.robv.android.xposed.XposedBridge;
|
||||||
import io.github.libxposed.api.XposedInterface;
|
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;
|
import io.github.libxposed.api.errors.HookFailedError;
|
||||||
|
|
||||||
public class LSPosedBridge {
|
public class LSPosedBridge {
|
||||||
|
|
@ -215,14 +218,16 @@ public class LSPosedBridge {
|
||||||
throw new IllegalArgumentException("Cannot hook Method.invoke");
|
throw new IllegalArgumentException("Cannot hook Method.invoke");
|
||||||
} else if (hooker == null) {
|
} else if (hooker == null) {
|
||||||
throw new IllegalArgumentException("hooker should not be 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;
|
Method beforeInvocation = null, afterInvocation = null;
|
||||||
var modifiers = Modifier.PUBLIC | Modifier.STATIC;
|
var modifiers = Modifier.PUBLIC | Modifier.STATIC;
|
||||||
for (var method : hooker.getDeclaredMethods()) {
|
for (var method : hooker.getDeclaredMethods()) {
|
||||||
if (method.getName().equals("before")) {
|
if (method.getAnnotation(BeforeInvocation.class) != null) {
|
||||||
if (beforeInvocation != null) {
|
if (beforeInvocation != null) {
|
||||||
throw new IllegalArgumentException("More than one method named before");
|
throw new IllegalArgumentException("More than one method annotated with @BeforeInvocation");
|
||||||
}
|
}
|
||||||
boolean valid = (method.getModifiers() & modifiers) == modifiers;
|
boolean valid = (method.getModifiers() & modifiers) == modifiers;
|
||||||
var params = method.getParameterTypes();
|
var params = method.getParameterTypes();
|
||||||
|
|
@ -232,12 +237,13 @@ public class LSPosedBridge {
|
||||||
valid = false;
|
valid = false;
|
||||||
}
|
}
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
throw new IllegalArgumentException("before method format is invalid");
|
throw new IllegalArgumentException("BeforeInvocation method format is invalid");
|
||||||
}
|
}
|
||||||
beforeInvocation = method;
|
beforeInvocation = method;
|
||||||
} else if (method.getName().equals("after")) {
|
}
|
||||||
|
if (method.getAnnotation(AfterInvocation.class) != null) {
|
||||||
if (afterInvocation != null) {
|
if (afterInvocation != null) {
|
||||||
throw new IllegalArgumentException("More than one method named after");
|
throw new IllegalArgumentException("More than one method annotated with @AfterInvocation");
|
||||||
}
|
}
|
||||||
boolean valid = (method.getModifiers() & modifiers) == modifiers;
|
boolean valid = (method.getModifiers() & modifiers) == modifiers;
|
||||||
valid &= method.getReturnType().equals(void.class);
|
valid &= method.getReturnType().equals(void.class);
|
||||||
|
|
@ -248,13 +254,13 @@ public class LSPosedBridge {
|
||||||
valid = false;
|
valid = false;
|
||||||
}
|
}
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
throw new IllegalArgumentException("after method format is invalid");
|
throw new IllegalArgumentException("AfterInvocation method format is invalid");
|
||||||
}
|
}
|
||||||
afterInvocation = method;
|
afterInvocation = method;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (beforeInvocation == null && afterInvocation == null) {
|
if (beforeInvocation == null && afterInvocation == null) {
|
||||||
throw new IllegalArgumentException("No method named before or after found in " + hooker.getName());
|
throw new IllegalArgumentException("No method annotated with @BeforeInvocation or @AfterInvocation");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (beforeInvocation == null) {
|
if (beforeInvocation == null) {
|
||||||
|
|
@ -265,7 +271,7 @@ public class LSPosedBridge {
|
||||||
var ret = beforeInvocation.getReturnType();
|
var ret = beforeInvocation.getReturnType();
|
||||||
var params = afterInvocation.getParameterTypes();
|
var params = afterInvocation.getParameterTypes();
|
||||||
if (ret != void.class && params.length == 2 && !ret.equals(params[1])) {
|
if (ret != void.class && params.length == 2 && !ret.equals(params[1])) {
|
||||||
throw new IllegalArgumentException("before and after method format is invalid");
|
throw new IllegalArgumentException("BeforeInvocation and AfterInvocation method format is invalid");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (NoSuchMethodException e) {
|
} catch (NoSuchMethodException e) {
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ import java.io.IOException;
|
||||||
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Constructor;
|
||||||
import java.lang.reflect.Executable;
|
import java.lang.reflect.Executable;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.lang.reflect.Modifier;
|
import java.lang.reflect.Modifier;
|
||||||
import java.lang.reflect.Proxy;
|
import java.lang.reflect.Proxy;
|
||||||
|
|
@ -76,27 +75,6 @@ public class LSPosedContext implements XposedInterface {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void callOnPackageReady(
|
|
||||||
String packageName,
|
|
||||||
ApplicationInfo applicationInfo,
|
|
||||||
boolean isFirstPackage,
|
|
||||||
ClassLoader defaultClassLoader,
|
|
||||||
ClassLoader classLoader,
|
|
||||||
Object appComponentFactory
|
|
||||||
) {
|
|
||||||
var lifecycle = new CompatLifecycleData(
|
|
||||||
packageName,
|
|
||||||
applicationInfo,
|
|
||||||
isFirstPackage,
|
|
||||||
defaultClassLoader,
|
|
||||||
classLoader,
|
|
||||||
appComponentFactory
|
|
||||||
);
|
|
||||||
for (XposedModule module : modules) {
|
|
||||||
invokeCompatLifecycle(module, "onPackageReady", lifecycle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void callOnSystemServerLoaded(XposedModuleInterface.SystemServerLoadedParam param) {
|
public static void callOnSystemServerLoaded(XposedModuleInterface.SystemServerLoadedParam param) {
|
||||||
for (XposedModule module : modules) {
|
for (XposedModule module : modules) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -104,18 +82,6 @@ public class LSPosedContext implements XposedInterface {
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
Log.e(TAG, "Error when calling onSystemServerLoaded of " + module.getApplicationInfo().packageName, t);
|
Log.e(TAG, "Error when calling onSystemServerLoaded of " + module.getApplicationInfo().packageName, t);
|
||||||
}
|
}
|
||||||
invokeCompatLifecycle(
|
|
||||||
module,
|
|
||||||
"onSystemServerStarting",
|
|
||||||
new CompatLifecycleData(
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
false,
|
|
||||||
param.getClassLoader(),
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,7 +112,19 @@ public class LSPosedContext implements XposedInterface {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
var moduleContext = instantiateModuleCompat(moduleClass, ctx);
|
var moduleEntry = moduleClass.getConstructor(XposedInterface.class, XposedModuleInterface.ModuleLoadedParam.class);
|
||||||
|
var moduleContext = (XposedModule) moduleEntry.newInstance(ctx, new XposedModuleInterface.ModuleLoadedParam() {
|
||||||
|
@Override
|
||||||
|
public boolean isSystemServer() {
|
||||||
|
return isSystemServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String getProcessName() {
|
||||||
|
return processName;
|
||||||
|
}
|
||||||
|
});
|
||||||
modules.add(moduleContext);
|
modules.add(moduleContext);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Log.e(TAG, " Failed to load class " + moduleClass, e);
|
Log.e(TAG, " Failed to load class " + moduleClass, e);
|
||||||
|
|
@ -161,127 +139,6 @@ public class LSPosedContext implements XposedInterface {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static XposedModule instantiateModuleCompat(Class<?> moduleClass, LSPosedContext context) throws Throwable {
|
|
||||||
// New API-style path: no-arg constructor + attachFramework + onModuleLoaded callback.
|
|
||||||
try {
|
|
||||||
var ctor = moduleClass.getDeclaredConstructor();
|
|
||||||
ctor.setAccessible(true);
|
|
||||||
var instance = ctor.newInstance();
|
|
||||||
if (instance instanceof XposedModule) {
|
|
||||||
var typed = (XposedModule) instance;
|
|
||||||
typed.attachFramework(context);
|
|
||||||
typed.onModuleLoaded(new XposedModuleInterface.ModuleLoadedParam() {
|
|
||||||
@Override
|
|
||||||
public boolean isSystemServer() {
|
|
||||||
return isSystemServer;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public String getProcessName() {
|
|
||||||
return processName;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return (XposedModule) instance;
|
|
||||||
}
|
|
||||||
} catch (NoSuchMethodException ignored) {
|
|
||||||
}
|
|
||||||
|
|
||||||
// Old API-style path: constructor takes (XposedInterface, ModuleLoadedParam).
|
|
||||||
var moduleEntry = moduleClass.getConstructor(XposedInterface.class, XposedModuleInterface.ModuleLoadedParam.class);
|
|
||||||
return (XposedModule) moduleEntry.newInstance(context, new XposedModuleInterface.ModuleLoadedParam() {
|
|
||||||
@Override
|
|
||||||
public boolean isSystemServer() {
|
|
||||||
return isSystemServer;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public String getProcessName() {
|
|
||||||
return processName;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class CompatLifecycleData {
|
|
||||||
final String packageName;
|
|
||||||
final ApplicationInfo applicationInfo;
|
|
||||||
final boolean isFirstPackage;
|
|
||||||
final ClassLoader defaultClassLoader;
|
|
||||||
final ClassLoader classLoader;
|
|
||||||
final Object appComponentFactory;
|
|
||||||
|
|
||||||
CompatLifecycleData(
|
|
||||||
String packageName,
|
|
||||||
ApplicationInfo applicationInfo,
|
|
||||||
boolean isFirstPackage,
|
|
||||||
ClassLoader defaultClassLoader,
|
|
||||||
ClassLoader classLoader,
|
|
||||||
Object appComponentFactory
|
|
||||||
) {
|
|
||||||
this.packageName = packageName;
|
|
||||||
this.applicationInfo = applicationInfo;
|
|
||||||
this.isFirstPackage = isFirstPackage;
|
|
||||||
this.defaultClassLoader = defaultClassLoader;
|
|
||||||
this.classLoader = classLoader;
|
|
||||||
this.appComponentFactory = appComponentFactory;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void invokeCompatLifecycle(XposedModule module, String methodName, CompatLifecycleData data) {
|
|
||||||
try {
|
|
||||||
for (Method method : module.getClass().getMethods()) {
|
|
||||||
if (!method.getName().equals(methodName) || method.getParameterCount() != 1) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
var paramType = method.getParameterTypes()[0];
|
|
||||||
var proxy = Proxy.newProxyInstance(
|
|
||||||
paramType.getClassLoader(),
|
|
||||||
new Class<?>[]{paramType},
|
|
||||||
(proxyObj, invokedMethod, args) -> {
|
|
||||||
String invokedName = invokedMethod.getName();
|
|
||||||
return switch (invokedName) {
|
|
||||||
case "getPackageName" -> data.packageName;
|
|
||||||
case "getApplicationInfo" -> data.applicationInfo;
|
|
||||||
case "isFirstPackage" -> data.isFirstPackage;
|
|
||||||
case "getDefaultClassLoader" -> data.defaultClassLoader;
|
|
||||||
case "getClassLoader" -> data.classLoader;
|
|
||||||
case "getAppComponentFactory" -> data.appComponentFactory;
|
|
||||||
case "getClassLoaderForSystemServer", "getSystemServerClassLoader" -> data.defaultClassLoader;
|
|
||||||
// Keep Object-contract methods stable for proxy users.
|
|
||||||
case "toString" -> "CompatLifecycleProxy(" + methodName + "," + data.packageName + ")";
|
|
||||||
case "hashCode" -> System.identityHashCode(proxyObj);
|
|
||||||
case "equals" -> proxyObj == (args == null || args.length == 0 ? null : args[0]);
|
|
||||||
default -> null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
method.invoke(module, proxy);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (Throwable t) {
|
|
||||||
String modulePkg = "<unknown>";
|
|
||||||
try {
|
|
||||||
if (module.getApplicationInfo() != null && module.getApplicationInfo().packageName != null) {
|
|
||||||
modulePkg = module.getApplicationInfo().packageName;
|
|
||||||
}
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
}
|
|
||||||
Throwable root = t;
|
|
||||||
if (t instanceof InvocationTargetException && ((InvocationTargetException) t).getTargetException() != null) {
|
|
||||||
root = ((InvocationTargetException) t).getTargetException();
|
|
||||||
} else if (t.getCause() != null) {
|
|
||||||
root = t.getCause();
|
|
||||||
}
|
|
||||||
String brief = "LSP_INVOKE_ERR method=" + methodName
|
|
||||||
+ " module=" + modulePkg
|
|
||||||
+ " type=" + root.getClass().getName()
|
|
||||||
+ " msg=" + String.valueOf(root.getMessage());
|
|
||||||
android.util.Log.e(TAG, brief);
|
|
||||||
Log.e(TAG, brief, root);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public String getFrameworkName() {
|
public String getFrameworkName() {
|
||||||
|
|
@ -332,27 +189,6 @@ public class LSPosedContext implements XposedInterface {
|
||||||
return LSPosedBridge.doHook(origin, priority, hooker);
|
return LSPosedBridge.doHook(origin, priority, hooker);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
@NonNull
|
|
||||||
public <T> MethodUnhooker<Constructor<T>> hookClassInitializer(@NonNull Class<T> origin, @NonNull Class<? extends Hooker> hooker) {
|
|
||||||
return hookClassInitializer(origin, PRIORITY_DEFAULT, hooker);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@NonNull
|
|
||||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
|
||||||
public <T> MethodUnhooker<Constructor<T>> hookClassInitializer(@NonNull Class<T> origin, int priority, @NonNull Class<? extends Hooker> hooker) {
|
|
||||||
Method staticInitializer = HookBridge.getStaticInitializer(origin);
|
|
||||||
|
|
||||||
// The class might not have a static initializer block
|
|
||||||
if (staticInitializer == null) {
|
|
||||||
throw new IllegalArgumentException("Class " + origin.getName() + " has no static initializer");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the existing doHook logic. It will return a MethodUnhooker<Method>.
|
|
||||||
return (MethodUnhooker) LSPosedBridge.doHook(staticInitializer, priority, hooker);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean doDeoptimize(@NonNull Executable method) {
|
private static boolean doDeoptimize(@NonNull Executable method) {
|
||||||
if (Modifier.isAbstract(method.getModifiers())) {
|
if (Modifier.isAbstract(method.getModifiers())) {
|
||||||
throw new IllegalArgumentException("Cannot deoptimize abstract methods: " + method);
|
throw new IllegalArgumentException("Cannot deoptimize abstract methods: " + method);
|
||||||
|
|
@ -374,16 +210,10 @@ public class LSPosedContext implements XposedInterface {
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public Object invokeOrigin(@NonNull Method method, @Nullable Object thisObject, Object... args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException {
|
public Object invokeOrigin(@NonNull Method method, @Nullable Object thisObject, Object[] args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException {
|
||||||
return HookBridge.invokeOriginalMethod(method, thisObject, args);
|
return HookBridge.invokeOriginalMethod(method, thisObject, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> void invokeOrigin(@NonNull Constructor<T> constructor, @NonNull T thisObject, Object... args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException {
|
|
||||||
// The bridge returns an Object (null for void/constructors), which we discard.
|
|
||||||
HookBridge.invokeOriginalMethod(constructor, thisObject, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static char getTypeShorty(Class<?> type) {
|
private static char getTypeShorty(Class<?> type) {
|
||||||
if (type == int.class) {
|
if (type == int.class) {
|
||||||
return 'I';
|
return 'I';
|
||||||
|
|
@ -427,11 +257,6 @@ public class LSPosedContext implements XposedInterface {
|
||||||
return HookBridge.invokeSpecialMethod(method, getExecutableShorty(method), method.getDeclaringClass(), thisObject, args);
|
return HookBridge.invokeSpecialMethod(method, getExecutableShorty(method), method.getDeclaringClass(), thisObject, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> void invokeSpecial(@NonNull Constructor<T> constructor, @NonNull T thisObject, Object... args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException {
|
|
||||||
HookBridge.invokeSpecialMethod(constructor, getExecutableShorty(constructor), constructor.getDeclaringClass(), thisObject, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public <T> T newInstanceOrigin(@NonNull Constructor<T> constructor, Object... args) throws InvocationTargetException, IllegalAccessException, InstantiationException {
|
public <T> T newInstanceOrigin(@NonNull Constructor<T> constructor, Object... args) throws InvocationTargetException, IllegalAccessException, InstantiationException {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package org.lsposed.lspd.nativebridge;
|
package org.lsposed.lspd.nativebridge;
|
||||||
|
|
||||||
import java.lang.reflect.Executable;
|
import java.lang.reflect.Executable;
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
|
||||||
import dalvik.annotation.optimization.FastNative;
|
import dalvik.annotation.optimization.FastNative;
|
||||||
|
|
@ -26,12 +25,4 @@ public class HookBridge {
|
||||||
public static native boolean setTrusted(Object cookie);
|
public static native boolean setTrusted(Object cookie);
|
||||||
|
|
||||||
public static native Object[][] callbackSnapshot(Class<?> hooker_callback, Executable method);
|
public static native Object[][] callbackSnapshot(Class<?> hooker_callback, Executable method);
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the static initializer (<clinit>) of a class as a Method object.
|
|
||||||
* Standard Java reflection cannot access this.
|
|
||||||
* @param clazz The class to inspect.
|
|
||||||
* @return A Method object for the static initializer, or null if it doesn't exist.
|
|
||||||
*/
|
|
||||||
public static native Method getStaticInitializer(Class<?> clazz);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -322,21 +322,6 @@ LSP_DEF_NATIVE_METHOD(jobjectArray, HookBridge, callbackSnapshot, jclass callbac
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
LSP_DEF_NATIVE_METHOD(jobject, HookBridge, getStaticInitializer, jclass target_class) {
|
|
||||||
// <clinit> is the internal name for a static initializer.
|
|
||||||
// Its signature is always ()V (no arguments, void return).
|
|
||||||
jmethodID mid = env->GetStaticMethodID(target_class, "<clinit>", "()V");
|
|
||||||
if (!mid) {
|
|
||||||
// If GetStaticMethodID fails, it throws an exception.
|
|
||||||
// We clear it and return null to let the Java side handle it gracefully.
|
|
||||||
env->ExceptionClear();
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
// Convert the method ID to a java.lang.reflect.Method object.
|
|
||||||
// The last parameter must be JNI_TRUE because it's a static method.
|
|
||||||
return env->ToReflectedMethod(target_class, mid, JNI_TRUE);
|
|
||||||
}
|
|
||||||
|
|
||||||
static JNINativeMethod gMethods[] = {
|
static JNINativeMethod gMethods[] = {
|
||||||
LSP_NATIVE_METHOD(HookBridge, hookMethod, "(ZLjava/lang/reflect/Executable;Ljava/lang/Class;ILjava/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, unhookMethod, "(ZLjava/lang/reflect/Executable;Ljava/lang/Object;)Z"),
|
||||||
|
|
@ -347,7 +332,6 @@ static JNINativeMethod gMethods[] = {
|
||||||
LSP_NATIVE_METHOD(HookBridge, instanceOf, "(Ljava/lang/Object;Ljava/lang/Class;)Z"),
|
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, setTrusted, "(Ljava/lang/Object;)Z"),
|
||||||
LSP_NATIVE_METHOD(HookBridge, callbackSnapshot, "(Ljava/lang/Class;Ljava/lang/reflect/Executable;)[[Ljava/lang/Object;"),
|
LSP_NATIVE_METHOD(HookBridge, callbackSnapshot, "(Ljava/lang/Class;Ljava/lang/reflect/Executable;)[[Ljava/lang/Object;"),
|
||||||
LSP_NATIVE_METHOD(HookBridge, getStaticInitializer, "(Ljava/lang/Class;)Ljava/lang/reflect/Method;"),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
void RegisterHookBridge(JNIEnv *env) {
|
void RegisterHookBridge(JNIEnv *env) {
|
||||||
|
|
|
||||||
|
|
@ -193,18 +193,9 @@ public class ConfigFileManager {
|
||||||
public static boolean chattr0(Path path) {
|
public static boolean chattr0(Path path) {
|
||||||
try {
|
try {
|
||||||
var dir = Os.open(path.toString(), OsConstants.O_RDONLY, 0);
|
var dir = Os.open(path.toString(), OsConstants.O_RDONLY, 0);
|
||||||
// Clear all special file attributes on the directory
|
|
||||||
HiddenApiBridge.Os_ioctlInt(dir, Process.is64Bit() ? 0x40086602 : 0x40046602, 0);
|
HiddenApiBridge.Os_ioctlInt(dir, Process.is64Bit() ? 0x40086602 : 0x40046602, 0);
|
||||||
Os.close(dir);
|
Os.close(dir);
|
||||||
return true;
|
return true;
|
||||||
} catch (ErrnoException e) {
|
|
||||||
// If the operation is not supported (ENOTSUP), it means the filesystem doesn't support attributes.
|
|
||||||
// We can assume the file is not immutable and proceed.
|
|
||||||
if (e.errno == OsConstants.ENOTSUP) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
Log.d(TAG, "chattr 0", e);
|
|
||||||
return false;
|
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Log.d(TAG, "chattr 0", e);
|
Log.d(TAG, "chattr 0", e);
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,6 @@ import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileDescriptor;
|
import java.io.FileDescriptor;
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
|
@ -68,74 +67,15 @@ public class Dex2OatService implements Runnable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks the ELF header of the target file.
|
|
||||||
* If 32-bit -> Assigns to Index 0 (Release) or 1 (Debug).
|
|
||||||
* If 64-bit -> Assigns to Index 2 (Release) or 3 (Debug).
|
|
||||||
*/
|
|
||||||
private void checkAndAddDex2Oat(String path) {
|
|
||||||
if (path == null)
|
|
||||||
return;
|
|
||||||
File file = new File(path);
|
|
||||||
if (!file.exists())
|
|
||||||
return;
|
|
||||||
|
|
||||||
try (FileInputStream fis = new FileInputStream(file)) {
|
|
||||||
byte[] header = new byte[5];
|
|
||||||
if (fis.read(header) != 5)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// 1. Verify ELF Magic: 0x7F 'E' 'L' 'F'
|
|
||||||
if (header[0] != 0x7F || header[1] != 'E' || header[2] != 'L' || header[3] != 'F') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Check Architecture (header[4]): 1 = 32-bit, 2 = 64-bit
|
|
||||||
boolean is32Bit = (header[4] == 1);
|
|
||||||
boolean is64Bit = (header[4] == 2);
|
|
||||||
boolean isDebug = path.contains("dex2oatd");
|
|
||||||
|
|
||||||
int index = -1;
|
|
||||||
|
|
||||||
if (is32Bit) {
|
|
||||||
index = isDebug ? 1 : 0; // Index 0/1 maps to r32/d32 in C++
|
|
||||||
} else if (is64Bit) {
|
|
||||||
index = isDebug ? 3 : 2; // Index 2/3 maps to r64/d64 in C++
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Assign to the detected slot
|
|
||||||
if (index != -1 && dex2oatArray[index] == null) {
|
|
||||||
dex2oatArray[index] = path;
|
|
||||||
try {
|
|
||||||
// Open the FD for the wrapper to use later
|
|
||||||
fdArray[index] = Os.open(path, OsConstants.O_RDONLY, 0);
|
|
||||||
Log.i(TAG, "Detected " + path + " as " + (is64Bit ? "64-bit" : "32-bit") + " -> Assigned Index "
|
|
||||||
+ index);
|
|
||||||
} catch (ErrnoException e) {
|
|
||||||
Log.e(TAG, "Failed to open FD for " + path, e);
|
|
||||||
dex2oatArray[index] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
// File not readable, skip
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Dex2OatService() {
|
public Dex2OatService() {
|
||||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
|
||||||
// Android 10: Check the standard path.
|
openDex2oat(Process.is64Bit() ? 2 : 0, "/apex/com.android.runtime/bin/dex2oat");
|
||||||
// Logic will detect if it is 32-bit and put it in Index 0.
|
openDex2oat(Process.is64Bit() ? 3 : 1, "/apex/com.android.runtime/bin/dex2oatd");
|
||||||
checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oat");
|
|
||||||
checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oatd");
|
|
||||||
|
|
||||||
// Check for explicit 64-bit paths (just in case)
|
|
||||||
checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oat64");
|
|
||||||
checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oatd64");
|
|
||||||
} else {
|
} else {
|
||||||
checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oat32");
|
openDex2oat(0, "/apex/com.android.art/bin/dex2oat32");
|
||||||
checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oatd32");
|
openDex2oat(1, "/apex/com.android.art/bin/dex2oatd32");
|
||||||
checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oat64");
|
openDex2oat(2, "/apex/com.android.art/bin/dex2oat64");
|
||||||
checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oatd64");
|
openDex2oat(3, "/apex/com.android.art/bin/dex2oatd64");
|
||||||
}
|
}
|
||||||
|
|
||||||
openDex2oat(4, "/data/adb/modules/zygisk_lsposed/bin/liboat_hook32.so");
|
openDex2oat(4, "/data/adb/modules/zygisk_lsposed/bin/liboat_hook32.so");
|
||||||
|
|
|
||||||
|
|
@ -268,7 +268,7 @@ public class LSPManagerService extends ILSPManagerService.Stub {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getXposedApiVersion() {
|
public int getXposedApiVersion() {
|
||||||
return IXposedService.LIB_API;
|
return IXposedService.API;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ public class LSPModuleService extends IXposedService.Stub {
|
||||||
@Override
|
@Override
|
||||||
public int getAPIVersion() throws RemoteException {
|
public int getAPIVersion() throws RemoteException {
|
||||||
ensureModule();
|
ensureModule();
|
||||||
return IXposedService.LIB_API;
|
return API;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -152,12 +152,6 @@ public class LSPModuleService extends IXposedService.Stub {
|
||||||
return IXposedService.FRAMEWORK_PRIVILEGE_ROOT;
|
return IXposedService.FRAMEWORK_PRIVILEGE_ROOT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getFrameworkProperties() throws RemoteException {
|
|
||||||
ensureModule();
|
|
||||||
return IXposedService.PROP_CAP_SYSTEM | IXposedService.PROP_CAP_REMOTE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<String> getScope() throws RemoteException {
|
public List<String> getScope() throws RemoteException {
|
||||||
ensureModule();
|
ensureModule();
|
||||||
|
|
|
||||||
|
|
@ -229,8 +229,7 @@ void Logcat::ProcessBuffer(struct log_msg *buf) {
|
||||||
tag == "APatchD"sv || tag == "Dobby"sv || tag.starts_with("dex2oat"sv) ||
|
tag == "APatchD"sv || tag == "Dobby"sv || tag.starts_with("dex2oat"sv) ||
|
||||||
tag == "KernelSU"sv || tag == "LSPlant"sv || tag == "LSPlt"sv ||
|
tag == "KernelSU"sv || tag == "LSPlant"sv || tag == "LSPlt"sv ||
|
||||||
tag.starts_with("LSPosed"sv) || tag == "Magisk"sv || tag == "SELinux"sv ||
|
tag.starts_with("LSPosed"sv) || tag == "Magisk"sv || tag == "SELinux"sv ||
|
||||||
tag == "TEESimulator"sv || tag.starts_with("Vector"sv) ||
|
tag == "TEESimulator"sv || tag.starts_with("zygisk"sv))) [[unlikely]] {
|
||||||
tag.starts_with("zygisk"sv))) [[unlikely]] {
|
|
||||||
verbose_print_count_ += PrintLogLine(entry, verbose_file_.get());
|
verbose_print_count_ += PrintLogLine(entry, verbose_file_.get());
|
||||||
}
|
}
|
||||||
if (entry.pid == my_pid_ && tag == "LSPosedLogcat"sv) [[unlikely]] {
|
if (entry.pid == my_pid_ && tag == "LSPosedLogcat"sv) [[unlikely]] {
|
||||||
|
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
# VectorDex2Oat
|
|
||||||
|
|
||||||
VectorDex2Oat is a specialized wrapper and instrumentation suite for the Android `dex2oat` (Ahead-of-Time compiler) binary. It is designed to intercept the compilation process, force specific compiler behaviors (specifically disabling method inlining), and transparently spoof the resulting OAT metadata to hide the presence of the wrapper.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
In the Android Runtime (ART), `dex2oat` compiles DEX files into OAT files. Modern ART optimizations often inline methods, making it difficult for instrumentation tools to hook specific function calls.
|
|
||||||
|
|
||||||
This project consists of two primary components:
|
|
||||||
1. **dex2oat (Wrapper):** A replacement binary that intercepts the execution, communicates via Unix Domain Sockets to obtain the original compiler binary, and executes it with forced flags.
|
|
||||||
2. **liboat_hook.so (Hooker):** A shared library injected into the `dex2oat` process via `LD_PRELOAD` that utilizes PLT hooking to sanitize the OAT header's command-line metadata.
|
|
||||||
|
|
||||||
## Key Features
|
|
||||||
|
|
||||||
* **Inlining Suppression:** Appends `--inline-max-code-units=0` to the compiler arguments, ensuring all methods remain discrete and hookable.
|
|
||||||
* **FD-Based Execution:** Executes the original `dex2oat` via the system linker using `/proc/self/fd/` paths, avoiding direct execution of files on the disk.
|
|
||||||
* **Metadata Spoofing:** Intercepts `art::OatHeader::ComputeChecksum` or `art::OatHeader::GetKeyValueStore` to remove traces of the wrapper and its injected flags from the final `.oat` file.
|
|
||||||
* **Abstract Socket Communication:** Uses the Linux Abstract Namespace for Unix sockets to coordinate file descriptor passing between the controller and the wrapper.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### The Wrapper [dex2oat.cpp](src/main/cpp/dex2oat.cpp)
|
|
||||||
The wrapper acts as a "man-in-the-middle" for the compiler. When called by the system, it
|
|
||||||
1. connects to a predefined Unix socket (the stub name `5291374ceda0...` will be replaced during installation of `Vector`);
|
|
||||||
2. identifies the target architecture (32-bit vs 64-bit) and debug status;
|
|
||||||
3. receives File Descriptors (FDs) for both the original `dex2oat` binary and the `oat_hook` library;
|
|
||||||
4. reconstructs the command line, replacing the wrapper path with the original binary path and appending the "no-inline" flags;
|
|
||||||
5. clears `LD_LIBRARY_PATH` and sets `LD_PRELOAD` to the hooker library's FD;
|
|
||||||
6. invokes the dynamic linker (`linker64`) to execute the compiler.
|
|
||||||
|
|
||||||
### The Hooker [oat_hook.cpp](src/main/cpp/oat_hook.cpp)
|
|
||||||
The hooker library is preloaded into the compiler's address space. It uses the [LSPlt](https://github.com/JingMatrix/LSPlt) library to:
|
|
||||||
1. Scan the memory map to find the `dex2oat` binary.
|
|
||||||
2. Locate and hook internal ART functions:
|
|
||||||
* [art::OatHeader::GetKeyValueStore](https://cs.android.com/android/platform/superproject/+/android-latest-release:art/runtime/oat/oat.cc;l=366)
|
|
||||||
* [art::OatHeader::ComputeChecksum](https://cs.android.com/android/platform/superproject/+/android-latest-release:art/runtime/oat/oat.cc;l=366)
|
|
||||||
3. When the compiler attempts to write the "dex2oat-cmdline" key into the OAT header, the hooker intercepts the call, parses the key-value store, and removes the wrapper-specific flags and paths.
|
|
||||||
|
|
@ -1,15 +1,47 @@
|
||||||
|
/*
|
||||||
|
* This file is part of LSPosed.
|
||||||
|
*
|
||||||
|
* LSPosed is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LSPosed is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LSPosed. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2022 LSPosed Contributors
|
||||||
|
*/
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.agp.lib)
|
alias(libs.plugins.agp.lib)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "org.matrix.vector.dex2oat"
|
namespace = "org.lsposed.dex2oat"
|
||||||
|
|
||||||
androidResources.enable = false
|
buildFeatures {
|
||||||
|
androidResources = false
|
||||||
|
buildConfig = false
|
||||||
|
prefab = true
|
||||||
|
prefabPublishing = true
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 29
|
||||||
|
}
|
||||||
|
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
cmake {
|
||||||
path("src/main/cpp/CMakeLists.txt")
|
path("src/main/cpp/CMakeLists.txt")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prefab {
|
||||||
|
register("dex2oat")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.10)
|
||||||
project(dex2oat)
|
project(dex2oat)
|
||||||
|
|
||||||
add_executable(dex2oat dex2oat.cpp)
|
add_executable(dex2oat dex2oat.cpp)
|
||||||
add_library(oat_hook SHARED oat_hook.cpp)
|
add_library(oat_hook SHARED oat_hook.cpp oat.cpp)
|
||||||
|
|
||||||
OPTION(LSPLT_BUILD_SHARED OFF)
|
OPTION(LSPLT_BUILD_SHARED OFF)
|
||||||
add_subdirectory(${EXTERNAL_ROOT}/lsplt/lsplt/src/main/jni external)
|
add_subdirectory(${EXTERNAL_ROOT}/lsplt/lsplt/src/main/jni external)
|
||||||
|
|
|
||||||
|
|
@ -1,195 +1,148 @@
|
||||||
|
/*
|
||||||
|
* This file is part of LSPosed.
|
||||||
|
*
|
||||||
|
* LSPosed is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LSPosed is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LSPosed. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2022 LSPosed Contributors
|
||||||
|
*/
|
||||||
|
|
||||||
|
//
|
||||||
|
// Created by Nullptr on 2022/4/1.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
#include <sys/socket.h>
|
#include <sys/socket.h>
|
||||||
#include <sys/un.h>
|
#include <sys/un.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#include <cstdio>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <cstring>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "logging.h"
|
#include "logging.h"
|
||||||
|
|
||||||
// Access to the process environment variables
|
|
||||||
extern "C" char **environ;
|
|
||||||
|
|
||||||
#if defined(__LP64__)
|
#if defined(__LP64__)
|
||||||
#define LP_SELECT(lp32, lp64) lp64
|
#define LP_SELECT(lp32, lp64) lp64
|
||||||
#else
|
#else
|
||||||
#define LP_SELECT(lp32, lp64) lp32
|
#define LP_SELECT(lp32, lp64) lp32
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace {
|
#define ID_VEC(is64, is_debug) (((is64) << 1) | (is_debug))
|
||||||
|
|
||||||
constexpr char kSockName[] = "5291374ceda0aef7c5d86cd2a4f6a3ac";
|
const char kSockName[] = "5291374ceda0aef7c5d86cd2a4f6a3ac\0";
|
||||||
|
|
||||||
/**
|
static ssize_t xrecvmsg(int sockfd, struct msghdr *msg, int flags) {
|
||||||
* Calculates a vector ID based on architecture and debug status.
|
int rec = recvmsg(sockfd, msg, flags);
|
||||||
*/
|
|
||||||
inline int get_id_vec(bool is64, bool is_debug) {
|
|
||||||
return (static_cast<int>(is64) << 1) | static_cast<int>(is_debug);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wraps recvmsg with error logging.
|
|
||||||
*/
|
|
||||||
ssize_t xrecvmsg(int sockfd, struct msghdr *msg, int flags) {
|
|
||||||
ssize_t rec = recvmsg(sockfd, msg, flags);
|
|
||||||
if (rec < 0) {
|
if (rec < 0) {
|
||||||
PLOGE("recvmsg");
|
PLOGE("recvmsg");
|
||||||
}
|
}
|
||||||
return rec;
|
return rec;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
static void *recv_fds(int sockfd, char *cmsgbuf, size_t bufsz, int cnt) {
|
||||||
* Receives file descriptors passed over a Unix domain socket using SCM_RIGHTS.
|
|
||||||
*
|
|
||||||
* @return Pointer to the FD data on success, nullptr on failure.
|
|
||||||
*/
|
|
||||||
void *recv_fds(int sockfd, char *cmsgbuf, size_t bufsz, int cnt) {
|
|
||||||
struct iovec iov = {
|
struct iovec iov = {
|
||||||
.iov_base = &cnt,
|
.iov_base = &cnt,
|
||||||
.iov_len = sizeof(cnt),
|
.iov_len = sizeof(cnt),
|
||||||
};
|
};
|
||||||
struct msghdr msg = {.msg_name = nullptr,
|
struct msghdr msg = {
|
||||||
.msg_namelen = 0,
|
.msg_iov = &iov, .msg_iovlen = 1, .msg_control = cmsgbuf, .msg_controllen = bufsz};
|
||||||
.msg_iov = &iov,
|
|
||||||
.msg_iovlen = 1,
|
|
||||||
.msg_control = cmsgbuf,
|
|
||||||
.msg_controllen = bufsz,
|
|
||||||
.msg_flags = 0};
|
|
||||||
|
|
||||||
if (xrecvmsg(sockfd, &msg, MSG_WAITALL) < 0) return nullptr;
|
|
||||||
|
|
||||||
|
xrecvmsg(sockfd, &msg, MSG_WAITALL);
|
||||||
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
|
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
|
||||||
|
|
||||||
if (msg.msg_controllen != bufsz || cmsg == nullptr ||
|
if (msg.msg_controllen != bufsz || cmsg == NULL ||
|
||||||
cmsg->cmsg_len != CMSG_LEN(sizeof(int) * cnt) || cmsg->cmsg_level != SOL_SOCKET ||
|
cmsg->cmsg_len != CMSG_LEN(sizeof(int) * cnt) || cmsg->cmsg_level != SOL_SOCKET ||
|
||||||
cmsg->cmsg_type != SCM_RIGHTS) {
|
cmsg->cmsg_type != SCM_RIGHTS) {
|
||||||
return nullptr;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
return CMSG_DATA(cmsg);
|
return CMSG_DATA(cmsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
static int recv_fd(int sockfd) {
|
||||||
* Helper to receive a single FD from the socket.
|
|
||||||
*/
|
|
||||||
int recv_fd(int sockfd) {
|
|
||||||
char cmsgbuf[CMSG_SPACE(sizeof(int))];
|
char cmsgbuf[CMSG_SPACE(sizeof(int))];
|
||||||
|
|
||||||
void *data = recv_fds(sockfd, cmsgbuf, sizeof(cmsgbuf), 1);
|
void *data = recv_fds(sockfd, cmsgbuf, sizeof(cmsgbuf), 1);
|
||||||
if (data == nullptr) return -1;
|
if (data == NULL) return -1;
|
||||||
|
|
||||||
int result;
|
int result;
|
||||||
std::memcpy(&result, data, sizeof(int));
|
memcpy(&result, data, sizeof(int));
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
static int read_int(int fd) {
|
||||||
* Reads an integer acknowledgment from the socket.
|
|
||||||
*/
|
|
||||||
int read_int(int fd) {
|
|
||||||
int val;
|
int val;
|
||||||
if (read(fd, &val, sizeof(val)) != sizeof(val)) return -1;
|
if (read(fd, &val, sizeof(val)) != sizeof(val)) return -1;
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
static void write_int(int fd, int val) {
|
||||||
* Writes an integer command/ID to the socket.
|
|
||||||
*/
|
|
||||||
void write_int(int fd, int val) {
|
|
||||||
if (fd < 0) return;
|
if (fd < 0) return;
|
||||||
(void)write(fd, &val, sizeof(val));
|
write(fd, &val, sizeof(val));
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
int main(int argc, char **argv) {
|
int main(int argc, char **argv) {
|
||||||
LOGD("dex2oat wrapper ppid=%d", getppid());
|
LOGD("dex2oat wrapper ppid=%d", getppid());
|
||||||
|
|
||||||
// Prepare Unix domain socket address (Abstract Namespace)
|
|
||||||
struct sockaddr_un sock = {};
|
struct sockaddr_un sock = {};
|
||||||
sock.sun_family = AF_UNIX;
|
sock.sun_family = AF_UNIX;
|
||||||
// sock.sun_path[0] is already \0, so we copy name into sun_path + 1
|
strlcpy(sock.sun_path + 1, kSockName, sizeof(sock.sun_path) - 1);
|
||||||
std::strncpy(sock.sun_path + 1, kSockName, sizeof(sock.sun_path) - 2);
|
|
||||||
|
|
||||||
// Abstract socket length: family + leading \0 + string length
|
|
||||||
socklen_t len = sizeof(sock.sun_family) + strlen(kSockName) + 1;
|
|
||||||
|
|
||||||
// 1. Get original dex2oat binary FD
|
|
||||||
int sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
int sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||||
if (connect(sock_fd, reinterpret_cast<struct sockaddr *>(&sock), len)) {
|
size_t len = sizeof(sa_family_t) + strlen(sock.sun_path + 1) + 1;
|
||||||
|
if (connect(sock_fd, (struct sockaddr *)&sock, len)) {
|
||||||
PLOGE("failed to connect to %s", sock.sun_path + 1);
|
PLOGE("failed to connect to %s", sock.sun_path + 1);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
write_int(sock_fd, ID_VEC(LP_SELECT(0, 1), strstr(argv[0], "dex2oatd") != NULL));
|
||||||
bool is_debug = (argv[0] != nullptr && std::strstr(argv[0], "dex2oatd") != nullptr);
|
|
||||||
write_int(sock_fd, get_id_vec(LP_SELECT(false, true), is_debug));
|
|
||||||
|
|
||||||
int stock_fd = recv_fd(sock_fd);
|
int stock_fd = recv_fd(sock_fd);
|
||||||
read_int(sock_fd); // Sync
|
read_int(sock_fd);
|
||||||
close(sock_fd);
|
close(sock_fd);
|
||||||
|
|
||||||
// 2. Get liboat_hook.so FD
|
|
||||||
sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||||
if (connect(sock_fd, reinterpret_cast<struct sockaddr *>(&sock), len)) {
|
if (connect(sock_fd, (struct sockaddr *)&sock, len)) {
|
||||||
PLOGE("failed to connect to %s", sock.sun_path + 1);
|
PLOGE("failed to connect to %s", sock.sun_path + 1);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
write_int(sock_fd, LP_SELECT(4, 5));
|
write_int(sock_fd, LP_SELECT(4, 5));
|
||||||
int hooker_fd = recv_fd(sock_fd);
|
int hooker_fd = recv_fd(sock_fd);
|
||||||
read_int(sock_fd); // Sync
|
read_int(sock_fd);
|
||||||
close(sock_fd);
|
close(sock_fd);
|
||||||
|
|
||||||
if (hooker_fd == -1) {
|
if (hooker_fd == -1) {
|
||||||
LOGE("failed to read liboat_hook.so");
|
PLOGE("failed to read liboat_hook.so");
|
||||||
}
|
}
|
||||||
LOGD("sock: %s stock_fd: %d", sock.sun_path + 1, stock_fd);
|
LOGD("sock: %s %d", sock.sun_path + 1, stock_fd);
|
||||||
|
|
||||||
// Prepare arguments for execve
|
const char *new_argv[argc + 2];
|
||||||
// Logic: [linker] [/proc/self/fd/stock_fd] [original_args...] [--inline-max-code-units=0]
|
for (int i = 0; i < argc; i++) new_argv[i] = argv[i];
|
||||||
std::vector<const char *> exec_argv;
|
new_argv[argc] = "--inline-max-code-units=0";
|
||||||
|
new_argv[argc + 1] = NULL;
|
||||||
|
|
||||||
const char *linker_path =
|
if (getenv("LD_LIBRARY_PATH") == NULL) {
|
||||||
LP_SELECT("/apex/com.android.runtime/bin/linker", "/apex/com.android.runtime/bin/linker64");
|
char const *libenv = LP_SELECT(
|
||||||
|
"LD_LIBRARY_PATH=/apex/com.android.art/lib:/apex/com.android.os.statsd/lib",
|
||||||
char stock_fd_path[64];
|
"LD_LIBRARY_PATH=/apex/com.android.art/lib64:/apex/com.android.os.statsd/lib64");
|
||||||
std::snprintf(stock_fd_path, sizeof(stock_fd_path), "/proc/self/fd/%d", stock_fd);
|
putenv((char *)libenv);
|
||||||
|
|
||||||
exec_argv.push_back(linker_path);
|
|
||||||
exec_argv.push_back(stock_fd_path);
|
|
||||||
|
|
||||||
// Append original arguments starting from argv[1]
|
|
||||||
for (int i = 1; i < argc; ++i) {
|
|
||||||
exec_argv.push_back(argv[i]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append hooking flags to disable inline, which is our purpose of this wrapper, since we cannot
|
// Set LD_PRELOAD to load liboat_hook.so
|
||||||
// hook inlined target methods.
|
const int STRING_BUFFER = 50;
|
||||||
exec_argv.push_back("--inline-max-code-units=0");
|
char env_str[STRING_BUFFER];
|
||||||
exec_argv.push_back(nullptr);
|
snprintf(env_str, STRING_BUFFER, "LD_PRELOAD=/proc/%d/fd/%d", getpid(), hooker_fd);
|
||||||
|
putenv(env_str);
|
||||||
|
LOGD("Set env %s", env_str);
|
||||||
|
|
||||||
// Setup Environment variables
|
fexecve(stock_fd, (char **)new_argv, environ);
|
||||||
// Clear LD_LIBRARY_PATH to let the linker use internal config
|
|
||||||
unsetenv("LD_LIBRARY_PATH");
|
|
||||||
|
|
||||||
// Set LD_PRELOAD to point to the hooker library FD
|
PLOGE("fexecve failed");
|
||||||
std::string preload_val = "LD_PRELOAD=/proc/self/fd/" + std::to_string(hooker_fd);
|
|
||||||
setenv("LD_PRELOAD", ("/proc/self/fd/" + std::to_string(hooker_fd)).c_str(), 1);
|
|
||||||
|
|
||||||
// Pass original argv[0] as DEX2OAT_CMD
|
|
||||||
if (argv[0]) {
|
|
||||||
setenv("DEX2OAT_CMD", argv[0], 1);
|
|
||||||
LOGD("DEX2OAT_CMD set to %s", argv[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGI("Executing via linker: %s executing %s", linker_path, stock_fd_path);
|
|
||||||
|
|
||||||
// Perform the execution
|
|
||||||
execve(linker_path, const_cast<char *const *>(exec_argv.data()), environ);
|
|
||||||
|
|
||||||
// If we reach here, execve failed
|
|
||||||
PLOGE("execve failed");
|
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <android/log.h>
|
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
#include <android/log.h>
|
||||||
|
|
||||||
#ifndef LOG_TAG
|
#ifndef LOG_TAG
|
||||||
#define LOG_TAG "VectorDex2Oat"
|
#define LOG_TAG "LSPosedDex2Oat"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef LOG_DISABLED
|
#ifdef LOG_DISABLED
|
||||||
|
|
@ -15,7 +15,11 @@
|
||||||
#define LOGE(...) 0
|
#define LOGE(...) 0
|
||||||
#else
|
#else
|
||||||
#ifndef NDEBUG
|
#ifndef NDEBUG
|
||||||
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
|
#define LOGD(fmt, ...) \
|
||||||
|
__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, \
|
||||||
|
"%s:%d#%s" \
|
||||||
|
": " fmt, \
|
||||||
|
__FILE_NAME__, __LINE__, __PRETTY_FUNCTION__ __VA_OPT__(, ) __VA_ARGS__)
|
||||||
#define LOGV(fmt, ...) \
|
#define LOGV(fmt, ...) \
|
||||||
__android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, \
|
__android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, \
|
||||||
"%s:%d#%s" \
|
"%s:%d#%s" \
|
||||||
|
|
|
||||||
|
|
@ -72,9 +72,17 @@ public:
|
||||||
static constexpr const char kTrueValue[] = "true";
|
static constexpr const char kTrueValue[] = "true";
|
||||||
static constexpr const char kFalseValue[] = "false";
|
static constexpr const char kFalseValue[] = "false";
|
||||||
|
|
||||||
// Added helper to access the key_value_store_ field, which could be fragile across
|
static constexpr size_t Get_key_value_store_size_Offset() {
|
||||||
// different Android versions and compiler optimizations.
|
return offsetof(OatHeader, key_value_store_size_);
|
||||||
const uint8_t* getKeyValueStore() const { return key_value_store_; }
|
}
|
||||||
|
static constexpr size_t Get_key_value_store_Offset() {
|
||||||
|
return offsetof(OatHeader, key_value_store_);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t GetKeyValueStoreSize() const;
|
||||||
|
const uint8_t* GetKeyValueStore() const;
|
||||||
|
|
||||||
|
void SetKeyValueStoreSize(uint32_t new_size);
|
||||||
|
|
||||||
void ComputeChecksum(/*inout*/ uint32_t* checksum) const;
|
void ComputeChecksum(/*inout*/ uint32_t* checksum) const;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
#include "oat.h"
|
||||||
|
|
||||||
|
namespace art {
|
||||||
|
|
||||||
|
uint32_t OatHeader::GetKeyValueStoreSize() const {
|
||||||
|
return *(uint32_t*)((uintptr_t)this + OatHeader::Get_key_value_store_size_Offset());
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t* OatHeader::GetKeyValueStore() const {
|
||||||
|
return (const uint8_t*)((uintptr_t)this + OatHeader::Get_key_value_store_Offset());
|
||||||
|
}
|
||||||
|
|
||||||
|
void OatHeader::SetKeyValueStoreSize(uint32_t new_size) {
|
||||||
|
*reinterpret_cast<uint32_t*>((uintptr_t)this + Get_key_value_store_size_Offset()) = new_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace art
|
||||||
|
|
@ -1,206 +1,122 @@
|
||||||
#include <dlfcn.h>
|
#include <dlfcn.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <cinttypes>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstring>
|
|
||||||
#include <lsplt.hpp>
|
#include <lsplt.hpp>
|
||||||
#include <map>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "logging.h"
|
#include "logging.h"
|
||||||
#include "oat.h"
|
#include "oat.h"
|
||||||
|
|
||||||
/**
|
const std::string_view param_to_remove = " --inline-max-code-units=0";
|
||||||
* This library is injected into dex2oat to intercept the generation of OAT headers. Our wrapper
|
|
||||||
* runs dex2oat via the linker with extra flags. Without this hook, the resulting OAT file would
|
|
||||||
* record the transferred fd path of wrapper and the extra flags in its "dex2oat-cmdline" key, which
|
|
||||||
* can be used to detect the wrapper.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace {
|
#define DCL_HOOK_FUNC(ret, func, ...) \
|
||||||
const std::string_view kParamToRemove = "--inline-max-code-units=0";
|
ret (*old_##func)(__VA_ARGS__); \
|
||||||
std::string g_binary_path = getenv("DEX2OAT_CMD"); // The original binary path
|
ret new_##func(__VA_ARGS__)
|
||||||
} // namespace
|
|
||||||
|
|
||||||
/**
|
bool store_resized = false;
|
||||||
* Sanitizes the command line string by:
|
|
||||||
* 1. Replacing the first token (the linker/binary path) with the original dex2oat path.
|
|
||||||
* 2. Removing the specific optimization flag we injected.
|
|
||||||
*/
|
|
||||||
std::string process_cmd(std::string_view sv, std::string_view new_cmd_path) {
|
|
||||||
std::vector<std::string> tokens;
|
|
||||||
std::string current;
|
|
||||||
|
|
||||||
// Simple split by space
|
bool ModifyStoreInPlace(uint8_t* store, uint32_t store_size) {
|
||||||
for (char c : sv) {
|
if (store == nullptr || store_size == 0) {
|
||||||
if (c == ' ') {
|
|
||||||
if (!current.empty()) {
|
|
||||||
tokens.push_back(std::move(current));
|
|
||||||
current.clear();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
current.push_back(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!current.empty()) tokens.push_back(std::move(current));
|
|
||||||
|
|
||||||
// 1. Replace the command path (argv[0])
|
|
||||||
if (!tokens.empty()) {
|
|
||||||
tokens[0] = std::string(new_cmd_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Remove the injected parameter if it exists
|
|
||||||
auto it = std::remove(tokens.begin(), tokens.end(), std::string(kParamToRemove));
|
|
||||||
tokens.erase(it, tokens.end());
|
|
||||||
|
|
||||||
// 3. Join tokens back into a single string
|
|
||||||
std::string result;
|
|
||||||
for (size_t i = 0; i < tokens.size(); ++i) {
|
|
||||||
result += tokens[i];
|
|
||||||
if (i != tokens.size() - 1) result += ' ';
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Re-serializes the Key-Value map back into the OAT header memory space.
|
|
||||||
*/
|
|
||||||
uint8_t* WriteKeyValueStore(const std::map<std::string, std::string>& key_values, uint8_t* store) {
|
|
||||||
LOGD("Writing KeyValueStore back to memory");
|
|
||||||
char* data_ptr = reinterpret_cast<char*>(store);
|
|
||||||
|
|
||||||
for (const auto& [key, value] : key_values) {
|
|
||||||
// Copy key + null terminator
|
|
||||||
std::memcpy(data_ptr, key.c_str(), key.length() + 1);
|
|
||||||
data_ptr += key.length() + 1;
|
|
||||||
// Copy value + null terminator
|
|
||||||
std::memcpy(data_ptr, value.c_str(), value.length() + 1);
|
|
||||||
data_ptr += value.length() + 1;
|
|
||||||
}
|
|
||||||
LOGD("Written KeyValueStore with size: %zu", reinterpret_cast<uint8_t*>(data_ptr) - store);
|
|
||||||
|
|
||||||
return reinterpret_cast<uint8_t*>(data_ptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to test if a header field could have variable length
|
|
||||||
bool IsNonDeterministic(const std::string_view& key) {
|
|
||||||
auto variable_fields = art::OatHeader::kNonDeterministicFieldsAndLengths;
|
|
||||||
return std::any_of(variable_fields.begin(), variable_fields.end(),
|
|
||||||
[&key](const auto& pair) { return pair.first.compare(key) == 0; });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the OAT KeyValueStore and spoofs the "dex2oat-cmdline" entry.
|
|
||||||
*
|
|
||||||
* @return true if the store was modified in-place or successfully rebuilt.
|
|
||||||
*/
|
|
||||||
bool SpoofKeyValueStore(uint8_t* store) {
|
|
||||||
if (!store) return false;
|
|
||||||
|
|
||||||
uint32_t* const store_size_ptr = reinterpret_cast<uint32_t*>(store - sizeof(uint32_t));
|
|
||||||
uint32_t const store_size = *store_size_ptr;
|
|
||||||
|
|
||||||
const char* ptr = reinterpret_cast<const char*>(store);
|
|
||||||
const char* const store_end = ptr + store_size;
|
|
||||||
std::map<std::string, std::string> new_store_map;
|
|
||||||
LOGI("Parsing KeyValueStore [%p - %p] of size %u", ptr, store_end, store_size);
|
|
||||||
|
|
||||||
bool store_modified = false;
|
|
||||||
|
|
||||||
while (ptr < store_end && *ptr != '\0') {
|
|
||||||
// Find key
|
|
||||||
const char* key_end = reinterpret_cast<const char*>(std::memchr(ptr, 0, store_end - ptr));
|
|
||||||
if (!key_end) break;
|
|
||||||
std::string_view key(ptr, key_end - ptr);
|
|
||||||
|
|
||||||
// Find value
|
|
||||||
const char* value_start = key_end + 1;
|
|
||||||
if (value_start >= store_end) break;
|
|
||||||
const char* value_end =
|
|
||||||
reinterpret_cast<const char*>(std::memchr(value_start, 0, store_end - value_start));
|
|
||||||
if (!value_end) break;
|
|
||||||
std::string_view value(value_start, value_end - value_start);
|
|
||||||
|
|
||||||
const bool has_padding =
|
|
||||||
value_end + 1 < store_end && *(value_end + 1) == '\0' && IsNonDeterministic(key);
|
|
||||||
|
|
||||||
if (key == art::OatHeader::kDex2OatCmdLineKey &&
|
|
||||||
value.find(kParamToRemove) != std::string_view::npos) {
|
|
||||||
std::string cleaned_cmd = process_cmd(value, g_binary_path);
|
|
||||||
LOGI("Spoofing cmdline: Original size %zu -> New size %zu", value.length(),
|
|
||||||
cleaned_cmd.length());
|
|
||||||
|
|
||||||
// We can overwrite in-place if the padding is enabled
|
|
||||||
if (has_padding) {
|
|
||||||
LOGI("In-place spoofing dex2oat-cmdline (padding detected)");
|
|
||||||
|
|
||||||
// Zero out the entire original value range to be safe
|
|
||||||
size_t original_capacity = value.length();
|
|
||||||
std::memset(const_cast<char*>(value_start), 0, original_capacity);
|
|
||||||
|
|
||||||
// Write the new command.
|
|
||||||
std::memcpy(const_cast<char*>(value_start), cleaned_cmd.c_str(),
|
|
||||||
std::min(cleaned_cmd.length(), original_capacity));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Standard logic: store in map and rebuild later
|
|
||||||
new_store_map[std::string(key)] = std::move(cleaned_cmd);
|
|
||||||
store_modified = true;
|
|
||||||
} else {
|
|
||||||
new_store_map[std::string(key)] = std::string(value);
|
|
||||||
LOGI("Parsed item:\t[%s:%s]", key.data(), value.data());
|
|
||||||
}
|
|
||||||
|
|
||||||
ptr = value_end + 1;
|
|
||||||
if (has_padding) {
|
|
||||||
while (*ptr == '\0') {
|
|
||||||
ptr++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (store_modified) {
|
|
||||||
uint8_t* const new_store_end = WriteKeyValueStore(new_store_map, store);
|
|
||||||
*store_size_ptr = new_store_end - store;
|
|
||||||
LOGI("Store size set to %u", *store_size_ptr);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
#define DCL_HOOK_FUNC(ret, func, ...) \
|
// Define the search space
|
||||||
ret (*old_##func)(__VA_ARGS__) = nullptr; \
|
uint8_t* const store_begin = store;
|
||||||
ret new_##func(__VA_ARGS__)
|
uint8_t* const store_end = store + store_size;
|
||||||
|
|
||||||
// For Android version < 16
|
// 1. Search for the parameter in the memory buffer
|
||||||
DCL_HOOK_FUNC(uint8_t*, _ZNK3art9OatHeader16GetKeyValueStoreEv, void* header) {
|
auto it = std::search(store_begin, store_end, param_to_remove.begin(), param_to_remove.end());
|
||||||
uint8_t* const key_value_store = old__ZNK3art9OatHeader16GetKeyValueStoreEv(header);
|
|
||||||
|
|
||||||
SpoofKeyValueStore(key_value_store);
|
// Check if the parameter was found
|
||||||
|
if (it == store_end) {
|
||||||
return key_value_store;
|
LOGD("Parameter '%.*s' not found.", (int)param_to_remove.size(), param_to_remove.data());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t* location_of_param = it;
|
||||||
|
LOGD("Parameter found at offset %td.", location_of_param - store_begin);
|
||||||
|
|
||||||
|
// 2. Check if there is padding immediately after the string
|
||||||
|
uint8_t* const byte_after_param = location_of_param + param_to_remove.size();
|
||||||
|
bool has_padding = false;
|
||||||
|
|
||||||
|
// Boundary check: ensure the byte after the parameter is within the buffer
|
||||||
|
if (byte_after_param + 1 < store_end) {
|
||||||
|
if (*(byte_after_param + 1) == '\0') {
|
||||||
|
has_padding = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Perform the conditional action
|
||||||
|
if (has_padding) {
|
||||||
|
// CASE A: Padding exists. Overwrite the parameter with zeros.
|
||||||
|
LOGD("Padding found. Overwriting parameter with zeros.");
|
||||||
|
memset(location_of_param, 0, param_to_remove.size());
|
||||||
|
return false; // Size did not change
|
||||||
|
} else {
|
||||||
|
// CASE B: No padding exists (or parameter is at the very end).
|
||||||
|
// Remove the parameter by shifting the rest of the memory forward.
|
||||||
|
LOGD("No padding found. Removing parameter and shifting memory.");
|
||||||
|
|
||||||
|
// Calculate what to move
|
||||||
|
uint8_t* source = byte_after_param;
|
||||||
|
uint8_t* destination = location_of_param;
|
||||||
|
size_t bytes_to_move = store_end - source;
|
||||||
|
|
||||||
|
// memmove is required because the source and destination buffers overlap
|
||||||
|
if (bytes_to_move > 0) {
|
||||||
|
memmove(destination, source, bytes_to_move);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Update the total size of the store
|
||||||
|
store_size -= param_to_remove.size();
|
||||||
|
LOGD("Store size changed. New size: %u", store_size);
|
||||||
|
|
||||||
|
return true; // Size changed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DCL_HOOK_FUNC(uint32_t, _ZNK3art9OatHeader20GetKeyValueStoreSizeEv, void* header) {
|
||||||
|
uint32_t size = old__ZNK3art9OatHeader20GetKeyValueStoreSizeEv(header);
|
||||||
|
if (store_resized) {
|
||||||
|
LOGD("OatHeader::GetKeyValueStoreSize() called on object at %p\n", header);
|
||||||
|
size = size - param_to_remove.size();
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
DCL_HOOK_FUNC(uint8_t*, _ZNK3art9OatHeader16GetKeyValueStoreEv, void* header) {
|
||||||
|
LOGD("OatHeader::GetKeyValueStore() called on object at %p\n", header);
|
||||||
|
uint8_t* key_value_store_ = old__ZNK3art9OatHeader16GetKeyValueStoreEv(header);
|
||||||
|
uint32_t key_value_store_size_ = old__ZNK3art9OatHeader20GetKeyValueStoreSizeEv(header);
|
||||||
|
LOGD("KeyValueStore via hook: [addr: %p, size: %u]", key_value_store_, key_value_store_size_);
|
||||||
|
store_resized = ModifyStoreInPlace(key_value_store_, key_value_store_size_);
|
||||||
|
|
||||||
|
return key_value_store_;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For Android version 16+ : Intercept during checksum calculation
|
|
||||||
DCL_HOOK_FUNC(void, _ZNK3art9OatHeader15ComputeChecksumEPj, void* header, uint32_t* checksum) {
|
DCL_HOOK_FUNC(void, _ZNK3art9OatHeader15ComputeChecksumEPj, void* header, uint32_t* checksum) {
|
||||||
auto* oat_header = reinterpret_cast<art::OatHeader*>(header);
|
art::OatHeader* oat_header = reinterpret_cast<art::OatHeader*>(header);
|
||||||
uint8_t* const store = const_cast<uint8_t*>(oat_header->getKeyValueStore());
|
const uint8_t* key_value_store_ = oat_header->GetKeyValueStore();
|
||||||
|
uint32_t key_value_store_size_ = oat_header->GetKeyValueStoreSize();
|
||||||
SpoofKeyValueStore(store);
|
LOGD("KeyValueStore via offset: [addr: %p, size: %u]", key_value_store_, key_value_store_size_);
|
||||||
|
store_resized =
|
||||||
// Call original to compute checksum on our modified data
|
ModifyStoreInPlace(const_cast<uint8_t*>(key_value_store_), key_value_store_size_);
|
||||||
|
if (store_resized) {
|
||||||
|
oat_header->SetKeyValueStoreSize(key_value_store_size_ - param_to_remove.size());
|
||||||
|
}
|
||||||
old__ZNK3art9OatHeader15ComputeChecksumEPj(header, checksum);
|
old__ZNK3art9OatHeader15ComputeChecksumEPj(header, checksum);
|
||||||
LOGV("OAT Checksum recalculated: 0x%08X", *checksum);
|
LOGD("ComputeChecksum called: %" PRIu32, *checksum);
|
||||||
}
|
}
|
||||||
|
|
||||||
#undef DCL_HOOK_FUNC
|
#undef DCL_HOOK_FUNC
|
||||||
|
|
||||||
void register_hook(dev_t dev, ino_t inode, const char* symbol, void* new_func, void** old_func) {
|
void register_hook(dev_t dev, ino_t inode, const char* symbol, void* new_func, void** old_func) {
|
||||||
|
LOGD("RegisterHook: %s, %p, %p", symbol, new_func, old_func);
|
||||||
if (!lsplt::RegisterHook(dev, inode, symbol, new_func, old_func)) {
|
if (!lsplt::RegisterHook(dev, inode, symbol, new_func, old_func)) {
|
||||||
LOGE("Failed to register PLT hook: %s", symbol);
|
LOGE("Failed to register plt_hook \"%s\"\n", symbol);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -213,28 +129,16 @@ void register_hook(dev_t dev, ino_t inode, const char* symbol, void* new_func, v
|
||||||
__attribute__((constructor)) static void initialize() {
|
__attribute__((constructor)) static void initialize() {
|
||||||
dev_t dev = 0;
|
dev_t dev = 0;
|
||||||
ino_t inode = 0;
|
ino_t inode = 0;
|
||||||
|
for (auto& info : lsplt::MapInfo::Scan()) {
|
||||||
// Locate the dex2oat binary in memory to get its device and inode for PLT hooking
|
if (info.path.starts_with("/apex/com.android.art/bin/dex2oat")) {
|
||||||
for (const auto& info : lsplt::MapInfo::Scan()) {
|
|
||||||
if (info.path.find("bin/dex2oat") != std::string::npos) {
|
|
||||||
dev = info.dev;
|
dev = info.dev;
|
||||||
inode = info.inode;
|
inode = info.inode;
|
||||||
if (g_binary_path.empty()) g_binary_path = std::string(info.path);
|
|
||||||
LOGD("Found target: %s (dev: %ju, inode: %ju)", info.path.data(), (uintmax_t)dev,
|
|
||||||
(uintmax_t)inode);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dev == 0) {
|
PLT_HOOK_REGISTER(dev, inode, _ZNK3art9OatHeader20GetKeyValueStoreSizeEv);
|
||||||
LOGE("Could not locate dex2oat memory map");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register hook for the standard KeyValueStore getter
|
|
||||||
PLT_HOOK_REGISTER(dev, inode, _ZNK3art9OatHeader16GetKeyValueStoreEv);
|
PLT_HOOK_REGISTER(dev, inode, _ZNK3art9OatHeader16GetKeyValueStoreEv);
|
||||||
|
|
||||||
// If the standard store hook fails (e.g., on Android 16+), try the Checksum hook
|
|
||||||
if (!lsplt::CommitHook()) {
|
if (!lsplt::CommitHook()) {
|
||||||
PLT_HOOK_REGISTER(dev, inode, _ZNK3art9OatHeader15ComputeChecksumEPj);
|
PLT_HOOK_REGISTER(dev, inode, _ZNK3art9OatHeader15ComputeChecksumEPj);
|
||||||
lsplt::CommitHook();
|
lsplt::CommitHook();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
allow dex2oat dex2oat_exec file execute_no_trans
|
allow dex2oat dex2oat_exec file execute_no_trans
|
||||||
allow dex2oat system_linker_exec file execute_no_trans
|
|
||||||
|
|
||||||
allow shell shell dir write
|
allow shell shell dir write
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ import org.lsposed.lspd.service.BridgeService;
|
||||||
import org.lsposed.lspd.util.Utils;
|
import org.lsposed.lspd.util.Utils;
|
||||||
|
|
||||||
import io.github.libxposed.api.XposedInterface;
|
import io.github.libxposed.api.XposedInterface;
|
||||||
|
import io.github.libxposed.api.annotations.AfterInvocation;
|
||||||
|
import io.github.libxposed.api.annotations.XposedHooker;
|
||||||
|
|
||||||
|
|
||||||
public class ParasiticManagerSystemHooker implements HandleSystemServerProcessHooker.Callback {
|
public class ParasiticManagerSystemHooker implements HandleSystemServerProcessHooker.Callback {
|
||||||
|
|
@ -31,8 +33,10 @@ public class ParasiticManagerSystemHooker implements HandleSystemServerProcessHo
|
||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
|
@XposedHooker
|
||||||
private static class Hooker implements XposedInterface.Hooker {
|
private static class Hooker implements XposedInterface.Hooker {
|
||||||
public static void after(XposedInterface.AfterHookCallback callback) throws Throwable {
|
@AfterInvocation
|
||||||
|
public static void afterHookedMethod(XposedInterface.AfterHookCallback callback) throws Throwable {
|
||||||
var intent = (Intent) callback.getArgs()[0];
|
var intent = (Intent) callback.getArgs()[0];
|
||||||
if (intent == null) return;
|
if (intent == null) return;
|
||||||
if (!intent.hasCategory("org.lsposed.manager.LAUNCH_MANAGER")) return;
|
if (!intent.hasCategory("org.lsposed.manager.LAUNCH_MANAGER")) return;
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,33 @@
|
||||||
# LSPosed v1.11.0 🎐
|
🎉 To celebrate the release of Android 16, we are excited to announce a new stable version of LSPosed!
|
||||||
|
|
||||||
This release brings major improvements for **Android 16 Beta** readiness, resolves specific quirks on Android 10 and OnePlus devices, and significantly reinforces overall system stability.
|
To better understand LSPosed, we recommend reading our [troubleshooting guide](https://github.com/JingMatrix/LSPosed/issues/123).
|
||||||
|
|
||||||
### 📱 Compatibility & Core
|
### ✨ What's New
|
||||||
* **Android 16 Beta Support:** Fixed compatibility issues with Android 16 QPR Beta 3 (specifically `UserManager` changes) and recent ART updates affecting the `dex2oat` wrapper.
|
* Fully support Android 16.
|
||||||
* **Android 10 Fixes:** Resolved `dex2oat` crashes caused by 32-bit/64-bit architecture mismatches.
|
* Hide traces introduced by the `dex2oat` hook.
|
||||||
* **OnePlus Compatibility:** Restored `Application#attach` hooking capabilities, overcoming aggressive method inlining found in recent OOS updates.
|
* The LSPosed manager can now be opened via the Action button.
|
||||||
* **Dex2Oat Overhaul:** Refactored the wrapper to utilize the APEX linker directly, eliminating missing symbol errors and boosting reliability.
|
* New options have been added to the `Select` menu for scopes.
|
||||||
|
* Allow users to toggle off detectable logging of LSPosed.
|
||||||
|
|
||||||
### 🛠️ Stability & Fixes
|
### 🐛 Bug Fixes
|
||||||
* **Database Integrity:** Resolved critical crashes and potential corruption during database initialization and migration.
|
* The `LSPlt` hook has been abandoned for efficiency considerations.
|
||||||
* **Frida Compatibility:** Fixed `SIGSEGV` crashes when running alongside Frida by making memory mapping parsing more robust.
|
* Resolved an issue where modules targeting `systemui` (e.g., `ClassicPowerMenu`) were not working.
|
||||||
* **SELinux:** Corrected file contexts for the modern Xposed API 100 (`openRemoteFile`) and ensured they persist across reboots.
|
* Removed Telemetry monitoring.
|
||||||
* **Injection Reliability:** Implemented retry logic for System Server injection to minimize start-up failures.
|
|
||||||
|
|
||||||
### ⚡ Internal Changes
|
### 🔄 Other Changes
|
||||||
* **Kotlin Refactor:** The `DexParser` has been rewritten in Kotlin for improved performance and maintainability.
|
* The dependency on `topjohnwu/libcxx` has been removed in favor of the official C++ implementation. This will result in a larger release archive for LSPosed.
|
||||||
* **WebUI Removal:** Removed the WebUI integration as it is no longer required.
|
|
||||||
|
|
||||||
---
|
### 🚀 High-Priority Plans
|
||||||
|
* Creating comprehensive development documentation for LSPosed.
|
||||||
|
* Resolving open issues with assignees.
|
||||||
|
|
||||||
## 🔮 Development Plan
|
**Full Changelog**: [v1.10.1...v1.10.2](https://github.com/JingMatrix/LSPosed/compare/v1.10.1...v1.10.2)
|
||||||
|
|
||||||
The current LSPosed fork is undergoing a complete refactor into a new project: **Vector**.
|
<details>
|
||||||
|
<summary>❤️ A personal note</summary>
|
||||||
|
|
||||||
We are in the process of rewriting the Java layer into Kotlin and adding extensive documentation for the native layer.
|
For the past few months, I have been focused on finishing my PhD thesis manuscript, which has limited my active development on LSPosed. I sincerely appreciate the community's passion and support during this time. It has been a pleasure to witness our community grow and thrive around this open-source fork. I am deeply indebted to your trust and respect, which has indeed helped me navigate the unavoidable challenges and depressions faced by a PhD candidate.
|
||||||
|
|
||||||
The name **Vector** was chosen to manifest its close mathematical relationship with **Matrix**, while symbolizing the framework's role as a precise injection vector for modules.
|
Maintaining this project is a joyful responsibility. However, life is a grand museum of passions, and I am constantly called by my devotion to research and teaching at the university. For users eagerly awaiting new features, I want to reassure you that the LSPosed codebase is quite stable and sufficient for its functionality. Moreover, I sincerely encourage developers to join the project. For all users, please consider participating in the [GitHub Discussions](https://github.com/JingMatrix/LSPosed/discussions) to share your experiences and various tips. Nothing is more valuable to an open-source project than an active community.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"version": "v1.11.0",
|
"version": "v1.10.2",
|
||||||
"versionCode": 7209,
|
"versionCode": 7182,
|
||||||
"zipUrl": "https://github.com/JingMatrix/LSPosed/releases/download/v1.11.0/LSPosed-v1.11.0-7209-zygisk-release.zip",
|
"zipUrl": "https://github.com/JingMatrix/LSPosed/releases/download/v1.10.2/LSPosed-v1.10.2-7182-zygisk-release.zip",
|
||||||
"changelog": "https://raw.githubusercontent.com/JingMatrix/LSPosed/master/magisk-loader/update/changelog.md"
|
"changelog": "https://raw.githubusercontent.com/JingMatrix/LSPosed/master/magisk-loader/update/changelog.md"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit ec7cb26aa215b20b2ab1baf7c7b5a91a021a9016
|
Subproject commit 496b76fa3e5af87958ebef97bd160319e05da79b
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 745afd390c7b87d9cd31efd246a7bc12cc190ac0
|
Subproject commit 54582730315ba4a3d7cfaf9baf9d23c419e07006
|
||||||
Loading…
Reference in New Issue