Commit 6d30843c44acb29c78cbb2103741fb19a566d444

Authored by yibo
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>
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 +}
... ...