Commit 6d30843c44acb29c78cbb2103741fb19a566d444
1 parent
fa4d53f6
feat(xhs_lsposed_a16_fix): LSPosed A16 修复 + xhshook 注入 XHS 成功,xy-common-params 捕获完成
修复 LSPosed(JingMatrix/Vector fork)在 Android 16 上 zygote64 崩溃问题, 并完成 xhshook Xposed 模块注入 XHS(com.xingin.xhs),实现 edith.xiaohongshu.com xy-common-params header 的实时拦截。 A16 崩溃根因:NDK 26 compile-time sizeof(JNINativeInterface) 不包含 Android 16 ART 新增的函数指针,memcpy 截断导致垃圾指针 → zygote 崩溃。 修复:从 env->functions->reserved0 读运行期实际大小,heap-alloc 后 memcpy。 其他修复: - service.sh 去掉 Android toybox 不支持的 --propagation slave flag - daemon 入口类名 org.lsposed.lspd.Main → org.matrix.vector.daemon.VectorDaemon xhshook 踩坑记录: - APK 必须含 assets/xposed_init(而非根目录),VectorConfigCache 只认这个路径 - adb install 必须加 --no-incremental,Incremental Install marker 破坏 ZipFile 解析 - Xposed API 类不能打进 module DEX,LSPosed 会检查 XposedBridge classloader 一致性 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Showing
10 changed files
with
1402 additions
and
0 deletions
xhs_lsposed_a16_fix/CLAUDE.md
0 → 100644
| 1 | +# CLAUDE.md | ||
| 2 | + | ||
| 3 | +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. | ||
| 4 | + | ||
| 5 | +## What This Is | ||
| 6 | + | ||
| 7 | +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. | ||
| 8 | + | ||
| 9 | +The fix reads the runtime table size from `reserved0` (ART stores the actual byte count there) and heap-allocates the copy. | ||
| 10 | + | ||
| 11 | +## Files | ||
| 12 | + | ||
| 13 | +- `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. | ||
| 14 | +- `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. | ||
| 15 | + | ||
| 16 | +## Apply + Build + Deploy | ||
| 17 | + | ||
| 18 | +```bash | ||
| 19 | +# 1. Clone LSPosed | ||
| 20 | +git clone --depth 1 --recursive https://github.com/JingMatrix/LSPosed.git | ||
| 21 | +cd LSPosed | ||
| 22 | + | ||
| 23 | +# 2. Apply patch (from this directory) | ||
| 24 | +bash /path/to/apply_a16_fix.sh . | ||
| 25 | + | ||
| 26 | +# 3. Build | ||
| 27 | +export ANDROID_HOME=/path/to/android-sdk | ||
| 28 | +./gradlew :zygisk:assembleRelease | ||
| 29 | +# Output: zygisk/build/outputs/apk/release/zygisk-release.apk | ||
| 30 | + | ||
| 31 | +# 4. Deploy | ||
| 32 | +unzip -o zygisk-release.apk -d zygisk_out | ||
| 33 | +adb push zygisk_out/lib/arm64-v8a/libzygisk.so /data/local/tmp/ | ||
| 34 | +adb shell su -c 'cp /data/local/tmp/libzygisk.so /data/adb/modules/zygisk_vector/zygisk/arm64-v8a.so' | ||
| 35 | +adb shell su -c 'magisk --sqlite "INSERT OR REPLACE INTO settings (key,value) VALUES (\"zygisk\",1);"' | ||
| 36 | +adb reboot | ||
| 37 | +``` | ||
| 38 | + | ||
| 39 | +## Verify After Reboot | ||
| 40 | + | ||
| 41 | +```bash | ||
| 42 | +adb shell su -c 'pidof lspd' # daemon alive | ||
| 43 | +adb shell su -c 'cat /proc/$(pidof zygote64 | awk "{print \$1}")/maps | grep zygisk' # injected | ||
| 44 | +adb shell su -c 'cat /proc/$(pidof com.xingin.xhs)/maps | grep xhshook' # XHS module loaded | ||
| 45 | +``` | ||
| 46 | + | ||
| 47 | +## Context | ||
| 48 | + | ||
| 49 | +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`). |
xhs_lsposed_a16_fix/README.md
0 → 100644
| 1 | +# LSPosed A16 Fix — JNINativeInterface Runtime Size Mismatch | ||
| 2 | + | ||
| 3 | +## Summary | ||
| 4 | + | ||
| 5 | +LSPosed (JingMatrix fork v1.11.0 / Vector v2.0) crashes zygote64 on Android 16 with: | ||
| 6 | + | ||
| 7 | +``` | ||
| 8 | +JNI DETECTED ERROR IN APPLICATION: negative buffer capacity in NewDirectByteBuffer | ||
| 9 | +from int com.android.internal.os.Zygote.nativeForkSystemServer | ||
| 10 | +``` | ||
| 11 | + | ||
| 12 | +Magisk detects repeated zygote crashes and auto-rolls-back Zygisk. Modules never inject. | ||
| 13 | + | ||
| 14 | +## Root Cause | ||
| 15 | + | ||
| 16 | +`zygisk/src/main/cpp/ipc_bridge.cpp` line 557: | ||
| 17 | + | ||
| 18 | +```cpp | ||
| 19 | +memcpy(&native_interface_hook_, env->functions, sizeof(JNINativeInterface)); | ||
| 20 | +``` | ||
| 21 | + | ||
| 22 | +**`sizeof(JNINativeInterface)` is a compile-time constant from the NDK used to build LSPosed (NDK 26 = ~2376 bytes).** | ||
| 23 | + | ||
| 24 | +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. | ||
| 25 | + | ||
| 26 | +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. | ||
| 27 | + | ||
| 28 | +**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. | ||
| 29 | + | ||
| 30 | +## Fix | ||
| 31 | + | ||
| 32 | +Replace compile-time `sizeof(JNINativeInterface)` with runtime `reserved0`, and dynamically allocate the copy. | ||
| 33 | + | ||
| 34 | +### File 1: `zygisk/src/main/cpp/include/ipc_bridge.h` | ||
| 35 | + | ||
| 36 | +```diff | ||
| 37 | +- JNINativeInterface native_interface_hook_{}; | ||
| 38 | ++ JNINativeInterface* native_interface_hook_ = nullptr; | ||
| 39 | ++ size_t table_size_ = 0; | ||
| 40 | +``` | ||
| 41 | + | ||
| 42 | +### File 2: `zygisk/src/main/cpp/ipc_bridge.cpp` | ||
| 43 | + | ||
| 44 | +```diff | ||
| 45 | +- memcpy(&native_interface_hook_, env->functions, sizeof(JNINativeInterface)); | ||
| 46 | ++ table_size_ = (size_t)env->functions->reserved0; | ||
| 47 | ++ if (table_size_ == 0 || table_size_ < sizeof(JNINativeInterface)) | ||
| 48 | ++ table_size_ = sizeof(JNINativeInterface); | ||
| 49 | ++ native_interface_hook_ = (JNINativeInterface*)malloc(table_size_); | ||
| 50 | ++ memcpy(native_interface_hook_, env->functions, table_size_); | ||
| 51 | + | ||
| 52 | +- call_boolean_method_v_backup_ = env->functions->CallBooleanMethodV; | ||
| 53 | ++ call_boolean_method_v_backup_ = native_interface_hook_->CallBooleanMethodV; | ||
| 54 | + | ||
| 55 | +- native_interface_hook_.CallBooleanMethodV = ... | ||
| 56 | ++ native_interface_hook_->CallBooleanMethodV = ... | ||
| 57 | + | ||
| 58 | +- set_table_override_func(&native_interface_hook_); | ||
| 59 | ++ set_table_override_func(native_interface_hook_); | ||
| 60 | +``` | ||
| 61 | + | ||
| 62 | +## How to Build | ||
| 63 | + | ||
| 64 | +```bash | ||
| 65 | +# 1. Clone | ||
| 66 | +git clone --depth 1 --recursive https://github.com/JingMatrix/LSPosed.git | ||
| 67 | +cd LSPosed | ||
| 68 | + | ||
| 69 | +# 2. Apply fix | ||
| 70 | +bash apply_a16_fix.sh . | ||
| 71 | + | ||
| 72 | +# 3. Build (requires Android SDK + NDK) | ||
| 73 | +export ANDROID_HOME=/path/to/android-sdk | ||
| 74 | +./gradlew :zygisk:assembleRelease | ||
| 75 | + | ||
| 76 | +# Output: zygisk/build/outputs/apk/release/ | ||
| 77 | +``` | ||
| 78 | + | ||
| 79 | +## How to Deploy on Device | ||
| 80 | + | ||
| 81 | +```bash | ||
| 82 | +# Extract the fixed zygisk .so from the APK | ||
| 83 | +unzip -o zygisk-release.apk -d zygisk_out | ||
| 84 | + | ||
| 85 | +# Push to device and replace | ||
| 86 | +adb push zygisk_out/lib/arm64-v8a/libzygisk.so /data/local/tmp/ | ||
| 87 | +adb shell su -c 'cp /data/local/tmp/libzygisk.so /data/adb/modules/zygisk_vector/zygisk/arm64-v8a.so' | ||
| 88 | + | ||
| 89 | +# Ensure Zygisk is enabled | ||
| 90 | +adb shell su -c 'magisk --sqlite "INSERT OR REPLACE INTO settings (key,value) VALUES (\"zygisk\",1);"' | ||
| 91 | + | ||
| 92 | +# Reboot | ||
| 93 | +adb reboot | ||
| 94 | +``` | ||
| 95 | + | ||
| 96 | +## Verification (after reboot) | ||
| 97 | + | ||
| 98 | +```bash | ||
| 99 | +# 1. Daemon running? | ||
| 100 | +adb shell su -c 'pidof lspd' | ||
| 101 | + | ||
| 102 | +# 2. Zygisk injected into zygote? | ||
| 103 | +adb shell su -c 'cat /proc/$(pidof zygote64 | awk "{print \$1}")/maps | grep zygisk' | head -3 | ||
| 104 | + | ||
| 105 | +# 3. Module injected into target app? | ||
| 106 | +adb shell su -c 'cat /proc/$(pidof com.xingin.xhs)/maps | grep xhshook' | ||
| 107 | +``` | ||
| 108 | + | ||
| 109 | +## Background | ||
| 110 | + | ||
| 111 | +Full reverse engineering session: Hermes Agent on Pixel 10 Pro Fold, Android 16, XHS 9.35.1. | ||
| 112 | + | ||
| 113 | +**Campaign goal:** MITM capture `xy-common-params` (ECFAAF/ECFAAF01 signatures) from edith.xiaohongshu.com. | ||
| 114 | + | ||
| 115 | +**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. | ||
| 116 | + | ||
| 117 | +**Other achievements from the session:** | ||
| 118 | +- Java TrustManager bypass works for non-edith domains (crash/as/t2/mediacloud) | ||
| 119 | +- sentry abort Interceptor works (Frida survives 35+ seconds) | ||
| 120 | +- `--pause` mode confirms edith goes through TrustManager (timing race is the issue) | ||
| 121 | +- xhshook module pre-built at `/data/adb/modules/xhshook/xhshook.apk` |
xhs_lsposed_a16_fix/STATUS.md
0 → 100644
| 1 | +# XHS MITM 进度备忘 — 2026-06-29(Hermes 接手版) | ||
| 2 | + | ||
| 3 | +写给设备上 Hermes Agent 的完整上下文。**直接可用**,无需 Mac 介入的操作都写了命令。 | ||
| 4 | + | ||
| 5 | +--- | ||
| 6 | + | ||
| 7 | +## 战役目标:✅ 已完成 | ||
| 8 | + | ||
| 9 | +抓 `edith.xiaohongshu.com` 的 `xy-common-params`(ECFAAF/ECFAAF01 签名参数),用于复现 XHS 请求鉴权。 | ||
| 10 | + | ||
| 11 | +**核心障碍**:edith 用 Google Mainline Cronet + 静态链接 BoringSSL,Frida Interceptor 无法 hook 静态符号。 | ||
| 12 | +**解法**:LSPosed(JingMatrix/Vector fork)在 XHS 进程 fork 时注入,hook OkHttp Java 层,在签名传入 Cronet 前拦截。 | ||
| 13 | + | ||
| 14 | +--- | ||
| 15 | + | ||
| 16 | +## 当前设备状态(2026-06-29 17:22 验证通过) | ||
| 17 | + | ||
| 18 | +``` | ||
| 19 | +设备:Pixel 10 Pro Fold | ||
| 20 | +Android:16 (API 36) | ||
| 21 | +Magisk:30.7 | ||
| 22 | +LSPosed (Vector fork):正常运行,lspd 进程存活 | ||
| 23 | +xhshook:注入 com.xingin.xhs 成功 | ||
| 24 | +xy-common-params:已捕获(见下方样本) | ||
| 25 | +``` | ||
| 26 | + | ||
| 27 | +### 模块布局 | ||
| 28 | + | ||
| 29 | +``` | ||
| 30 | +/data/adb/modules/ | ||
| 31 | +├── zygisk_vector/ # LSPosed Vector fork(含 A16 JNI size fix) | ||
| 32 | +│ ├── zygisk/arm64-v8a.so # 核心注入 so(1142464 字节) | ||
| 33 | +│ ├── daemon.apk # Java 守护进程(3199711 字节) | ||
| 34 | +│ ├── framework/lspd.dex # 注入 DEX(182840 字节) | ||
| 35 | +│ ├── daemon # 启动脚本(已修复入口类名) | ||
| 36 | +│ └── service.sh # Magisk 钩子(已去掉不兼容 flag) | ||
| 37 | +├── xhshook/ # Xposed 模块 | ||
| 38 | +│ ├── module.prop | ||
| 39 | +│ └── xhshook.apk # 已更新为 v2(无捆绑 XposedBridge) | ||
| 40 | +└── xhs_ca/ # MITM 代理 CA 证书 | ||
| 41 | + └── system/etc/security/cacerts/36522bbf.0 | ||
| 42 | +``` | ||
| 43 | + | ||
| 44 | +### xhshook 配置 | ||
| 45 | + | ||
| 46 | +``` | ||
| 47 | +已安装:com.hermes.xhshook(streamed install,非 incremental) | ||
| 48 | +APK 含:assets/xposed_init → com.hermes.xhshook.Hook | ||
| 49 | +modules_config.db:mid=3, enabled=1, scope=com.xingin.xhs | ||
| 50 | +``` | ||
| 51 | + | ||
| 52 | +--- | ||
| 53 | + | ||
| 54 | +## 捕获到的 xy-common-params 样本 | ||
| 55 | + | ||
| 56 | +打开 XHS 后,用 logcat 实时抓: | ||
| 57 | + | ||
| 58 | +```bash | ||
| 59 | +# 在 Termux 里(需要 root) | ||
| 60 | +su -c "logcat -b all | grep -F 'xy-common-params:'" | ||
| 61 | +``` | ||
| 62 | + | ||
| 63 | +示例值(session 级字段每次启动刷新): | ||
| 64 | + | ||
| 65 | +``` | ||
| 66 | +fid= | ||
| 67 | +gid=7c9eca1fb5ac54aae84944d1bb4d85a3591f09d9473599ae77728621 | ||
| 68 | +device_model=phone | ||
| 69 | +tz=Asia/Shanghai | ||
| 70 | +channel=YingYongBao | ||
| 71 | +versionName=9.35.1 | ||
| 72 | +deviceId=748327bc-6079-3b06-9351-1638281dbc6f ← 设备固定 | ||
| 73 | +platform=android | ||
| 74 | +sid=session.1782715007780944836188 ← 每次启动变 | ||
| 75 | +identifier_flag=4 | ||
| 76 | +cpu_abi=arm64-v8a | ||
| 77 | +nqe_score=89 | ||
| 78 | +project_id=ECFAAF | ||
| 79 | +app_id=ECFAAF01 | ||
| 80 | +id_token=VjEAAN3Pjix... ← 每次启动变 | ||
| 81 | +t=1782724968 ← Unix 时间戳,每次变 | ||
| 82 | +build=9351803 | ||
| 83 | +did=058844e5598e4d001b42a122d1a9d1c2 ← 设备固定 | ||
| 84 | +``` | ||
| 85 | + | ||
| 86 | +**固定字段**(可硬编码复现请求):`deviceId`、`did`、`gid`、`platform`、`versionName`、`build` | ||
| 87 | +**Session 字段**(需要 hook 实时拿):`id_token`、`sid`、`t` | ||
| 88 | + | ||
| 89 | +--- | ||
| 90 | + | ||
| 91 | +## xhshook 架构(Hermes 改 hook 用) | ||
| 92 | + | ||
| 93 | +### Hook 逻辑(当前 v2) | ||
| 94 | + | ||
| 95 | +```java | ||
| 96 | +// 源码位置(设备):/sdcard/Download/xhs_lsposed_a16_fix/xhshook_src/Hook.java | ||
| 97 | +// 源码位置(Mac):~/Documents/LemonAI/pixelphone/xhs_lsposed_a16_fix/xhshook_src/Hook.java | ||
| 98 | + | ||
| 99 | +hook 点:okhttp3.Request$Builder.build() | ||
| 100 | +过滤:只打印 edith.xiaohongshu.com 的请求 | ||
| 101 | +输出:logcat tag=VectorLegacyBridge | ||
| 102 | +``` | ||
| 103 | + | ||
| 104 | +### 改 hook 的完整流程(需 Mac 操作) | ||
| 105 | + | ||
| 106 | +```bash | ||
| 107 | +# 1. 编辑 Hook.java | ||
| 108 | +# ~/Documents/LemonAI/pixelphone/xhs_lsposed_a16_fix/xhshook_src/Hook.java | ||
| 109 | + | ||
| 110 | +# 2. 重新编译 + 打包(Mac 上运行) | ||
| 111 | +bash ~/Documents/LemonAI/pixelphone/xhs_lsposed_a16_fix/rebuild_xhshook.sh | ||
| 112 | + | ||
| 113 | +# 3. 推到设备 | ||
| 114 | +ADB=~/Documents/LemonAI/pixelphone/platform-tools/adb | ||
| 115 | +$ADB shell 'pm uninstall com.hermes.xhshook' && \ | ||
| 116 | +$ADB install --no-incremental ~/Documents/LemonAI/pixelphone/xhs_lsposed_a16_fix/xhshook_v2_signed.apk | ||
| 117 | + | ||
| 118 | +# 4. 更新 modules_config.db(脚本自动做) | ||
| 119 | +bash ~/Documents/LemonAI/pixelphone/xhs_lsposed_a16_fix/update_xhshook_db.sh | ||
| 120 | + | ||
| 121 | +# 5. 杀掉 XHS 进程重启(不需要重启整机!) | ||
| 122 | +$ADB shell 'am force-stop com.xingin.xhs' | ||
| 123 | +# 然后手动在屏幕上点 XHS 图标 | ||
| 124 | + | ||
| 125 | +# 6. 验证 | ||
| 126 | +$ADB shell 'logcat -d | grep "\[XHSHook\]"' | ||
| 127 | +``` | ||
| 128 | + | ||
| 129 | +### 可以加的下一个 hook:CertificatePinner bypass | ||
| 130 | + | ||
| 131 | +xhs_ca 模块已经把 MITM 代理 CA 证书装进系统了。补上 CertificatePinner bypass 后可以直接用 mitmproxy 抓 OkHttp 流量(edith 仍然走 Cronet,但其他接口会走 OkHttp): | ||
| 132 | + | ||
| 133 | +```java | ||
| 134 | +// 在 handleLoadPackage 里加(XHS 包名判断之后): | ||
| 135 | +try { | ||
| 136 | + XposedHelpers.findAndHookMethod( | ||
| 137 | + "okhttp3.CertificatePinner", lpparam.classLoader, | ||
| 138 | + "check", String.class, List.class, | ||
| 139 | + new XC_MethodHook() { | ||
| 140 | + @Override | ||
| 141 | + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { | ||
| 142 | + param.setResult(null); | ||
| 143 | + } | ||
| 144 | + } | ||
| 145 | + ); | ||
| 146 | + XposedBridge.log("[XHSHook] CertificatePinner bypassed!"); | ||
| 147 | +} catch (Throwable t) { | ||
| 148 | + XposedBridge.log("[XHSHook] CertificatePinner bypass failed: " + t); | ||
| 149 | +} | ||
| 150 | +``` | ||
| 151 | + | ||
| 152 | +--- | ||
| 153 | + | ||
| 154 | +## 下一步可以做的事(Hermes 接手) | ||
| 155 | + | ||
| 156 | +### A. 用捕获到的参数复现 XHS API 请求 | ||
| 157 | + | ||
| 158 | +`xy-common-params` 里的固定字段可以直接复用,session 字段需要先打开 XHS 触发 hook 实时拿。 | ||
| 159 | + | ||
| 160 | +```bash | ||
| 161 | +# Termux 里实时抓一次完整的 xy-common-params | ||
| 162 | +su -c "logcat -c && am force-stop com.xingin.xhs" | ||
| 163 | +# → 手动打开 XHS → | ||
| 164 | +su -c "logcat -d | grep 'xy-common-params:' | head -1" | ||
| 165 | +``` | ||
| 166 | + | ||
| 167 | +拿到后,用 curl 复现请求: | ||
| 168 | + | ||
| 169 | +```bash | ||
| 170 | +XY_PARAMS="fid=&gid=7c9eca...(完整值)" | ||
| 171 | +curl -H "xy-common-params: $XY_PARAMS" \ | ||
| 172 | + "https://edith.xiaohongshu.com/api/sns/v1/homefeed?..." | ||
| 173 | +``` | ||
| 174 | + | ||
| 175 | +### B. Hook 更多字段(如果 curl 复现失败) | ||
| 176 | + | ||
| 177 | +edith 除 `xy-common-params` 外可能还检查其他 header(如 `xy-sign`、cookie)。修改 xhshook 打印所有 edith 请求的全部 header,观察缺什么。 | ||
| 178 | + | ||
| 179 | +Hook.java 里已经打印了 `Headers.toString()` 的完整内容——重新抓一次 logcat 即可看到所有 header(logcat 多行输出,每个 header 一行)。 | ||
| 180 | + | ||
| 181 | +```bash | ||
| 182 | +su -c "logcat -d -b all | grep 'VectorLegacyBridge' | grep -v '\[XHSHook\]'" | head -30 | ||
| 183 | +``` | ||
| 184 | + | ||
| 185 | +### C. 修改 xhshook 做实时转发(高级) | ||
| 186 | + | ||
| 187 | +改 hook 在 `beforeHookedMethod` 里把参数通过 HTTP POST 转到 Hermes 的本地 server(如 `localhost:8765`),让 Hermes 实时处理每条请求。 | ||
| 188 | + | ||
| 189 | +--- | ||
| 190 | + | ||
| 191 | +## 文件索引(设备上) | ||
| 192 | + | ||
| 193 | +``` | ||
| 194 | +/sdcard/Download/xhs_lsposed_a16_fix/ | ||
| 195 | +├── STATUS.md ← 本文件 | ||
| 196 | +├── CLAUDE.md ← 项目说明(构建/部署命令) | ||
| 197 | +├── apply_a16_fix.sh ← A16 补丁脚本(已应用,留作参考) | ||
| 198 | +├── ipc_bridge.h ← 已打补丁的头文件 | ||
| 199 | +├── ipc_bridge.cpp ← 已打补丁的源文件 | ||
| 200 | +├── xhshook_src/ | ||
| 201 | +│ └── Hook.java ← xhshook v2 源码 | ||
| 202 | +└── xhshook_v2_signed.apk ← 预构建的 APK(当前已安装版本) | ||
| 203 | +``` | ||
| 204 | + | ||
| 205 | +--- | ||
| 206 | + | ||
| 207 | +## 已知不行的路线(别重新踩) | ||
| 208 | + | ||
| 209 | +- **Frida hook edith.xiaohongshu.com**:Cronet 静态链接 BoringSSL,Frida Interceptor 找不到符号。SSL_read/SSL_write 都 hook 不上。 | ||
| 210 | +- **Java TrustManager 替换**:时序问题,edith 在 app 启动很早就开始通信,Frida 注入晚了窗口就过了。LSPosed 没有这个问题。 | ||
| 211 | +- **常驻 Frida daemon 注入 XHS**:XHS 检测到反调试后崩溃(wechat_plain 里有记录,XHS 同理)。 | ||
| 212 | +- **把 Xposed API 类打进 xhshook DEX**:LSPosed 会检查 `XposedBridge.class.getClassLoader() == initLoader`,打进去的话直接 `Cannot load module`。必须用 `compileOnly`。 | ||
| 213 | +- **incremental install(`adb install` 默认)**:Incremental Install 的 marker 会让 `java.util.zip.ZipFile` 解析失败,LSPosed 找不到 `assets/xposed_init`。必须用 `adb install --no-incremental`。 |
xhs_lsposed_a16_fix/apply_a16_fix.sh
0 → 100755
| 1 | +#!/bin/bash | ||
| 2 | +# LSPosed A16 Fix — apply the JNINativeInterface runtime-size patch | ||
| 3 | +# Usage: bash apply_a16_fix.sh <path-to-LSPosed-repo> | ||
| 4 | +# Example: bash apply_a16_fix.sh ~/LSPosed | ||
| 5 | + | ||
| 6 | +set -e | ||
| 7 | + | ||
| 8 | +REPO="${1:-.}" | ||
| 9 | +HEADER="$REPO/zygisk/src/main/cpp/include/ipc_bridge.h" | ||
| 10 | +CPP="$REPO/zygisk/src/main/cpp/ipc_bridge.cpp" | ||
| 11 | + | ||
| 12 | +if [ ! -f "$HEADER" ] || [ ! -f "$CPP" ]; then | ||
| 13 | + echo "ERROR: LSPosed source not found at $REPO" | ||
| 14 | + echo "Clone first: git clone --depth 1 --recursive https://github.com/JingMatrix/LSPosed.git" | ||
| 15 | + exit 1 | ||
| 16 | +fi | ||
| 17 | + | ||
| 18 | +echo "=== Applying A16 JNINativeInterface runtime-size fix ===" | ||
| 19 | + | ||
| 20 | +# === Patch 1: ipc_bridge.h — change stack-allocated table to pointer === | ||
| 21 | +echo "[1/2] Patching $HEADER ..." | ||
| 22 | + | ||
| 23 | +# Create backup | ||
| 24 | +cp "$HEADER" "$HEADER.bak" | ||
| 25 | + | ||
| 26 | +python3 -c " | ||
| 27 | +import re | ||
| 28 | + | ||
| 29 | +with open('$HEADER', 'r') as f: | ||
| 30 | + content = f.read() | ||
| 31 | + | ||
| 32 | +old = ' // A complete copy of the original JNI function table, with our hook installed.\n JNINativeInterface native_interface_hook_{};' | ||
| 33 | + | ||
| 34 | +new = ''' // A complete copy of the original JNI function table, with our hook installed. | ||
| 35 | + // Uses runtime-size allocation to support A16+ where ART added new JNI functions. | ||
| 36 | + JNINativeInterface* native_interface_hook_ = nullptr; | ||
| 37 | + size_t table_size_ = 0;''' | ||
| 38 | + | ||
| 39 | +if old in content: | ||
| 40 | + content = content.replace(old, new) | ||
| 41 | + with open('$HEADER', 'w') as f: | ||
| 42 | + f.write(content) | ||
| 43 | + print(' ✓ ipc_bridge.h patched') | ||
| 44 | +else: | ||
| 45 | + print(' ⚠ ipc_bridge.h — pattern not found, may already be patched') | ||
| 46 | +" | ||
| 47 | + | ||
| 48 | +# === Patch 2: ipc_bridge.cpp — use reserved0 for runtime size === | ||
| 49 | +echo "[2/2] Patching $CPP ..." | ||
| 50 | + | ||
| 51 | +cp "$CPP" "$CPP.bak" | ||
| 52 | + | ||
| 53 | +python3 -c " | ||
| 54 | +with open('$CPP', 'r') as f: | ||
| 55 | + content = f.read() | ||
| 56 | + | ||
| 57 | +old = ''' // --- Step 3: Install the hook --- | ||
| 58 | + // Make a full copy of the existing JNI function table. | ||
| 59 | + memcpy(&native_interface_hook_, env->functions, sizeof(JNINativeInterface)); | ||
| 60 | + | ||
| 61 | + // Store a pointer to the original function we are about to replace. | ||
| 62 | + call_boolean_method_v_backup_ = env->functions->CallBooleanMethodV; | ||
| 63 | + | ||
| 64 | + // Overwrite the function pointer in our copy with our hook. | ||
| 65 | + native_interface_hook_.CallBooleanMethodV = &CallBooleanMethodV_Hook; | ||
| 66 | + | ||
| 67 | + // Atomically swap the process's JNI function table with our modified one. | ||
| 68 | + set_table_override_func(&native_interface_hook_);''' | ||
| 69 | + | ||
| 70 | +new = ''' // --- Step 3: Install the hook --- | ||
| 71 | + // Get the ACTUAL runtime size of the JNI function table. | ||
| 72 | + // Android 16 ART adds new JNI functions that increase the table size. | ||
| 73 | + // Using compile-time sizeof(JNINativeInterface) truncates the copy, | ||
| 74 | + // causing crashes when ART calls new functions via garbage pointers. | ||
| 75 | + table_size_ = (size_t)env->functions->reserved0; | ||
| 76 | + if (table_size_ == 0 || table_size_ < sizeof(JNINativeInterface)) { | ||
| 77 | + table_size_ = sizeof(JNINativeInterface); | ||
| 78 | + } | ||
| 79 | + LOGI(\"JNI table runtime size: %zu (compile-time: %zu)\", table_size_, sizeof(JNINativeInterface)); | ||
| 80 | + | ||
| 81 | + // Allocate and copy with runtime size | ||
| 82 | + native_interface_hook_ = (JNINativeInterface*)malloc(table_size_); | ||
| 83 | + if (!native_interface_hook_) { | ||
| 84 | + LOGE(\"Failed to allocate JNI hook table.\"); | ||
| 85 | + return; | ||
| 86 | + } | ||
| 87 | + memcpy(native_interface_hook_, env->functions, table_size_); | ||
| 88 | + | ||
| 89 | + // Store a pointer to the original function we are about to replace. | ||
| 90 | + call_boolean_method_v_backup_ = native_interface_hook_->CallBooleanMethodV; | ||
| 91 | + | ||
| 92 | + // Overwrite the function pointer in our copy with our hook. | ||
| 93 | + native_interface_hook_->CallBooleanMethodV = &CallBooleanMethodV_Hook; | ||
| 94 | + | ||
| 95 | + // Atomically swap the process's JNI function table with our modified one. | ||
| 96 | + set_table_override_func(native_interface_hook_);''' | ||
| 97 | + | ||
| 98 | +if old in content: | ||
| 99 | + content = content.replace(old, new) | ||
| 100 | + with open('$CPP', 'w') as f: | ||
| 101 | + f.write(content) | ||
| 102 | + print(' ✓ ipc_bridge.cpp patched') | ||
| 103 | +else: | ||
| 104 | + print(' ⚠ ipc_bridge.cpp — pattern not found, may already be patched') | ||
| 105 | +" | ||
| 106 | + | ||
| 107 | +echo "" | ||
| 108 | +echo "=== Done. Building ===" | ||
| 109 | +echo "" | ||
| 110 | +echo "To build:" | ||
| 111 | +echo " export ANDROID_HOME=/path/to/android-sdk" | ||
| 112 | +echo " cd $REPO" | ||
| 113 | +echo " ./gradlew :zygisk:assembleRelease" | ||
| 114 | +echo "" | ||
| 115 | +echo "Output: zygisk/build/outputs/apk/release/zygisk-release.apk" | ||
| 116 | +echo "The fixed zygisk .so inside can replace:" | ||
| 117 | +echo " /data/adb/modules/zygisk_vector/zygisk/arm64-v8a.so" |
xhs_lsposed_a16_fix/ipc_bridge.cpp
0 → 100644
| 1 | +#include "ipc_bridge.h" | ||
| 2 | + | ||
| 3 | +#include <elf/elf_image.h> | ||
| 4 | +#include <elf/symbol_cache.h> | ||
| 5 | +#include <unistd.h> | ||
| 6 | + | ||
| 7 | +#include <atomic> | ||
| 8 | +#include <string_view> | ||
| 9 | +#include <thread> | ||
| 10 | + | ||
| 11 | +using namespace std::literals::string_view_literals; | ||
| 12 | + | ||
| 13 | +#include <common/logging.h> | ||
| 14 | + | ||
| 15 | +namespace vector::native::module { | ||
| 16 | + | ||
| 17 | +// Store the ID of the last caller whose framework transaction failed. | ||
| 18 | +// It's initialized to a value that won't match. | ||
| 19 | +static std::atomic<uint64_t> g_last_failed_id = ~0; | ||
| 20 | + | ||
| 21 | +/** | ||
| 22 | + * @class BinderCaller | ||
| 23 | + * @brief A helper to get the UID and PID of the current Binder caller. | ||
| 24 | + * | ||
| 25 | + * This class encapsulates the logic for finding and calling private functions | ||
| 26 | + * within libbinder.so to identify the origin of an IPC transaction. | ||
| 27 | + */ | ||
| 28 | +class BinderCaller { | ||
| 29 | +public: | ||
| 30 | + /** | ||
| 31 | + * @brief Initializes the helper by finding the required function pointers. | ||
| 32 | + * This must be called once before GetId() is used. | ||
| 33 | + */ | ||
| 34 | + static void Initialize() { | ||
| 35 | + // Use the native library's symbol cache to find symbols in the loaded libbinder.so | ||
| 36 | + auto libbinder = ElfSymbolCache::GetLibBinder(); | ||
| 37 | + if (!libbinder) { | ||
| 38 | + LOGW("libbinder.so not found in cache, cannot get caller ID."); | ||
| 39 | + return; | ||
| 40 | + } | ||
| 41 | + | ||
| 42 | + s_self_or_null_fn = (IPCThreadState * (*)()) libbinder->getSymbAddress( | ||
| 43 | + "_ZN7android14IPCThreadState10selfOrNullEv"); | ||
| 44 | + s_get_calling_pid_fn = (pid_t(*)(IPCThreadState *))libbinder->getSymbAddress( | ||
| 45 | + "_ZNK7android14IPCThreadState13getCallingPidEv"); | ||
| 46 | + s_get_calling_uid_fn = (uid_t(*)(IPCThreadState *))libbinder->getSymbAddress( | ||
| 47 | + "_ZNK7android14IPCThreadState13getCallingUidEv"); | ||
| 48 | + | ||
| 49 | + if (!s_self_or_null_fn || !s_get_calling_pid_fn || !s_get_calling_uid_fn) { | ||
| 50 | + LOGW("Could not resolve all IPCThreadState symbols. Caller ID check will be disabled."); | ||
| 51 | + } else { | ||
| 52 | + LOGV("IPCThreadState symbols resolved successfully."); | ||
| 53 | + } | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + /** | ||
| 57 | + * @brief Gets the unique 64-bit ID of the current Binder caller. | ||
| 58 | + * @return A combined UID and PID, or 0 if symbols are not available. | ||
| 59 | + */ | ||
| 60 | + static uint64_t GetId() { | ||
| 61 | + if (!s_self_or_null_fn) [[unlikely]] | ||
| 62 | + return 0; | ||
| 63 | + | ||
| 64 | + IPCThreadState *self = s_self_or_null_fn(); | ||
| 65 | + if (!self) return 0; | ||
| 66 | + | ||
| 67 | + auto uid = s_get_calling_uid_fn(self); | ||
| 68 | + auto pid = s_get_calling_pid_fn(self); | ||
| 69 | + return (static_cast<uint64_t>(uid) << 32) | pid; | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | +private: | ||
| 73 | + // Forward declare the opaque struct from libbinder. | ||
| 74 | + struct IPCThreadState; | ||
| 75 | + | ||
| 76 | + inline static IPCThreadState *(*s_self_or_null_fn)() = nullptr; | ||
| 77 | + inline static pid_t (*s_get_calling_pid_fn)(IPCThreadState *) = nullptr; | ||
| 78 | + inline static uid_t (*s_get_calling_uid_fn)(IPCThreadState *) = nullptr; | ||
| 79 | +}; | ||
| 80 | + | ||
| 81 | +// --- Binder IPC Protocol Constants --- | ||
| 82 | +// These are the "secret handshakes" used to communicate with the Vector manager service. | ||
| 83 | + | ||
| 84 | +// The name of the system service we use as a rendezvous point to find our manager service. | ||
| 85 | +// Using "activity" is a common technique as it's always available. | ||
| 86 | +constexpr auto kBridgeServiceName = "activity"sv; | ||
| 87 | + | ||
| 88 | +// Transaction codes for specific actions. | ||
| 89 | +constexpr jint kBridgeTransactionCode = ('_' << 24) | ('V' << 16) | ('E' << 8) | 'C'; | ||
| 90 | +constexpr jint kDexTransactionCode = ('_' << 24) | ('D' << 16) | ('E' << 8) | 'X'; | ||
| 91 | +constexpr jint kObfuscationMapTransactionCode = ('_' << 24) | ('O' << 16) | ('B' << 8) | 'F'; | ||
| 92 | + | ||
| 93 | +// Action codes sent within a kBridgeTransactionCode transaction. | ||
| 94 | +constexpr jint kActionGetBinder = 2; | ||
| 95 | + | ||
| 96 | +// ========================================================================================= | ||
| 97 | +// Implementation of IPCBridge::ParcelWrapper (Private Nested Class) | ||
| 98 | +// ========================================================================================= | ||
| 99 | + | ||
| 100 | +/** | ||
| 101 | + * @brief Constructs the ParcelWrapper, obtaining two new Parcel objects from the pool. | ||
| 102 | + * @param env A valid JNI environment pointer. | ||
| 103 | + * @param bridge A pointer to the parent IPCBridge instance to access its cached JNI IDs. | ||
| 104 | + */ | ||
| 105 | +IPCBridge::ParcelWrapper::ParcelWrapper(JNIEnv *env, IPCBridge *bridge) | ||
| 106 | + : data(lsplant::JNI_CallStaticObjectMethod(env, bridge->parcel_class_, bridge->obtain_method_)), | ||
| 107 | + reply( | ||
| 108 | + lsplant::JNI_CallStaticObjectMethod(env, bridge->parcel_class_, bridge->obtain_method_)), | ||
| 109 | + env_(env), | ||
| 110 | + bridge_(bridge) {} | ||
| 111 | + | ||
| 112 | +/** | ||
| 113 | + * @brief Destructs the ParcelWrapper, ensuring both Parcel objects are recycled. | ||
| 114 | + * | ||
| 115 | + * This is the core of the RAII pattern for Parcels. | ||
| 116 | + * This destructor guarantees that recycle() is called, preventing resource leaks even if | ||
| 117 | + * errors occur during the transaction. | ||
| 118 | + */ | ||
| 119 | +IPCBridge::ParcelWrapper::~ParcelWrapper() { | ||
| 120 | + // Check if the parcel was successfully obtained before trying to recycle it. | ||
| 121 | + if (data) { | ||
| 122 | + lsplant::JNI_CallVoidMethod(env_, data.get(), bridge_->recycle_method_); | ||
| 123 | + } | ||
| 124 | + if (reply) { | ||
| 125 | + lsplant::JNI_CallVoidMethod(env_, reply.get(), bridge_->recycle_method_); | ||
| 126 | + } | ||
| 127 | +} | ||
| 128 | + | ||
| 129 | +// ========================================================================================= | ||
| 130 | +// IPCBridge Implementation | ||
| 131 | +// ========================================================================================= | ||
| 132 | + | ||
| 133 | +IPCBridge &IPCBridge::GetInstance() { | ||
| 134 | + static IPCBridge instance; | ||
| 135 | + return instance; | ||
| 136 | +} | ||
| 137 | + | ||
| 138 | +void IPCBridge::Initialize(JNIEnv *env) { | ||
| 139 | + if (initialized_) { | ||
| 140 | + return; | ||
| 141 | + } | ||
| 142 | + | ||
| 143 | + // --- Cache JNI Classes and Method IDs --- | ||
| 144 | + // Caching these at startup is more efficient and robust than looking them up on every IPC call. | ||
| 145 | + // If any of these fail, the IPC bridge is unusable. | ||
| 146 | + | ||
| 147 | + // ServiceManager | ||
| 148 | + auto sm_class = lsplant::ScopedLocalRef(env, env->FindClass("android/os/ServiceManager")); | ||
| 149 | + if (!sm_class) { | ||
| 150 | + LOGE("IPCBridge: ServiceManager class not found!"); | ||
| 151 | + return; | ||
| 152 | + } | ||
| 153 | + service_manager_class_ = (jclass)env->NewGlobalRef(sm_class.get()); | ||
| 154 | + get_service_method_ = lsplant::JNI_GetStaticMethodID( | ||
| 155 | + env, service_manager_class_, "getService", "(Ljava/lang/String;)Landroid/os/IBinder;"); | ||
| 156 | + if (!get_service_method_) { | ||
| 157 | + LOGE("IPCBridge: ServiceManager.getService method not found!"); | ||
| 158 | + return; | ||
| 159 | + } | ||
| 160 | + | ||
| 161 | + // IBinder | ||
| 162 | + auto ibinder_class = lsplant::ScopedLocalRef(env, env->FindClass("android/os/IBinder")); | ||
| 163 | + if (!ibinder_class) { | ||
| 164 | + LOGE("IPCBridge: IBinder class not found!"); | ||
| 165 | + return; | ||
| 166 | + } | ||
| 167 | + transact_method_ = lsplant::JNI_GetMethodID(env, ibinder_class.get(), "transact", | ||
| 168 | + "(ILandroid/os/Parcel;Landroid/os/Parcel;I)Z"); | ||
| 169 | + if (!transact_method_) { | ||
| 170 | + LOGE("IPCBridge: IBinder.transact method not found!"); | ||
| 171 | + return; | ||
| 172 | + } | ||
| 173 | + | ||
| 174 | + // Binder | ||
| 175 | + auto binder_class = lsplant::ScopedLocalRef(env, env->FindClass("android/os/Binder")); | ||
| 176 | + if (!binder_class) { | ||
| 177 | + LOGE("IPCBridge: Binder class not found!"); | ||
| 178 | + return; | ||
| 179 | + } | ||
| 180 | + binder_class_ = (jclass)env->NewGlobalRef(binder_class.get()); | ||
| 181 | + binder_ctor_ = lsplant::JNI_GetMethodID(env, binder_class_, "<init>", "()V"); | ||
| 182 | + if (!binder_ctor_) { | ||
| 183 | + LOGE("IPCBridge: Binder constructor not found!"); | ||
| 184 | + return; | ||
| 185 | + } | ||
| 186 | + | ||
| 187 | + // Parcel | ||
| 188 | + auto parcel_class = lsplant::ScopedLocalRef(env, env->FindClass("android/os/Parcel")); | ||
| 189 | + if (!parcel_class) { | ||
| 190 | + LOGE("IPCBridge: Parcel class not found!"); | ||
| 191 | + return; | ||
| 192 | + } | ||
| 193 | + parcel_class_ = (jclass)env->NewGlobalRef(parcel_class.get()); | ||
| 194 | + obtain_method_ = | ||
| 195 | + lsplant::JNI_GetStaticMethodID(env, parcel_class_, "obtain", "()Landroid/os/Parcel;"); | ||
| 196 | + recycle_method_ = lsplant::JNI_GetMethodID(env, parcel_class_, "recycle", "()V"); | ||
| 197 | + write_interface_token_method_ = lsplant::JNI_GetMethodID( | ||
| 198 | + env, parcel_class_, "writeInterfaceToken", "(Ljava/lang/String;)V"); | ||
| 199 | + write_int_method_ = lsplant::JNI_GetMethodID(env, parcel_class_, "writeInt", "(I)V"); | ||
| 200 | + write_string_method_ = | ||
| 201 | + lsplant::JNI_GetMethodID(env, parcel_class_, "writeString", "(Ljava/lang/String;)V"); | ||
| 202 | + write_strong_binder_method_ = lsplant::JNI_GetMethodID(env, parcel_class_, "writeStrongBinder", | ||
| 203 | + "(Landroid/os/IBinder;)V"); | ||
| 204 | + read_exception_method_ = lsplant::JNI_GetMethodID(env, parcel_class_, "readException", "()V"); | ||
| 205 | + read_strong_binder_method_ = | ||
| 206 | + lsplant::JNI_GetMethodID(env, parcel_class_, "readStrongBinder", "()Landroid/os/IBinder;"); | ||
| 207 | + read_file_descriptor_method_ = lsplant::JNI_GetMethodID( | ||
| 208 | + env, parcel_class_, "readFileDescriptor", "()Landroid/os/ParcelFileDescriptor;"); | ||
| 209 | + read_int_method_ = lsplant::JNI_GetMethodID(env, parcel_class_, "readInt", "()I"); | ||
| 210 | + read_long_method_ = lsplant::JNI_GetMethodID(env, parcel_class_, "readLong", "()J"); | ||
| 211 | + read_string_method_ = | ||
| 212 | + lsplant::JNI_GetMethodID(env, parcel_class_, "readString", "()Ljava/lang/String;"); | ||
| 213 | + | ||
| 214 | + // ParcelFileDescriptor | ||
| 215 | + auto pfd_class = | ||
| 216 | + lsplant::ScopedLocalRef(env, env->FindClass("android/os/ParcelFileDescriptor")); | ||
| 217 | + if (!pfd_class) { | ||
| 218 | + LOGE("IPCBridge: ParcelFileDescriptor class not found!"); | ||
| 219 | + return; | ||
| 220 | + } | ||
| 221 | + parcel_fd_class_ = (jclass)env->NewGlobalRef(pfd_class.get()); | ||
| 222 | + detach_fd_method_ = lsplant::JNI_GetMethodID(env, parcel_fd_class_, "detachFd", "()I"); | ||
| 223 | + if (!detach_fd_method_) { | ||
| 224 | + LOGE("IPCBridge: ParcelFileDescriptor.detachFd method not found!"); | ||
| 225 | + return; | ||
| 226 | + } | ||
| 227 | + | ||
| 228 | + LOGV("IPCBridge initialized successfully."); | ||
| 229 | + initialized_ = true; | ||
| 230 | +} | ||
| 231 | + | ||
| 232 | +lsplant::ScopedLocalRef<jobject> IPCBridge::RequestAppBinder(JNIEnv *env, jstring nice_name) { | ||
| 233 | + if (!initialized_) { | ||
| 234 | + LOGE("RequestAppBinder failed: IPCBridge not initialized."); | ||
| 235 | + return {env, nullptr}; | ||
| 236 | + } | ||
| 237 | + | ||
| 238 | + // Get the rendezvous service from the Android ServiceManager. | ||
| 239 | + auto service_name = lsplant::ScopedLocalRef(env, env->NewStringUTF(kBridgeServiceName.data())); | ||
| 240 | + auto bridge_service = lsplant::JNI_CallStaticObjectMethod( | ||
| 241 | + env, service_manager_class_, get_service_method_, service_name.get()); | ||
| 242 | + if (!bridge_service) { | ||
| 243 | + LOGE("Could not get rendezvous service '{}'. Manager not available?", | ||
| 244 | + kBridgeServiceName.data()); | ||
| 245 | + return {env, nullptr}; | ||
| 246 | + } | ||
| 247 | + | ||
| 248 | + // Prepare the IPC transaction. | ||
| 249 | + ParcelWrapper parcels(env, this); | ||
| 250 | + if (!parcels.data || !parcels.reply) { | ||
| 251 | + LOGE("Failed to obtain parcels for IPC."); | ||
| 252 | + return {env, nullptr}; | ||
| 253 | + } | ||
| 254 | + | ||
| 255 | + // This is a "heartbeat" binder. | ||
| 256 | + // If our process dies, the manager service will be notified that this binder has died, | ||
| 257 | + // allowing it to clean up resources. | ||
| 258 | + auto heartbeat_binder = | ||
| 259 | + lsplant::ScopedLocalRef(env, env->NewObject(binder_class_, binder_ctor_)); | ||
| 260 | + if (!heartbeat_binder) { | ||
| 261 | + LOGE("Failed to create heartbeat binder."); | ||
| 262 | + return {env, nullptr}; | ||
| 263 | + } | ||
| 264 | + | ||
| 265 | + // Write the request data to the 'data' parcel. | ||
| 266 | + lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_int_method_, kActionGetBinder); | ||
| 267 | + lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_string_method_, nice_name); | ||
| 268 | + lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_strong_binder_method_, | ||
| 269 | + heartbeat_binder.get()); | ||
| 270 | + | ||
| 271 | + // Perform the transaction. | ||
| 272 | + bool success = lsplant::JNI_CallBooleanMethod(env, bridge_service.get(), transact_method_, | ||
| 273 | + kBridgeTransactionCode, parcels.data.get(), | ||
| 274 | + parcels.reply.get(), 0); | ||
| 275 | + | ||
| 276 | + lsplant::ScopedLocalRef<jobject> result_binder = {env, nullptr}; | ||
| 277 | + if (success) { | ||
| 278 | + // Read the reply. CRITICAL: must call readException first. | ||
| 279 | + lsplant::JNI_CallVoidMethod(env, parcels.reply.get(), read_exception_method_); | ||
| 280 | + if (env->ExceptionCheck()) { | ||
| 281 | + LOGW("Remote exception received while requesting app binder."); | ||
| 282 | + env->ExceptionClear(); | ||
| 283 | + } else { | ||
| 284 | + result_binder = | ||
| 285 | + lsplant::JNI_CallObjectMethod(env, parcels.reply.get(), read_strong_binder_method_); | ||
| 286 | + if (result_binder) { | ||
| 287 | + // IMPORTANT: Keep the heartbeat binder alive by making it a global ref. | ||
| 288 | + // If we don't do this, it gets garbage collected and the remote service | ||
| 289 | + // thinks our process has died. | ||
| 290 | + env->NewGlobalRef(heartbeat_binder.get()); | ||
| 291 | + } | ||
| 292 | + } | ||
| 293 | + } else { | ||
| 294 | + LOGD("Transact call to request app binder failed."); | ||
| 295 | + } | ||
| 296 | + | ||
| 297 | + return result_binder; | ||
| 298 | +} | ||
| 299 | + | ||
| 300 | +lsplant::ScopedLocalRef<jobject> IPCBridge::RequestSystemServerBinder( | ||
| 301 | + JNIEnv *env, std::string bridgeServiceName) { | ||
| 302 | + if (!initialized_) { | ||
| 303 | + LOGE("RequestSystemServerBinder failed: IPCBridge not initialized."); | ||
| 304 | + return {env, nullptr}; | ||
| 305 | + } | ||
| 306 | + | ||
| 307 | + auto service_name = lsplant::ScopedLocalRef(env, env->NewStringUTF(bridgeServiceName.data())); | ||
| 308 | + lsplant::ScopedLocalRef<jobject> binder = {env, nullptr}; | ||
| 309 | + | ||
| 310 | + // The daemon process and system_server specialization run in parallel. | ||
| 311 | + // On slower devices, the daemon may take several seconds to call addService. | ||
| 312 | + // We poll for up to 10 seconds to ensure the early IPC channel for system_server is available. | ||
| 313 | + const int max_retry = 10; | ||
| 314 | + for (int i = 0; i < max_retry; ++i) { | ||
| 315 | + binder = lsplant::JNI_CallStaticObjectMethod(env, service_manager_class_, | ||
| 316 | + get_service_method_, service_name.get()); | ||
| 317 | + if (binder) { | ||
| 318 | + LOGI("Got system server binder via {} on attempt {}.", bridgeServiceName.data(), i + 1); | ||
| 319 | + return binder; | ||
| 320 | + } | ||
| 321 | + LOGW("Failed to get system server binder via {}, will retry in 1 second...", | ||
| 322 | + bridgeServiceName.data()); | ||
| 323 | + std::this_thread::sleep_for(std::chrono::seconds(1)); | ||
| 324 | + } | ||
| 325 | + | ||
| 326 | + LOGE("Failed to get system server binder after {} attempts. Aborting.", max_retry); | ||
| 327 | + return {env, nullptr}; | ||
| 328 | +} | ||
| 329 | + | ||
| 330 | +lsplant::ScopedLocalRef<jobject> IPCBridge::RequestManagerBinderFromSystemServer( | ||
| 331 | + JNIEnv *env, jobject system_server_binder) { | ||
| 332 | + ParcelWrapper parcels(env, this); | ||
| 333 | + auto heartbeat_binder = | ||
| 334 | + lsplant::ScopedLocalRef(env, env->NewObject(binder_class_, binder_ctor_)); | ||
| 335 | + auto system_name = lsplant::ScopedLocalRef(env, env->NewStringUTF("system")); | ||
| 336 | + | ||
| 337 | + // Populate the request | ||
| 338 | + lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_int_method_, getuid()); | ||
| 339 | + lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_int_method_, getpid()); | ||
| 340 | + lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_string_method_, system_name.get()); | ||
| 341 | + lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_strong_binder_method_, | ||
| 342 | + heartbeat_binder.get()); | ||
| 343 | + | ||
| 344 | + // Transact | ||
| 345 | + bool success = lsplant::JNI_CallBooleanMethod(env, system_server_binder, transact_method_, | ||
| 346 | + kBridgeTransactionCode, parcels.data.get(), | ||
| 347 | + parcels.reply.get(), 0); | ||
| 348 | + | ||
| 349 | + lsplant::ScopedLocalRef<jobject> result_binder = {env, nullptr}; | ||
| 350 | + if (success) { | ||
| 351 | + lsplant::JNI_CallVoidMethod(env, parcels.reply.get(), read_exception_method_); | ||
| 352 | + if (env->ExceptionCheck()) { | ||
| 353 | + LOGW("Remote exception while getting manager binder from system_server."); | ||
| 354 | + env->ExceptionClear(); | ||
| 355 | + } else { | ||
| 356 | + result_binder = | ||
| 357 | + lsplant::JNI_CallObjectMethod(env, parcels.reply.get(), read_strong_binder_method_); | ||
| 358 | + if (result_binder) { | ||
| 359 | + env->NewGlobalRef(heartbeat_binder.get()); // Keep heartbeat alive | ||
| 360 | + } | ||
| 361 | + } | ||
| 362 | + } | ||
| 363 | + LOGD("Manager binder from system_server: {}", static_cast<void *>(result_binder.get())); | ||
| 364 | + return result_binder; | ||
| 365 | +} | ||
| 366 | + | ||
| 367 | +std::tuple<int, size_t> IPCBridge::FetchFrameworkDex(JNIEnv *env, jobject binder) { | ||
| 368 | + if (!initialized_ || !binder) { | ||
| 369 | + return {-1, 0}; | ||
| 370 | + } | ||
| 371 | + | ||
| 372 | + ParcelWrapper parcels(env, this); | ||
| 373 | + bool success = | ||
| 374 | + lsplant::JNI_CallBooleanMethod(env, binder, transact_method_, kDexTransactionCode, | ||
| 375 | + parcels.data.get(), parcels.reply.get(), 0); | ||
| 376 | + | ||
| 377 | + if (!success) { | ||
| 378 | + LOGE("DEX fetch transaction failed."); | ||
| 379 | + return {-1, 0}; | ||
| 380 | + } | ||
| 381 | + | ||
| 382 | + lsplant::JNI_CallVoidMethod(env, parcels.reply.get(), read_exception_method_); | ||
| 383 | + if (env->ExceptionCheck()) { | ||
| 384 | + LOGE("Remote exception received while fetching DEX."); | ||
| 385 | + env->ExceptionClear(); | ||
| 386 | + return {-1, 0}; | ||
| 387 | + } | ||
| 388 | + | ||
| 389 | + auto pfd = | ||
| 390 | + lsplant::JNI_CallObjectMethod(env, parcels.reply.get(), read_file_descriptor_method_); | ||
| 391 | + if (!pfd) { | ||
| 392 | + LOGE("Received null ParcelFileDescriptor for DEX."); | ||
| 393 | + return {-1, 0}; | ||
| 394 | + } | ||
| 395 | + | ||
| 396 | + int fd = lsplant::JNI_CallIntMethod(env, pfd.get(), detach_fd_method_); | ||
| 397 | + size_t size = static_cast<size_t>( | ||
| 398 | + lsplant::JNI_CallLongMethod(env, parcels.reply.get(), read_long_method_)); | ||
| 399 | + | ||
| 400 | + LOGV("Fetched framework DEX: fd={}, size={}", fd, size); | ||
| 401 | + return {fd, size}; | ||
| 402 | +} | ||
| 403 | + | ||
| 404 | +std::map<std::string, std::string> IPCBridge::FetchObfuscationMap(JNIEnv *env, jobject binder) { | ||
| 405 | + std::map<std::string, std::string> result_map; | ||
| 406 | + if (!initialized_ || !binder) { | ||
| 407 | + return result_map; | ||
| 408 | + } | ||
| 409 | + | ||
| 410 | + ParcelWrapper parcels(env, this); | ||
| 411 | + bool success = lsplant::JNI_CallBooleanMethod(env, binder, transact_method_, | ||
| 412 | + kObfuscationMapTransactionCode, | ||
| 413 | + parcels.data.get(), parcels.reply.get(), 0); | ||
| 414 | + | ||
| 415 | + if (!success) { | ||
| 416 | + LOGE("Obfuscation map fetch transaction failed."); | ||
| 417 | + return result_map; | ||
| 418 | + } | ||
| 419 | + | ||
| 420 | + lsplant::JNI_CallVoidMethod(env, parcels.reply.get(), read_exception_method_); | ||
| 421 | + if (env->ExceptionCheck()) { | ||
| 422 | + LOGE("Remote exception received while fetching obfuscation map."); | ||
| 423 | + env->ExceptionClear(); | ||
| 424 | + return result_map; | ||
| 425 | + } | ||
| 426 | + | ||
| 427 | + int size = lsplant::JNI_CallIntMethod(env, parcels.reply.get(), read_int_method_); | ||
| 428 | + if (size < 0 || (size % 2 != 0)) { | ||
| 429 | + LOGE("Invalid size for obfuscation map received: %d", size); | ||
| 430 | + return result_map; | ||
| 431 | + } | ||
| 432 | + | ||
| 433 | + for (int i = 0; i < size / 2; ++i) { | ||
| 434 | + auto key_jstr = lsplant::JNI_Cast<jstring>( | ||
| 435 | + lsplant::JNI_CallObjectMethod(env, parcels.reply.get(), read_string_method_)); | ||
| 436 | + | ||
| 437 | + auto val_jstr = lsplant::JNI_Cast<jstring>( | ||
| 438 | + lsplant::JNI_CallObjectMethod(env, parcels.reply.get(), read_string_method_)); | ||
| 439 | + | ||
| 440 | + if (env->ExceptionCheck() || !key_jstr || !val_jstr) { | ||
| 441 | + LOGE("Error reading string from parcel for obfuscation map."); | ||
| 442 | + env->ExceptionClear(); | ||
| 443 | + result_map.clear(); // Return an empty map on error | ||
| 444 | + return result_map; | ||
| 445 | + } | ||
| 446 | + | ||
| 447 | + lsplant::JUTFString key_str(env, key_jstr.get()); | ||
| 448 | + lsplant::JUTFString val_str(env, val_jstr.get()); | ||
| 449 | + result_map[key_str.get()] = val_str.get(); | ||
| 450 | + } | ||
| 451 | + | ||
| 452 | + LOGV("Fetched obfuscation map with {} entries.", result_map.size()); | ||
| 453 | + return result_map; | ||
| 454 | +} | ||
| 455 | + | ||
| 456 | +jboolean IPCBridge::ExecTransact_Replace(jboolean *res, JNIEnv *env, jobject obj, va_list args) { | ||
| 457 | + va_list copy; | ||
| 458 | + va_copy(copy, args); | ||
| 459 | + // Extract arguments from the va_list for Binder.execTransact(int, long, long, int) | ||
| 460 | + auto code = va_arg(copy, jint); | ||
| 461 | + auto data_obj = va_arg(copy, jlong); | ||
| 462 | + auto reply_obj = va_arg(copy, jlong); | ||
| 463 | + auto flags = va_arg(copy, jint); | ||
| 464 | + va_end(copy); | ||
| 465 | + | ||
| 466 | + // If the transaction code matches our special code, intercept it. | ||
| 467 | + if (code == kBridgeTransactionCode) { | ||
| 468 | + // Call the static Java method in our framework's BridgeService to handle the call. | ||
| 469 | + *res = env->CallStaticBooleanMethod(GetInstance().bridge_service_class_, | ||
| 470 | + GetInstance().exec_transact_replace_method_id_, obj, | ||
| 471 | + code, data_obj, reply_obj, flags); | ||
| 472 | + if (env->ExceptionCheck()) { | ||
| 473 | + LOGW("Exception in Java BridgeService.execTransact handler."); | ||
| 474 | + env->ExceptionClear(); | ||
| 475 | + } | ||
| 476 | + if (*res == JNI_FALSE) { | ||
| 477 | + uint64_t caller_id = BinderCaller::GetId(); | ||
| 478 | + if (caller_id != 0) { | ||
| 479 | + // LOGV("Caller {} rejected by bridge service.", caller_id); | ||
| 480 | + g_last_failed_id.store(caller_id, std::memory_order_relaxed); | ||
| 481 | + } | ||
| 482 | + } else { | ||
| 483 | + g_last_failed_id.store(~0, std::memory_order_relaxed); | ||
| 484 | + } | ||
| 485 | + return true; // Return true to indicate we handled the call. | ||
| 486 | + } | ||
| 487 | + return false; // Not our transaction, let the original method run. | ||
| 488 | +} | ||
| 489 | + | ||
| 490 | +jboolean JNICALL IPCBridge::CallBooleanMethodV_Hook(JNIEnv *env, jobject obj, jmethodID methodId, | ||
| 491 | + va_list args) { | ||
| 492 | + // Check if the method being called is the one we want to intercept: Binder.execTransact() | ||
| 493 | + if (methodId == GetInstance().exec_transact_backup_method_id_) { | ||
| 494 | + uint64_t current_caller_id = BinderCaller::GetId(); | ||
| 495 | + | ||
| 496 | + jboolean res = false; | ||
| 497 | + // Attempt to handle the transaction with our replacement logic. | ||
| 498 | + if (current_caller_id != 0 && | ||
| 499 | + // If this caller is the one that just failed, | ||
| 500 | + // skip interception and go straight to the original function. | ||
| 501 | + current_caller_id != g_last_failed_id.load(std::memory_order_relaxed) && | ||
| 502 | + ExecTransact_Replace(&res, env, obj, args)) { | ||
| 503 | + return res; // If we handled it, return the result directly. | ||
| 504 | + } | ||
| 505 | + // If not handled, fall through to call the original method. | ||
| 506 | + } | ||
| 507 | + // Call the original, real CallBooleanMethodV function. | ||
| 508 | + return GetInstance().call_boolean_method_v_backup_(env, obj, methodId, args); | ||
| 509 | +} | ||
| 510 | + | ||
| 511 | +void IPCBridge::HookBridge(JNIEnv *env) { | ||
| 512 | + if (!initialized_) { | ||
| 513 | + LOGE("Cannot hook bridge: IPCBridge is not initialized."); | ||
| 514 | + return; | ||
| 515 | + } | ||
| 516 | + | ||
| 517 | + // Get framework-specific Java classes and methods --- | ||
| 518 | + const auto &obfs_map = ConfigBridge::GetInstance()->obfuscation_map(); | ||
| 519 | + std::string bridge_service_class_name; | ||
| 520 | + bridge_service_class_name = obfs_map.at("org.matrix.vector.service.") + "BridgeService"; | ||
| 521 | + | ||
| 522 | + auto bridge_class_ref = | ||
| 523 | + Context::GetInstance()->FindClassFromCurrentLoader(env, bridge_service_class_name); | ||
| 524 | + if (!bridge_class_ref) { | ||
| 525 | + LOGE("Failed to find BridgeService class '{}'", bridge_service_class_name.c_str()); | ||
| 526 | + return; | ||
| 527 | + } | ||
| 528 | + bridge_service_class_ = lsplant::JNI_NewGlobalRef(env, bridge_class_ref); | ||
| 529 | + | ||
| 530 | + exec_transact_replace_method_id_ = lsplant::JNI_GetStaticMethodID( | ||
| 531 | + env, bridge_service_class_, "execTransact", "(Landroid/os/IBinder;IJJI)Z"); | ||
| 532 | + if (!exec_transact_replace_method_id_) { | ||
| 533 | + LOGE("Failed to find static method BridgeService.execTransact!"); | ||
| 534 | + return; | ||
| 535 | + } | ||
| 536 | + | ||
| 537 | + // --- Prepare the JNI hook --- | ||
| 538 | + // Get the original method ID for android.os.Binder.execTransact | ||
| 539 | + exec_transact_backup_method_id_ = | ||
| 540 | + lsplant::JNI_GetMethodID(env, binder_class_, "execTransact", "(IJJI)Z"); | ||
| 541 | + if (!exec_transact_backup_method_id_) { | ||
| 542 | + LOGE("Failed to find original method Binder.execTransact!"); | ||
| 543 | + return; | ||
| 544 | + } | ||
| 545 | + | ||
| 546 | + // Use the native library's API to get the JNI table override function. | ||
| 547 | + auto set_table_override_func = | ||
| 548 | + (void (*)(const JNINativeInterface *))ElfSymbolCache::GetArt()->getSymbAddress( | ||
| 549 | + "_ZN3art9JNIEnvExt16SetTableOverrideEPK18JNINativeInterface"); | ||
| 550 | + if (!set_table_override_func) { | ||
| 551 | + LOGE("Failed to find ART symbol SetTableOverride!"); | ||
| 552 | + return; | ||
| 553 | + } | ||
| 554 | + | ||
| 555 | + // --- Step 3: Install the hook --- | ||
| 556 | + // Get the ACTUAL runtime size of the JNI function table. | ||
| 557 | + // Android 16 ART adds new JNI functions that increase the table size. | ||
| 558 | + // Using compile-time sizeof(JNINativeInterface) truncates the copy, | ||
| 559 | + // causing crashes when ART calls new functions via garbage pointers. | ||
| 560 | + table_size_ = (size_t)env->functions->reserved0; | ||
| 561 | + if (table_size_ == 0 || table_size_ < sizeof(JNINativeInterface)) { | ||
| 562 | + table_size_ = sizeof(JNINativeInterface); | ||
| 563 | + } | ||
| 564 | + LOGI("JNI table runtime size: %zu (compile-time: %zu)", table_size_, sizeof(JNINativeInterface)); | ||
| 565 | + | ||
| 566 | + // Allocate and copy with runtime size | ||
| 567 | + native_interface_hook_ = (JNINativeInterface*)malloc(table_size_); | ||
| 568 | + if (!native_interface_hook_) { | ||
| 569 | + LOGE("Failed to allocate JNI hook table."); | ||
| 570 | + return; | ||
| 571 | + } | ||
| 572 | + memcpy(native_interface_hook_, env->functions, table_size_); | ||
| 573 | + | ||
| 574 | + // Store a pointer to the original function we are about to replace. | ||
| 575 | + call_boolean_method_v_backup_ = native_interface_hook_->CallBooleanMethodV; | ||
| 576 | + | ||
| 577 | + // Overwrite the function pointer in our copy with our hook. | ||
| 578 | + native_interface_hook_->CallBooleanMethodV = &CallBooleanMethodV_Hook; | ||
| 579 | + | ||
| 580 | + // Atomically swap the process's JNI function table with our modified one. | ||
| 581 | + set_table_override_func(native_interface_hook_); | ||
| 582 | + | ||
| 583 | + BinderCaller::Initialize(); | ||
| 584 | + | ||
| 585 | + LOGI("IPC Bridge JNI hook installed successfully."); | ||
| 586 | +} | ||
| 587 | + | ||
| 588 | +} // namespace vector::native::module |
xhs_lsposed_a16_fix/ipc_bridge.h
0 → 100644
| 1 | +#pragma once | ||
| 2 | + | ||
| 3 | +#include <jni.h> | ||
| 4 | + | ||
| 5 | +#include <map> | ||
| 6 | +#include <string> | ||
| 7 | +#include <tuple> | ||
| 8 | + | ||
| 9 | +// This module is a client of the 'native' library. | ||
| 10 | +// We include the JNI bridge from 'native' to leverage its helper functions. | ||
| 11 | +#include <jni/jni_bridge.h> | ||
| 12 | + | ||
| 13 | +namespace vector::native::module { | ||
| 14 | + | ||
| 15 | +/** | ||
| 16 | + * @class IPCBridge | ||
| 17 | + * @brief Manages Binder IPC communication with the Vector host service. | ||
| 18 | + * | ||
| 19 | + * This singleton class is the communication arm of the Zygisk module. Its key responsibilities are: | ||
| 20 | + * 1. Discovering and connecting to the central host service (the "manager"). | ||
| 21 | + * 2. Requesting the framework's DEX file and obfuscation map from the service. | ||
| 22 | + * 3. Caching all necessary JNI class and method IDs for efficient reuse. | ||
| 23 | + */ | ||
| 24 | +class IPCBridge { | ||
| 25 | +public: | ||
| 26 | + // Enforce singleton pattern. | ||
| 27 | + IPCBridge(const IPCBridge &) = delete; | ||
| 28 | + IPCBridge &operator=(const IPCBridge &) = delete; | ||
| 29 | + | ||
| 30 | + /** | ||
| 31 | + * @brief Gets the singleton instance of the IPCBridge. | ||
| 32 | + */ | ||
| 33 | + static IPCBridge &GetInstance(); | ||
| 34 | + | ||
| 35 | + /** | ||
| 36 | + * @brief Caches JNI class and method IDs needed for Binder communication. | ||
| 37 | + * @param env A valid JNI environment pointer. | ||
| 38 | + */ | ||
| 39 | + void Initialize(JNIEnv *env); | ||
| 40 | + | ||
| 41 | + /** | ||
| 42 | + * @brief Requests an application-specific Binder from the host service. | ||
| 43 | + * @param env JNI environment pointer. | ||
| 44 | + * @param nice_name The process name. | ||
| 45 | + * @return A ScopedLocalRef to the Binder object, or nullptr on failure. | ||
| 46 | + */ | ||
| 47 | + lsplant::ScopedLocalRef<jobject> RequestAppBinder(JNIEnv *env, jstring nice_name); | ||
| 48 | + | ||
| 49 | + /** | ||
| 50 | + * @brief Requests the system_server's dedicated Binder from the host service. | ||
| 51 | + * @param env JNI environment pointer. | ||
| 52 | + * @param bridgeServiceName rendezvous point used by the system_server | ||
| 53 | + * @return A ScopedLocalRef to the Binder object, or nullptr on failure. | ||
| 54 | + */ | ||
| 55 | + lsplant::ScopedLocalRef<jobject> RequestSystemServerBinder(JNIEnv *env, | ||
| 56 | + std::string bridgeServiceName); | ||
| 57 | + | ||
| 58 | + /** | ||
| 59 | + * @brief Asks the system_server binder for the application manager binder. | ||
| 60 | + * @param env JNI environment pointer. | ||
| 61 | + * @param system_server_binder The binder connected to the system server service. | ||
| 62 | + * @return A ScopedLocalRef to the application manager Binder, or nullptr on failure. | ||
| 63 | + */ | ||
| 64 | + lsplant::ScopedLocalRef<jobject> RequestManagerBinderFromSystemServer( | ||
| 65 | + JNIEnv *env, jobject system_server_binder); | ||
| 66 | + | ||
| 67 | + /** | ||
| 68 | + * @brief Fetches the framework DEX file via the provided Binder connection. | ||
| 69 | + * @param env JNI environment pointer. | ||
| 70 | + * @param binder A live Binder connection to the host service. | ||
| 71 | + * @return A tuple containing the file descriptor and size of the DEX file. | ||
| 72 | + * Returns {-1, 0} on failure. | ||
| 73 | + */ | ||
| 74 | + std::tuple<int, size_t> FetchFrameworkDex(JNIEnv *env, jobject binder); | ||
| 75 | + | ||
| 76 | + /** | ||
| 77 | + * @brief Fetches the framework's obfuscation map via the provided Binder. | ||
| 78 | + * @param env JNI environment pointer. | ||
| 79 | + * @param binder A live Binder connection to the host service. | ||
| 80 | + * @return A map of original names to obfuscated names. | ||
| 81 | + */ | ||
| 82 | + std::map<std::string, std::string> FetchObfuscationMap(JNIEnv *env, jobject binder); | ||
| 83 | + | ||
| 84 | + /** | ||
| 85 | + * @brief Sets up the JNI hook to intercept Binder transactions. | ||
| 86 | + * | ||
| 87 | + * This is the core of the IPC interception mechanism. | ||
| 88 | + * It replaces the JNI function pointer for CallBooleanMethodV to | ||
| 89 | + * inspect calls to Binder.execTransact, allowing the framework to | ||
| 90 | + * handle its own custom transaction codes directly. | ||
| 91 | + * @param env JNI environment pointer. | ||
| 92 | + */ | ||
| 93 | + void HookBridge(JNIEnv *env); | ||
| 94 | + | ||
| 95 | +private: | ||
| 96 | + /** | ||
| 97 | + * @class ParcelWrapper | ||
| 98 | + * @brief A private RAII wrapper to ensure Parcel objects are always recycled. | ||
| 99 | + * | ||
| 100 | + * As a private nested class, it has access to the private members of IPCBridge | ||
| 101 | + * (like parcel_class_ and recycle_method_). | ||
| 102 | + */ | ||
| 103 | + class ParcelWrapper { | ||
| 104 | + public: | ||
| 105 | + ParcelWrapper(JNIEnv *env, IPCBridge *bridge); | ||
| 106 | + ~ParcelWrapper(); | ||
| 107 | + // Disable copy operations | ||
| 108 | + ParcelWrapper(const ParcelWrapper &) = delete; | ||
| 109 | + ParcelWrapper &operator=(const ParcelWrapper &) = delete; | ||
| 110 | + | ||
| 111 | + lsplant::ScopedLocalRef<jobject> data; | ||
| 112 | + lsplant::ScopedLocalRef<jobject> reply; | ||
| 113 | + | ||
| 114 | + private: | ||
| 115 | + JNIEnv *env_; | ||
| 116 | + IPCBridge *bridge_; | ||
| 117 | + }; | ||
| 118 | + | ||
| 119 | + // Private constructor for singleton. | ||
| 120 | + IPCBridge() = default; | ||
| 121 | + | ||
| 122 | + bool initialized_ = false; | ||
| 123 | + | ||
| 124 | + // --- Cached JNI References --- | ||
| 125 | + // These are initialized once and stored as global references for performance. | ||
| 126 | + | ||
| 127 | + // android.os.ServiceManager | ||
| 128 | + jclass service_manager_class_ = nullptr; | ||
| 129 | + jmethodID get_service_method_ = nullptr; | ||
| 130 | + | ||
| 131 | + // android.os.IBinder | ||
| 132 | + jmethodID transact_method_ = nullptr; | ||
| 133 | + | ||
| 134 | + // android.os.Binder | ||
| 135 | + jclass binder_class_ = nullptr; | ||
| 136 | + jmethodID binder_ctor_ = nullptr; | ||
| 137 | + | ||
| 138 | + // android.os.Parcel | ||
| 139 | + jclass parcel_class_ = nullptr; | ||
| 140 | + jmethodID obtain_method_ = nullptr; | ||
| 141 | + jmethodID recycle_method_ = nullptr; | ||
| 142 | + jmethodID write_interface_token_method_ = nullptr; | ||
| 143 | + jmethodID write_int_method_ = nullptr; | ||
| 144 | + jmethodID write_string_method_ = nullptr; | ||
| 145 | + jmethodID write_strong_binder_method_ = nullptr; | ||
| 146 | + jmethodID read_exception_method_ = nullptr; | ||
| 147 | + jmethodID read_strong_binder_method_ = nullptr; | ||
| 148 | + jmethodID read_file_descriptor_method_ = nullptr; | ||
| 149 | + jmethodID read_int_method_ = nullptr; | ||
| 150 | + jmethodID read_long_method_ = nullptr; | ||
| 151 | + jmethodID read_string_method_ = nullptr; | ||
| 152 | + | ||
| 153 | + // android.os.ParcelFileDescriptor | ||
| 154 | + jclass parcel_fd_class_ = nullptr; | ||
| 155 | + jmethodID detach_fd_method_ = nullptr; | ||
| 156 | + | ||
| 157 | + // --- JNI Hooking Members --- | ||
| 158 | + // These are required to store the state for our JNI function table override. | ||
| 159 | + | ||
| 160 | + // The C++ hook function that will replace the original CallBooleanMethodV. | ||
| 161 | + static jboolean JNICALL CallBooleanMethodV_Hook(JNIEnv *env, jobject obj, jmethodID methodId, | ||
| 162 | + va_list args); | ||
| 163 | + // The helper function that handles our specific transaction code. | ||
| 164 | + static jboolean ExecTransact_Replace(jboolean *res, JNIEnv *env, jobject obj, va_list args); | ||
| 165 | + | ||
| 166 | + // A complete copy of the original JNI function table, with our hook installed. | ||
| 167 | + // Uses runtime-size allocation to support A16+ where ART added new JNI functions. | ||
| 168 | + JNINativeInterface* native_interface_hook_ = nullptr; | ||
| 169 | + size_t table_size_ = 0; | ||
| 170 | + // The original jmethodID for android.os.Binder.execTransact(). | ||
| 171 | + jmethodID exec_transact_backup_method_id_ = nullptr; | ||
| 172 | + // A function pointer to the original CallBooleanMethodV implementation. | ||
| 173 | + jboolean (*call_boolean_method_v_backup_)(JNIEnv *, jobject, jmethodID, va_list) = nullptr; | ||
| 174 | + | ||
| 175 | + // A global reference to the framework's Java BridgeService class. | ||
| 176 | + jclass bridge_service_class_ = nullptr; | ||
| 177 | + // The jmethodID for the static Java method that handles the intercepted transaction. | ||
| 178 | + jmethodID exec_transact_replace_method_id_ = nullptr; | ||
| 179 | +}; | ||
| 180 | + | ||
| 181 | +} // namespace vector::native::module |
xhs_lsposed_a16_fix/rebuild_xhshook.sh
0 → 100755
| 1 | +#!/bin/bash | ||
| 2 | +# rebuild_xhshook.sh — 在 Mac 上重新编译 xhshook,产物写到本目录 | ||
| 3 | +set -e | ||
| 4 | + | ||
| 5 | +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" | ||
| 6 | +ANDROID_HOME="${ANDROID_HOME:-/opt/homebrew/share/android-commandlinetools}" | ||
| 7 | +ANDROID_JAR="$ANDROID_HOME/platforms/android-36/android.jar" | ||
| 8 | +D8="$ANDROID_HOME/build-tools/36.0.0/d8" | ||
| 9 | +APKSIGNER="$ANDROID_HOME/build-tools/36.0.0/apksigner" | ||
| 10 | +LSPOSED_DIR="${LSPOSED_DIR:-/tmp/LSPosed}" | ||
| 11 | +LEGACY_JAR="$LSPOSED_DIR/legacy/build/intermediates/compile_library_classes_jar/release/bundleLibCompileToJarRelease/classes.jar" | ||
| 12 | + | ||
| 13 | +# Fallback: download Xposed API from maven if LSPosed not built | ||
| 14 | +if [ ! -f "$LEGACY_JAR" ]; then | ||
| 15 | + echo "LSPosed legacy JAR not found at $LEGACY_JAR" | ||
| 16 | + echo "Run: git clone --depth 1 --recursive https://github.com/JingMatrix/LSPosed.git $LSPOSED_DIR && cd $LSPOSED_DIR && ./gradlew :legacy:assembleRelease" | ||
| 17 | + exit 1 | ||
| 18 | +fi | ||
| 19 | + | ||
| 20 | +BUILD_DIR=/tmp/xhshook_rebuild | ||
| 21 | +mkdir -p "$BUILD_DIR/compiled" "$BUILD_DIR/dex" "$BUILD_DIR/pack/assets" | ||
| 22 | + | ||
| 23 | +echo "== Compiling Hook.java ==" | ||
| 24 | +javac --release 11 \ | ||
| 25 | + -classpath "$ANDROID_JAR:$LEGACY_JAR" \ | ||
| 26 | + -d "$BUILD_DIR/compiled" \ | ||
| 27 | + "$SCRIPT_DIR/xhshook_src/Hook.java" | ||
| 28 | + | ||
| 29 | +echo "== Converting to DEX ==" | ||
| 30 | +$D8 \ | ||
| 31 | + --classpath "$ANDROID_JAR" \ | ||
| 32 | + --classpath "$LEGACY_JAR" \ | ||
| 33 | + --min-api 27 \ | ||
| 34 | + --output "$BUILD_DIR/dex" \ | ||
| 35 | + "$BUILD_DIR/compiled/com/hermes/xhshook/Hook.class" \ | ||
| 36 | + "$BUILD_DIR/compiled/com/hermes/xhshook/Hook\$1.class" | ||
| 37 | + | ||
| 38 | +echo "== Packing APK ==" | ||
| 39 | +cp "$SCRIPT_DIR/xhshook_src/AndroidManifest.xml" "$BUILD_DIR/pack/" 2>/dev/null || \ | ||
| 40 | + cp /tmp/xhshook_extracted/AndroidManifest.xml "$BUILD_DIR/pack/" | ||
| 41 | +cp "$BUILD_DIR/dex/classes.dex" "$BUILD_DIR/pack/" | ||
| 42 | +echo "com.hermes.xhshook.Hook" > "$BUILD_DIR/pack/assets/xposed_init" | ||
| 43 | +cd "$BUILD_DIR/pack" | ||
| 44 | +zip -0 "$BUILD_DIR/xhshook.apk" AndroidManifest.xml classes.dex assets/xposed_init | ||
| 45 | + | ||
| 46 | +# Create keystore if needed | ||
| 47 | +if [ ! -f /tmp/debug.keystore ]; then | ||
| 48 | + keytool -genkey -v -keystore /tmp/debug.keystore -alias androiddebugkey \ | ||
| 49 | + -keyalg RSA -keysize 2048 -validity 10000 \ | ||
| 50 | + -storepass android -keypass android \ | ||
| 51 | + -dname "CN=Android Debug,O=Android,C=US" 2>/dev/null | ||
| 52 | +fi | ||
| 53 | + | ||
| 54 | +echo "== Signing ==" | ||
| 55 | +$APKSIGNER sign \ | ||
| 56 | + --ks /tmp/debug.keystore \ | ||
| 57 | + --ks-key-alias androiddebugkey \ | ||
| 58 | + --ks-pass pass:android \ | ||
| 59 | + --key-pass pass:android \ | ||
| 60 | + --out "$SCRIPT_DIR/xhshook_v2_signed.apk" \ | ||
| 61 | + "$BUILD_DIR/xhshook.apk" 2>/dev/null | ||
| 62 | + | ||
| 63 | +echo "== Done: $SCRIPT_DIR/xhshook_v2_signed.apk ==" | ||
| 64 | +echo "APK size: $(wc -c < "$SCRIPT_DIR/xhshook_v2_signed.apk") bytes" |
xhs_lsposed_a16_fix/update_xhshook_db.sh
0 → 100755
| 1 | +#!/bin/bash | ||
| 2 | +# update_xhshook_db.sh — 重装 xhshook APK 后更新 LSPosed modules_config.db | ||
| 3 | +# 必须在 adb install --no-incremental 之后运行 | ||
| 4 | +set -e | ||
| 5 | + | ||
| 6 | +ADB="${ADB:-$(dirname "$0")/../platform-tools/adb}" | ||
| 7 | +if [ ! -f "$ADB" ]; then ADB=adb; fi | ||
| 8 | + | ||
| 9 | +NEW_PATH=$($ADB shell 'pm path com.hermes.xhshook' | tr -d '\r' | sed 's/package://') | ||
| 10 | +echo "New APK path: $NEW_PATH" | ||
| 11 | + | ||
| 12 | +$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"' | ||
| 13 | +$ADB pull /data/local/tmp/mc_upd.db /tmp/mc_upd.db | ||
| 14 | +sqlite3 /tmp/mc_upd.db "UPDATE modules SET apk_path='${NEW_PATH}' WHERE module_pkg_name='com.hermes.xhshook';" | ||
| 15 | +$ADB push /tmp/mc_upd.db /data/local/tmp/mc_upd.db | ||
| 16 | +$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"' | ||
| 17 | +echo "DB updated. Now: am force-stop com.xingin.xhs → open XHS manually." |
xhs_lsposed_a16_fix/xhshook_src/AndroidManifest.xml
0 → 100644
No preview for this file type
xhs_lsposed_a16_fix/xhshook_src/Hook.java
0 → 100644
| 1 | +package com.hermes.xhshook; | ||
| 2 | + | ||
| 3 | +import de.robv.android.xposed.IXposedHookLoadPackage; | ||
| 4 | +import de.robv.android.xposed.XC_MethodHook; | ||
| 5 | +import de.robv.android.xposed.XposedBridge; | ||
| 6 | +import de.robv.android.xposed.XposedHelpers; | ||
| 7 | +import de.robv.android.xposed.callbacks.XC_LoadPackage; | ||
| 8 | + | ||
| 9 | +public class Hook implements IXposedHookLoadPackage { | ||
| 10 | + @Override | ||
| 11 | + public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { | ||
| 12 | + if (!lpparam.packageName.equals("com.xingin.xhs")) return; | ||
| 13 | + | ||
| 14 | + XposedBridge.log("[XHSHook] Loaded into XHS!"); | ||
| 15 | + | ||
| 16 | + try { | ||
| 17 | + Class<?> clz = XposedHelpers.findClass("okhttp3.Request$Builder", lpparam.classLoader); | ||
| 18 | + XposedHelpers.findAndHookMethod(clz, "build", new XC_MethodHook() { | ||
| 19 | + @Override | ||
| 20 | + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { | ||
| 21 | + try { | ||
| 22 | + Object url = XposedHelpers.getObjectField(param.thisObject, "url"); | ||
| 23 | + String urlStr = url != null ? url.toString() : "null"; | ||
| 24 | + | ||
| 25 | + // Only log edith.xiaohongshu.com (the signed API endpoint) | ||
| 26 | + if (!urlStr.contains("edith.xiaohongshu.com")) return; | ||
| 27 | + | ||
| 28 | + Object method = XposedHelpers.getObjectField(param.thisObject, "method"); | ||
| 29 | + String methodStr = method != null ? method.toString() : "GET"; | ||
| 30 | + | ||
| 31 | + // headers field is Headers.Builder — call build() to get actual headers | ||
| 32 | + Object headersBuilder = XposedHelpers.getObjectField(param.thisObject, "headers"); | ||
| 33 | + String headersStr = "null"; | ||
| 34 | + if (headersBuilder != null) { | ||
| 35 | + Object headers = XposedHelpers.callMethod(headersBuilder, "build"); | ||
| 36 | + headersStr = headers != null ? headers.toString() : "null"; | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + XposedBridge.log("[XHSHook] " + methodStr + " " + urlStr); | ||
| 40 | + XposedBridge.log("[XHSHook] Headers:\n" + headersStr); | ||
| 41 | + } catch (Throwable t) { | ||
| 42 | + XposedBridge.log("[XHSHook] Error in hook: " + t.getMessage()); | ||
| 43 | + } | ||
| 44 | + } | ||
| 45 | + }); | ||
| 46 | + XposedBridge.log("[XHSHook] OkHttp Request.Builder.build() hooked!"); | ||
| 47 | + } catch (Throwable t) { | ||
| 48 | + XposedBridge.log("[XHSHook] Failed to hook OkHttp: " + t.getMessage()); | ||
| 49 | + XposedBridge.log(t); | ||
| 50 | + } | ||
| 51 | + } | ||
| 52 | +} |