STATUS.md 12.5 KB

XHS MITM 进度备忘 — 2026-06-30(Hermes 接手版)

写给设备上 Hermes Agent 的完整上下文。直接可用,无需 Mac 介入的操作都写了命令。


战役目标:✅ 已完成

抓 edith.xiaohongshu.com 的 xy-common-params(ECFAAF/ECFAAF01 签名参数),用于复现 XHS 请求鉴权。

核心障碍:edith 用 Google Mainline Cronet + 静态链接 BoringSSL,Frida Interceptor 无法 hook 静态符号。 解法:LSPosed(JingMatrix/Vector fork)在 XHS 进程 fork 时注入,hook OkHttp Java 层,在签名传入 Cronet 前拦截。


当前设备状态(2026-06-30 01:17 验证通过)

设备:Pixel 10 Pro Fold
Android:16 (API 36)
Magisk:30.7
LSPosed (Vector fork):正常运行,lspd 进程存活
xhshook v3:注入 com.xingin.xhs 成功(mid=4,APK=16863 字节,DEX=6328 字节)
xy-common-params:已捕获(见下方样本)
curl HTTP 200:已用捕获参数成功复现 API 调用
xhshook HTTP 转发:POST /xhs_params 已实现,event_server.py 已更新
event_server.py:已推送到设备 ~/.hermes/event_bus/event_server.py

模块布局

/data/adb/modules/
├── zygisk_vector/          # LSPosed Vector fork(含 A16 JNI size fix)
│   ├── zygisk/arm64-v8a.so # 核心注入 so(1142464 字节)
│   ├── daemon.apk          # Java 守护进程(3199711 字节)
│   ├── framework/lspd.dex  # 注入 DEX(182840 字节)
│   ├── daemon              # 启动脚本(已修复入口类名)
│   └── service.sh          # Magisk 钩子(已去掉不兼容 flag)
├── xhshook/                # Xposed 模块(备用 APK,随 Magisk 入库)
│   ├── module.prop
│   └── xhshook.apk         # v2(无捆绑 XposedBridge,classes.dex=3400 字节)
└── xhs_ca/                 # MITM 代理 CA 证书
    └── system/etc/security/cacerts/36522bbf.0

xhshook 配置

已安装:com.hermes.xhshook(streamed install --no-incremental)
APK 含:assets/xposed_init → com.hermes.xhshook.Hook
modules_config.db:mid=4, enabled=1, scope=(4, com.xingin.xhs, 0)
APK 路径:/data/app/~~F2OUnKKEvaoaCC4wwrGOsg==/com.hermes.xhshook-NAAljEo_GRPgf6S_5pp7kw==/base.apk
xhshook v3 新增:HTTP POST 转发到 127.0.0.1:8765/xhs_params(后台线程,1s 超时,静默失败)

打开 XHS 后,用 logcat 实时抓:

# 在 Termux 里(需要 root)
su -c "logcat -b all | grep -E 'xy-common-params:|x-mini-|x-legacy-sid|shield:|xy-platform-info'"

xy-common-params(主要签名参数)

示例值(session 级字段每次启动刷新):

fid=
gid=7c9ea3f0b4d054aae8494006bb4d85a3591f472947359203774768ef
device_model=phone
tz=Asia/Shanghai
channel=YingYongBao
versionName=9.35.1
deviceId=748327bc-6079-3b06-9351-1638281dbc6f   ← 设备固定
platform=android
sid=session.1782715007780944836188               ← 每次启动变
identifier_flag=4
cpu_abi=arm64-v8a
nqe_score=89
project_id=ECFAAF
app_id=ECFAAF01
id_token=VjEAANwAP8KY...                         ← 每次启动变
t=1782750823                                     ← Unix 时间戳,每次变
build=9351803
did=058844e5598e4d001b42a122d1a9d1c2            ← 设备固定
launch_id=1782750821                             ← 每次启动变

固定字段(可硬编码复现请求):deviceId、did、gid、platform、versionName、build、channel Session 字段(需要 hook 实时拿):id_token、sid、t、launch_id

额外签名 Header(2026-06-30 新发现)

edith 请求还带以下 header,复现 API 时可能需要:

x-legacy-sid: session.1782715007780944836188         ← 同 sid
x-mini-gid: 7c9ea3f0b4d054aae8494006bb4d85a3591f... ← 同 gid
x-mini-s1: AAsAAAABA8AXUVb7YluNAgap...               ← 每次变,base64
x-mini-sig: de47f603a5d8c1894b08a71008c1c9da...      ← 每次变,hex
x-mini-mua: eyJhIjoiRUNGQUFGMDEi...                  ← JWT 格式,每次变
xy-direction: 90
xy-platform-info: platform=android&build=9351803&deviceId=...
xy-scene: fs=1&point=-1
shield: XYAAQABAAAAAEAAABTAAAAUzUWEe0x...             ← 每次变
User-Agent: Dalvik/2.1.0 (Linux; U; Android 16; Pixel 10 Pro Fold Build/CP1A.260505.005)
             Resolution/1080*2364 Version/9.35.1 Build/9351803

