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 | +} | ... | ... |