From 7dc6a63934695dbcd519a54821f41946889afdd9 Mon Sep 17 00:00:00 2001 From: tom613951 <111942871+tom613951@users.noreply.github.com> Date: Mon, 21 Sep 2026 18:28:19 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=EF=BB=BF[bug]=20fix=20legacy=20scene=20poi?= =?UTF-8?q?nter=20chain=20broken=20by=20the=2025715=20refactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 25715 refactor (2970a6c) reworked handleOnLoadStart for the new flue layout and broke the config.SceneOffsets branch: 1. The final hop referenced an undeclared variable (remoteDebugParametersPtr), throwing "ReferenceError: 'remoteDebugParametersPtr' is not defined" on every AppletIndexContainer::OnLoadStart call. 2. The chain was split after an extra dereference of offset[2], so the legacy six-hop chain was walked incorrectly and landed on the wrong struct even once the name was fixed. Because the exception aborts onEnter, miniappScenePtr was never computed, writeInt(1101) never ran, and mini-programs never entered remote-debug mode. The debug server still started and reported the correct WMPF version, so the breakage only became visible when a mini-program was launched. Restore the legacy branch to a single six-hop pass, matching the pre-refactor implementation, and keep remoteDebugConfigPtr assigned so the code below works for both branches. The modern branch (MiniAppConfigStructOffsets) is unchanged. Verified on Windows x64 with WMPF 25510: previously every mini-program load threw the ReferenceError; now the hook reports "[hook] scene: 1256" and rewrites it to 1101. --- frida/hook.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/frida/hook.js b/frida/hook.js index db36e8b..eecc2b6 100644 --- a/frida/hook.js +++ b/frida/hook.js @@ -71,20 +71,23 @@ const handleOnLoadStart = (a1, config) => { // legacy scene config if (config.SceneOffsets) { + // Legacy configs describe a single 6-hop scene chain. Keep it in one + // pass: re-splitting it like the modern path inserts an extra + // dereference after offset[2] and lands on the wrong struct. miniappLaunchConfigPtr = a1 .add(structOffsets[0]) .readPointer() .add(structOffsets[1]) - .readPointer() - .add(structOffsets[2]) .readPointer(); - remoteDebugConfigPtr = miniappLaunchConfigPtr + miniappScenePtr = miniappLaunchConfigPtr + .add(structOffsets[2]) + .readPointer() .add(structOffsets[3]) .readPointer() .add(structOffsets[4]) - .readPointer(); - - miniappScenePtr = remoteDebugParametersPtr.add(structOffsets[5]); + .readPointer() + .add(structOffsets[5]); + remoteDebugConfigPtr = miniappLaunchConfigPtr; } else { // later wmpf builds (win32) const launchConfigOffsets = structOffsets.LaunchConfigOffsets; @@ -134,7 +137,9 @@ const handleOnLoadStart = (a1, config) => { miniappScenePtr.writeInt(1101); if (config.SceneOffsets) { - // legacy path, we are done here + // Legacy path: debug mode is enabled by the args[1] |= 0x1 write in + // patchOnLoadStart. There is no websocket-URL / remote-debug-mode field + // to patch on these builds, so we are done here. return; } From 68bc903e4c8b8f45b73efeb827d7be02705a1a36 Mon Sep 17 00:00:00 2001 From: tom613951 <111942871+tom613951@users.noreply.github.com> Date: Mon, 21 Sep 2026 18:51:08 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=EF=BB=BF[auto-detect]=20recover=20unpaired?= =?UTF-8?q?=20scene=20guards=20on=20legacy=20builds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit recoverRemoteGuard assumed the scene and mode comparisons always share a base expression (baseKey === scene.baseKey). Legacy builds such as 25510 do not: the scene value sits behind a deep pointer chain while the remote-debug flag is read straight off the function's first argument. On 25510 the detected helper looks like: mov rsi, rcx mov rax, [rcx + 8] mov rcx, [rax + 0x5c0] mov rcx, [rcx + 0x10] cmp dword ptr [rcx + 0x1c8], 0x44d cmp byte ptr [rsi + 0x29], 1 so the scene comparison resolves to argumentIndex 0 with offsets [8, 1472, 16] while the mode comparison resolves to a different base with an empty chain. baseKey can never match, no candidate is produced, and detectLegacy fails with "expected one candidate, found 0". The former frida/version-detect.js had no such constraint: it located the scene by scanning opcode patterns, which is why auto-detect used to work on these builds. Legacy detection only needs the scene side (SceneOffsets is assembled from rootOffsets + sceneOffset), so fall back to an unpaired scene candidate instead of rejecting the build. A paired mode field is still preferred when present, so modern builds are unaffected, and the modern path now rejects a null mode offset so the fallback cannot emit a partial config. Recovered offsets on 25510 now match the hand-verified addresses.25510.json exactly. --- frida/autodetect/win32.js | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/frida/autodetect/win32.js b/frida/autodetect/win32.js index cadc9a7..dbcb17b 100644 --- a/frida/autodetect/win32.js +++ b/frida/autodetect/win32.js @@ -546,15 +546,31 @@ const recoverRemoteGuard = (instructions, name) => { const candidates = []; for (const scene of comparisons.filter((item) => item.immediate === 1101)) { - for (const mode of comparisons.filter( + const sameSource = comparisons.filter( (item) => item.immediate === 1 && item.baseKey === scene.baseKey, - )) { + ); + for (const mode of sameSource) { candidates.push({ rootOffsets: scene.rootOffsets, sceneOffset: scene.fieldOffset, modeOffset: mode.fieldOffset, }); } + + // Some legacy builds read the remote-debug flag straight off the first + // argument (e.g. `cmp byte ptr [rsi + 0x29], 1` after `mov rsi, rcx`) + // instead of storing it next to the scene value. There the mode field + // has no shared base with the scene field, so `baseKey` never matches. + // Legacy detection only needs the scene side (SceneOffsets is built + // from rootOffsets + sceneOffset), so fall back to an unpaired scene + // candidate rather than rejecting the build outright. + if (sameSource.length === 0) { + candidates.push({ + rootOffsets: scene.rootOffsets, + sceneOffset: scene.fieldOffset, + modeOffset: null, + }); + } } const unique = new Map( @@ -759,6 +775,12 @@ const detectModern = (module, functions, xrefs, loadStart) => { ) { throw new Error("OnLoadStart and Start remote-debug fields disagree"); } + // Modern builds always expose a paired mode field; a null here means the + // unpaired-scene fallback was taken, which is only valid for legacy + // detection. Refuse rather than emit a config with a null offset. + if (startGuard.modeOffset === null) { + throw new Error("modern WMPF layout is missing the remote-debug mode field"); + } const launchExpression = loadExpression(argumentExpression(0), startGuard.rootOffsets[0]); const websocketUrlOffset = recoverWebSocketUrlOffset(startInstructions, launchExpression);