xhshook 架构(Hermes 改 hook 用)

Hook 逻辑(当前 v2)

// 源码位置(设备):/sdcard/Download/xhs_lsposed_a16_fix/xhshook_src/Hook.java
// 源码位置(Mac):~/Documents/LemonAI/pixelphone/xhs_lsposed_a16_fix/xhshook_src/Hook.java

hook 点:okhttp3.Request$Builder.build()
过滤:只打印 edith.xiaohongshu.com 的请求
输出:logcat tag=VectorLegacyBridge

改 hook 的完整流程(必须在 Mac 上执行,设备端无法独立完成)

⚠️ Hermes 不要在设备端跑这段流程。 步骤 2-4 都需要 Mac adb + Mac 上的构建工具。如果只在设备端跑 pm uninstall 而不走 Mac 端 update_xhshook_db.sh,APK 重装后路径变了,lspd 会在下次启动时 因路径失效而崩溃(modules_config.db 里旧路径 → SQLiteCantOpenDatabaseException)。

# 1. 编辑 Hook.java
# ~/Documents/LemonAI/pixelphone/xhs_lsposed_a16_fix/xhshook_src/Hook.java

# 2. 重新编译 + 打包(Mac 上运行)
bash ~/Documents/LemonAI/pixelphone/xhs_lsposed_a16_fix/rebuild_xhshook.sh

# 3. 推到设备(Mac adb)
ADB=~/Documents/LemonAI/pixelphone/platform-tools/adb
$ADB shell 'pm uninstall com.hermes.xhshook' && \
$ADB install --no-incremental ~/Documents/LemonAI/pixelphone/xhs_lsposed_a16_fix/xhshook_v2_signed.apk

# 4. 更新 modules_config.db(Mac 上运行脚本,通过 adb 操作设备)
bash ~/Documents/LemonAI/pixelphone/xhs_lsposed_a16_fix/update_xhshook_db.sh

# 5. 杀掉 XHS 进程重启(不需要重启整机!)
$ADB shell 'am force-stop com.xingin.xhs'
# 然后手动在屏幕上点 XHS 图标

# 6. 验证
$ADB shell 'logcat -d | grep "\[XHSHook\]"'

update_xhshook_db.sh 的 inode 陷阱(2026-06-30 教训)

cp / adb push 替换文件会改变 inode,lspd 的 FileObserver 监听的是旧 inode, 改完后 lspd 内存缓存不会自动刷新,GET_BINDER 调用不触发注入。

update_xhshook_db.sh 已修复为用 Termux Python 原地 UPDATE(保留 inode), 同时做 wal_checkpoint(TRUNCATE) 确保写入主文件。不需要重启 lspd 或重启整机。

lspd crash 恢复流程(如果注入彻底失效)

# 1. 检查 lspd 日志
adb shell su -c "tail -50 /data/adb/lspd/log/verbose_$(ls /data/adb/lspd/log/ | tail -1)"

# 2. 检查 modules_config.db 里的 apk_path 是否还匹配已安装的包
INSTALLED=$(adb shell pm path com.hermes.xhshook | tr -d '\r' | sed 's/package://')
# 对比 DB 里的路径是否一致

# 3. 如果路径不对,用 update_xhshook_db.sh 修复(Mac 端跑)
bash ~/Documents/LemonAI/pixelphone/xhs_lsposed_a16_fix/update_xhshook_db.sh

# 4. 如果 lspd 进程本身挂了(pidof lspd 无输出),直接重启设备
adb reboot
# 重启后 Magisk 会通过 service.sh 重新拉起 lspd(unshare -m 方式,无法手动模拟)

# 5. 重启后需要 PIN 解锁(FBE CE 存储)才能启动 App
# 屏幕上输入 PIN → 解锁后手动点 XHS 图标

可以加的下一个 hook:CertificatePinner bypass

xhs_ca 模块已经把 MITM 代理 CA 证书装进系统了。补上 CertificatePinner bypass 后可以直接用 mitmproxy 抓 OkHttp 流量(edith 仍然走 Cronet,但其他接口会走 OkHttp):

// 在 handleLoadPackage 里加(XHS 包名判断之后):
try {
    XposedHelpers.findAndHookMethod(
        "okhttp3.CertificatePinner", lpparam.classLoader,
        "check", String.class, List.class,
        new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                param.setResult(null);
            }
        }
    );
    XposedBridge.log("[XHSHook] CertificatePinner bypassed!");
} catch (Throwable t) {
    XposedBridge.log("[XHSHook] CertificatePinner bypass failed: " + t);
}

下一步可以做的事(Hermes 接手)

A. 用捕获到的参数复现 XHS API 请求 ✅ curl HTTP 200 已验证

