rebuild_wechathook.sh 8.5 KB
#!/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'\""