#!/bin/bash # rebuild_wechathook.sh — compile WeChatHook, repackage, redeploy to device # Does not need the LSPosed source tree — uses bundled XposedBridge stubs. set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" ADB="${ADB:-$SCRIPT_DIR/../platform-tools/adb}" [ ! -f "$ADB" ] && ADB=adb ANDROID_HOME="${ANDROID_HOME:-/opt/homebrew/share/android-commandlinetools}" ANDROID_JAR="$ANDROID_HOME/platforms/android-36/android.jar" D8="$ANDROID_HOME/build-tools/36.0.0/d8" APKSIGNER="$ANDROID_HOME/build-tools/36.0.0/apksigner" ZIPALIGN="$ANDROID_HOME/build-tools/36.0.0/zipalign" JAVA_HOME="${JAVA_HOME:-/opt/homebrew/opt/openjdk@17}" JAVAC="$JAVA_HOME/bin/javac" KEYTOOL="$JAVA_HOME/bin/keytool" GOLDEN_APK="$SCRIPT_DIR/wechathook_v2_signed.apk" DEBUG_KS="$HOME/.android/debug.keystore" STUBS_DIR="/tmp/xposed_stubs" BUILD_DIR="/tmp/wechathook_rebuild" # ── Write XposedBridge stubs (minimal, for compilation only) ────────────────── echo "== Writing XposedBridge stubs ==" mkdir -p "$STUBS_DIR/de/robv/android/xposed/callbacks" cat > "$STUBS_DIR/de/robv/android/xposed/XposedBridge.java" << 'JAVA' package de.robv.android.xposed; public class XposedBridge { public static void log(String text) {} public static void log(Throwable t) {} public static XC_MethodHook.Unhook hookMethod( java.lang.reflect.Member hookMethod, XC_MethodHook callback) { return null; } } JAVA cat > "$STUBS_DIR/de/robv/android/xposed/XC_MethodHook.java" << 'JAVA' package de.robv.android.xposed; public abstract class XC_MethodHook { public class Unhook { public void unhook() {} } public static class MethodHookParam { public Object thisObject; public Object[] args; public Object getResult() { return null; } public void setResult(Object result) {} public Throwable getThrowable() { return null; } public boolean hasThrowable() { return false; } } protected void beforeHookedMethod(MethodHookParam param) throws Throwable {} protected void afterHookedMethod(MethodHookParam param) throws Throwable {} } JAVA cat > "$STUBS_DIR/de/robv/android/xposed/XposedHelpers.java" << 'JAVA' package de.robv.android.xposed; public class XposedHelpers { public static Class findClass(String className, ClassLoader classLoader) { return null; } public static Object callStaticMethod(Class clazz, String methodName, Object... args) { return null; } public static Object callMethod(Object obj, String methodName, Object... args) { return null; } public static Object callMethod(Object obj, String methodName, Class[] paramTypes, Object... args) { return null; } public static Object newInstance(Class clazz, Object... args) { return null; } public static Object newInstance(Class clazz, Class[] paramTypes, Object... args) { return null; } public static Object getObjectField(Object obj, String fieldName) { return null; } public static void setObjectField(Object obj, String fieldName, Object value) {} public static XC_MethodHook.Unhook findAndHookMethod( String className, ClassLoader cl, String methodName, Object... paramTypesAndCallback) { return null; } public static XC_MethodHook.Unhook findAndHookMethod( Class clazz, String methodName, Object... paramTypesAndCallback) { return null; } public static XC_MethodHook.Unhook findAndHookConstructor( String className, ClassLoader cl, Object... paramTypesAndCallback) { return null; } public static class InvocationTargetError extends Error { public InvocationTargetError(String msg, Throwable cause) { super(msg, cause); } } } JAVA cat > "$STUBS_DIR/de/robv/android/xposed/IXposedHookLoadPackage.java" << 'JAVA' package de.robv.android.xposed; public interface IXposedHookLoadPackage { void handleLoadPackage(de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam lpparam) throws Throwable; } JAVA cat > "$STUBS_DIR/de/robv/android/xposed/callbacks/XC_LoadPackage.java" << 'JAVA' package de.robv.android.xposed.callbacks; public class XC_LoadPackage { public static class LoadPackageParam { public String packageName; public String processName; public ClassLoader classLoader; public android.content.pm.ApplicationInfo appInfo; public boolean isFirstApplication; } } JAVA # ── Compile ──────────────────────────────────────────────────────────────────── rm -rf "$BUILD_DIR" mkdir -p "$BUILD_DIR/compiled" "$BUILD_DIR/dex" "$BUILD_DIR/pack" echo "== Compiling XposedBridge stubs ==" "$JAVAC" -source 11 -target 11 -cp "$ANDROID_JAR" \ -d "$BUILD_DIR/compiled" \ $(find "$STUBS_DIR" -name "*.java") 2>/dev/null echo "== Compiling WeChatHook.java ==" "$JAVAC" -source 11 -target 11 \ -cp "$ANDROID_JAR:$BUILD_DIR/compiled" \ -d "$BUILD_DIR/compiled" \ "$SCRIPT_DIR/wechathook_src/WeChatHook.java" echo "== Converting to DEX ==" "$D8" --min-api 30 --output "$BUILD_DIR/dex/" \ $(find "$BUILD_DIR/compiled/com/example/wechathook" -name "*.class") # ── Package ─────────────────────────────────────────────────────────────────── echo "== Packing APK ==" cp "$GOLDEN_APK" "$BUILD_DIR/pack/wechathook.apk" # Replace DEX in place (the golden APK already has correct manifest + xposed_init) cd "$BUILD_DIR/pack" zip -u wechathook.apk -j "$BUILD_DIR/dex/classes.dex" > /dev/null "$ZIPALIGN" -v -f 4 wechathook.apk wechathook_aligned.apk > /dev/null # ── Sign with Android debug keystore (same as golden APK) ──────────────────── echo "== Signing ==" OUT="$BUILD_DIR/wechathook_signed.apk" "$APKSIGNER" sign \ --ks "$DEBUG_KS" \ --ks-key-alias androiddebugkey \ --ks-pass pass:android \ --key-pass pass:android \ --out "$OUT" \ wechathook_aligned.apk 2>/dev/null echo "APK size: $(wc -c < "$OUT") bytes" # ── Deploy to device ────────────────────────────────────────────────────────── echo "== Deploying to device ==" # Push to stable path $ADB push "$OUT" /data/local/tmp/wechathook.apk # Install via PKMS (use -r to update in-place if same sig; if first time, just install) INSTALL_OUT=$($ADB shell "su 0 sh -c 'pm install -r /data/local/tmp/wechathook.apk 2>&1'" | tr -d '\r') echo "pm install: $INSTALL_OUT" if echo "$INSTALL_OUT" | grep -q "FAILED"; then echo "Retrying without -r (signature mismatch or first install)..." $ADB shell "su 0 sh -c 'pm uninstall com.example.wechathook 2>&1 || true'" $ADB shell "su 0 sh -c 'pm install /data/local/tmp/wechathook.apk 2>&1'" fi # Get new PKMS path NEW_PATH=$($ADB shell "pm path com.example.wechathook 2>/dev/null | tr -d '\r' | sed 's/package://'" 2>/dev/null) echo "New PKMS path: $NEW_PATH" # Update lspd DB (device must be unlocked — Termux CE storage must be accessible) SQLITE3="LD_LIBRARY_PATH=/data/data/com.termux/files/usr/lib /data/data/com.termux/files/usr/bin/sqlite3" DB="/data/adb/lspd/config/modules_config.db" $ADB shell "su -c \"printf \\\"INSERT OR REPLACE INTO modules(module_pkg_name,apk_path,enabled) VALUES('com.example.wechathook','$NEW_PATH',1);\\n\ INSERT OR IGNORE INTO scope(mid,app_pkg_name,user_id) SELECT mid,'com.tencent.mm',0 FROM modules WHERE module_pkg_name='com.example.wechathook';\\n\ SELECT mid,module_pkg_name,apk_path FROM modules;\\n\\\" > /data/local/tmp/update.sql\"" $ADB shell "su -c '$SQLITE3 $DB < /data/local/tmp/update.sql 2>&1'" # Ensure scope file exists $ADB shell "su -c 'mkdir -p /data/adb/lspd/config/modules/com.example.wechathook && echo com.tencent.mm > /data/adb/lspd/config/modules/com.example.wechathook/scope && chmod 644 /data/adb/lspd/config/modules/com.example.wechathook/scope'" # WAL checkpoint + sync to all .bak files $ADB shell "su -c '$SQLITE3 $DB \"PRAGMA wal_checkpoint(TRUNCATE);\" && \ cp $DB ${DB}.bak && cp $DB ${DB}.bak2 && cp $DB ${DB}.bak3 && cp $DB ${DB}.bak4 && echo baks_done'" # Restart WeChat to pick up new module (no reboot needed) $ADB shell "su 0 sh -c 'am force-stop com.tencent.mm'" sleep 2 $ADB shell "am start -n com.tencent.mm/.ui.LauncherUI 2>/dev/null || true" echo "" echo "== Done. Test with: ==" echo " $ADB shell \"su 0 sh -c 'am broadcast -a com.hermes.wechat.SEND_MSG_INTERNAL --es to filehelper --es text test'\""