From 4e792ba2923fc0199cff979f79b61c66eb8fc4c1 Mon Sep 17 00:00:00 2001 From: 5ec1cff Date: Wed, 12 Jun 2024 18:34:55 +0800 Subject: [PATCH] support hook whole class & fix getClassLoader of HookFunction & add fst --- .../a13e300/tools/StethoxAppInterceptor.java | 15 ++++- .../tools/objects/FindStackTraceFunction.java | 49 ++++++++++++++++ .../a13e300/tools/objects/HookFunction.java | 57 ++++++++++++++++--- 3 files changed, 112 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/io/github/a13e300/tools/objects/FindStackTraceFunction.java diff --git a/app/src/main/java/io/github/a13e300/tools/StethoxAppInterceptor.java b/app/src/main/java/io/github/a13e300/tools/StethoxAppInterceptor.java index 02be71c..b2043df 100644 --- a/app/src/main/java/io/github/a13e300/tools/StethoxAppInterceptor.java +++ b/app/src/main/java/io/github/a13e300/tools/StethoxAppInterceptor.java @@ -27,6 +27,7 @@ import de.robv.android.xposed.XC_MethodHook; import de.robv.android.xposed.XposedBridge; import de.robv.android.xposed.XposedHelpers; +import io.github.a13e300.tools.objects.FindStackTraceFunction; import io.github.a13e300.tools.objects.GetStackTraceFunction; import io.github.a13e300.tools.objects.HookFunction; import io.github.a13e300.tools.objects.HookParam; @@ -40,6 +41,7 @@ public class StethoxAppInterceptor implements IXposedHookZygoteInit { private boolean initialized = false; private boolean mWaiting = false; + public static ClassLoader mClassLoader = null; private synchronized void cont(ScriptableObject ignore) { if (mWaiting) { @@ -71,12 +73,18 @@ private synchronized void initializeStetho(Context context) throws InterruptedEx } catch (IllegalArgumentException e) { Logger.e("failed to find suspend provider, please ensure module process without restriction", e); } + try { + mClassLoader = context.getClassLoader(); + } catch (Throwable t) { + Logger.e("failed to get classloader for context", t); + } Stetho.initialize(Stetho.newInitializerBuilder(context) .enableWebKitInspector( () -> new Stetho.DefaultInspectorModulesBuilder(context).runtimeRepl( new JsRuntimeReplFactoryBuilder(context) .addFunction("getStackTrace", new GetStackTraceFunction()) .addFunction("printStackTrace", new PrintStackTraceFunction()) + .addFunction("findStackTrace", new FindStackTraceFunction()) .addFunction("runOnUiThread", new RunOnHandlerFunction(new Handler(Looper.getMainLooper()))) .addFunction("runOnHandler", new RunOnHandlerFunction()) .addFunction("JArray", new JArrayFunction(null)) @@ -95,7 +103,9 @@ private synchronized void initializeStetho(Context context) throws InterruptedEx .addVariable("boolean", boolean.class) .addVariable("char", char.class) .importPackage("java.lang") - .onInitScope(scope -> { + .addVariable("xposed_bridge", XposedBridge.class) + .addVariable("xposed_helper", XposedHelpers.class) + .onInitScope((jsContext, scope) -> { try { scope.defineProperty("activities", null, Utils.class.getDeclaredMethod("getActivities", ScriptableObject.class), null, ScriptableObject.READONLY); scope.defineProperty("current", null, Utils.class.getDeclaredMethod("getCurrentActivity", ScriptableObject.class), null, ScriptableObject.READONLY); @@ -112,6 +122,9 @@ private synchronized void initializeStetho(Context context) throws InterruptedEx scope.defineProperty("cont", StethoxAppInterceptor.this, method, null, ScriptableObject.READONLY); } } + jsContext.evaluateString(scope, + "let st, pst, fst; st = pst = printStackTrace; fst = findStackTrace;", + "initializer", 1, null); } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException | diff --git a/app/src/main/java/io/github/a13e300/tools/objects/FindStackTraceFunction.java b/app/src/main/java/io/github/a13e300/tools/objects/FindStackTraceFunction.java new file mode 100644 index 0000000..7411bfc --- /dev/null +++ b/app/src/main/java/io/github/a13e300/tools/objects/FindStackTraceFunction.java @@ -0,0 +1,49 @@ +package io.github.a13e300.tools.objects; + +import android.app.Activity; +import android.util.Log; +import android.view.View; +import android.view.Window; + +import com.facebook.stetho.rhino.JsConsole; + +import org.mozilla.javascript.BaseFunction; +import org.mozilla.javascript.Context; +import org.mozilla.javascript.Scriptable; +import org.mozilla.javascript.Wrapper; + +import de.robv.android.xposed.XposedHelpers; + +public class FindStackTraceFunction extends BaseFunction { + @Override + public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + if (args.length == 1) { + var obj = args[0]; + Throwable t; + if (obj instanceof Wrapper) { + obj = ((Wrapper) obj).unwrap(); + } + if (obj instanceof Throwable) { + t = (Throwable) obj; + } else { + if (obj instanceof Activity) { + obj = ((Activity) obj).getWindow(); + } + if (obj instanceof Window) { + obj = ((Window) obj).getDecorView(); + } + if (obj instanceof View) { + t = (Throwable) XposedHelpers.getObjectField( + XposedHelpers.callMethod(obj, "getViewRootImpl"), + "mLocation" + ); + } else { + throw new IllegalArgumentException("required a view or throwable!"); + } + } + JsConsole.fromScope(scope).log(Log.getStackTraceString(t)); + return null; + } + throw new IllegalArgumentException("must be 1 view or throwable arg !"); + } +} diff --git a/app/src/main/java/io/github/a13e300/tools/objects/HookFunction.java b/app/src/main/java/io/github/a13e300/tools/objects/HookFunction.java index 0c58e74..bde441a 100644 --- a/app/src/main/java/io/github/a13e300/tools/objects/HookFunction.java +++ b/app/src/main/java/io/github/a13e300/tools/objects/HookFunction.java @@ -2,7 +2,6 @@ import static io.github.a13e300.tools.Utils.enterJsContext; -import android.app.AndroidAppHelper; import android.util.Log; import com.facebook.stetho.rhino.JsConsole; @@ -24,12 +23,14 @@ import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; import java.util.stream.Collectors; import de.robv.android.xposed.XC_MethodHook; import de.robv.android.xposed.XC_MethodReplacement; import de.robv.android.xposed.XposedBridge; import io.github.a13e300.tools.NativeUtils; +import io.github.a13e300.tools.StethoxAppInterceptor; import io.github.a13e300.tools.Utils; public class HookFunction extends BaseFunction { @@ -59,9 +60,9 @@ public String getClassName() { synchronized ClassLoader getClassLoader() { if (mClassLoader == null) { - var app = AndroidAppHelper.currentApplication(); - if (app == null) return ClassLoader.getSystemClassLoader(); - mClassLoader = app.getClassLoader(); + ClassLoader cl = StethoxAppInterceptor.mClassLoader; + if (cl == null) return ClassLoader.getSystemClassLoader(); + mClassLoader = cl; } return mClassLoader; } @@ -113,7 +114,9 @@ private UnhookFunction hook(Context cx, Scriptable scope, Object[] args) { if (pos + 1 == args.length - 2 && args[pos + 1] == null) { emptySig = true; } - } else throw new IllegalArgumentException("method name is required"); + } else { + methodName = null; + } pos++; Class[] methodSig; if (!emptySig) { @@ -139,7 +142,38 @@ private UnhookFunction hook(Context cx, Scriptable scope, Object[] args) { methodSig = new Class[0]; } try { - if ("".equals(methodName)) { + if (methodName == null) { + if (methodSig.length != 0) throw new IllegalArgumentException("should not specify method signature!"); + hookMembers = List.of(hookClass.getDeclaredMethods()); + } else if (methodName.startsWith("@")) { + if (methodName.equals("@")) throw new IllegalArgumentException("must contain at least one of: method,all,ALL,constructor,init,public,instance"); + var queryArgs = methodName.substring(1).split(","); + var isPublic = false; + var allMethod = false; + var allConstructor = false; + var isInstance = false; + for (var c: queryArgs) { + if ("all".equals(c) || "method".equals(c)) allMethod = true; + else if ("ALL".equals(c)) allMethod = allConstructor = true; + else if ("constructor".equals(c) || "init".equals(c)) allConstructor = true; + else if ("public".equals(c)) allMethod = isPublic = true; + else if ("instance".equals(c)) allMethod = isInstance = true; + else throw new IllegalArgumentException("must contain at least one of: method,all,ALL,constructor,init,public,instance"); + } + var fIsPublic = isPublic; + var fIsInstance = isInstance; + if (methodSig.length != 0) throw new IllegalArgumentException("should not specify method signature!"); + var result = new ArrayList(); + Consumer consumer = (it) -> { + var match = true; + if (fIsPublic) match = Modifier.isPublic(it.getModifiers()); + if (fIsInstance) match &= !Modifier.isStatic(it.getModifiers()); + if (match) result.add(it); + }; + if (allMethod) Arrays.stream(hookClass.getDeclaredMethods()).forEach(consumer); + if (allConstructor) Arrays.stream(hookClass.getDeclaredConstructors()).forEach(consumer); + hookMembers = result; + } else if ("".equals(methodName)) { if (methodSig.length == 0 && !emptySig) { hookMembers = List.of(hookClass.getDeclaredConstructors()); } else { @@ -275,12 +309,14 @@ synchronized void setClassLoader(ClassLoader cl) { + " When you disconnected from the console, all hooks are automatically removed\n" + " Utils:\n" + " getStackTrace()\n" - + " printStackTrace()\n" + + " printStackTrace() / pst()\n" + + " findStackTrace(obj) / fst(obj): find stack trace and print, such as View\n" + " hook.findClass(String[, Classloader])\n" + " hook.setClassLoader / getClassLoader\n" + " hook.getClassLoaders (get an array of all classloaders)\n" + " runOnHandler(callback, handler) & runOnUiThread(callback)\n" - + " trace() / traces(): parameters like hook, but without callback, the hook will print corresponding information automatically (traces contains stack trace)"; + + " trace() / traces(): parameters like hook, but without callback, the hook will print corresponding information automatically (traces contains stack trace)\n" + + " deoptimizeMethod(method)"; @JSFunction public static String toString(Context cx, Scriptable thisObj, Object[] args, Function funObj) { @@ -380,4 +416,9 @@ protected void afterHookedMethod(MethodHookParam param) throws Throwable { console.info("start tracing " + unhook.mUnhooks.size() + " methods (stackTrace=" + stack + ")"); return unhook; } + + @JSFunction + public static void deoptimizeMethod(Context cx, Scriptable thisObj, Object[] args, Function funObj) throws Throwable { + XposedBridge.class.getDeclaredMethod("deoptimizeMethod", Member.class).invoke(null, args[0]); + } }