diff --git a/xhs_lsposed_a16_fix/CLAUDE.md b/xhs_lsposed_a16_fix/CLAUDE.md new file mode 100644 index 0000000..44728ee --- /dev/null +++ b/xhs_lsposed_a16_fix/CLAUDE.md @@ -0,0 +1,49 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What This Is + +A self-contained patch kit that fixes LSPosed (JingMatrix fork v1.11.0 / Vector v2.0) crashing zygote64 on Android 16. Root cause: `memcpy(&native_interface_hook_, env->functions, sizeof(JNINativeInterface))` uses a compile-time NDK 26 size; Android 16 ART extended `JNINativeInterface` with new function pointers, so the copy is truncated and ART calls garbage pointers → crash. + +The fix reads the runtime table size from `reserved0` (ART stores the actual byte count there) and heap-allocates the copy. + +## Files + +- `ipc_bridge.h` / `ipc_bridge.cpp` — the **already-patched** source files. These are drop-in replacements for `zygisk/src/main/cpp/{include/,}` inside a cloned LSPosed repo. +- `apply_a16_fix.sh` — applies the patch via Python `str.replace` on a cloned repo (does exact string match; creates `.bak` backups). If LSPosed upstream changes those exact strings, the script will print a warning and skip. + +## Apply + Build + Deploy + +```bash +# 1. Clone LSPosed +git clone --depth 1 --recursive https://github.com/JingMatrix/LSPosed.git +cd LSPosed + +# 2. Apply patch (from this directory) +bash /path/to/apply_a16_fix.sh . + +# 3. Build +export ANDROID_HOME=/path/to/android-sdk +./gradlew :zygisk:assembleRelease +# Output: zygisk/build/outputs/apk/release/zygisk-release.apk + +# 4. Deploy +unzip -o zygisk-release.apk -d zygisk_out +adb push zygisk_out/lib/arm64-v8a/libzygisk.so /data/local/tmp/ +adb shell su -c 'cp /data/local/tmp/libzygisk.so /data/adb/modules/zygisk_vector/zygisk/arm64-v8a.so' +adb shell su -c 'magisk --sqlite "INSERT OR REPLACE INTO settings (key,value) VALUES (\"zygisk\",1);"' +adb reboot +``` + +## Verify After Reboot + +```bash +adb shell su -c 'pidof lspd' # daemon alive +adb shell su -c 'cat /proc/$(pidof zygote64 | awk "{print \$1}")/maps | grep zygisk' # injected +adb shell su -c 'cat /proc/$(pidof com.xingin.xhs)/maps | grep xhshook' # XHS module loaded +``` + +## Context + +This patch was developed during a reverse engineering session targeting XHS (`com.xingin.xhs`) on Android 16. The goal was MITM capture of `xy-common-params` (ECFAAF signatures) from `edith.xiaohongshu.com`, which uses Cronet with static BoringSSL — Frida Interceptor cannot hook static-linked symbols. LSPosed is needed to install a CertificatePinner bypass module (`xhshook`). diff --git a/xhs_lsposed_a16_fix/README.md b/xhs_lsposed_a16_fix/README.md new file mode 100644 index 0000000..1536a39 --- /dev/null +++ b/xhs_lsposed_a16_fix/README.md @@ -0,0 +1,121 @@ +# LSPosed A16 Fix — JNINativeInterface Runtime Size Mismatch + +## Summary + +LSPosed (JingMatrix fork v1.11.0 / Vector v2.0) crashes zygote64 on Android 16 with: + +``` +JNI DETECTED ERROR IN APPLICATION: negative buffer capacity in NewDirectByteBuffer +from int com.android.internal.os.Zygote.nativeForkSystemServer +``` + +Magisk detects repeated zygote crashes and auto-rolls-back Zygisk. Modules never inject. + +## Root Cause + +`zygisk/src/main/cpp/ipc_bridge.cpp` line 557: + +```cpp +memcpy(&native_interface_hook_, env->functions, sizeof(JNINativeInterface)); +``` + +**`sizeof(JNINativeInterface)` is a compile-time constant from the NDK used to build LSPosed (NDK 26 = ~2376 bytes).** + +Android 16 ART added new JNI functions to the `JNINativeInterface` structure (e.g. `NewDirectByteBuffer` overloads). The runtime structure is larger than the compile-time size. + +The `memcpy` truncates the table — function pointers for new JNI functions beyond the compile-time boundary become uninitialized garbage values. ART later calls them → crash. + +**Key detail:** `reserved0` in `JNINativeInterface` stores the actual runtime size (set by ART). The truncated copy preserves `reserved0` (it's at offset 0), so ART's `SetTableOverride` sees the correct size and treats garbage as valid function pointers. + +## Fix + +Replace compile-time `sizeof(JNINativeInterface)` with runtime `reserved0`, and dynamically allocate the copy. + +### File 1: `zygisk/src/main/cpp/include/ipc_bridge.h` + +```diff +- JNINativeInterface native_interface_hook_{}; ++ JNINativeInterface* native_interface_hook_ = nullptr; ++ size_t table_size_ = 0; +``` + +### File 2: `zygisk/src/main/cpp/ipc_bridge.cpp` + +```diff +- memcpy(&native_interface_hook_, env->functions, sizeof(JNINativeInterface)); ++ table_size_ = (size_t)env->functions->reserved0; ++ if (table_size_ == 0 || table_size_ < sizeof(JNINativeInterface)) ++ table_size_ = sizeof(JNINativeInterface); ++ native_interface_hook_ = (JNINativeInterface*)malloc(table_size_); ++ memcpy(native_interface_hook_, env->functions, table_size_); + +- call_boolean_method_v_backup_ = env->functions->CallBooleanMethodV; ++ call_boolean_method_v_backup_ = native_interface_hook_->CallBooleanMethodV; + +- native_interface_hook_.CallBooleanMethodV = ... ++ native_interface_hook_->CallBooleanMethodV = ... + +- set_table_override_func(&native_interface_hook_); ++ set_table_override_func(native_interface_hook_); +``` + +## How to Build + +```bash +# 1. Clone +git clone --depth 1 --recursive https://github.com/JingMatrix/LSPosed.git +cd LSPosed + +# 2. Apply fix +bash apply_a16_fix.sh . + +# 3. Build (requires Android SDK + NDK) +export ANDROID_HOME=/path/to/android-sdk +./gradlew :zygisk:assembleRelease + +# Output: zygisk/build/outputs/apk/release/ +``` + +## How to Deploy on Device + +```bash +# Extract the fixed zygisk .so from the APK +unzip -o zygisk-release.apk -d zygisk_out + +# Push to device and replace +adb push zygisk_out/lib/arm64-v8a/libzygisk.so /data/local/tmp/ +adb shell su -c 'cp /data/local/tmp/libzygisk.so /data/adb/modules/zygisk_vector/zygisk/arm64-v8a.so' + +# Ensure Zygisk is enabled +adb shell su -c 'magisk --sqlite "INSERT OR REPLACE INTO settings (key,value) VALUES (\"zygisk\",1);"' + +# Reboot +adb reboot +``` + +## Verification (after reboot) + +```bash +# 1. Daemon running? +adb shell su -c 'pidof lspd' + +# 2. Zygisk injected into zygote? +adb shell su -c 'cat /proc/$(pidof zygote64 | awk "{print \$1}")/maps | grep zygisk' | head -3 + +# 3. Module injected into target app? +adb shell su -c 'cat /proc/$(pidof com.xingin.xhs)/maps | grep xhshook' +``` + +## Background + +Full reverse engineering session: Hermes Agent on Pixel 10 Pro Fold, Android 16, XHS 9.35.1. + +**Campaign goal:** MITM capture `xy-common-params` (ECFAAF/ECFAAF01 signatures) from edith.xiaohongshu.com. + +**Confirmed:** edith uses Google Mainline Cronet (`libmainlinecronet.141.0.7340.3.so`, 6MB) with static BoringSSL. Frida Interceptor cannot hook static-linked symbols. Only viable bypass is LSPosed CertificatePinner hook → MITM proxy capture. + +**Other achievements from the session:** +- Java TrustManager bypass works for non-edith domains (crash/as/t2/mediacloud) +- sentry abort Interceptor works (Frida survives 35+ seconds) +- `--pause` mode confirms edith goes through TrustManager (timing race is the issue) +- xhshook module pre-built at `/data/adb/modules/xhshook/xhshook.apk` diff --git a/xhs_lsposed_a16_fix/STATUS.md b/xhs_lsposed_a16_fix/STATUS.md new file mode 100644 index 0000000..7788d02 --- /dev/null +++ b/xhs_lsposed_a16_fix/STATUS.md @@ -0,0 +1,213 @@ +# XHS MITM 进度备忘 — 2026-06-29(Hermes 接手版) + +写给设备上 Hermes Agent 的完整上下文。**直接可用**,无需 Mac 介入的操作都写了命令。 + +--- + +## 战役目标:✅ 已完成 + +抓 `edith.xiaohongshu.com` 的 `xy-common-params`(ECFAAF/ECFAAF01 签名参数),用于复现 XHS 请求鉴权。 + +**核心障碍**:edith 用 Google Mainline Cronet + 静态链接 BoringSSL,Frida Interceptor 无法 hook 静态符号。 +**解法**:LSPosed(JingMatrix/Vector fork)在 XHS 进程 fork 时注入,hook OkHttp Java 层,在签名传入 Cronet 前拦截。 + +--- + +## 当前设备状态(2026-06-29 17:22 验证通过) + +``` +设备:Pixel 10 Pro Fold +Android:16 (API 36) +Magisk:30.7 +LSPosed (Vector fork):正常运行,lspd 进程存活 +xhshook:注入 com.xingin.xhs 成功 +xy-common-params:已捕获(见下方样本) +``` + +### 模块布局 + +``` +/data/adb/modules/ +├── zygisk_vector/ # LSPosed Vector fork(含 A16 JNI size fix) +│ ├── zygisk/arm64-v8a.so # 核心注入 so(1142464 字节) +│ ├── daemon.apk # Java 守护进程(3199711 字节) +│ ├── framework/lspd.dex # 注入 DEX(182840 字节) +│ ├── daemon # 启动脚本(已修复入口类名) +│ └── service.sh # Magisk 钩子(已去掉不兼容 flag) +├── xhshook/ # Xposed 模块 +│ ├── module.prop +│ └── xhshook.apk # 已更新为 v2(无捆绑 XposedBridge) +└── xhs_ca/ # MITM 代理 CA 证书 + └── system/etc/security/cacerts/36522bbf.0 +``` + +### xhshook 配置 + +``` +已安装:com.hermes.xhshook(streamed install,非 incremental) +APK 含:assets/xposed_init → com.hermes.xhshook.Hook +modules_config.db:mid=3, enabled=1, scope=com.xingin.xhs +``` + +--- + +## 捕获到的 xy-common-params 样本 + +打开 XHS 后,用 logcat 实时抓: + +```bash +# 在 Termux 里(需要 root) +su -c "logcat -b all | grep -F 'xy-common-params:'" +``` + +示例值(session 级字段每次启动刷新): + +``` +fid= +gid=7c9eca1fb5ac54aae84944d1bb4d85a3591f09d9473599ae77728621 +device_model=phone +tz=Asia/Shanghai +channel=YingYongBao +versionName=9.35.1 +deviceId=748327bc-6079-3b06-9351-1638281dbc6f ← 设备固定 +platform=android +sid=session.1782715007780944836188 ← 每次启动变 +identifier_flag=4 +cpu_abi=arm64-v8a +nqe_score=89 +project_id=ECFAAF +app_id=ECFAAF01 +id_token=VjEAAN3Pjix... ← 每次启动变 +t=1782724968 ← Unix 时间戳,每次变 +build=9351803 +did=058844e5598e4d001b42a122d1a9d1c2 ← 设备固定 +``` + +**固定字段**(可硬编码复现请求):`deviceId`、`did`、`gid`、`platform`、`versionName`、`build` +**Session 字段**(需要 hook 实时拿):`id_token`、`sid`、`t` + +--- + +## xhshook 架构(Hermes 改 hook 用) + +### Hook 逻辑(当前 v2) + +```java +// 源码位置(设备):/sdcard/Download/xhs_lsposed_a16_fix/xhshook_src/Hook.java +// 源码位置(Mac):~/Documents/LemonAI/pixelphone/xhs_lsposed_a16_fix/xhshook_src/Hook.java + +hook 点:okhttp3.Request$Builder.build() +过滤:只打印 edith.xiaohongshu.com 的请求 +输出:logcat tag=VectorLegacyBridge +``` + +### 改 hook 的完整流程(需 Mac 操作) + +```bash +# 1. 编辑 Hook.java +# ~/Documents/LemonAI/pixelphone/xhs_lsposed_a16_fix/xhshook_src/Hook.java + +# 2. 重新编译 + 打包(Mac 上运行) +bash ~/Documents/LemonAI/pixelphone/xhs_lsposed_a16_fix/rebuild_xhshook.sh + +# 3. 推到设备 +ADB=~/Documents/LemonAI/pixelphone/platform-tools/adb +$ADB shell 'pm uninstall com.hermes.xhshook' && \ +$ADB install --no-incremental ~/Documents/LemonAI/pixelphone/xhs_lsposed_a16_fix/xhshook_v2_signed.apk + +# 4. 更新 modules_config.db(脚本自动做) +bash ~/Documents/LemonAI/pixelphone/xhs_lsposed_a16_fix/update_xhshook_db.sh + +# 5. 杀掉 XHS 进程重启(不需要重启整机!) +$ADB shell 'am force-stop com.xingin.xhs' +# 然后手动在屏幕上点 XHS 图标 + +# 6. 验证 +$ADB shell 'logcat -d | grep "\[XHSHook\]"' +``` + +### 可以加的下一个 hook:CertificatePinner bypass + +xhs_ca 模块已经把 MITM 代理 CA 证书装进系统了。补上 CertificatePinner bypass 后可以直接用 mitmproxy 抓 OkHttp 流量(edith 仍然走 Cronet,但其他接口会走 OkHttp): + +```java +// 在 handleLoadPackage 里加(XHS 包名判断之后): +try { + XposedHelpers.findAndHookMethod( + "okhttp3.CertificatePinner", lpparam.classLoader, + "check", String.class, List.class, + new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + param.setResult(null); + } + } + ); + XposedBridge.log("[XHSHook] CertificatePinner bypassed!"); +} catch (Throwable t) { + XposedBridge.log("[XHSHook] CertificatePinner bypass failed: " + t); +} +``` + +--- + +## 下一步可以做的事(Hermes 接手) + +### A. 用捕获到的参数复现 XHS API 请求 + +`xy-common-params` 里的固定字段可以直接复用,session 字段需要先打开 XHS 触发 hook 实时拿。 + +```bash +# Termux 里实时抓一次完整的 xy-common-params +su -c "logcat -c && am force-stop com.xingin.xhs" +# → 手动打开 XHS → +su -c "logcat -d | grep 'xy-common-params:' | head -1" +``` + +拿到后,用 curl 复现请求: + +```bash +XY_PARAMS="fid=&gid=7c9eca...(完整值)" +curl -H "xy-common-params: $XY_PARAMS" \ + "https://edith.xiaohongshu.com/api/sns/v1/homefeed?..." +``` + +### B. Hook 更多字段(如果 curl 复现失败) + +edith 除 `xy-common-params` 外可能还检查其他 header(如 `xy-sign`、cookie)。修改 xhshook 打印所有 edith 请求的全部 header,观察缺什么。 + +Hook.java 里已经打印了 `Headers.toString()` 的完整内容——重新抓一次 logcat 即可看到所有 header(logcat 多行输出,每个 header 一行)。 + +```bash +su -c "logcat -d -b all | grep 'VectorLegacyBridge' | grep -v '\[XHSHook\]'" | head -30 +``` + +### C. 修改 xhshook 做实时转发(高级) + +改 hook 在 `beforeHookedMethod` 里把参数通过 HTTP POST 转到 Hermes 的本地 server(如 `localhost:8765`),让 Hermes 实时处理每条请求。 + +--- + +## 文件索引(设备上) + +``` +/sdcard/Download/xhs_lsposed_a16_fix/ +├── STATUS.md ← 本文件 +├── CLAUDE.md ← 项目说明(构建/部署命令) +├── apply_a16_fix.sh ← A16 补丁脚本(已应用,留作参考) +├── ipc_bridge.h ← 已打补丁的头文件 +├── ipc_bridge.cpp ← 已打补丁的源文件 +├── xhshook_src/ +│ └── Hook.java ← xhshook v2 源码 +└── xhshook_v2_signed.apk ← 预构建的 APK(当前已安装版本) +``` + +--- + +## 已知不行的路线(别重新踩) + +- **Frida hook edith.xiaohongshu.com**:Cronet 静态链接 BoringSSL,Frida Interceptor 找不到符号。SSL_read/SSL_write 都 hook 不上。 +- **Java TrustManager 替换**:时序问题,edith 在 app 启动很早就开始通信,Frida 注入晚了窗口就过了。LSPosed 没有这个问题。 +- **常驻 Frida daemon 注入 XHS**:XHS 检测到反调试后崩溃(wechat_plain 里有记录,XHS 同理)。 +- **把 Xposed API 类打进 xhshook DEX**:LSPosed 会检查 `XposedBridge.class.getClassLoader() == initLoader`,打进去的话直接 `Cannot load module`。必须用 `compileOnly`。 +- **incremental install(`adb install` 默认)**:Incremental Install 的 marker 会让 `java.util.zip.ZipFile` 解析失败,LSPosed 找不到 `assets/xposed_init`。必须用 `adb install --no-incremental`。 diff --git a/xhs_lsposed_a16_fix/apply_a16_fix.sh b/xhs_lsposed_a16_fix/apply_a16_fix.sh new file mode 100755 index 0000000..a71ad3e --- /dev/null +++ b/xhs_lsposed_a16_fix/apply_a16_fix.sh @@ -0,0 +1,117 @@ +#!/bin/bash +# LSPosed A16 Fix — apply the JNINativeInterface runtime-size patch +# Usage: bash apply_a16_fix.sh +# Example: bash apply_a16_fix.sh ~/LSPosed + +set -e + +REPO="${1:-.}" +HEADER="$REPO/zygisk/src/main/cpp/include/ipc_bridge.h" +CPP="$REPO/zygisk/src/main/cpp/ipc_bridge.cpp" + +if [ ! -f "$HEADER" ] || [ ! -f "$CPP" ]; then + echo "ERROR: LSPosed source not found at $REPO" + echo "Clone first: git clone --depth 1 --recursive https://github.com/JingMatrix/LSPosed.git" + exit 1 +fi + +echo "=== Applying A16 JNINativeInterface runtime-size fix ===" + +# === Patch 1: ipc_bridge.h — change stack-allocated table to pointer === +echo "[1/2] Patching $HEADER ..." + +# Create backup +cp "$HEADER" "$HEADER.bak" + +python3 -c " +import re + +with open('$HEADER', 'r') as f: + content = f.read() + +old = ' // A complete copy of the original JNI function table, with our hook installed.\n JNINativeInterface native_interface_hook_{};' + +new = ''' // A complete copy of the original JNI function table, with our hook installed. + // Uses runtime-size allocation to support A16+ where ART added new JNI functions. + JNINativeInterface* native_interface_hook_ = nullptr; + size_t table_size_ = 0;''' + +if old in content: + content = content.replace(old, new) + with open('$HEADER', 'w') as f: + f.write(content) + print(' ✓ ipc_bridge.h patched') +else: + print(' ⚠ ipc_bridge.h — pattern not found, may already be patched') +" + +# === Patch 2: ipc_bridge.cpp — use reserved0 for runtime size === +echo "[2/2] Patching $CPP ..." + +cp "$CPP" "$CPP.bak" + +python3 -c " +with open('$CPP', 'r') as f: + content = f.read() + +old = ''' // --- Step 3: Install the hook --- + // Make a full copy of the existing JNI function table. + memcpy(&native_interface_hook_, env->functions, sizeof(JNINativeInterface)); + + // Store a pointer to the original function we are about to replace. + call_boolean_method_v_backup_ = env->functions->CallBooleanMethodV; + + // Overwrite the function pointer in our copy with our hook. + native_interface_hook_.CallBooleanMethodV = &CallBooleanMethodV_Hook; + + // Atomically swap the process's JNI function table with our modified one. + set_table_override_func(&native_interface_hook_);''' + +new = ''' // --- Step 3: Install the hook --- + // Get the ACTUAL runtime size of the JNI function table. + // Android 16 ART adds new JNI functions that increase the table size. + // Using compile-time sizeof(JNINativeInterface) truncates the copy, + // causing crashes when ART calls new functions via garbage pointers. + table_size_ = (size_t)env->functions->reserved0; + if (table_size_ == 0 || table_size_ < sizeof(JNINativeInterface)) { + table_size_ = sizeof(JNINativeInterface); + } + LOGI(\"JNI table runtime size: %zu (compile-time: %zu)\", table_size_, sizeof(JNINativeInterface)); + + // Allocate and copy with runtime size + native_interface_hook_ = (JNINativeInterface*)malloc(table_size_); + if (!native_interface_hook_) { + LOGE(\"Failed to allocate JNI hook table.\"); + return; + } + memcpy(native_interface_hook_, env->functions, table_size_); + + // Store a pointer to the original function we are about to replace. + call_boolean_method_v_backup_ = native_interface_hook_->CallBooleanMethodV; + + // Overwrite the function pointer in our copy with our hook. + native_interface_hook_->CallBooleanMethodV = &CallBooleanMethodV_Hook; + + // Atomically swap the process's JNI function table with our modified one. + set_table_override_func(native_interface_hook_);''' + +if old in content: + content = content.replace(old, new) + with open('$CPP', 'w') as f: + f.write(content) + print(' ✓ ipc_bridge.cpp patched') +else: + print(' ⚠ ipc_bridge.cpp — pattern not found, may already be patched') +" + +echo "" +echo "=== Done. Building ===" +echo "" +echo "To build:" +echo " export ANDROID_HOME=/path/to/android-sdk" +echo " cd $REPO" +echo " ./gradlew :zygisk:assembleRelease" +echo "" +echo "Output: zygisk/build/outputs/apk/release/zygisk-release.apk" +echo "The fixed zygisk .so inside can replace:" +echo " /data/adb/modules/zygisk_vector/zygisk/arm64-v8a.so" diff --git a/xhs_lsposed_a16_fix/ipc_bridge.cpp b/xhs_lsposed_a16_fix/ipc_bridge.cpp new file mode 100644 index 0000000..03f71e1 --- /dev/null +++ b/xhs_lsposed_a16_fix/ipc_bridge.cpp @@ -0,0 +1,588 @@ +#include "ipc_bridge.h" + +#include +#include +#include + +#include +#include +#include + +using namespace std::literals::string_view_literals; + +#include + +namespace vector::native::module { + +// Store the ID of the last caller whose framework transaction failed. +// It's initialized to a value that won't match. +static std::atomic g_last_failed_id = ~0; + +/** + * @class BinderCaller + * @brief A helper to get the UID and PID of the current Binder caller. + * + * This class encapsulates the logic for finding and calling private functions + * within libbinder.so to identify the origin of an IPC transaction. + */ +class BinderCaller { +public: + /** + * @brief Initializes the helper by finding the required function pointers. + * This must be called once before GetId() is used. + */ + static void Initialize() { + // Use the native library's symbol cache to find symbols in the loaded libbinder.so + auto libbinder = ElfSymbolCache::GetLibBinder(); + if (!libbinder) { + LOGW("libbinder.so not found in cache, cannot get caller ID."); + return; + } + + s_self_or_null_fn = (IPCThreadState * (*)()) libbinder->getSymbAddress( + "_ZN7android14IPCThreadState10selfOrNullEv"); + s_get_calling_pid_fn = (pid_t(*)(IPCThreadState *))libbinder->getSymbAddress( + "_ZNK7android14IPCThreadState13getCallingPidEv"); + s_get_calling_uid_fn = (uid_t(*)(IPCThreadState *))libbinder->getSymbAddress( + "_ZNK7android14IPCThreadState13getCallingUidEv"); + + if (!s_self_or_null_fn || !s_get_calling_pid_fn || !s_get_calling_uid_fn) { + LOGW("Could not resolve all IPCThreadState symbols. Caller ID check will be disabled."); + } else { + LOGV("IPCThreadState symbols resolved successfully."); + } + } + + /** + * @brief Gets the unique 64-bit ID of the current Binder caller. + * @return A combined UID and PID, or 0 if symbols are not available. + */ + static uint64_t GetId() { + if (!s_self_or_null_fn) [[unlikely]] + return 0; + + IPCThreadState *self = s_self_or_null_fn(); + if (!self) return 0; + + auto uid = s_get_calling_uid_fn(self); + auto pid = s_get_calling_pid_fn(self); + return (static_cast(uid) << 32) | pid; + } + +private: + // Forward declare the opaque struct from libbinder. + struct IPCThreadState; + + inline static IPCThreadState *(*s_self_or_null_fn)() = nullptr; + inline static pid_t (*s_get_calling_pid_fn)(IPCThreadState *) = nullptr; + inline static uid_t (*s_get_calling_uid_fn)(IPCThreadState *) = nullptr; +}; + +// --- Binder IPC Protocol Constants --- +// These are the "secret handshakes" used to communicate with the Vector manager service. + +// The name of the system service we use as a rendezvous point to find our manager service. +// Using "activity" is a common technique as it's always available. +constexpr auto kBridgeServiceName = "activity"sv; + +// Transaction codes for specific actions. +constexpr jint kBridgeTransactionCode = ('_' << 24) | ('V' << 16) | ('E' << 8) | 'C'; +constexpr jint kDexTransactionCode = ('_' << 24) | ('D' << 16) | ('E' << 8) | 'X'; +constexpr jint kObfuscationMapTransactionCode = ('_' << 24) | ('O' << 16) | ('B' << 8) | 'F'; + +// Action codes sent within a kBridgeTransactionCode transaction. +constexpr jint kActionGetBinder = 2; + +// ========================================================================================= +// Implementation of IPCBridge::ParcelWrapper (Private Nested Class) +// ========================================================================================= + +/** + * @brief Constructs the ParcelWrapper, obtaining two new Parcel objects from the pool. + * @param env A valid JNI environment pointer. + * @param bridge A pointer to the parent IPCBridge instance to access its cached JNI IDs. + */ +IPCBridge::ParcelWrapper::ParcelWrapper(JNIEnv *env, IPCBridge *bridge) + : data(lsplant::JNI_CallStaticObjectMethod(env, bridge->parcel_class_, bridge->obtain_method_)), + reply( + lsplant::JNI_CallStaticObjectMethod(env, bridge->parcel_class_, bridge->obtain_method_)), + env_(env), + bridge_(bridge) {} + +/** + * @brief Destructs the ParcelWrapper, ensuring both Parcel objects are recycled. + * + * This is the core of the RAII pattern for Parcels. + * This destructor guarantees that recycle() is called, preventing resource leaks even if + * errors occur during the transaction. + */ +IPCBridge::ParcelWrapper::~ParcelWrapper() { + // Check if the parcel was successfully obtained before trying to recycle it. + if (data) { + lsplant::JNI_CallVoidMethod(env_, data.get(), bridge_->recycle_method_); + } + if (reply) { + lsplant::JNI_CallVoidMethod(env_, reply.get(), bridge_->recycle_method_); + } +} + +// ========================================================================================= +// IPCBridge Implementation +// ========================================================================================= + +IPCBridge &IPCBridge::GetInstance() { + static IPCBridge instance; + return instance; +} + +void IPCBridge::Initialize(JNIEnv *env) { + if (initialized_) { + return; + } + + // --- Cache JNI Classes and Method IDs --- + // Caching these at startup is more efficient and robust than looking them up on every IPC call. + // If any of these fail, the IPC bridge is unusable. + + // ServiceManager + auto sm_class = lsplant::ScopedLocalRef(env, env->FindClass("android/os/ServiceManager")); + if (!sm_class) { + LOGE("IPCBridge: ServiceManager class not found!"); + return; + } + service_manager_class_ = (jclass)env->NewGlobalRef(sm_class.get()); + get_service_method_ = lsplant::JNI_GetStaticMethodID( + env, service_manager_class_, "getService", "(Ljava/lang/String;)Landroid/os/IBinder;"); + if (!get_service_method_) { + LOGE("IPCBridge: ServiceManager.getService method not found!"); + return; + } + + // IBinder + auto ibinder_class = lsplant::ScopedLocalRef(env, env->FindClass("android/os/IBinder")); + if (!ibinder_class) { + LOGE("IPCBridge: IBinder class not found!"); + return; + } + transact_method_ = lsplant::JNI_GetMethodID(env, ibinder_class.get(), "transact", + "(ILandroid/os/Parcel;Landroid/os/Parcel;I)Z"); + if (!transact_method_) { + LOGE("IPCBridge: IBinder.transact method not found!"); + return; + } + + // Binder + auto binder_class = lsplant::ScopedLocalRef(env, env->FindClass("android/os/Binder")); + if (!binder_class) { + LOGE("IPCBridge: Binder class not found!"); + return; + } + binder_class_ = (jclass)env->NewGlobalRef(binder_class.get()); + binder_ctor_ = lsplant::JNI_GetMethodID(env, binder_class_, "", "()V"); + if (!binder_ctor_) { + LOGE("IPCBridge: Binder constructor not found!"); + return; + } + + // Parcel + auto parcel_class = lsplant::ScopedLocalRef(env, env->FindClass("android/os/Parcel")); + if (!parcel_class) { + LOGE("IPCBridge: Parcel class not found!"); + return; + } + parcel_class_ = (jclass)env->NewGlobalRef(parcel_class.get()); + obtain_method_ = + lsplant::JNI_GetStaticMethodID(env, parcel_class_, "obtain", "()Landroid/os/Parcel;"); + recycle_method_ = lsplant::JNI_GetMethodID(env, parcel_class_, "recycle", "()V"); + write_interface_token_method_ = lsplant::JNI_GetMethodID( + env, parcel_class_, "writeInterfaceToken", "(Ljava/lang/String;)V"); + write_int_method_ = lsplant::JNI_GetMethodID(env, parcel_class_, "writeInt", "(I)V"); + write_string_method_ = + lsplant::JNI_GetMethodID(env, parcel_class_, "writeString", "(Ljava/lang/String;)V"); + write_strong_binder_method_ = lsplant::JNI_GetMethodID(env, parcel_class_, "writeStrongBinder", + "(Landroid/os/IBinder;)V"); + read_exception_method_ = lsplant::JNI_GetMethodID(env, parcel_class_, "readException", "()V"); + read_strong_binder_method_ = + lsplant::JNI_GetMethodID(env, parcel_class_, "readStrongBinder", "()Landroid/os/IBinder;"); + read_file_descriptor_method_ = lsplant::JNI_GetMethodID( + env, parcel_class_, "readFileDescriptor", "()Landroid/os/ParcelFileDescriptor;"); + read_int_method_ = lsplant::JNI_GetMethodID(env, parcel_class_, "readInt", "()I"); + read_long_method_ = lsplant::JNI_GetMethodID(env, parcel_class_, "readLong", "()J"); + read_string_method_ = + lsplant::JNI_GetMethodID(env, parcel_class_, "readString", "()Ljava/lang/String;"); + + // ParcelFileDescriptor + auto pfd_class = + lsplant::ScopedLocalRef(env, env->FindClass("android/os/ParcelFileDescriptor")); + if (!pfd_class) { + LOGE("IPCBridge: ParcelFileDescriptor class not found!"); + return; + } + parcel_fd_class_ = (jclass)env->NewGlobalRef(pfd_class.get()); + detach_fd_method_ = lsplant::JNI_GetMethodID(env, parcel_fd_class_, "detachFd", "()I"); + if (!detach_fd_method_) { + LOGE("IPCBridge: ParcelFileDescriptor.detachFd method not found!"); + return; + } + + LOGV("IPCBridge initialized successfully."); + initialized_ = true; +} + +lsplant::ScopedLocalRef IPCBridge::RequestAppBinder(JNIEnv *env, jstring nice_name) { + if (!initialized_) { + LOGE("RequestAppBinder failed: IPCBridge not initialized."); + return {env, nullptr}; + } + + // Get the rendezvous service from the Android ServiceManager. + auto service_name = lsplant::ScopedLocalRef(env, env->NewStringUTF(kBridgeServiceName.data())); + auto bridge_service = lsplant::JNI_CallStaticObjectMethod( + env, service_manager_class_, get_service_method_, service_name.get()); + if (!bridge_service) { + LOGE("Could not get rendezvous service '{}'. Manager not available?", + kBridgeServiceName.data()); + return {env, nullptr}; + } + + // Prepare the IPC transaction. + ParcelWrapper parcels(env, this); + if (!parcels.data || !parcels.reply) { + LOGE("Failed to obtain parcels for IPC."); + return {env, nullptr}; + } + + // This is a "heartbeat" binder. + // If our process dies, the manager service will be notified that this binder has died, + // allowing it to clean up resources. + auto heartbeat_binder = + lsplant::ScopedLocalRef(env, env->NewObject(binder_class_, binder_ctor_)); + if (!heartbeat_binder) { + LOGE("Failed to create heartbeat binder."); + return {env, nullptr}; + } + + // Write the request data to the 'data' parcel. + lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_int_method_, kActionGetBinder); + lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_string_method_, nice_name); + lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_strong_binder_method_, + heartbeat_binder.get()); + + // Perform the transaction. + bool success = lsplant::JNI_CallBooleanMethod(env, bridge_service.get(), transact_method_, + kBridgeTransactionCode, parcels.data.get(), + parcels.reply.get(), 0); + + lsplant::ScopedLocalRef result_binder = {env, nullptr}; + if (success) { + // Read the reply. CRITICAL: must call readException first. + lsplant::JNI_CallVoidMethod(env, parcels.reply.get(), read_exception_method_); + if (env->ExceptionCheck()) { + LOGW("Remote exception received while requesting app binder."); + env->ExceptionClear(); + } else { + result_binder = + lsplant::JNI_CallObjectMethod(env, parcels.reply.get(), read_strong_binder_method_); + if (result_binder) { + // IMPORTANT: Keep the heartbeat binder alive by making it a global ref. + // If we don't do this, it gets garbage collected and the remote service + // thinks our process has died. + env->NewGlobalRef(heartbeat_binder.get()); + } + } + } else { + LOGD("Transact call to request app binder failed."); + } + + return result_binder; +} + +lsplant::ScopedLocalRef IPCBridge::RequestSystemServerBinder( + JNIEnv *env, std::string bridgeServiceName) { + if (!initialized_) { + LOGE("RequestSystemServerBinder failed: IPCBridge not initialized."); + return {env, nullptr}; + } + + auto service_name = lsplant::ScopedLocalRef(env, env->NewStringUTF(bridgeServiceName.data())); + lsplant::ScopedLocalRef binder = {env, nullptr}; + + // The daemon process and system_server specialization run in parallel. + // On slower devices, the daemon may take several seconds to call addService. + // We poll for up to 10 seconds to ensure the early IPC channel for system_server is available. + const int max_retry = 10; + for (int i = 0; i < max_retry; ++i) { + binder = lsplant::JNI_CallStaticObjectMethod(env, service_manager_class_, + get_service_method_, service_name.get()); + if (binder) { + LOGI("Got system server binder via {} on attempt {}.", bridgeServiceName.data(), i + 1); + return binder; + } + LOGW("Failed to get system server binder via {}, will retry in 1 second...", + bridgeServiceName.data()); + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + + LOGE("Failed to get system server binder after {} attempts. Aborting.", max_retry); + return {env, nullptr}; +} + +lsplant::ScopedLocalRef IPCBridge::RequestManagerBinderFromSystemServer( + JNIEnv *env, jobject system_server_binder) { + ParcelWrapper parcels(env, this); + auto heartbeat_binder = + lsplant::ScopedLocalRef(env, env->NewObject(binder_class_, binder_ctor_)); + auto system_name = lsplant::ScopedLocalRef(env, env->NewStringUTF("system")); + + // Populate the request + lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_int_method_, getuid()); + lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_int_method_, getpid()); + lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_string_method_, system_name.get()); + lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_strong_binder_method_, + heartbeat_binder.get()); + + // Transact + bool success = lsplant::JNI_CallBooleanMethod(env, system_server_binder, transact_method_, + kBridgeTransactionCode, parcels.data.get(), + parcels.reply.get(), 0); + + lsplant::ScopedLocalRef result_binder = {env, nullptr}; + if (success) { + lsplant::JNI_CallVoidMethod(env, parcels.reply.get(), read_exception_method_); + if (env->ExceptionCheck()) { + LOGW("Remote exception while getting manager binder from system_server."); + env->ExceptionClear(); + } else { + result_binder = + lsplant::JNI_CallObjectMethod(env, parcels.reply.get(), read_strong_binder_method_); + if (result_binder) { + env->NewGlobalRef(heartbeat_binder.get()); // Keep heartbeat alive + } + } + } + LOGD("Manager binder from system_server: {}", static_cast(result_binder.get())); + return result_binder; +} + +std::tuple IPCBridge::FetchFrameworkDex(JNIEnv *env, jobject binder) { + if (!initialized_ || !binder) { + return {-1, 0}; + } + + ParcelWrapper parcels(env, this); + bool success = + lsplant::JNI_CallBooleanMethod(env, binder, transact_method_, kDexTransactionCode, + parcels.data.get(), parcels.reply.get(), 0); + + if (!success) { + LOGE("DEX fetch transaction failed."); + return {-1, 0}; + } + + lsplant::JNI_CallVoidMethod(env, parcels.reply.get(), read_exception_method_); + if (env->ExceptionCheck()) { + LOGE("Remote exception received while fetching DEX."); + env->ExceptionClear(); + return {-1, 0}; + } + + auto pfd = + lsplant::JNI_CallObjectMethod(env, parcels.reply.get(), read_file_descriptor_method_); + if (!pfd) { + LOGE("Received null ParcelFileDescriptor for DEX."); + return {-1, 0}; + } + + int fd = lsplant::JNI_CallIntMethod(env, pfd.get(), detach_fd_method_); + size_t size = static_cast( + lsplant::JNI_CallLongMethod(env, parcels.reply.get(), read_long_method_)); + + LOGV("Fetched framework DEX: fd={}, size={}", fd, size); + return {fd, size}; +} + +std::map IPCBridge::FetchObfuscationMap(JNIEnv *env, jobject binder) { + std::map result_map; + if (!initialized_ || !binder) { + return result_map; + } + + ParcelWrapper parcels(env, this); + bool success = lsplant::JNI_CallBooleanMethod(env, binder, transact_method_, + kObfuscationMapTransactionCode, + parcels.data.get(), parcels.reply.get(), 0); + + if (!success) { + LOGE("Obfuscation map fetch transaction failed."); + return result_map; + } + + lsplant::JNI_CallVoidMethod(env, parcels.reply.get(), read_exception_method_); + if (env->ExceptionCheck()) { + LOGE("Remote exception received while fetching obfuscation map."); + env->ExceptionClear(); + return result_map; + } + + int size = lsplant::JNI_CallIntMethod(env, parcels.reply.get(), read_int_method_); + if (size < 0 || (size % 2 != 0)) { + LOGE("Invalid size for obfuscation map received: %d", size); + return result_map; + } + + for (int i = 0; i < size / 2; ++i) { + auto key_jstr = lsplant::JNI_Cast( + lsplant::JNI_CallObjectMethod(env, parcels.reply.get(), read_string_method_)); + + auto val_jstr = lsplant::JNI_Cast( + lsplant::JNI_CallObjectMethod(env, parcels.reply.get(), read_string_method_)); + + if (env->ExceptionCheck() || !key_jstr || !val_jstr) { + LOGE("Error reading string from parcel for obfuscation map."); + env->ExceptionClear(); + result_map.clear(); // Return an empty map on error + return result_map; + } + + lsplant::JUTFString key_str(env, key_jstr.get()); + lsplant::JUTFString val_str(env, val_jstr.get()); + result_map[key_str.get()] = val_str.get(); + } + + LOGV("Fetched obfuscation map with {} entries.", result_map.size()); + return result_map; +} + +jboolean IPCBridge::ExecTransact_Replace(jboolean *res, JNIEnv *env, jobject obj, va_list args) { + va_list copy; + va_copy(copy, args); + // Extract arguments from the va_list for Binder.execTransact(int, long, long, int) + auto code = va_arg(copy, jint); + auto data_obj = va_arg(copy, jlong); + auto reply_obj = va_arg(copy, jlong); + auto flags = va_arg(copy, jint); + va_end(copy); + + // If the transaction code matches our special code, intercept it. + if (code == kBridgeTransactionCode) { + // Call the static Java method in our framework's BridgeService to handle the call. + *res = env->CallStaticBooleanMethod(GetInstance().bridge_service_class_, + GetInstance().exec_transact_replace_method_id_, obj, + code, data_obj, reply_obj, flags); + if (env->ExceptionCheck()) { + LOGW("Exception in Java BridgeService.execTransact handler."); + env->ExceptionClear(); + } + if (*res == JNI_FALSE) { + uint64_t caller_id = BinderCaller::GetId(); + if (caller_id != 0) { + // LOGV("Caller {} rejected by bridge service.", caller_id); + g_last_failed_id.store(caller_id, std::memory_order_relaxed); + } + } else { + g_last_failed_id.store(~0, std::memory_order_relaxed); + } + return true; // Return true to indicate we handled the call. + } + return false; // Not our transaction, let the original method run. +} + +jboolean JNICALL IPCBridge::CallBooleanMethodV_Hook(JNIEnv *env, jobject obj, jmethodID methodId, + va_list args) { + // Check if the method being called is the one we want to intercept: Binder.execTransact() + if (methodId == GetInstance().exec_transact_backup_method_id_) { + uint64_t current_caller_id = BinderCaller::GetId(); + + jboolean res = false; + // Attempt to handle the transaction with our replacement logic. + if (current_caller_id != 0 && + // If this caller is the one that just failed, + // skip interception and go straight to the original function. + current_caller_id != g_last_failed_id.load(std::memory_order_relaxed) && + ExecTransact_Replace(&res, env, obj, args)) { + return res; // If we handled it, return the result directly. + } + // If not handled, fall through to call the original method. + } + // Call the original, real CallBooleanMethodV function. + return GetInstance().call_boolean_method_v_backup_(env, obj, methodId, args); +} + +void IPCBridge::HookBridge(JNIEnv *env) { + if (!initialized_) { + LOGE("Cannot hook bridge: IPCBridge is not initialized."); + return; + } + + // Get framework-specific Java classes and methods --- + const auto &obfs_map = ConfigBridge::GetInstance()->obfuscation_map(); + std::string bridge_service_class_name; + bridge_service_class_name = obfs_map.at("org.matrix.vector.service.") + "BridgeService"; + + auto bridge_class_ref = + Context::GetInstance()->FindClassFromCurrentLoader(env, bridge_service_class_name); + if (!bridge_class_ref) { + LOGE("Failed to find BridgeService class '{}'", bridge_service_class_name.c_str()); + return; + } + bridge_service_class_ = lsplant::JNI_NewGlobalRef(env, bridge_class_ref); + + exec_transact_replace_method_id_ = lsplant::JNI_GetStaticMethodID( + env, bridge_service_class_, "execTransact", "(Landroid/os/IBinder;IJJI)Z"); + if (!exec_transact_replace_method_id_) { + LOGE("Failed to find static method BridgeService.execTransact!"); + return; + } + + // --- Prepare the JNI hook --- + // Get the original method ID for android.os.Binder.execTransact + exec_transact_backup_method_id_ = + lsplant::JNI_GetMethodID(env, binder_class_, "execTransact", "(IJJI)Z"); + if (!exec_transact_backup_method_id_) { + LOGE("Failed to find original method Binder.execTransact!"); + return; + } + + // Use the native library's API to get the JNI table override function. + auto set_table_override_func = + (void (*)(const JNINativeInterface *))ElfSymbolCache::GetArt()->getSymbAddress( + "_ZN3art9JNIEnvExt16SetTableOverrideEPK18JNINativeInterface"); + if (!set_table_override_func) { + LOGE("Failed to find ART symbol SetTableOverride!"); + return; + } + + // --- Step 3: Install the hook --- + // Get the ACTUAL runtime size of the JNI function table. + // Android 16 ART adds new JNI functions that increase the table size. + // Using compile-time sizeof(JNINativeInterface) truncates the copy, + // causing crashes when ART calls new functions via garbage pointers. + table_size_ = (size_t)env->functions->reserved0; + if (table_size_ == 0 || table_size_ < sizeof(JNINativeInterface)) { + table_size_ = sizeof(JNINativeInterface); + } + LOGI("JNI table runtime size: %zu (compile-time: %zu)", table_size_, sizeof(JNINativeInterface)); + + // Allocate and copy with runtime size + native_interface_hook_ = (JNINativeInterface*)malloc(table_size_); + if (!native_interface_hook_) { + LOGE("Failed to allocate JNI hook table."); + return; + } + memcpy(native_interface_hook_, env->functions, table_size_); + + // Store a pointer to the original function we are about to replace. + call_boolean_method_v_backup_ = native_interface_hook_->CallBooleanMethodV; + + // Overwrite the function pointer in our copy with our hook. + native_interface_hook_->CallBooleanMethodV = &CallBooleanMethodV_Hook; + + // Atomically swap the process's JNI function table with our modified one. + set_table_override_func(native_interface_hook_); + + BinderCaller::Initialize(); + + LOGI("IPC Bridge JNI hook installed successfully."); +} + +} // namespace vector::native::module diff --git a/xhs_lsposed_a16_fix/ipc_bridge.h b/xhs_lsposed_a16_fix/ipc_bridge.h new file mode 100644 index 0000000..fcae088 --- /dev/null +++ b/xhs_lsposed_a16_fix/ipc_bridge.h @@ -0,0 +1,181 @@ +#pragma once + +#include + +#include +#include +#include + +// This module is a client of the 'native' library. +// We include the JNI bridge from 'native' to leverage its helper functions. +#include + +namespace vector::native::module { + +/** + * @class IPCBridge + * @brief Manages Binder IPC communication with the Vector host service. + * + * This singleton class is the communication arm of the Zygisk module. Its key responsibilities are: + * 1. Discovering and connecting to the central host service (the "manager"). + * 2. Requesting the framework's DEX file and obfuscation map from the service. + * 3. Caching all necessary JNI class and method IDs for efficient reuse. + */ +class IPCBridge { +public: + // Enforce singleton pattern. + IPCBridge(const IPCBridge &) = delete; + IPCBridge &operator=(const IPCBridge &) = delete; + + /** + * @brief Gets the singleton instance of the IPCBridge. + */ + static IPCBridge &GetInstance(); + + /** + * @brief Caches JNI class and method IDs needed for Binder communication. + * @param env A valid JNI environment pointer. + */ + void Initialize(JNIEnv *env); + + /** + * @brief Requests an application-specific Binder from the host service. + * @param env JNI environment pointer. + * @param nice_name The process name. + * @return A ScopedLocalRef to the Binder object, or nullptr on failure. + */ + lsplant::ScopedLocalRef RequestAppBinder(JNIEnv *env, jstring nice_name); + + /** + * @brief Requests the system_server's dedicated Binder from the host service. + * @param env JNI environment pointer. + * @param bridgeServiceName rendezvous point used by the system_server + * @return A ScopedLocalRef to the Binder object, or nullptr on failure. + */ + lsplant::ScopedLocalRef RequestSystemServerBinder(JNIEnv *env, + std::string bridgeServiceName); + + /** + * @brief Asks the system_server binder for the application manager binder. + * @param env JNI environment pointer. + * @param system_server_binder The binder connected to the system server service. + * @return A ScopedLocalRef to the application manager Binder, or nullptr on failure. + */ + lsplant::ScopedLocalRef RequestManagerBinderFromSystemServer( + JNIEnv *env, jobject system_server_binder); + + /** + * @brief Fetches the framework DEX file via the provided Binder connection. + * @param env JNI environment pointer. + * @param binder A live Binder connection to the host service. + * @return A tuple containing the file descriptor and size of the DEX file. + * Returns {-1, 0} on failure. + */ + std::tuple FetchFrameworkDex(JNIEnv *env, jobject binder); + + /** + * @brief Fetches the framework's obfuscation map via the provided Binder. + * @param env JNI environment pointer. + * @param binder A live Binder connection to the host service. + * @return A map of original names to obfuscated names. + */ + std::map FetchObfuscationMap(JNIEnv *env, jobject binder); + + /** + * @brief Sets up the JNI hook to intercept Binder transactions. + * + * This is the core of the IPC interception mechanism. + * It replaces the JNI function pointer for CallBooleanMethodV to + * inspect calls to Binder.execTransact, allowing the framework to + * handle its own custom transaction codes directly. + * @param env JNI environment pointer. + */ + void HookBridge(JNIEnv *env); + +private: + /** + * @class ParcelWrapper + * @brief A private RAII wrapper to ensure Parcel objects are always recycled. + * + * As a private nested class, it has access to the private members of IPCBridge + * (like parcel_class_ and recycle_method_). + */ + class ParcelWrapper { + public: + ParcelWrapper(JNIEnv *env, IPCBridge *bridge); + ~ParcelWrapper(); + // Disable copy operations + ParcelWrapper(const ParcelWrapper &) = delete; + ParcelWrapper &operator=(const ParcelWrapper &) = delete; + + lsplant::ScopedLocalRef data; + lsplant::ScopedLocalRef reply; + + private: + JNIEnv *env_; + IPCBridge *bridge_; + }; + + // Private constructor for singleton. + IPCBridge() = default; + + bool initialized_ = false; + + // --- Cached JNI References --- + // These are initialized once and stored as global references for performance. + + // android.os.ServiceManager + jclass service_manager_class_ = nullptr; + jmethodID get_service_method_ = nullptr; + + // android.os.IBinder + jmethodID transact_method_ = nullptr; + + // android.os.Binder + jclass binder_class_ = nullptr; + jmethodID binder_ctor_ = nullptr; + + // android.os.Parcel + jclass parcel_class_ = nullptr; + jmethodID obtain_method_ = nullptr; + jmethodID recycle_method_ = nullptr; + jmethodID write_interface_token_method_ = nullptr; + jmethodID write_int_method_ = nullptr; + jmethodID write_string_method_ = nullptr; + jmethodID write_strong_binder_method_ = nullptr; + jmethodID read_exception_method_ = nullptr; + jmethodID read_strong_binder_method_ = nullptr; + jmethodID read_file_descriptor_method_ = nullptr; + jmethodID read_int_method_ = nullptr; + jmethodID read_long_method_ = nullptr; + jmethodID read_string_method_ = nullptr; + + // android.os.ParcelFileDescriptor + jclass parcel_fd_class_ = nullptr; + jmethodID detach_fd_method_ = nullptr; + + // --- JNI Hooking Members --- + // These are required to store the state for our JNI function table override. + + // The C++ hook function that will replace the original CallBooleanMethodV. + static jboolean JNICALL CallBooleanMethodV_Hook(JNIEnv *env, jobject obj, jmethodID methodId, + va_list args); + // The helper function that handles our specific transaction code. + static jboolean ExecTransact_Replace(jboolean *res, JNIEnv *env, jobject obj, va_list args); + + // A complete copy of the original JNI function table, with our hook installed. + // Uses runtime-size allocation to support A16+ where ART added new JNI functions. + JNINativeInterface* native_interface_hook_ = nullptr; + size_t table_size_ = 0; + // The original jmethodID for android.os.Binder.execTransact(). + jmethodID exec_transact_backup_method_id_ = nullptr; + // A function pointer to the original CallBooleanMethodV implementation. + jboolean (*call_boolean_method_v_backup_)(JNIEnv *, jobject, jmethodID, va_list) = nullptr; + + // A global reference to the framework's Java BridgeService class. + jclass bridge_service_class_ = nullptr; + // The jmethodID for the static Java method that handles the intercepted transaction. + jmethodID exec_transact_replace_method_id_ = nullptr; +}; + +} // namespace vector::native::module diff --git a/xhs_lsposed_a16_fix/rebuild_xhshook.sh b/xhs_lsposed_a16_fix/rebuild_xhshook.sh new file mode 100755 index 0000000..2ef8e06 --- /dev/null +++ b/xhs_lsposed_a16_fix/rebuild_xhshook.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# rebuild_xhshook.sh — 在 Mac 上重新编译 xhshook,产物写到本目录 +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +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" +LSPOSED_DIR="${LSPOSED_DIR:-/tmp/LSPosed}" +LEGACY_JAR="$LSPOSED_DIR/legacy/build/intermediates/compile_library_classes_jar/release/bundleLibCompileToJarRelease/classes.jar" + +# Fallback: download Xposed API from maven if LSPosed not built +if [ ! -f "$LEGACY_JAR" ]; then + echo "LSPosed legacy JAR not found at $LEGACY_JAR" + echo "Run: git clone --depth 1 --recursive https://github.com/JingMatrix/LSPosed.git $LSPOSED_DIR && cd $LSPOSED_DIR && ./gradlew :legacy:assembleRelease" + exit 1 +fi + +BUILD_DIR=/tmp/xhshook_rebuild +mkdir -p "$BUILD_DIR/compiled" "$BUILD_DIR/dex" "$BUILD_DIR/pack/assets" + +echo "== Compiling Hook.java ==" +javac --release 11 \ + -classpath "$ANDROID_JAR:$LEGACY_JAR" \ + -d "$BUILD_DIR/compiled" \ + "$SCRIPT_DIR/xhshook_src/Hook.java" + +echo "== Converting to DEX ==" +$D8 \ + --classpath "$ANDROID_JAR" \ + --classpath "$LEGACY_JAR" \ + --min-api 27 \ + --output "$BUILD_DIR/dex" \ + "$BUILD_DIR/compiled/com/hermes/xhshook/Hook.class" \ + "$BUILD_DIR/compiled/com/hermes/xhshook/Hook\$1.class" + +echo "== Packing APK ==" +cp "$SCRIPT_DIR/xhshook_src/AndroidManifest.xml" "$BUILD_DIR/pack/" 2>/dev/null || \ + cp /tmp/xhshook_extracted/AndroidManifest.xml "$BUILD_DIR/pack/" +cp "$BUILD_DIR/dex/classes.dex" "$BUILD_DIR/pack/" +echo "com.hermes.xhshook.Hook" > "$BUILD_DIR/pack/assets/xposed_init" +cd "$BUILD_DIR/pack" +zip -0 "$BUILD_DIR/xhshook.apk" AndroidManifest.xml classes.dex assets/xposed_init + +# Create keystore if needed +if [ ! -f /tmp/debug.keystore ]; then + keytool -genkey -v -keystore /tmp/debug.keystore -alias androiddebugkey \ + -keyalg RSA -keysize 2048 -validity 10000 \ + -storepass android -keypass android \ + -dname "CN=Android Debug,O=Android,C=US" 2>/dev/null +fi + +echo "== Signing ==" +$APKSIGNER sign \ + --ks /tmp/debug.keystore \ + --ks-key-alias androiddebugkey \ + --ks-pass pass:android \ + --key-pass pass:android \ + --out "$SCRIPT_DIR/xhshook_v2_signed.apk" \ + "$BUILD_DIR/xhshook.apk" 2>/dev/null + +echo "== Done: $SCRIPT_DIR/xhshook_v2_signed.apk ==" +echo "APK size: $(wc -c < "$SCRIPT_DIR/xhshook_v2_signed.apk") bytes" diff --git a/xhs_lsposed_a16_fix/update_xhshook_db.sh b/xhs_lsposed_a16_fix/update_xhshook_db.sh new file mode 100755 index 0000000..58fa6ff --- /dev/null +++ b/xhs_lsposed_a16_fix/update_xhshook_db.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# update_xhshook_db.sh — 重装 xhshook APK 后更新 LSPosed modules_config.db +# 必须在 adb install --no-incremental 之后运行 +set -e + +ADB="${ADB:-$(dirname "$0")/../platform-tools/adb}" +if [ ! -f "$ADB" ]; then ADB=adb; fi + +NEW_PATH=$($ADB shell 'pm path com.hermes.xhshook' | tr -d '\r' | sed 's/package://') +echo "New APK path: $NEW_PATH" + +$ADB shell 'su -c "cp /data/adb/lspd/config/modules_config.db /data/local/tmp/mc_upd.db && chmod 644 /data/local/tmp/mc_upd.db"' +$ADB pull /data/local/tmp/mc_upd.db /tmp/mc_upd.db +sqlite3 /tmp/mc_upd.db "UPDATE modules SET apk_path='${NEW_PATH}' WHERE module_pkg_name='com.hermes.xhshook';" +$ADB push /tmp/mc_upd.db /data/local/tmp/mc_upd.db +$ADB shell 'su -c "cp /data/local/tmp/mc_upd.db /data/adb/lspd/config/modules_config.db && rm -f /data/adb/lspd/config/modules_config.db-wal /data/adb/lspd/config/modules_config.db-shm && echo done"' +echo "DB updated. Now: am force-stop com.xingin.xhs → open XHS manually." diff --git a/xhs_lsposed_a16_fix/xhshook_src/AndroidManifest.xml b/xhs_lsposed_a16_fix/xhshook_src/AndroidManifest.xml new file mode 100644 index 0000000..83da3aa --- /dev/null +++ b/xhs_lsposed_a16_fix/xhshook_src/AndroidManifest.xml diff --git a/xhs_lsposed_a16_fix/xhshook_src/Hook.java b/xhs_lsposed_a16_fix/xhshook_src/Hook.java new file mode 100644 index 0000000..c1fd0d0 --- /dev/null +++ b/xhs_lsposed_a16_fix/xhshook_src/Hook.java @@ -0,0 +1,52 @@ +package com.hermes.xhshook; + +import de.robv.android.xposed.IXposedHookLoadPackage; +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedHelpers; +import de.robv.android.xposed.callbacks.XC_LoadPackage; + +public class Hook implements IXposedHookLoadPackage { + @Override + public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { + if (!lpparam.packageName.equals("com.xingin.xhs")) return; + + XposedBridge.log("[XHSHook] Loaded into XHS!"); + + try { + Class clz = XposedHelpers.findClass("okhttp3.Request$Builder", lpparam.classLoader); + XposedHelpers.findAndHookMethod(clz, "build", new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + try { + Object url = XposedHelpers.getObjectField(param.thisObject, "url"); + String urlStr = url != null ? url.toString() : "null"; + + // Only log edith.xiaohongshu.com (the signed API endpoint) + if (!urlStr.contains("edith.xiaohongshu.com")) return; + + Object method = XposedHelpers.getObjectField(param.thisObject, "method"); + String methodStr = method != null ? method.toString() : "GET"; + + // headers field is Headers.Builder — call build() to get actual headers + Object headersBuilder = XposedHelpers.getObjectField(param.thisObject, "headers"); + String headersStr = "null"; + if (headersBuilder != null) { + Object headers = XposedHelpers.callMethod(headersBuilder, "build"); + headersStr = headers != null ? headers.toString() : "null"; + } + + XposedBridge.log("[XHSHook] " + methodStr + " " + urlStr); + XposedBridge.log("[XHSHook] Headers:\n" + headersStr); + } catch (Throwable t) { + XposedBridge.log("[XHSHook] Error in hook: " + t.getMessage()); + } + } + }); + XposedBridge.log("[XHSHook] OkHttp Request.Builder.build() hooked!"); + } catch (Throwable t) { + XposedBridge.log("[XHSHook] Failed to hook OkHttp: " + t.getMessage()); + XposedBridge.log(t); + } + } +}