diff --git a/scripts/prepare-wechat-chatter-runtime.cjs b/scripts/prepare-wechat-chatter-runtime.cjs
index 74d07de..31ec50a 100644
--- a/scripts/prepare-wechat-chatter-runtime.cjs
+++ b/scripts/prepare-wechat-chatter-runtime.cjs
@@ -86,41 +86,91 @@ for (const required of [executable, script, config]) {
function patchPerSendPayload(scriptPath) {
let source = fs.readFileSync(scriptPath, 'utf8')
- if (source.includes('var activeTriggerX1Payload = ptr(0);')) return
+ if (!source.includes('var activeTriggerX1Payload = ptr(0);')) return
- const declarations = 'var triggerX1Payload;\nvar triggerX0;'
- const patchedDeclarations =
- 'var triggerX1Payload;\nvar activeTriggerX1Payload = ptr(0);\nvar triggerX0;'
- const originalSend = ` const payloadData = hexToByteArray(payloadHex);
- triggerX1Payload.writeByteArray(payloadData);
- triggerX1Payload.add(0x18).writePointer(info.cgiAddr);
- triggerX1Payload.add(0xb8).writePointer(triggerX1Payload.add(0xc0));
- triggerX1Payload.add(0x190).writePointer(triggerX1Payload.add(0x198));`
- const patchedSend = ` const payloadData = hexToByteArray(payloadHex);
+ const activeSend = ` const payloadData = hexToByteArray(payloadHex);
activeTriggerX1Payload = Memory.alloc(payloadData.length);
activeTriggerX1Payload.writeByteArray(payloadData);
activeTriggerX1Payload.add(0x18).writePointer(info.cgiAddr);
activeTriggerX1Payload.add(0xb8).writePointer(activeTriggerX1Payload.add(0xc0));
activeTriggerX1Payload.add(0x190).writePointer(activeTriggerX1Payload.add(0x198));`
+ const upstreamSend = ` const payloadData = hexToByteArray(payloadHex);
+ triggerX1Payload.writeByteArray(payloadData);
+ triggerX1Payload.add(0x18).writePointer(info.cgiAddr);
+ triggerX1Payload.add(0xb8).writePointer(triggerX1Payload.add(0xc0));
+ triggerX1Payload.add(0x190).writePointer(triggerX1Payload.add(0x198));`
+ if (!source.includes(activeSend)) throw new Error('无法定位 wechat_chatter 连续发送补丁位置')
+ source = source
+ .replace(
+ 'var triggerX1Payload;\nvar activeTriggerX1Payload = ptr(0);\nvar triggerX0;',
+ 'var triggerX1Payload;\nvar triggerX0;'
+ )
+ .replace(activeSend, upstreamSend)
+ .replace(
+ ' MMStartTask(triggerX0, activeTriggerX1Payload);',
+ ' MMStartTask(triggerX0, triggerX1Payload);'
+ )
+ .replace(
+ ' activeTriggerX1Payload = ptr(0);\n console.error("[!] Error trigger " + msgType + " MMStartTask: " + e);',
+ ' console.error("[!] Error trigger " + msgType + " MMStartTask: " + e);'
+ )
+ .replace(
+ '\t\t\t\tpendingSendMsgType = "";\n\t\t\t\tactiveTriggerX1Payload = ptr(0);\n\t\t\t\treturn',
+ '\t\t\t\tpendingSendMsgType = "";\n\t\t\t\treturn'
+ )
+ fs.writeFileSync(scriptPath, source)
+ console.log('[wechat-personal] 已恢复原生发送 payload 布局')
+}
- if (!source.includes(declarations) || !source.includes(originalSend)) {
- throw new Error('无法定位 wechat_chatter 连续发送补丁位置')
+function patchSendContextCapture(scriptPath, strict = true) {
+ let source = fs.readFileSync(scriptPath, 'utf8')
+ if (source.includes('function isLikelySendContext(')) return
+ const original = `function AttachSendFunc() {
+ Interceptor.attach(sendFuncAddr.add(0x10), {
+ onEnter: function (args) {
+
+ if (triggerX1Payload) {
+ return
+ }
+
+ triggerX0 = this.context.x0;
+ triggerX1Payload = this.context.x1;
+ console.log(\`[+] 捕获到 StartTask 调用,X0:\${triggerX0}, Payload: \${triggerX1Payload}\`);
+ }
+ })
+}`
+ const patched = `function isLikelySendContext(candidateX0, candidateX1) {
+ try {
+ if (!isReadablePointer(candidateX0) || !isReadablePointer(candidateX1)) return false;
+ var manager = readPointerIfReadable(candidateX0.add(0x18));
+ var cgi = readUtf8StringIfReadable(readPointerIfReadable(candidateX1.add(0x18)));
+ console.log("[debug] StartTask candidate x0=" + candidateX0 + " x1=" + candidateX1 + " x0+0x18=" + manager + " cgi=" + cgi);
+ return !manager.equals(ptr(0));
+ } catch (e) {
+ console.error("[debug] StartTask candidate inspect failed: " + e);
+ return false;
+ }
+}
+
+function AttachSendFunc() {
+ Interceptor.attach(sendFuncAddr.add(0x10), {
+ onEnter: function (args) {
+ if (triggerX1Payload) return;
+ var candidateX0 = this.context.x0;
+ var candidateX1 = this.context.x1;
+ if (!isLikelySendContext(candidateX0, candidateX1)) return;
+ triggerX0 = candidateX0;
+ triggerX1Payload = candidateX1;
+ console.log(\`[+] 捕获到有效 StartTask 上下文,X0:\${triggerX0}, Payload: \${triggerX1Payload}\`);
+ }
+ })
+}`
+ if (!source.includes(original)) {
+ if (strict) throw new Error('无法定位 StartTask 上下文 Hook')
+ return
}
- source = source.replace(declarations, patchedDeclarations).replace(originalSend, patchedSend)
- source = source.replace(
- ' MMStartTask(triggerX0, triggerX1Payload);',
- ' MMStartTask(triggerX0, activeTriggerX1Payload);'
- )
- source = source.replace(
- ' } catch (e) {\n console.error("[!] Error trigger " + msgType + " MMStartTask: " + e);',
- ' } catch (e) {\n activeTriggerX1Payload = ptr(0);\n console.error("[!] Error trigger " + msgType + " MMStartTask: " + e);'
- )
- source = source.replace(
- '\t\t\t\tpendingSendMsgType = "";\n\t\t\t\treturn',
- '\t\t\t\tpendingSendMsgType = "";\n\t\t\t\tactiveTriggerX1Payload = ptr(0);\n\t\t\t\treturn'
- )
+ source = source.replace(original, patched)
fs.writeFileSync(scriptPath, source)
- console.log('[wechat-personal] 已应用逐条发送 payload 隔离补丁')
}
function patchVoiceAudioBuffer(scriptPath) {
@@ -170,6 +220,163 @@ function patchImageHookReadiness(scriptPath) {
console.log('[wechat-personal] 已应用图片 Hook 状态补丁')
}
+// Backport of wechat_chatter PR #36 by @Leslielu:
+// https://github.com/yincongcyincong/wechat_chatter/pull/36
+// TraceMemo adds the verified macOS WeChat 4.1.11.53 addresses.
+// macOS WeChat 4.1.11.53, located and verified by TraceMemo
+function patchCdnColdStart(scriptPath) {
+ let source = fs.readFileSync(scriptPath, 'utf8')
+ if (source.includes('function resolveCdnManager()')) return
+
+ const initAddresses = ` uploadImageAddr = baseAddr.add({{.uploadImageAddr}});
+ cndOnCompleteAddr = baseAddr.add({{.cndOnCompleteAddr}});`
+ const patchedInitAddresses = ` uploadImageAddr = baseAddr.add({{.uploadImageAddr}});
+ cndOnCompleteAddr = baseAddr.add({{.cndOnCompleteAddr}});
+ // 冷启动 CdnManager 解析(旧版本缺少可选键时保持 hook 捕获行为)
+ {{if .cdnGetServiceAddr}}cdnGetServiceAddr = baseAddr.add({{.cdnGetServiceAddr}});{{end}}
+ {{if .cdnManagerGetterAddr}}cdnManagerGetterAddr = baseAddr.add({{.cdnManagerGetterAddr}});{{end}}`
+ if (!source.includes(initAddresses)) throw new Error('下载的微信版本配置与当前应用不兼容')
+ source = source.replace(initAddresses, patchedInitAddresses)
+
+ const downloadChunkEnd = `}
+
+function fillUploadX1AndStart`
+ const resolver = `}
+
+// 上传和下载共用同一个 mars::cdn::CdnManager。冷启动时通过服务定位器
+// 取得 [ctx + 0x40],避免必须先手动发送图片才能让 Hook 捕获上下文。
+function resolveCdnManager() {
+ if (cdnGetServiceAddr.equals(ptr(0)) || cdnManagerGetterAddr.equals(ptr(0))) {
+ return ptr(0);
+ }
+ try {
+ // libc++ SSO 短字符串:数据在 +0,长度写在 +0x17。
+ var strDefault = Memory.alloc(24);
+ strDefault.writeUtf8String("default");
+ strDefault.add(0x17).writeU8(7);
+
+ var getService = new NativeFunction(cdnGetServiceAddr, 'pointer', ['pointer']);
+ var svc = getService(strDefault);
+ if (!isReadablePointer(svc)) {
+ console.error("[!] GetService(\\"default\\") 返回不可读: " + svc);
+ return ptr(0);
+ }
+ var getCtx = new NativeFunction(cdnManagerGetterAddr, 'pointer', ['pointer']);
+ var ctx = getCtx(svc);
+ if (!isReadablePointer(ctx)) {
+ console.error("[!] CdnManager getter 返回不可读: " + ctx);
+ return ptr(0);
+ }
+ var mgr = readPointerIfReadable(ctx.add(0x40));
+ if (!isReadablePointer(mgr)) {
+ console.error("[!] ctx+0x40 管理器指针不可读: ctx=" + ctx);
+ return ptr(0);
+ }
+ return mgr;
+ } catch (e) {
+ console.error("[!] resolveCdnManager 异常: " + e);
+ return ptr(0);
+ }
+}
+
+function ensureCdnManagerX0() {
+ if (uploadGlobalX0.equals(ptr(0)) && downloadGlobalX0 && !downloadGlobalX0.equals(ptr(0))) {
+ uploadGlobalX0 = downloadGlobalX0;
+ console.log("[+] downloadGlobalX0 回填 uploadGlobalX0: " + uploadGlobalX0);
+ }
+ if ((!downloadGlobalX0 || downloadGlobalX0.equals(ptr(0))) && !uploadGlobalX0.equals(ptr(0))) {
+ downloadGlobalX0 = uploadGlobalX0;
+ console.log("[+] uploadGlobalX0 回填 downloadGlobalX0: " + downloadGlobalX0);
+ }
+ if (uploadGlobalX0.equals(ptr(0))) {
+ var mgr = resolveCdnManager();
+ if (!mgr.equals(ptr(0))) {
+ uploadGlobalX0 = mgr;
+ if (!downloadGlobalX0 || downloadGlobalX0.equals(ptr(0))) {
+ downloadGlobalX0 = mgr;
+ }
+ console.log("[+] 冷启动服务定位器解析 CdnManager: " + mgr);
+ }
+ }
+ return !uploadGlobalX0.equals(ptr(0));
+}
+
+function fillUploadX1AndStart`
+ if (!source.includes(downloadChunkEnd)) throw new Error('无法定位 wechat_chatter 媒体上传逻辑')
+ source = source.replace(downloadChunkEnd, resolver)
+
+ const declarations = 'var uploadImageAddr;\n'
+ const patchedDeclarations =
+ 'var uploadImageAddr;\nvar cdnGetServiceAddr = ptr(0);\nvar cdnManagerGetterAddr = ptr(0);\n'
+ if (!source.includes(declarations)) throw new Error('无法定位 wechat_chatter 媒体地址声明')
+ source = source.replace(declarations, patchedDeclarations)
+
+ const uploadGuard = `function fillUploadX1AndStart(idAddr, pathAddr, x1Buffer, receiver, md5, filePath, payloadHex) {
+ if (uploadGlobalX0.equals(ptr(0))) {`
+ const patchedUploadGuard = `function fillUploadX1AndStart(idAddr, pathAddr, x1Buffer, receiver, md5, filePath, payloadHex) {
+ if (uploadGlobalX0.equals(ptr(0))) {
+ ensureCdnManagerX0();
+ }
+ if (uploadGlobalX0.equals(ptr(0))) {`
+ if (!source.includes(uploadGuard)) throw new Error('无法定位 wechat_chatter 媒体上传入口')
+ source = source.replace(uploadGuard, patchedUploadGuard)
+
+ const voiceGuard = `function triggerUploadVoice(receiver, voicePath, payloadHex, audioDataHex, durationMs) {
+ if (uploadGlobalX0.equals(ptr(0))) {`
+ const patchedVoiceGuard = `function triggerUploadVoice(receiver, voicePath, payloadHex, audioDataHex, durationMs) {
+ if (uploadGlobalX0.equals(ptr(0))) {
+ ensureCdnManagerX0();
+ }
+ if (uploadGlobalX0.equals(ptr(0))) {`
+ if (!source.includes(voiceGuard)) throw new Error('无法定位 wechat_chatter 语音上传入口')
+ source = source.replace(voiceGuard, patchedVoiceGuard)
+
+ const uploadHook = `\t\t\tuploadGlobalX0 = capturedUploadX0;`
+ const patchedUploadHook = `\t\t\tuploadGlobalX0 = capturedUploadX0;
+ if ((!downloadGlobalX0 || downloadGlobalX0.equals(ptr(0))) && !capturedUploadX0.equals(ptr(0))) {
+ downloadGlobalX0 = capturedUploadX0;
+ console.log("[+] 上传hook回填 downloadGlobalX0: " + downloadGlobalX0);
+ }`
+ if (!source.includes(uploadHook)) throw new Error('无法定位 wechat_chatter 图片 Hook')
+ source = source.replace(uploadHook, patchedUploadHook)
+
+ const downloadHook = ` downloadGlobalX0 = this.context.x0;`
+ const patchedDownloadHook = ` downloadGlobalX0 = this.context.x0;
+ if (uploadGlobalX0.equals(ptr(0)) && !downloadGlobalX0.equals(ptr(0))) {
+ uploadGlobalX0 = downloadGlobalX0;
+ console.log("[+] 下载hook回填 uploadGlobalX0: " + uploadGlobalX0);
+ }`
+ if (!source.includes(downloadHook)) throw new Error('无法定位 wechat_chatter 下载 Hook')
+ source = source.replace(downloadHook, patchedDownloadHook)
+
+ const downloadGuard = `function triggerDownload(receiver, cdnUrl, aesKey, filePath, fileType) {
+ if (!downloadGlobalX0) {`
+ const patchedDownloadGuard = `function triggerDownload(receiver, cdnUrl, aesKey, filePath, fileType) {
+ if (!downloadGlobalX0 || downloadGlobalX0.equals(ptr(0))) {
+ ensureCdnManagerX0();
+ }
+ if (!downloadGlobalX0) {`
+ if (!source.includes(downloadGuard)) throw new Error('无法定位 wechat_chatter 媒体下载入口')
+ source = source.replace(downloadGuard, patchedDownloadGuard)
+
+ fs.writeFileSync(scriptPath, source)
+ console.log('[wechat-personal] 已应用 CdnManager 冷启动解析补丁')
+}
+
+function patchCdnColdStartConfig(configPath) {
+ const source = fs.readFileSync(configPath, 'utf8')
+ let config
+ try {
+ config = JSON.parse(source)
+ } catch {
+ throw new Error('4.1.11.53 版本配置不是有效 JSON')
+ }
+ config.cdnGetServiceAddr = '0x50a15d0'
+ config.cdnManagerGetterAddr = '0x5259290'
+ fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`)
+ console.log('[wechat-personal] 已写入 4.1.11.53 CdnManager 地址')
+}
+
function patchWechatCoreModuleBase(scriptPath) {
let source = fs.readFileSync(scriptPath, 'utf8')
if (source.includes('WeChat core module base:')) return
@@ -207,7 +414,7 @@ function addModifiedWorkNotice(scriptPath) {
* Runtime version: v0.0.18
* License: GNU General Public License version 3 (GPL-3.0)
* Changes: WeChat module discovery, per-send payload isolation, dynamic voice upload buffers,
- * and image Hook readiness logging.
+ * CdnManager cold-start resolution, media hook backfill, and image Hook readiness logging.
* These modifications are not provided by the upstream author.
*/
@@ -219,8 +426,11 @@ function addModifiedWorkNotice(scriptPath) {
patchWechatCoreModuleBase(script)
patchPerSendPayload(script)
+patchSendContextCapture(script)
patchVoiceAudioBuffer(script)
patchImageHookReadiness(script)
+patchCdnColdStartConfig(config)
+patchCdnColdStart(script)
addModifiedWorkNotice(script)
fs.chmodSync(executable, 0o755)
console.log('[wechat-personal] 运行时准备完成')
diff --git a/src/main/services/personal-wechat-capability-service.ts b/src/main/services/personal-wechat-capability-service.ts
index 9e3be55..f843a2c 100644
--- a/src/main/services/personal-wechat-capability-service.ts
+++ b/src/main/services/personal-wechat-capability-service.ts
@@ -26,7 +26,7 @@ export class PersonalWechatCapabilityService {
image: Boolean(senderStatus.canSendImage),
voice: Boolean(senderStatus.canSendVoice)
}
- const status = this.mapState(senderStatus, capabilities.image)
+ const status = this.mapState(senderStatus)
const ready = status === 'ready'
return {
supported: status !== 'unsupported',
@@ -40,8 +40,8 @@ export class PersonalWechatCapabilityService {
: senderStatus.message ||
(status === 'needs_binding' || status === 'unconfigured'
? '请先绑定个人微信'
- : status === 'needs_verification'
- ? '请先完成微信消息能力检测'
+ : status === 'initializing'
+ ? '正在初始化微信发送能力'
: status === 'ready'
? '个人微信已准备好发送日报'
: senderStatus.error || '个人微信发送能力异常'),
@@ -49,32 +49,34 @@ export class PersonalWechatCapabilityService {
}
}
- private mapState(
- senderStatus: PersonalWechatSenderStatus,
- canSendImage: boolean
- ): PersonalWechatSendCapabilityState {
+ private mapState(senderStatus: PersonalWechatSenderStatus): PersonalWechatSendCapabilityState {
if (senderStatus.platform === 'win32') {
if (senderStatus.canSend) return 'ready'
if (!senderStatus.endpoint) return 'unconfigured'
if (senderStatus.state === 'error' && senderStatus.endpointReady) return 'error'
- return 'needs_verification'
+ return 'initializing'
}
if (senderStatus.platform !== 'darwin' || senderStatus.state === 'unsupported_platform') {
return 'unsupported'
}
if (senderStatus.state === 'error') return 'error'
- const hasBinding = Boolean(senderStatus.boundWechatPid)
- if (!hasBinding) {
+ const hasCurrentBinding = Boolean(
+ senderStatus.endpointReady &&
+ senderStatus.attachReady &&
+ senderStatus.wechatPid &&
+ senderStatus.boundWechatPid === senderStatus.wechatPid
+ )
+ if (!hasCurrentBinding) {
return senderStatus.runtimeReady ? 'needs_binding' : 'unconfigured'
}
- if (canSendImage) return 'ready'
+ if (senderStatus.canSend) return 'ready'
if (
senderStatus.state === 'hook_not_ready' ||
senderStatus.state === 'online' ||
senderStatus.state === 'starting' ||
senderStatus.state === 'stopped'
) {
- return 'needs_verification'
+ return 'initializing'
}
return 'error'
}
diff --git a/src/main/services/personal-wechat-runtime-manager.ts b/src/main/services/personal-wechat-runtime-manager.ts
index 55a3759..aaf59c9 100644
--- a/src/main/services/personal-wechat-runtime-manager.ts
+++ b/src/main/services/personal-wechat-runtime-manager.ts
@@ -21,39 +21,78 @@ const ARCHIVE_SHA256 = 'ee1e11bccef7cec1cf944cd8b2ac3fadaadb9376ba24cd823e340914
function patchPerSendPayload(scriptPath: string): void {
let source = readFileSync(scriptPath, 'utf8')
- if (source.includes('var activeTriggerX1Payload = ptr(0);')) return
+ if (!source.includes('var activeTriggerX1Payload = ptr(0);')) return
- const declarations = 'var triggerX1Payload;\nvar triggerX0;'
- const patchedDeclarations =
- 'var triggerX1Payload;\nvar activeTriggerX1Payload = ptr(0);\nvar triggerX0;'
- const originalSend = ` const payloadData = hexToByteArray(payloadHex);
- triggerX1Payload.writeByteArray(payloadData);
- triggerX1Payload.add(0x18).writePointer(info.cgiAddr);
- triggerX1Payload.add(0xb8).writePointer(triggerX1Payload.add(0xc0));
- triggerX1Payload.add(0x190).writePointer(triggerX1Payload.add(0x198));`
- const patchedSend = ` const payloadData = hexToByteArray(payloadHex);
+ const activeSend = ` const payloadData = hexToByteArray(payloadHex);
activeTriggerX1Payload = Memory.alloc(payloadData.length);
activeTriggerX1Payload.writeByteArray(payloadData);
activeTriggerX1Payload.add(0x18).writePointer(info.cgiAddr);
activeTriggerX1Payload.add(0xb8).writePointer(activeTriggerX1Payload.add(0xc0));
activeTriggerX1Payload.add(0x190).writePointer(activeTriggerX1Payload.add(0x198));`
-
- if (!source.includes(declarations) || !source.includes(originalSend)) {
+ const upstreamSend = ` const payloadData = hexToByteArray(payloadHex);
+ triggerX1Payload.writeByteArray(payloadData);
+ triggerX1Payload.add(0x18).writePointer(info.cgiAddr);
+ triggerX1Payload.add(0xb8).writePointer(triggerX1Payload.add(0xc0));
+ triggerX1Payload.add(0x190).writePointer(triggerX1Payload.add(0x198));`
+ if (!source.includes(activeSend)) {
throw new Error('下载的发送组件与当前应用不兼容')
}
- source = source.replace(declarations, patchedDeclarations).replace(originalSend, patchedSend)
- source = source.replace(
- ' MMStartTask(triggerX0, triggerX1Payload);',
- ' MMStartTask(triggerX0, activeTriggerX1Payload);'
- )
- source = source.replace(
- ' } catch (e) {\n console.error("[!] Error trigger " + msgType + " MMStartTask: " + e);',
- ' } catch (e) {\n activeTriggerX1Payload = ptr(0);\n console.error("[!] Error trigger " + msgType + " MMStartTask: " + e);'
- )
- source = source.replace(
- '\t\t\t\tpendingSendMsgType = "";\n\t\t\t\treturn',
- '\t\t\t\tpendingSendMsgType = "";\n\t\t\t\tactiveTriggerX1Payload = ptr(0);\n\t\t\t\treturn'
- )
+ source = source.replace('var triggerX1Payload;\nvar activeTriggerX1Payload = ptr(0);\nvar triggerX0;', 'var triggerX1Payload;\nvar triggerX0;')
+ .replace(activeSend, upstreamSend)
+ .replace(' MMStartTask(triggerX0, activeTriggerX1Payload);', ' MMStartTask(triggerX0, triggerX1Payload);')
+ .replace(' activeTriggerX1Payload = ptr(0);\n console.error("[!] Error trigger " + msgType + " MMStartTask: " + e);', ' console.error("[!] Error trigger " + msgType + " MMStartTask: " + e);')
+ .replace('\t\t\t\tpendingSendMsgType = "";\n\t\t\t\tactiveTriggerX1Payload = ptr(0);\n\t\t\t\treturn', '\t\t\t\tpendingSendMsgType = "";\n\t\t\t\treturn')
+ writeFileSync(scriptPath, source)
+}
+
+function patchSendContextCapture(scriptPath: string, strict = true): void {
+ let source = readFileSync(scriptPath, 'utf8')
+ if (source.includes('function isLikelySendContext(')) return
+ const original = `function AttachSendFunc() {
+ Interceptor.attach(sendFuncAddr.add(0x10), {
+ onEnter: function (args) {
+
+ if (triggerX1Payload) {
+ return
+ }
+
+ triggerX0 = this.context.x0;
+ triggerX1Payload = this.context.x1;
+ console.log(\`[+] 捕获到 StartTask 调用,X0:\${triggerX0}, Payload: \${triggerX1Payload}\`);
+ }
+ })
+}`
+ const patched = `function isLikelySendContext(candidateX0, candidateX1) {
+ try {
+ if (!isReadablePointer(candidateX0) || !isReadablePointer(candidateX1)) return false;
+ var manager = readPointerIfReadable(candidateX0.add(0x18));
+ var cgi = readUtf8StringIfReadable(readPointerIfReadable(candidateX1.add(0x18)));
+ console.log("[debug] StartTask candidate x0=" + candidateX0 + " x1=" + candidateX1 + " x0+0x18=" + manager + " cgi=" + cgi);
+ return !manager.equals(ptr(0));
+ } catch (e) {
+ console.error("[debug] StartTask candidate inspect failed: " + e);
+ return false;
+ }
+}
+
+function AttachSendFunc() {
+ Interceptor.attach(sendFuncAddr.add(0x10), {
+ onEnter: function (args) {
+ if (triggerX1Payload) return;
+ var candidateX0 = this.context.x0;
+ var candidateX1 = this.context.x1;
+ if (!isLikelySendContext(candidateX0, candidateX1)) return;
+ triggerX0 = candidateX0;
+ triggerX1Payload = candidateX1;
+ console.log(\`[+] 捕获到有效 StartTask 上下文,X0:\${triggerX0}, Payload: \${triggerX1Payload}\`);
+ }
+ })
+}`
+ if (!source.includes(original)) {
+ if (strict) throw new Error('下载的微信版本与当前应用不兼容')
+ return
+ }
+ source = source.replace(original, patched)
writeFileSync(scriptPath, source)
}
@@ -105,6 +144,181 @@ function patchImageHookReadiness(scriptPath: string): void {
writeFileSync(scriptPath, source)
}
+function patchCdnColdStart(scriptPath: string, strict = true): void {
+ let source = readFileSync(scriptPath, 'utf8')
+ if (source.includes('function resolveCdnManager()')) return
+
+ const initAddresses = ` uploadImageAddr = baseAddr.add({{.uploadImageAddr}});
+ cndOnCompleteAddr = baseAddr.add({{.cndOnCompleteAddr}});`
+ const patchedInitAddresses = ` uploadImageAddr = baseAddr.add({{.uploadImageAddr}});
+ cndOnCompleteAddr = baseAddr.add({{.cndOnCompleteAddr}});
+ // 冷启动 CdnManager 解析(旧版本缺少可选键时保持 hook 捕获行为)
+ {{if .cdnGetServiceAddr}}cdnGetServiceAddr = baseAddr.add({{.cdnGetServiceAddr}});{{end}}
+ {{if .cdnManagerGetterAddr}}cdnManagerGetterAddr = baseAddr.add({{.cdnManagerGetterAddr}});{{end}}`
+ if (!source.includes(initAddresses)) {
+ if (strict) throw new Error('下载的微信版本配置与当前应用不兼容')
+ return
+ }
+ source = source.replace(initAddresses, patchedInitAddresses)
+
+ const downloadChunkEnd = `}
+
+function fillUploadX1AndStart`
+ const resolver = `}
+
+// 上传和下载共用同一个 mars::cdn::CdnManager。冷启动时通过服务定位器
+// 取得 [ctx + 0x40],避免必须先手动发送图片才能让 Hook 捕获上下文。
+function resolveCdnManager() {
+ if (cdnGetServiceAddr.equals(ptr(0)) || cdnManagerGetterAddr.equals(ptr(0))) {
+ return ptr(0);
+ }
+ try {
+ // libc++ SSO 短字符串:数据在 +0,长度写在 +0x17。
+ var strDefault = Memory.alloc(24);
+ strDefault.writeUtf8String("default");
+ strDefault.add(0x17).writeU8(7);
+
+ var getService = new NativeFunction(cdnGetServiceAddr, 'pointer', ['pointer']);
+ var svc = getService(strDefault);
+ if (!isReadablePointer(svc)) {
+ console.error("[!] GetService(\\"default\\") 返回不可读: " + svc);
+ return ptr(0);
+ }
+ var getCtx = new NativeFunction(cdnManagerGetterAddr, 'pointer', ['pointer']);
+ var ctx = getCtx(svc);
+ if (!isReadablePointer(ctx)) {
+ console.error("[!] CdnManager getter 返回不可读: " + ctx);
+ return ptr(0);
+ }
+ var mgr = readPointerIfReadable(ctx.add(0x40));
+ if (!isReadablePointer(mgr)) {
+ console.error("[!] ctx+0x40 管理器指针不可读: ctx=" + ctx);
+ return ptr(0);
+ }
+ return mgr;
+ } catch (e) {
+ console.error("[!] resolveCdnManager 异常: " + e);
+ return ptr(0);
+ }
+}
+
+function ensureCdnManagerX0() {
+ if (uploadGlobalX0.equals(ptr(0)) && downloadGlobalX0 && !downloadGlobalX0.equals(ptr(0))) {
+ uploadGlobalX0 = downloadGlobalX0;
+ console.log("[+] downloadGlobalX0 回填 uploadGlobalX0: " + uploadGlobalX0);
+ }
+ if ((!downloadGlobalX0 || downloadGlobalX0.equals(ptr(0))) && !uploadGlobalX0.equals(ptr(0))) {
+ downloadGlobalX0 = uploadGlobalX0;
+ console.log("[+] uploadGlobalX0 回填 downloadGlobalX0: " + downloadGlobalX0);
+ }
+ if (uploadGlobalX0.equals(ptr(0))) {
+ var mgr = resolveCdnManager();
+ if (!mgr.equals(ptr(0))) {
+ uploadGlobalX0 = mgr;
+ if (!downloadGlobalX0 || downloadGlobalX0.equals(ptr(0))) {
+ downloadGlobalX0 = mgr;
+ }
+ console.log("[+] 冷启动服务定位器解析 CdnManager: " + mgr);
+ }
+ }
+ return !uploadGlobalX0.equals(ptr(0));
+}
+
+function fillUploadX1AndStart`
+ if (!source.includes(downloadChunkEnd)) {
+ if (strict) throw new Error('无法定位 wechat_chatter 媒体上传逻辑')
+ return
+ }
+ source = source.replace(downloadChunkEnd, resolver)
+
+ const declarations = 'var uploadImageAddr;\n'
+ const patchedDeclarations =
+ 'var uploadImageAddr;\nvar cdnGetServiceAddr = ptr(0);\nvar cdnManagerGetterAddr = ptr(0);\n'
+ if (!source.includes(declarations)) {
+ if (strict) throw new Error('无法定位 wechat_chatter 媒体地址声明')
+ return
+ }
+ source = source.replace(declarations, patchedDeclarations)
+
+ const uploadGuard = `function fillUploadX1AndStart(idAddr, pathAddr, x1Buffer, receiver, md5, filePath, payloadHex) {
+ if (uploadGlobalX0.equals(ptr(0))) {`
+ const patchedUploadGuard = `function fillUploadX1AndStart(idAddr, pathAddr, x1Buffer, receiver, md5, filePath, payloadHex) {
+ if (uploadGlobalX0.equals(ptr(0))) {
+ ensureCdnManagerX0();
+ }
+ if (uploadGlobalX0.equals(ptr(0))) {`
+ if (!source.includes(uploadGuard)) {
+ if (strict) throw new Error('无法定位 wechat_chatter 媒体上传入口')
+ return
+ }
+ source = source.replace(uploadGuard, patchedUploadGuard)
+
+ const voiceGuard = `function triggerUploadVoice(receiver, voicePath, payloadHex, audioDataHex, durationMs) {
+ if (uploadGlobalX0.equals(ptr(0))) {`
+ const patchedVoiceGuard = `function triggerUploadVoice(receiver, voicePath, payloadHex, audioDataHex, durationMs) {
+ if (uploadGlobalX0.equals(ptr(0))) {
+ ensureCdnManagerX0();
+ }
+ if (uploadGlobalX0.equals(ptr(0))) {`
+ if (!source.includes(voiceGuard)) {
+ if (strict) throw new Error('无法定位 wechat_chatter 语音上传入口')
+ return
+ }
+ source = source.replace(voiceGuard, patchedVoiceGuard)
+
+ const uploadHook = `\t\t\tuploadGlobalX0 = capturedUploadX0;`
+ const patchedUploadHook = `\t\t\tuploadGlobalX0 = capturedUploadX0;
+ if ((!downloadGlobalX0 || downloadGlobalX0.equals(ptr(0))) && !capturedUploadX0.equals(ptr(0))) {
+ downloadGlobalX0 = capturedUploadX0;
+ console.log("[+] 上传hook回填 downloadGlobalX0: " + downloadGlobalX0);
+ }`
+ if (!source.includes(uploadHook)) {
+ if (strict) throw new Error('无法定位 wechat_chatter 图片 Hook')
+ return
+ }
+ source = source.replace(uploadHook, patchedUploadHook)
+
+ const downloadHook = ` downloadGlobalX0 = this.context.x0;`
+ const patchedDownloadHook = ` downloadGlobalX0 = this.context.x0;
+ if (uploadGlobalX0.equals(ptr(0)) && !downloadGlobalX0.equals(ptr(0))) {
+ uploadGlobalX0 = downloadGlobalX0;
+ console.log("[+] 下载hook回填 uploadGlobalX0: " + uploadGlobalX0);
+ }`
+ if (!source.includes(downloadHook)) {
+ if (strict) throw new Error('无法定位 wechat_chatter 下载 Hook')
+ return
+ }
+ source = source.replace(downloadHook, patchedDownloadHook)
+
+ const downloadGuard = `function triggerDownload(receiver, cdnUrl, aesKey, filePath, fileType) {
+ if (!downloadGlobalX0) {`
+ const patchedDownloadGuard = `function triggerDownload(receiver, cdnUrl, aesKey, filePath, fileType) {
+ if (!downloadGlobalX0 || downloadGlobalX0.equals(ptr(0))) {
+ ensureCdnManagerX0();
+ }
+ if (!downloadGlobalX0) {`
+ if (!source.includes(downloadGuard)) {
+ if (strict) throw new Error('无法定位 wechat_chatter 媒体下载入口')
+ return
+ }
+ source = source.replace(downloadGuard, patchedDownloadGuard)
+
+ writeFileSync(scriptPath, source)
+}
+
+function patchCdnColdStartConfig(configPath: string, strict = true): void {
+ let config: Record 请打开微信,可以给任意好友发送一张图片,完成一次能力初始化。 完成后点击“重新检测”。 绑定微信后,TraceMemo 会自动完成文字、图片和语音发送所需的初始化。
- 暂未检测到语音发送能力,请确认微信已完成初始化后再次检测。
+ {detecting ? '正在初始化发送能力…' : '正在等待发送运行时完成初始化…'}
+
+ {senderStatus.message || '发送能力初始化失败,请重新绑定。'}
- 语音能力还未就绪,请在微信中完成一次媒体消息初始化后重新检测。
-
+ 发送能力准备完成后会自动进入已就绪状态。
+