From 4b6e1572ef46120a307a55ef898974747a732289 Mon Sep 17 00:00:00 2001 From: yibo Date: Tue, 30 Jun 2026 01:50:04 +0800 Subject: [PATCH] feat(xhshook): v3.1 签名代理模式 — OkHttp proxy poller + body 捕获 --- phoneuse/event_server.py | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- xhs_lsposed_a16_fix/xhshook_src/Hook.java | 227 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------- xhs_lsposed_a16_fix/xhshook_v2_signed.apk | Bin 16863 -> 0 bytes 3 files changed, 318 insertions(+), 15 deletions(-) diff --git a/phoneuse/event_server.py b/phoneuse/event_server.py index ed4dec4..be65207 100644 --- a/phoneuse/event_server.py +++ b/phoneuse/event_server.py @@ -219,7 +219,7 @@ class XhsParams(BaseModel): method: str url: str headers: dict[str, str] = {} - + body: str = "" @app.post("/xhs_params", status_code=200) def post_xhs_params(params: XhsParams): @@ -228,7 +228,10 @@ def post_xhs_params(params: XhsParams): with get_db() as conn: cursor = conn.execute( "INSERT INTO events (type, app, data, ts, source) VALUES (?, ?, ?, ?, ?)", - ("xhs_request", "com.xingin.xhs", json.dumps({"method": params.method, "url": params.url, "headers": params.headers}), time.time(), "xhshook") + ("xhs_request", "com.xingin.xhs", + json.dumps({"method": params.method, "url": params.url, + "headers": params.headers, "body": params.body}), + time.time(), "xhshook") ) conn.commit() return {"id": cursor.lastrowid} @@ -243,3 +246,102 @@ def get_xhs_params(limit: int = Query(default=10, le=100)): (limit,) ).fetchall() return [{"id": r["id"], "ts": r["ts"], **json.loads(r["data"])} for r in reversed(rows)] + + +# ─── XHS Proxy (Hermes → xhshook → OkHttp) ─── + +class XhsProxyRequest(BaseModel): + url: str + method: str = "GET" + body: str = "" + headers_extra: dict[str, str] = {} + +class XhsProxyResult(BaseModel): + proxy_id: int + status: str # "ok" | "error" + http_code: int = 0 + response_body: str = "" + error: str = "" + +def _init_proxy_table(): + with get_db() as conn: + conn.execute(""" + CREATE TABLE IF NOT EXISTS proxy_requests ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + status TEXT DEFAULT 'pending', + created_ts REAL, + req_url TEXT, + req_method TEXT, + req_body TEXT, + req_headers_extra TEXT, + resp_http_code INTEGER, + resp_body TEXT, + resp_error TEXT + ) + """) + conn.commit() + +_init_proxy_table() + +@app.post("/xhs_proxy", status_code=201) +def submit_proxy_request(req: XhsProxyRequest): + """Hermes submits a request to be proxied through XHS's OkHttp (with auto-signing).""" + with get_db() as conn: + cur = conn.execute( + "INSERT INTO proxy_requests (status, created_ts, req_url, req_method, req_body, req_headers_extra) " + "VALUES ('pending', ?, ?, ?, ?, ?)", + (time.time(), req.url, req.method, req.body, json.dumps(req.headers_extra)) + ) + conn.commit() + return {"proxy_id": cur.lastrowid} + +@app.get("/xhs_proxy/pending") +def get_pending_proxy(): + """xhshook polls this. Returns oldest pending request (null JSON if none).""" + with get_db() as conn: + row = conn.execute( + "SELECT id, req_url, req_method, req_body, req_headers_extra FROM proxy_requests " + "WHERE status='pending' ORDER BY id ASC LIMIT 1" + ).fetchone() + if not row: + return None + # Mark as processing + with get_db() as conn: + conn.execute("UPDATE proxy_requests SET status='processing' WHERE id=?", (row["id"],)) + conn.commit() + return { + "proxy_id": row["id"], + "url": row["req_url"], + "method": row["req_method"], + "body": row["req_body"], + "headers_extra": json.loads(row["req_headers_extra"] or "{}") + } + +@app.post("/xhs_proxy/result", status_code=200) +def submit_proxy_result(result: XhsProxyResult): + """xhshook posts the API response here.""" + with get_db() as conn: + conn.execute( + "UPDATE proxy_requests SET status=?, resp_http_code=?, resp_body=?, resp_error=? WHERE id=?", + (result.status, result.http_code, result.response_body, result.error, result.proxy_id) + ) + conn.commit() + return {"ok": True} + +@app.get("/xhs_proxy/result/{proxy_id}") +def get_proxy_result(proxy_id: int): + """Hermes polls this for the result.""" + with get_db() as conn: + row = conn.execute( + "SELECT * FROM proxy_requests WHERE id=?", (proxy_id,) + ).fetchone() + if not row: + raise HTTPException(404, "proxy_id not found") + return { + "proxy_id": row["id"], + "status": row["status"], + "created_ts": row["created_ts"], + "http_code": row["resp_http_code"], + "response_body": row["resp_body"], + "error": row["resp_error"] + } diff --git a/xhs_lsposed_a16_fix/xhshook_src/Hook.java b/xhs_lsposed_a16_fix/xhshook_src/Hook.java index 5ad39d2..a0ec4c1 100644 --- a/xhs_lsposed_a16_fix/xhshook_src/Hook.java +++ b/xhs_lsposed_a16_fix/xhshook_src/Hook.java @@ -6,20 +6,28 @@ import de.robv.android.xposed.XposedBridge; import de.robv.android.xposed.XposedHelpers; import de.robv.android.xposed.callbacks.XC_LoadPackage; +import java.io.InputStream; import java.io.OutputStream; +import java.io.ByteArrayOutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.util.Scanner; public class Hook implements IXposedHookLoadPackage { - // Hermes event_server endpoint — POST xhs_params events here + // Hermes event_server endpoints private static final String HERMES_URL = "http://127.0.0.1:8765/xhs_params"; + private static final String PROXY_POLL_URL = "http://127.0.0.1:8765/xhs_proxy/pending"; + private static final String PROXY_RESULT_URL = "http://127.0.0.1:8765/xhs_proxy/result"; + private static ClassLoader appClassLoader; + private static boolean proxyRunning = false; @Override public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { if (!lpparam.packageName.equals("com.xingin.xhs")) return; XposedBridge.log("[XHSHook] Loaded into XHS!"); + appClassLoader = lpparam.classLoader; try { Class clz = XposedHelpers.findClass("okhttp3.Request$Builder", lpparam.classLoader); @@ -42,13 +50,25 @@ public class Hook implements IXposedHookLoadPackage { headersStr = headers != null ? headers.toString() : ""; } - XposedBridge.log("[XHSHook] " + methodStr + " " + urlStr); - XposedBridge.log("[XHSHook] Headers:\n" + headersStr); + // Capture request body (for POST/PUT requests) + String bodyStr = ""; + try { + Object bodyObj = XposedHelpers.getObjectField(param.thisObject, "body"); + if (bodyObj != null) { + Class bufferClz = XposedHelpers.findClass("okio.Buffer", lpparam.classLoader); + Object buffer = bufferClz.newInstance(); + XposedHelpers.callMethod(bodyObj, "writeTo", buffer); + bodyStr = (String) XposedHelpers.callMethod(buffer, "readUtf8"); + if (bodyStr != null && bodyStr.length() > 2000) { + bodyStr = bodyStr.substring(0, 2000); + } + } + } catch (Throwable t) { + bodyStr = ""; + } - final String json = buildJson(methodStr, urlStr, headersStr); - new Thread(new Runnable() { - public void run() { postToHermes(json); } - }).start(); + final String json = buildJson(methodStr, urlStr, headersStr, bodyStr); + new Thread(() -> postToHermes(json)).start(); } catch (Throwable t) { XposedBridge.log("[XHSHook] Error in hook: " + t.getMessage()); @@ -56,17 +76,201 @@ public class Hook implements IXposedHookLoadPackage { } }); XposedBridge.log("[XHSHook] OkHttp Request.Builder.build() hooked!"); + + // Start proxy poller in background + startProxyPoller(); + } catch (Throwable t) { XposedBridge.log("[XHSHook] Failed to hook OkHttp: " + t.getMessage()); XposedBridge.log(t); } } - private static String buildJson(String method, String url, String headersStr) { + // ─── Proxy Poller ───────────────────────────────────────────── + + private static void startProxyPoller() { + if (proxyRunning) return; + proxyRunning = true; + new Thread(() -> { + XposedBridge.log("[XHSHook] Proxy poller started"); + while (true) { + try { + String json = httpGet(PROXY_POLL_URL); + if (json != null && !json.equals("null") && !json.isEmpty()) { + int proxyId = parseJsonInt(json, "proxy_id"); + String reqUrl = parseJsonString(json, "url"); + String method = parseJsonString(json, "method"); + String body = parseJsonString(json, "body"); + + XposedBridge.log("[XHSHook] Proxy exec #" + proxyId + " " + method + " " + reqUrl); + + String[] result = executeOkHttp(proxyId, method, reqUrl, body); + String status = result[0]; + int httpCode = Integer.parseInt(result[1]); + String respBody = result[2]; + String error = result[3]; + + postProxyResult(proxyId, status, httpCode, respBody, error); + } + Thread.sleep(1000); + } catch (Throwable t) { + XposedBridge.log("[XHSHook] Proxy poller error: " + t.getMessage()); + try { Thread.sleep(3000); } catch (Throwable tt) {} + } + } + }).start(); + } + + private static String[] executeOkHttp(int proxyId, String method, String url, String body) { + // Returns [status, httpCode, responseBody, error] + try { + Class okHttpClz = XposedHelpers.findClass("okhttp3.OkHttpClient", appClassLoader); + Class requestClz = XposedHelpers.findClass("okhttp3.Request", appClassLoader); + Class requestBuilderClz = XposedHelpers.findClass("okhttp3.Request$Builder", appClassLoader); + Class mediaTypeClz = XposedHelpers.findClass("okhttp3.MediaType", appClassLoader); + Class requestBodyClz = XposedHelpers.findClass("okhttp3.RequestBody", appClassLoader); + + // Create OkHttpClient (new instance — auto-injects interceptors from XHS) + Object client = okHttpClz.newInstance(); + + // Build request + Object builder = requestBuilderClz.newInstance(); + builder = XposedHelpers.callMethod(builder, "url", url); + builder = XposedHelpers.callMethod(builder, "method", method, + body != null && !body.isEmpty() ? + XposedHelpers.callStaticMethod(requestBodyClz, "create", + XposedHelpers.callStaticMethod(mediaTypeClz, "parse", "application/json; charset=utf-8"), + body) + : null); + + // Add standard XHS headers (User-Agent, Referer) + builder = XposedHelpers.callMethod(builder, "addHeader", "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"); + builder = XposedHelpers.callMethod(builder, "addHeader", "Referer", "https://app.xhs.cn/"); + + Object request = XposedHelpers.callMethod(builder, "build"); + + // Execute + Object call = XposedHelpers.callMethod(client, "newCall", request); + Object response = XposedHelpers.callMethod(call, "execute"); + + int httpCode = (Integer) XposedHelpers.callMethod(response, "code"); + Object respBodyObj = XposedHelpers.callMethod(response, "body"); + Object source = XposedHelpers.callMethod(respBodyObj, "source"); + Object buffer = XposedHelpers.findClass("okio.Buffer", appClassLoader).newInstance(); + XposedHelpers.callMethod(source, "readAll", buffer); + String respBody = (String) XposedHelpers.callMethod(buffer, "readUtf8"); + + // Truncate + if (respBody != null && respBody.length() > 10000) { + respBody = respBody.substring(0, 10000); + } + + XposedHelpers.callMethod(response, "close"); + return new String[]{"ok", String.valueOf(httpCode), respBody, ""}; + + } catch (Throwable t) { + String errMsg = t.getMessage(); + if (errMsg == null) errMsg = t.getClass().getName(); + return new String[]{"error", "0", "", errMsg}; + } + } + + private static void postProxyResult(int proxyId, String status, int httpCode, String respBody, String error) { + String json = "{\"proxy_id\":" + proxyId + + ",\"status\":\"" + status + "\"" + + ",\"http_code\":" + httpCode + + ",\"response_body\":" + jsonStr(respBody) + + ",\"error\":" + jsonStr(error) + "}"; + try { + HttpURLConnection conn = (HttpURLConnection) new URL(PROXY_RESULT_URL).openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/json"); + conn.setDoOutput(true); + conn.setConnectTimeout(2000); + conn.setReadTimeout(2000); + byte[] bodyBytes = json.getBytes(StandardCharsets.UTF_8); + conn.setFixedLengthStreamingMode(bodyBytes.length); + try (OutputStream os = conn.getOutputStream()) { + os.write(bodyBytes); + } + int code = conn.getResponseCode(); + conn.disconnect(); + if (code != 200) { + XposedBridge.log("[XHSHook] Proxy result POST " + code); + } + } catch (Throwable t) { + XposedBridge.log("[XHSHook] Failed to post proxy result: " + t.getMessage()); + } + } + + // ─── HTTP helpers ───────────────────────────────────────────── + + private static String httpGet(String urlStr) { + try { + HttpURLConnection conn = (HttpURLConnection) new URL(urlStr).openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(2000); + conn.setReadTimeout(2000); + int code = conn.getResponseCode(); + if (code == 200) { + Scanner s = new Scanner(conn.getInputStream(), "UTF-8").useDelimiter("\\A"); + String result = s.hasNext() ? s.next() : ""; + s.close(); + conn.disconnect(); + return result; + } + conn.disconnect(); + } catch (Throwable t) {} + return null; + } + + // ─── Minimal JSON parsing (no external deps) ────────────────── + + private static int parseJsonInt(String json, String key) { + int idx = json.indexOf("\"" + key + "\""); + if (idx < 0) return 0; + idx = json.indexOf(":", idx); + if (idx < 0) return 0; + idx++; + while (idx < json.length() && (json.charAt(idx) == ' ' || json.charAt(idx) == '\t')) idx++; + StringBuilder num = new StringBuilder(); + while (idx < json.length() && (Character.isDigit(json.charAt(idx)) || json.charAt(idx) == '-')) { + num.append(json.charAt(idx)); + idx++; + } + try { return Integer.parseInt(num.toString()); } catch (Throwable t) { return 0; } + } + + private static String parseJsonString(String json, String key) { + int idx = json.indexOf("\"" + key + "\""); + if (idx < 0) return ""; + idx = json.indexOf("\"", idx + key.length() + 2); + if (idx < 0) return ""; + int start = idx; + idx++; + while (idx < json.length()) { + char c = json.charAt(idx); + if (c == '\\') { idx += 2; continue; } + if (c == '"') break; + idx++; + } + return json.substring(start + 1, idx) + .replace("\\\"", "\"") + .replace("\\\\", "\\") + .replace("\\n", "\n"); + } + + // ─── JSON builders ──────────────────────────────────────────── + + private static String buildJson(String method, String url, String headersStr, String body) { StringBuilder sb = new StringBuilder(); sb.append("{"); sb.append("\"method\":").append(jsonStr(method)).append(","); sb.append("\"url\":").append(jsonStr(url)).append(","); + sb.append("\"body\":").append(jsonStr(body)).append(","); sb.append("\"headers\":{"); boolean first = true; for (String line : headersStr.split("\n")) { @@ -103,13 +307,10 @@ public class Hook implements IXposedHookLoadPackage { try (OutputStream os = conn.getOutputStream()) { os.write(body); } - int code = conn.getResponseCode(); + conn.getResponseCode(); conn.disconnect(); - if (code != 200 && code != 201 && code != 204) { - XposedBridge.log("[XHSHook] Hermes POST " + code); - } } catch (Throwable t) { - // Hermes server not running — silent fail, logcat capture still works + // Hermes server not running — silent fail } } } diff --git a/xhs_lsposed_a16_fix/xhshook_v2_signed.apk b/xhs_lsposed_a16_fix/xhshook_v2_signed.apk index a851d60..3180d0a 100644 Binary files a/xhs_lsposed_a16_fix/xhshook_v2_signed.apk and b/xhs_lsposed_a16_fix/xhshook_v2_signed.apk differ -- libgit2 0.21.4