From 001b2aff6d13cf4b46eda49f75f7d42d00511768 Mon Sep 17 00:00:00 2001 From: elnehcc+github Date: Mon, 7 Sep 2026 12:50:36 +0800 Subject: [PATCH] [platform] support Linux WMPF 4067695881 (WeChat 4.1.13.9, flatpak) - select the root WeChatAppEx process by checking that its parent is not another WeChatAppEx process; newer builds spawn zygote/renderer/ gpu/utility children whose ppid points at intermediate nodes, so the ppid frequency heuristic could pick a non-root process - fall back to --client_version from /proc//cmdline when the embedded ",x.y.z." literal is missing; the 4.1.13+ runtime no longer contains it, which made findWmpfProcess throw "[frida] error in find wmpf version" - resolve sandbox paths reported by frida (/app/extra/wechat/...) to host paths via /proc//root so version detection works when the target runs inside flatpak/bwrap - add addresses.4067695881.json for WeChat 4.1.13.9 (client_version 4067695881) Offsets were derived with a small static-analysis toolchain (rip-rel xref scan + .eh_frame_hdr function boundaries) that was validated by reproducing every value in addresses.14978.json exactly on the known-good 14978 binary (WeChat 4.1.1.8) before being applied to the new binary. Verified end-to-end on flatpak com.tencent.WeChat 4.1.13.9: OnLoadStart fires and rewrites scene 1027 -> 1101, the CDP filter hook patches repeatedly, and DevTools connects. --- frida/config/linux/addresses.4067695881.json | 6 + src/platform/linux.ts | 126 +++++++++++++++---- 2 files changed, 110 insertions(+), 22 deletions(-) create mode 100644 frida/config/linux/addresses.4067695881.json diff --git a/frida/config/linux/addresses.4067695881.json b/frida/config/linux/addresses.4067695881.json new file mode 100644 index 0000000..4d0d740 --- /dev/null +++ b/frida/config/linux/addresses.4067695881.json @@ -0,0 +1,6 @@ +{ + "Version": 4067695881, + "LoadStartHookOffset": "0x900eee0", + "CDPFilterHookOffset": "0xdc1c670", + "SceneOffsets": [56, 1528, 8, 1464, 16, 456] +} diff --git a/src/platform/linux.ts b/src/platform/linux.ts index d1316b6..e86b9f0 100644 --- a/src/platform/linux.ts +++ b/src/platform/linux.ts @@ -1,6 +1,8 @@ import { IPlatform, WmpfProcessInfo } from "./types"; import * as frida from "frida" import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; function searchWmpfVersionInFile(filePath: string): number { const buffer = fs.readFileSync(filePath); @@ -12,6 +14,72 @@ function searchWmpfVersionInFile(filePath: string): number { : 0; } +/** + * Read the --client_version argument from the process command line + * (e.g. "--client_version=4067695881"). Works inside flatpak/bwrap since we + * access /proc//cmdline directly. + */ +function searchClientVersionInCmdline(pid: number): number { + try { + const cmdline = fs.readFileSync(`/proc/${pid}/cmdline`, "latin1"); + const match = /--client_version=(\d+)/.exec(cmdline.replace(/\x00/g, " ")); + return match && match[1] ? Number(match[1]) : 0; + } catch { + return 0; + } +} + +/** + * Resolve the executable path reported by frida to a path accessible on the + * host filesystem. When the target process runs inside a sandbox (e.g. the + * flatpak com.tencent.WeChat), frida reports the path as seen from inside the + * sandbox (e.g. /app/extra/wechat/...), which does not exist on the host. + * + * Resolution order: + * 1. Use the path as-is (native installation). + * 2. Access it through /proc//root, which points to the filesystem root + * of the sandboxed process (works for flatpak/bwrap namespaces). + * 3. Map the flatpak /app prefix onto the app files directory of any + * installed flatpak (user or system), i.e. + * /app//current/active/files/... + */ +function resolveHostPath(pid: number, sandboxPath: string): string | null { + if (fs.existsSync(sandboxPath)) { + return sandboxPath; + } + + const procRootPath = path.join("/proc", String(pid), "root", sandboxPath); + if (fs.existsSync(procRootPath)) { + return procRootPath; + } + + if (sandboxPath.startsWith("/app/")) { + const relativePath = sandboxPath.slice("/app/".length); + const flatpakAppRoots = [ + path.join(os.homedir(), ".local/share/flatpak/app"), + "/var/lib/flatpak/app", + ]; + for (const flatpakAppRoot of flatpakAppRoots) { + if (!fs.existsSync(flatpakAppRoot)) { + continue; + } + for (const appId of fs.readdirSync(flatpakAppRoot)) { + const candidate = path.join( + flatpakAppRoot, + appId, + "current/active/files", + relativePath, + ); + if (fs.existsSync(candidate)) { + return candidate; + } + } + } + } + + return null; +} + export class LinuxPlatform implements IPlatform { async findWmpfProcess(): Promise { const localDevice = await frida.getLocalDevice(); @@ -21,31 +89,45 @@ export class LinuxPlatform implements IPlatform { const wmpfProcesses = processes.filter( (process) => process.name === "WeChatAppEx", ); - const wmpfPids = wmpfProcesses.map((p) => - p.parameters.ppid ? p.parameters.ppid : 0, - ); - - // find the parent process - const wmpfPid = wmpfPids - .sort( - (a, b) => - wmpfPids.filter((v) => v === a).length - - wmpfPids.filter((v) => v === b).length, - ) - .pop(); - if (wmpfPid === undefined) { + if (wmpfProcesses.length === 0) { throw new Error("[frida] WeChatAppEx process not found"); } - const wmpfProcess = processes.filter( - (process) => process.pid === wmpfPid, - )[0]; - const wmpfProcessPath = wmpfProcess.parameters.path as string | undefined; - const wmpfVersion = wmpfProcessPath - ? searchWmpfVersionInFile(wmpfProcessPath) - : 0; + + // Select the root WeChatAppEx process: the one whose parent is not + // itself a WeChatAppEx process. Newer builds spawn zygote/renderer/ + // gpu/utility children whose ppid points at intermediate nodes, so a + // frequency heuristic on ppids may pick the wrong process. + const wmpfPids = new Set(wmpfProcesses.map((p) => p.pid)); + const wmpfProcess = wmpfProcesses.find((p) => { + const ppid = p.parameters.ppid ? Number(p.parameters.ppid) : 0; + return ppid !== 0 && !wmpfPids.has(ppid); + }); + if (wmpfProcess === undefined) { + throw new Error("[frida] WeChatAppEx root process not found"); + } + const wmpfPid = Number(wmpfProcess.pid); + const wmpfProcessPath = wmpfProcess.parameters.path as + | string + | undefined; + const hostPath = + wmpfProcessPath && wmpfPid !== 0 + ? resolveHostPath(wmpfPid, wmpfProcessPath) + : null; + + // Version detection: + // 1. WMPF <= 14978 embeds a ",x.y.z." literal; find it. + // 2. Newer builds (WeChat 4.1.13+) dropped that literal; fall back to + // the --client_version command line argument (see issue #167). + let wmpfVersion = + hostPath !== null ? searchWmpfVersionInFile(hostPath) : 0; + if (wmpfVersion === 0) { + wmpfVersion = searchClientVersionInCmdline(wmpfPid); + } if (wmpfVersion === 0) { - throw new Error("[frida] error in find wmpf version"); + throw new Error( + `[frida] error in find wmpf version (process path: ${hostPath ?? wmpfProcessPath})`, + ); } - return { pid: Number(wmpfPid), version: wmpfVersion }; + return { pid: wmpfPid, version: wmpfVersion }; } }