xy-common-params 里的固定字段可以直接复用,session 字段需要先打开 XHS 触发 hook 实时拿。

# Termux 里实时抓一次完整的 xy-common-params
su -c "logcat -c && am force-stop com.xingin.xhs"
# → 手动打开 XHS →
su -c "logcat -d | grep 'xy-common-params:' | head -1"

拿到后,用 curl 复现请求(已验证 HTTP 200):

XY_PARAMS="fid=&gid=7c9ea3f0b4d054...(完整值)"
curl -H "xy-common-params: $XY_PARAMS" \
     -H "xy-direction: 90" \
     -H "User-Agent: Dalvik/2.1.0 (Linux; U; Android 16; Pixel 10 Pro Fold Build/CP1A.260505.005) Resolution/1080*2364 Version/9.35.1 Build/9351803 Device/(Google;Pixel 10 Pro Fold) discover/9.35.1 NetType/WiFi" \
     "https://edith.xiaohongshu.com/api/sns/v3/system_service/flag_exp?fetchSceneTag=&deviceLevel=8"

B. 研究 x-mini-sig / shield 签名结构

edith 除 xy-common-params 外还带 x-mini-sig(hex)、x-mini-mua(JWT)、shield(自定义 base64)。 这些字段每次启动都变——研究是否由设备固定字段派生,还是需要服务端签名。

Hook.java 里已经打印了 Headers.toString() 的完整内容——重新抓一次 logcat 即可看到所有 header:

su -c "logcat -d -b all | grep 'VectorLegacyBridge'" | grep -v '\[XHSHook\]' | head -30

C. 实时转发 → Hermes event_server ✅ 已完成(2026-06-30 01:17)

xhshook v3(Hook.java)已在 beforeHookedMethod 里启动后台线程 HTTP POST 到 127.0.0.1:8765/xhs_params。 event_server.py 已新增两个端点(已推送到设备 ~/.hermes/event_bus/event_server.py):

# Hermes 启动 event_server 后(uvicorn event_server:app --host 127.0.0.1 --port 8765),
# 打开 XHS,hook 自动转发 edith 请求,Hermes 可以查询:

# 查最近捕获的 XHS 请求(含完整 headers)
curl http://127.0.0.1:8765/xhs_params

# 或通过通用 events 接口
curl "http://127.0.0.1:8765/events/latest?type=xhs_request&app=com.xingin.xhs"

转发的 JSON 格式:{"method":"GET","url":"https://edith.xiaohongshu.com/...","headers":{"xy-common-params":"...","x-mini-sig":"...",...}}

启动 event_server 步骤(Termux 里):

cd ~/.hermes/event_bus
uvicorn event_server:app --host 127.0.0.1 --port 8765
# 然后打开 XHS,xhshook 会自动 POST 每条 edith 请求过来

文件索引(设备上)

/sdcard/Download/xhs_lsposed_a16_fix/
├── STATUS.md               ← 本文件
├── CLAUDE.md               ← 项目说明(构建/部署命令)
├── apply_a16_fix.sh        ← A16 补丁脚本(已应用,留作参考)
├── ipc_bridge.h            ← 已打补丁的头文件
├── ipc_bridge.cpp          ← 已打补丁的源文件
├── xhshook_src/
│   └── Hook.java           ← xhshook v2 源码
└── xhshook_v2_signed.apk   ← 预构建的 APK(当前已安装版本,classes.dex=3400 字节)

已知不行的路线(别重新踩)

  • Frida hook edith.xiaohongshu.com:Cronet 静态链接 BoringSSL,Frida Interceptor 找不到符号。SSL_read/SSL_write 都 hook 不上。
  • Java TrustManager 替换:时序问题,edith 在 app 启动很早就开始通信,Frida 注入晚了窗口就过了。LSPosed 没有这个问题。
  • 常驻 Frida daemon 注入 XHS:XHS 检测到反调试后崩溃(wechat_plain 里有记录,XHS 同理)。
  • 把 Xposed API 类打进 xhshook DEX:LSPosed 会检查 XposedBridge.class.getClassLoader() == initLoader,打进去的话直接 Cannot load module。必须用 compileOnly。
  • incremental install(adb install 默认):Incremental Install 的 marker 会让 java.util.zip.ZipFile 解析失败,LSPosed 找不到 assets/xposed_init。必须用 adb install --no-incremental。
  • adb push 替换 modules_config.db:改变文件 inode,lspd FileObserver 监听旧 inode,缓存不刷新,注入会静默失败(有 GET_BINDER 但无 "Loading Vector" 日志)。必须用 Termux Python 原地 UPDATE。
  • kill lspd 后手动重启:lspd 是 init 直接子进程,由 Magisk service.sh 用 unshare -m 拉起,手动 app_process 启动缺少必要的 mount namespace,拉不起来。唯一办法是 adb reboot。