# -*- coding: utf-8 -*- """ Plaintext CSV hook for the Zhaocaibao Electron/Node engine. This launcher is intentionally observational: - it does not change request options, return values, TLS settings, or crypto output; - it writes only plaintext request/response rows to CSV under captures/plain_csv_/; - it skips encrypted envelopes and other raw hook events; - it targets 47.239.81.174 by default for network events. Use only in a local analysis environment where you are allowed to inspect the app. """ from __future__ import annotations import argparse import json import os import shutil import subprocess import sys import threading import time from pathlib import Path WORKSPACE = Path(__file__).resolve().parent DEFAULT_APP_DIR = Path(r"D:\zhaocaibao - " + "\u526f\u672c") DEFAULT_EXE = DEFAULT_APP_DIR / ("\u62db\u8d22\u5b9d.exe") DEFAULT_ENGINE_LOADER = DEFAULT_APP_DIR / "resources" / "engine" / "wechat_engine.js" DEFAULT_HOSTS = "47.239.81.174" PRELOAD_JS = r""" 'use strict'; const fs = require('node:fs'); const path = require('node:path'); const crypto = require('node:crypto'); const http = require('node:http'); const https = require('node:https'); const net = require('node:net'); const tls = require('node:tls'); const childProcess = require('node:child_process'); const LOG_DIR = process.env.ZCB_DEEP_LOG_DIR || process.cwd(); const MAX_BYTES = Math.max(128, Number.parseInt(process.env.ZCB_DEEP_MAX_BYTES || '8192', 10) || 8192); const CONSOLE_PREVIEW = process.env.ZCB_DEEP_CONSOLE === '1'; const CONSOLE_BYTES = Math.max(128, Number.parseInt(process.env.ZCB_DEEP_CONSOLE_BYTES || '1200', 10) || 1200); const CSV_MAX_BYTES = Math.max(1024, Number.parseInt(process.env.ZCB_PLAIN_CSV_MAX_BYTES || '33554432', 10) || 33554432); const INCLUDE_SECRETS = process.env.ZCB_DEEP_INCLUDE_SECRETS === '1'; const LOG_HASHES = process.env.ZCB_DEEP_LOG_HASHES === '1'; const PRINT_ALL_DECIPHER = process.env.ZCB_DEEP_PRINT_ALL_DECIPHER === '1'; const RAW_CREATE_HASH = crypto.createHash.bind(crypto); const RAW_JSON_STRINGIFY = JSON.stringify.bind(JSON); const RAW_JSON_PARSE = JSON.parse.bind(JSON); const ONLY_HOSTS = new Set(String(process.env.ZCB_ONLY_HOSTS || '47.239.81.174') .split(',') .map((x) => x.trim().toLowerCase()) .filter(Boolean)); fs.mkdirSync(LOG_DIR, { recursive: true }); const CSV_PATH = path.join(LOG_DIR, `plaintext_${process.pid}.csv`); const BODY_DIR = path.join(LOG_DIR, 'bodies'); fs.mkdirSync(BODY_DIR, { recursive: true }); let nextId = 0; let nextBodyFileId = 0; const interestingText = /(47\.239\.81\.174|\/api\/|login|password|username|token|user_?id|user_?key|serverAddress|goods|submitted|submit|req_time|request_trace_id|resp_result|invite_code|team|task|pdd|PddUid|clientIp)/i; const recentPlaintexts = []; const pendingRequestPlaintexts = []; const recentUrls = []; const requestMeta = new Map(); const responseMetaByUrl = new Map(); const SCRUB_ENV_NAMES = new Set([ 'NODE_OPTIONS', 'ELECTRON_ENABLE_LOGGING', 'ELECTRON_RUN_AS_NODE', 'ZCB_DEEP_LOG_DIR', 'ZCB_DEEP_MAX_BYTES', 'ZCB_DEEP_CONSOLE', 'ZCB_DEEP_CONSOLE_BYTES', 'ZCB_DEEP_INCLUDE_SECRETS', 'ZCB_DEEP_LOG_HASHES', 'ZCB_DEEP_PRINT_ALL_DECIPHER', 'ZCB_ONLY_HOSTS', ]); function nowIso() { return new Date().toISOString(); } function id(prefix) { nextId += 1; return `${prefix}:${process.pid}:${Date.now()}:${nextId}`; } function writeLog(event) { try { handlePlaintextEvent(event || {}); } catch (_) { } } function ensureCsvHeader() { try { if (fs.existsSync(CSV_PATH)) return; const header = [ 'ts', 'pid', 'kind', 'url', 'method', 'status_code', 'request_id', 'source', 'headers_json', 'body_full_length', 'body_file', 'body_path', 'body_sha256', 'body_truncated', ]; fs.appendFileSync(CSV_PATH, '\ufeff' + header.map(csvCell).join(',') + '\r\n'); } catch (_) { } } function csvCell(value) { const text = value === undefined || value === null ? '' : String(value); return `"${text.replace(/"/g, '""')}"`; } function stableHeaders(headers) { try { return RAW_JSON_STRINGIFY(headers || {}); } catch (_) { return ''; } } function bodyPayload(text) { const body = String(text); if (body.length <= CSV_MAX_BYTES) return { body, truncated: false, fullLength: body.length }; return { body: body.slice(0, CSV_MAX_BYTES), truncated: true, fullLength: body.length }; } function safeFilePart(value) { return String(value || '') .replace(/[<>:"/\\|?*\x00-\x1f]+/g, '_') .replace(/\s+/g, '_') .replace(/^_+|_+$/g, '') .slice(0, 90) || 'body'; } function endpointFilePart(url) { try { const parsed = new URL(String(url || '')); const endpoint = `${parsed.hostname}${parsed.pathname}`.replace(/^\/+/, ''); return safeFilePart(endpoint); } catch (_) { return safeFilePart(url || 'unknown_url'); } } function writeBodyFile(row, normalized) { nextBodyFileId += 1; const hash = RAW_CREATE_HASH('sha256').update(normalized.body, 'utf8').digest('hex'); const kind = safeFilePart(row.kind || 'plaintext'); const method = safeFilePart(row.method || 'NO_METHOD'); const endpoint = endpointFilePart(row.url || ''); const requestIdPart = safeFilePart(row.requestId || '').slice(-36) || String(Date.now()); const fileName = [ String(nextBodyFileId).padStart(6, '0'), kind, method, endpoint, requestIdPart, hash.slice(0, 12), ].filter(Boolean).join('__') + '.txt'; const filePath = path.join(BODY_DIR, fileName); fs.writeFileSync(filePath, normalized.body, 'utf8'); return { fileName, filePath, relPath: path.join('bodies', fileName), sha256: hash, }; } function writePlainCsv(row) { try { if (!row || !row.body || looksLikeEnvelope(row.body)) return; const normalized = bodyPayload(row.body); if (!normalized.body) return; const bodyFile = writeBodyFile(row, normalized); ensureCsvHeader(); const line = [ nowIso(), process.pid, row.kind || '', row.url || '', row.method || '', row.statusCode || '', row.requestId || '', row.source || '', stableHeaders(row.headers || {}), normalized.fullLength, bodyFile.fileName, bodyFile.relPath, bodyFile.sha256, normalized.truncated ? '1' : '0', ].map(csvCell).join(','); fs.appendFileSync(CSV_PATH, line + '\r\n'); if (CONSOLE_PREVIEW) { const tag = row.kind === 'request_plaintext' ? 'REQ-PLAINTEXT' : 'RES-PLAINTEXT'; console.log(`[ZCB-CSV ${tag}] ${row.url || ''}`); console.log(`[body-file] ${bodyFile.relPath}`); console.log(cleanConsoleText(normalized.body).slice(0, CONSOLE_BYTES)); } } catch (_) { } } function handlePlaintextEvent(row) { const type = String(row.type || ''); if (type === 'hook.loaded') return; if (type.endsWith('.request')) { requestMeta.set(row.requestId, { url: row.url || '', method: row.method || '', headers: row.headers || {}, }); return; } if (type.endsWith('.response')) { const meta = requestMeta.get(row.requestId) || {}; const responseMeta = { requestId: row.requestId || '', url: row.url || meta.url || '', method: meta.method || '', statusCode: row.statusCode || '', headers: row.headers || {}, }; if (responseMeta.url) { responseMetaByUrl.set(responseMeta.url, responseMeta); rememberUrl(responseMeta.url, 'res', responseMeta); } return; } if (type.endsWith('.request.write') || type.endsWith('.request.end_body') || type === 'fetch.request') { const meta = requestMeta.get(row.requestId) || {}; const plain = consumePendingRequestPlaintext() || pullRecentPlaintext(); if (plain && row.url) { rememberUrl(row.url, 'req', { requestId: row.requestId, method: row.method || meta.method }); writePlainCsv({ kind: 'request_plaintext', url: row.url || meta.url || '', method: row.method || meta.method || '', requestId: row.requestId || '', source: plain.source, headers: meta.headers || {}, body: plain.text, }); } return; } if (type.endsWith('.response.data') || type === 'fetch.response.body') { const bodyText = row.body && row.body.utf8Preview ? row.body.utf8Preview : ''; const meta = responseMetaByUrl.get(row.url || '') || {}; if (bodyText && usefulPlainOutput(bodyText)) { rememberUrl(row.url || '', 'res', meta); writePlainCsv({ kind: 'response_plaintext', url: row.url || meta.url || '', method: meta.method || '', statusCode: meta.statusCode || '', requestId: row.requestId || meta.requestId || '', source: type, headers: meta.headers || {}, body: bodyText, }); } return; } if (type === 'crypto.decipher.update' || type === 'crypto.decipher.final') { const outputText = row.output && row.output.utf8Preview ? row.output.utf8Preview : ''; if (!usefulPlainOutput(outputText)) return; const recent = pullRecentUrl('res'); const url = typeof recent === 'string' ? recent : (recent && recent.url) || ''; const meta = responseMetaByUrl.get(url) || (recent && typeof recent === 'object' ? recent : {}); writePlainCsv({ kind: 'response_plaintext', url, method: meta.method || '', statusCode: meta.statusCode || '', requestId: meta.requestId || '', source: type, headers: meta.headers || {}, body: outputText, }); } } function rememberPlaintext(source, text) { try { const cleaned = cleanConsoleText(text); if (!cleaned || !interestingText.test(cleaned)) return; if (looksLikeEnvelope(cleaned)) return; const item = { ts: Date.now(), source, text: cleaned, consumed: false }; recentPlaintexts.push(item); while (recentPlaintexts.length > 20) recentPlaintexts.shift(); if (isRequestPlaintextCandidate(source, cleaned)) { pendingRequestPlaintexts.push(item); while (pendingRequestPlaintexts.length > 100) pendingRequestPlaintexts.shift(); } } catch (_) { } } function isRequestPlaintextCandidate(source, text) { if (!text || looksLikeEnvelope(text)) return false; if (String(source || '').startsWith('crypto.cipher.update')) return true; const trimmed = String(text).trim(); if (!(trimmed.startsWith('{') || trimmed.startsWith('['))) return false; if (/"method"\s*:\s*"Runtime\.evaluate"|"method"\s*:\s*"Page\./.test(trimmed)) return false; return /"payload"\s*:|"username"\s*:|"password"\s*:|"user_key"\s*:|"user_id"\s*:|"serverAddress"\s*:/.test(trimmed); } function consumePendingRequestPlaintext() { const now = Date.now(); for (let i = pendingRequestPlaintexts.length - 1; i >= 0; i -= 1) { const item = pendingRequestPlaintexts[i]; if (item.consumed) continue; if (now - item.ts > 30000) continue; item.consumed = true; return item; } return null; } function rememberUrl(url, direction, meta = {}) { if (!url) return; recentUrls.push({ ts: Date.now(), url: String(url), direction, ...meta }); while (recentUrls.length > 20) recentUrls.shift(); } function pullRecentUrl(direction) { const now = Date.now(); for (let i = recentUrls.length - 1; i >= 0; i -= 1) { const item = recentUrls[i]; if (now - item.ts <= 5000 && (!direction || item.direction === direction)) return item; } for (let i = recentUrls.length - 1; i >= 0; i -= 1) { const item = recentUrls[i]; if (now - item.ts <= 5000) return item; } return null; } function pullRecentPlaintext() { const now = Date.now(); for (let i = recentPlaintexts.length - 1; i >= 0; i -= 1) { const item = recentPlaintexts[i]; if (now - item.ts <= 5000) return item; } return null; } function looksLikeEnvelope(text) { try { const parsed = RAW_JSON_PARSE(String(text).trim()); if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return false; const keys = Object.keys(parsed); return parsed.v === 1 && typeof parsed.iv === 'string' && typeof parsed.ct === 'string' && (typeof parsed.tag === 'string' || typeof parsed.ek === 'string') && keys.every((key) => ['v', 'ek', 'iv', 'ct', 'tag'].includes(key)); } catch (_) { return false; } } function usefulPlainOutput(text) { if (!text) return false; if (looksLikeEnvelope(text)) return false; if (PRINT_ALL_DECIPHER) return true; return interestingText.test(text); } function maybeConsole(row) { if (!CONSOLE_PREVIEW) return; try { const type = String(row.type || ''); const isRequestBody = /\.(request\.(write|end_body))$/.test(type) || type === 'fetch.request'; const isResponseBody = /\.(response\.data)$/.test(type) || type === 'fetch.response.body'; if (!isRequestBody && !isResponseBody) { return; } let preview = ''; if (row.body && row.body.utf8Preview) preview = row.body.utf8Preview; else return; if (!interestingText.test(preview) && !interestingText.test(RAW_JSON_STRINGIFY(row).slice(0, 2000))) { return; } preview = cleanConsoleText(preview).slice(0, CONSOLE_BYTES); const direction = isRequestBody ? 'REQ' : 'RES'; rememberUrl(row.url || '', isRequestBody ? 'req' : 'res'); console.log(`[ZCB-DEEP ${direction}] ${row.url || ''}`); if (isRequestBody) { const plain = pullRecentPlaintext(); if (plain) { console.log(`[plaintext:${plain.source}] ${plain.text.slice(0, CONSOLE_BYTES)}`); } else { console.log(preview); } } else { console.log(preview); } } catch (_) { } } function cleanConsoleText(text) { let out = String(text); for (let i = 0; i < 2; i += 1) { const trimmed = out.trim(); if (!((trimmed.startsWith('"') && trimmed.endsWith('"')) || trimmed.startsWith('{') || trimmed.startsWith('['))) { break; } try { const parsed = RAW_JSON_PARSE(trimmed); if (typeof parsed === 'string') out = parsed; else out = RAW_JSON_STRINGIFY(parsed); } catch (_) { break; } } return out .replace(/\\\//g, '/') .replace(/\\n/g, '\n') .replace(/\\r/g, '\r') .replace(/\\t/g, '\t') .replace(/\r?\n\s*/g, ' ') .replace(/\s+/g, ' '); } function isBufferLike(value) { return Buffer.isBuffer(value) || value instanceof Uint8Array || value instanceof ArrayBuffer; } function toBuffer(value, encoding) { if (value === undefined || value === null) return Buffer.alloc(0); if (Buffer.isBuffer(value)) return Buffer.from(value); if (value instanceof Uint8Array) return Buffer.from(value); if (value instanceof ArrayBuffer) return Buffer.from(new Uint8Array(value)); if (typeof value === 'string') return Buffer.from(value, encoding || 'utf8'); try { return Buffer.from(String(value), encoding || 'utf8'); } catch (_) { return Buffer.alloc(0); } } function sha256Hex(value) { try { return RAW_CREATE_HASH('sha256').update(toBuffer(value)).digest('hex'); } catch (_) { return ''; } } function payload(value, encoding) { const body = toBuffer(value, encoding); const cut = body.subarray(0, Math.min(body.length, MAX_BYTES)); return { length: body.length, capturedLength: cut.length, truncated: body.length > cut.length, sha256: sha256Hex(body), utf8Preview: cut.toString('utf8'), base64: cut.toString('base64'), }; } function secretPayload(value, encoding) { if (INCLUDE_SECRETS) return payload(value, encoding); const body = toBuffer(value, encoding); return { length: body.length, sha256: sha256Hex(body), redacted: true, }; } function simpleValue(value, depth = 0) { if (depth > 3) return '[MaxDepth]'; if (value === undefined || value === null) return value; if (typeof value === 'string') { return value.length > 2048 ? `${value.slice(0, 2048)}...[truncated ${value.length}]` : value; } if (typeof value === 'number' || typeof value === 'boolean') return value; if (typeof value === 'function') return `[Function ${value.name || 'anonymous'}]`; if (isBufferLike(value)) return secretPayload(value); if (value instanceof URL) return String(value); if (Array.isArray(value)) return value.slice(0, 32).map((x) => simpleValue(x, depth + 1)); if (typeof value === 'object') { const out = {}; for (const key of Object.keys(value).slice(0, 80)) { if (/^(key|cert|ca|pfx|passphrase)$/i.test(key)) { out[key] = secretPayload(value[key]); } else { out[key] = simpleValue(value[key], depth + 1); } } return out; } return String(value); } function hostAllowed(host) { if (!host) return false; const normalized = String(host).split(':')[0].toLowerCase(); return ONLY_HOSTS.size === 0 || ONLY_HOSTS.has(normalized); } function optionObjectFromArgs(args, defaultProtocol) { let urlObj = null; let opts = {}; const first = args[0]; const second = args[1]; try { if (typeof first === 'string' || first instanceof URL) { urlObj = new URL(first); } } catch (_) { } if (first && typeof first === 'object' && !(first instanceof URL) && !Buffer.isBuffer(first)) { opts = { ...first }; } if (second && typeof second === 'object' && typeof second !== 'function') { opts = { ...opts, ...second }; } const protocol = opts.protocol || (urlObj && urlObj.protocol) || defaultProtocol; const hostname = opts.hostname || opts.host || (urlObj && urlObj.hostname) || ''; const port = opts.port || (urlObj && urlObj.port) || (protocol === 'https:' ? '443' : '80'); const pathName = opts.path || ( urlObj ? `${urlObj.pathname || '/'}${urlObj.search || ''}` : '/' ); const method = String(opts.method || 'GET').toUpperCase(); return { protocol, hostname: String(hostname).split(':')[0], port: String(port), path: pathName, method, headers: simpleValue(opts.headers || {}), options: simpleValue(opts), url: `${protocol}//${hostname}${port ? `:${port}` : ''}${pathName}`, }; } function patchRequestModule(mod, defaultProtocol, label) { if (!mod || mod.__zcbDeepPatched) return; Object.defineProperty(mod, '__zcbDeepPatched', { value: true }); const originalRequest = mod.request; const originalGet = mod.get; mod.request = function patchedRequest(...args) { const info = optionObjectFromArgs(args, defaultProtocol); const requestId = id(`${label}.request`); const shouldLog = hostAllowed(info.hostname); if (shouldLog) { writeLog({ type: `${label}.request`, requestId, ...info }); } const req = originalRequest.apply(this, args); if (!req || req.__zcbDeepWrapped) return req; Object.defineProperty(req, '__zcbDeepWrapped', { value: true }); const originalWrite = req.write; const originalEnd = req.end; req.write = function patchedClientWrite(chunk, encoding, cb) { if (shouldLog && chunk !== undefined && chunk !== null) { writeLog({ type: `${label}.request.write`, requestId, url: info.url, method: info.method, body: payload(chunk, typeof encoding === 'string' ? encoding : undefined), }); } return originalWrite.apply(this, arguments); }; req.end = function patchedClientEnd(chunk, encoding, cb) { if (shouldLog) { if (chunk !== undefined && chunk !== null) { writeLog({ type: `${label}.request.end_body`, requestId, url: info.url, method: info.method, body: payload(chunk, typeof encoding === 'string' ? encoding : undefined), }); } writeLog({ type: `${label}.request.end`, requestId, url: info.url, method: info.method }); } return originalEnd.apply(this, arguments); }; req.on('response', (res) => { if (!shouldLog) return; writeLog({ type: `${label}.response`, requestId, url: info.url, statusCode: res.statusCode, statusMessage: res.statusMessage, headers: simpleValue(res.headers || {}), }); res.on('data', (chunk) => { writeLog({ type: `${label}.response.data`, requestId, url: info.url, body: payload(chunk), }); }); res.on('end', () => { writeLog({ type: `${label}.response.end`, requestId, url: info.url }); }); }); req.on('error', (err) => { if (shouldLog) { writeLog({ type: `${label}.request.error`, requestId, url: info.url, error: String(err && err.stack || err) }); } }); return req; }; mod.get = function patchedGet(...args) { const req = mod.request.apply(this, args); req.end(); return req; }; Object.defineProperty(mod.get, '__zcbOriginalGet', { value: originalGet }); } function parseConnectArgs(args) { const first = args[0]; const second = args[1]; if (first && typeof first === 'object') { return { host: String(first.hostname || first.host || first.servername || '').split(':')[0], port: String(first.port || ''), servername: first.servername || '', rejectUnauthorized: first.rejectUnauthorized, hasCa: Boolean(first.ca), hasCert: Boolean(first.cert), hasKey: Boolean(first.key), options: simpleValue(first), }; } if (typeof first === 'number') { return { host: typeof second === 'string' ? second : '', port: String(first), }; } if (typeof first === 'string') { return { path: first }; } return {}; } function patchConnect() { const originalNetConnect = net.connect; const originalNetCreateConnection = net.createConnection; const originalTlsConnect = tls.connect; net.connect = function patchedNetConnect(...args) { const info = parseConnectArgs(args); if (hostAllowed(info.host)) writeLog({ type: 'net.connect', connectId: id('net'), ...info }); return originalNetConnect.apply(this, args); }; net.createConnection = function patchedNetCreateConnection(...args) { const info = parseConnectArgs(args); if (hostAllowed(info.host)) writeLog({ type: 'net.createConnection', connectId: id('net'), ...info }); return originalNetCreateConnection.apply(this, args); }; tls.connect = function patchedTlsConnect(...args) { const info = parseConnectArgs(args); if (hostAllowed(info.host)) writeLog({ type: 'tls.connect', connectId: id('tls'), ...info }); return originalTlsConnect.apply(this, args); }; } function wrapTransform(name, obj, transformId, algorithm) { if (!obj || obj.__zcbDeepCryptoWrapped) return obj; Object.defineProperty(obj, '__zcbDeepCryptoWrapped', { value: true }); const originalUpdate = obj.update; const originalFinal = obj.final; if (typeof originalUpdate === 'function') { obj.update = function patchedUpdate(data, inputEncoding, outputEncoding) { const out = originalUpdate.apply(this, arguments); try { const inputText = toBuffer(data, inputEncoding).toString('utf8'); if (name === 'cipher' && interestingText.test(inputText)) rememberPlaintext(`crypto.${name}.update`, inputText); if (name === 'decipher') { const outputText = toBuffer(out, outputEncoding).toString('utf8'); if (usefulPlainOutput(outputText)) { rememberPlaintext(`crypto.${name}.update`, outputText); if (CONSOLE_PREVIEW) { const recent = pullRecentUrl('res'); const url = recent && recent.url ? recent.url : ''; console.log(`[ZCB-DEEP RES-PLAINTEXT] ${url}`); console.log(cleanConsoleText(outputText).slice(0, CONSOLE_BYTES)); } } } } catch (_) { } writeLog({ type: `crypto.${name}.update`, cryptoId: transformId, algorithm, input: payload(data, inputEncoding), output: isBufferLike(out) || typeof out === 'string' ? payload(out, outputEncoding) : simpleValue(out), }); return out; }; } if (typeof originalFinal === 'function') { obj.final = function patchedFinal(outputEncoding) { const out = originalFinal.apply(this, arguments); try { if (name === 'decipher') { const outputText = toBuffer(out, outputEncoding).toString('utf8'); if (usefulPlainOutput(outputText)) { rememberPlaintext(`crypto.${name}.final`, outputText); if (CONSOLE_PREVIEW) { const recent = pullRecentUrl('res'); const url = recent && recent.url ? recent.url : ''; console.log(`[ZCB-DEEP RES-PLAINTEXT] ${url}`); console.log(cleanConsoleText(outputText).slice(0, CONSOLE_BYTES)); } } } } catch (_) { } writeLog({ type: `crypto.${name}.final`, cryptoId: transformId, algorithm, output: isBufferLike(out) || typeof out === 'string' ? payload(out, outputEncoding) : simpleValue(out), }); return out; }; } return obj; } function patchCrypto() { const directNames = [ 'publicEncrypt', 'privateEncrypt', 'publicDecrypt', 'privateDecrypt', 'randomBytes', 'randomFillSync', 'pbkdf2Sync', 'scryptSync', ]; for (const name of directNames) { if (typeof crypto[name] !== 'function') continue; const original = crypto[name]; crypto[name] = function patchedCryptoDirect(...args) { const callId = id(`crypto.${name}`); let result; let threw = false; try { result = original.apply(this, args); return result; } catch (err) { threw = true; writeLog({ type: `crypto.${name}.error`, cryptoId: callId, args: simpleValue(args), error: String(err && err.stack || err), }); throw err; } finally { if (!threw) { writeLog({ type: `crypto.${name}`, cryptoId: callId, args: simpleValue(args), result: isBufferLike(result) || typeof result === 'string' ? payload(result) : simpleValue(result), }); } } }; } const originalCreateCipheriv = crypto.createCipheriv; crypto.createCipheriv = function patchedCreateCipheriv(algorithm, key, iv, options) { const cryptoId = id('cipher'); writeLog({ type: 'crypto.createCipheriv', cryptoId, algorithm, key: secretPayload(key), iv: payload(iv), options: simpleValue(options || {}), }); const obj = originalCreateCipheriv.apply(this, arguments); return wrapTransform('cipher', obj, cryptoId, algorithm); }; const originalCreateDecipheriv = crypto.createDecipheriv; crypto.createDecipheriv = function patchedCreateDecipheriv(algorithm, key, iv, options) { const cryptoId = id('decipher'); writeLog({ type: 'crypto.createDecipheriv', cryptoId, algorithm, key: secretPayload(key), iv: payload(iv), options: simpleValue(options || {}), }); const obj = originalCreateDecipheriv.apply(this, arguments); return wrapTransform('decipher', obj, cryptoId, algorithm); }; if (LOG_HASHES) { const originalCreateHash = crypto.createHash; crypto.createHash = function patchedCreateHash(algorithm, options) { const cryptoId = id('hash'); writeLog({ type: 'crypto.createHash', cryptoId, algorithm, options: simpleValue(options || {}) }); const obj = originalCreateHash.apply(this, arguments); return wrapTransform('hash', obj, cryptoId, algorithm); }; } const originalCreateHmac = crypto.createHmac; crypto.createHmac = function patchedCreateHmac(algorithm, key, options) { const cryptoId = id('hmac'); writeLog({ type: 'crypto.createHmac', cryptoId, algorithm, key: secretPayload(key), options: simpleValue(options || {}), }); const obj = originalCreateHmac.apply(this, arguments); return wrapTransform('hmac', obj, cryptoId, algorithm); }; } function patchFs() { const originalReadFileSync = fs.readFileSync; fs.readFileSync = function patchedReadFileSync(file, options) { const result = originalReadFileSync.apply(this, arguments); try { const fileText = String(file); if (/secure|\.crt$|\.key$|\.pem$|client|ca/i.test(fileText)) { writeLog({ type: 'fs.readFileSync', file: fileText, options: simpleValue(options || {}), result: /\.key$/i.test(fileText) ? secretPayload(result) : payload(result), }); } } catch (_) { } return result; }; } function patchFetch() { if (typeof globalThis.fetch !== 'function' || globalThis.fetch.__zcbDeepPatched) return; const originalFetch = globalThis.fetch; globalThis.fetch = async function patchedFetch(input, init) { let urlText = ''; let host = ''; try { const u = new URL(typeof input === 'string' || input instanceof URL ? input : input.url); urlText = String(u); host = u.hostname; } catch (_) { } const requestId = id('fetch'); const shouldLog = hostAllowed(host); if (shouldLog) { writeLog({ type: 'fetch.request', requestId, url: urlText, init: simpleValue(init || {}), body: init && init.body !== undefined ? payload(init.body) : undefined, }); } const res = await originalFetch.apply(this, arguments); if (shouldLog) { writeLog({ type: 'fetch.response', requestId, url: urlText, status: res.status, statusText: res.statusText, headers: simpleValue(Object.fromEntries(res.headers.entries())), }); try { const clone = res.clone(); clone.arrayBuffer().then((buf) => { writeLog({ type: 'fetch.response.body', requestId, url: urlText, body: payload(Buffer.from(buf)) }); }).catch(() => {}); } catch (_) { } } return res; }; Object.defineProperty(globalThis.fetch, '__zcbDeepPatched', { value: true }); } function patchPlaintextSinks() { const originalStringify = JSON.stringify; JSON.stringify = function patchedStringify(value, replacer, space) { const out = originalStringify.apply(this, arguments); try { if (typeof out === 'string' && interestingText.test(out) && !looksLikeEnvelope(out)) { rememberPlaintext('json.stringify', out); writeLog({ type: 'json.stringify', jsonId: id('json'), text: out.length > MAX_BYTES ? out.slice(0, MAX_BYTES) : out, length: out.length, truncated: out.length > MAX_BYTES, }); } } catch (_) { } return out; }; if (typeof TextEncoder !== 'undefined' && TextEncoder.prototype && TextEncoder.prototype.encode) { const originalEncode = TextEncoder.prototype.encode; TextEncoder.prototype.encode = function patchedTextEncoderEncode(input) { try { if (typeof input === 'string' && interestingText.test(input) && !looksLikeEnvelope(input)) { rememberPlaintext('textencoder.encode', input); writeLog({ type: 'textencoder.encode', textId: id('textencoder'), text: input.length > MAX_BYTES ? input.slice(0, MAX_BYTES) : input, length: input.length, truncated: input.length > MAX_BYTES, }); } } catch (_) { } return originalEncode.apply(this, arguments); }; } } function scrubChildEnv(env) { const source = env && typeof env === 'object' ? env : process.env; const cleaned = { ...source }; const removed = []; for (const name of SCRUB_ENV_NAMES) { if (Object.prototype.hasOwnProperty.call(cleaned, name)) { delete cleaned[name]; removed.push(name); } } return { cleaned, removed }; } function sanitizeSpawnOptions(options) { const opts = options && typeof options === 'object' && !Array.isArray(options) ? { ...options } : {}; const { cleaned, removed } = scrubChildEnv(opts.env); opts.env = cleaned; return { opts, removed }; } function patchChildProcess() { const originalSpawn = childProcess.spawn && childProcess.spawn.bind(childProcess); const originalExecFile = childProcess.execFile && childProcess.execFile.bind(childProcess); const originalExec = childProcess.exec && childProcess.exec.bind(childProcess); const originalFork = childProcess.fork && childProcess.fork.bind(childProcess); if (originalSpawn && !childProcess.spawn.__zcbDeepPatched) { childProcess.spawn = function patchedSpawn(command, args, options) { let finalArgs = args; let finalOptions = options; if (Array.isArray(args)) { const result = sanitizeSpawnOptions(options); finalOptions = result.opts; logChildSanitized('child_process.spawn', command, args, result.removed); } else { const result = sanitizeSpawnOptions(args); finalArgs = []; finalOptions = result.opts; logChildSanitized('child_process.spawn', command, [], result.removed); } return originalSpawn(command, finalArgs, finalOptions); }; Object.defineProperty(childProcess.spawn, '__zcbDeepPatched', { value: true }); } if (originalExecFile && !childProcess.execFile.__zcbDeepPatched) { childProcess.execFile = function patchedExecFile(file, args, options, callback) { let finalArgs = args; let finalOptions = options; let finalCallback = callback; if (!Array.isArray(args)) { finalCallback = options; const result = sanitizeSpawnOptions(args); finalArgs = []; finalOptions = result.opts; logChildSanitized('child_process.execFile', file, [], result.removed); } else { const result = sanitizeSpawnOptions(options); finalOptions = result.opts; logChildSanitized('child_process.execFile', file, args, result.removed); } return originalExecFile(file, finalArgs, finalOptions, finalCallback); }; Object.defineProperty(childProcess.execFile, '__zcbDeepPatched', { value: true }); } if (originalExec && !childProcess.exec.__zcbDeepPatched) { childProcess.exec = function patchedExec(command, options, callback) { let finalOptions = options; let finalCallback = callback; if (typeof options === 'function') { finalCallback = options; finalOptions = undefined; } const result = sanitizeSpawnOptions(finalOptions); logChildSanitized('child_process.exec', command, [], result.removed); return originalExec(command, result.opts, finalCallback); }; Object.defineProperty(childProcess.exec, '__zcbDeepPatched', { value: true }); } if (originalFork && !childProcess.fork.__zcbDeepPatched) { childProcess.fork = function patchedFork(modulePath, args, options) { let finalArgs = args; let finalOptions = options; if (Array.isArray(args)) { const result = sanitizeSpawnOptions(options); finalOptions = result.opts; logChildSanitized('child_process.fork', modulePath, args, result.removed); } else { const result = sanitizeSpawnOptions(args); finalArgs = []; finalOptions = result.opts; logChildSanitized('child_process.fork', modulePath, [], result.removed); } return originalFork(modulePath, finalArgs, finalOptions); }; Object.defineProperty(childProcess.fork, '__zcbDeepPatched', { value: true }); } } function logChildSanitized(type, command, args, removed) { try { writeLog({ type, childId: id('child'), command: String(command || ''), args: simpleValue(args || []), scrubbedEnv: removed, }); } catch (_) { } } function install() { patchChildProcess(); patchRequestModule(http, 'http:', 'http'); patchRequestModule(https, 'https:', 'https'); patchConnect(); patchCrypto(); patchFs(); patchFetch(); patchPlaintextSinks(); writeLog({ type: 'hook.loaded', versions: process.versions, onlyHosts: Array.from(ONLY_HOSTS), maxBytes: MAX_BYTES, consolePreview: CONSOLE_PREVIEW, consoleBytes: CONSOLE_BYTES, includeSecrets: INCLUDE_SECRETS, nodeOptions: process.env.NODE_OPTIONS || '', electronRunAsNode: process.env.ELECTRON_RUN_AS_NODE || '', }); } process.on('uncaughtException', (err) => { writeLog({ type: 'process.uncaughtException', error: String(err && err.stack || err) }); }); process.on('unhandledRejection', (err) => { writeLog({ type: 'process.unhandledRejection', error: String(err && err.stack || err) }); }); install(); """ def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Launch Zhaocaibao with a plaintext-only CSV Node/Electron hook." ) parser.add_argument("--exe", default=str(DEFAULT_EXE), help="Path to target exe.") parser.add_argument("--hosts", default=DEFAULT_HOSTS, help="Comma-separated hosts to log for network events.") parser.add_argument("--max-bytes", type=int, default=8192, help="Max captured bytes per payload.") parser.add_argument( "--csv-max-bytes", type=int, default=33554432, help="Max total plaintext characters per captured body. Defaults to 32 MiB.", ) parser.add_argument( "--console", action="store_true", help="Print a small filtered preview of interesting captured data to the terminal.", ) parser.add_argument( "--console-bytes", type=int, default=1200, help="Max characters printed per terminal preview line.", ) parser.add_argument( "--include-secrets", action="store_true", help="Also log raw key/cert/key-like payload previews. Default keeps key material redacted.", ) parser.add_argument( "--log-hashes", action="store_true", help="Also log crypto.createHash update/final calls. Very noisy; off by default.", ) parser.add_argument( "--print-all-decipher", action="store_true", help="Print all crypto decipher output to the terminal, even if it does not match known keywords.", ) parser.add_argument( "--patch-loader", action="store_true", help="Temporarily patch resources/engine/wechat_engine.js to require the preload, then restore on exit.", ) parser.add_argument("--loader", default=str(DEFAULT_ENGINE_LOADER), help="Path to wechat_engine.js.") parser.add_argument( "--engine-only", action="store_true", help="Run resources/engine/wechat_engine.js directly under ELECTRON_RUN_AS_NODE instead of launching the UI.", ) parser.add_argument("--no-launch", action="store_true", help="Only create hook files; do not start the app.") return parser.parse_args() def make_run_dir() -> Path: run_id = time.strftime("%Y%m%d_%H%M%S") run_dir = WORKSPACE / "captures" / f"plain_csv_{run_id}" run_dir.mkdir(parents=True, exist_ok=True) return run_dir def write_preload(run_dir: Path) -> Path: preload = run_dir / "deep_preload.js" preload.write_text(PRELOAD_JS, encoding="utf-8") return preload def patch_loader(loader: Path, preload: Path) -> Path | None: if not loader.exists(): raise FileNotFoundError(f"loader not found: {loader}") original = loader.read_text(encoding="utf-8", errors="replace") marker = "// ZCB_DEEP_HOOK_PRELOAD" if marker in original: return None backup = loader.with_suffix(loader.suffix + ".deep_hook_bak") if not backup.exists(): shutil.copy2(loader, backup) preload_js_path = preload.as_posix() injected = ( f"{marker}\n" f"require({json.dumps(preload_js_path)});\n" f"{original}" ) loader.write_text(injected, encoding="utf-8") return backup def restore_loader(loader: Path, backup: Path | None) -> None: if not backup or not backup.exists(): return shutil.copy2(backup, loader) def build_env(args: argparse.Namespace, run_dir: Path, preload: Path) -> dict[str, str]: env = os.environ.copy() env["ZCB_DEEP_LOG_DIR"] = str(run_dir) env["ZCB_ONLY_HOSTS"] = args.hosts env["ZCB_DEEP_MAX_BYTES"] = str(args.max_bytes) env["ZCB_PLAIN_CSV_MAX_BYTES"] = str(args.csv_max_bytes) env["ZCB_DEEP_CONSOLE"] = "1" if args.console else "0" env["ZCB_DEEP_CONSOLE_BYTES"] = str(args.console_bytes) env["ZCB_DEEP_INCLUDE_SECRETS"] = "1" if args.include_secrets else "0" env["ZCB_DEEP_LOG_HASHES"] = "1" if args.log_hashes else "0" env["ZCB_DEEP_PRINT_ALL_DECIPHER"] = "1" if args.print_all_decipher else "0" env["ELECTRON_ENABLE_LOGGING"] = "1" if args.patch_loader: env.pop("NODE_OPTIONS", None) else: require_option = f'--require="{preload}"' existing = env.get("NODE_OPTIONS", "").strip() env["NODE_OPTIONS"] = f"{existing} {require_option}".strip() return env def launch(args: argparse.Namespace, env: dict[str, str]) -> subprocess.Popen: exe = Path(args.exe) if not exe.exists(): raise FileNotFoundError(f"target exe not found: {exe}") if args.engine_only: loader = Path(args.loader) if not loader.exists(): raise FileNotFoundError(f"engine loader not found: {loader}") env = env.copy() env["ELECTRON_RUN_AS_NODE"] = "1" command = [str(exe), str(loader)] cwd = str(loader.parent) else: command = [str(exe)] cwd = str(exe.parent) return subprocess.Popen( command, cwd=cwd, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL, ) def relay_output(proc: subprocess.Popen) -> threading.Thread | None: if proc.stdout is None: return None def worker() -> None: assert proc.stdout is not None while True: chunk = proc.stdout.readline() if not chunk: break try: text = chunk.decode("utf-8") except UnicodeDecodeError: text = chunk.decode("gbk", errors="replace") print(text, end="", flush=True) thread = threading.Thread(target=worker, daemon=True) thread.start() return thread def main() -> int: args = parse_args() run_dir = make_run_dir() preload = write_preload(run_dir) env = build_env(args, run_dir, preload) print(f"[+] csv dir: {run_dir}") print(f"[+] preload: {preload}") print(f"[+] target hosts: {args.hosts}") print("[!] CSV can contain tokens and plaintext request/response bodies. Keep captures private.") backup = None loader = Path(args.loader) try: if args.patch_loader: backup = patch_loader(loader, preload) print(f"[+] patched loader: {loader}") if backup: print(f"[+] backup: {backup}") else: print("[+] loader already contained hook marker") if args.no_launch: print("[+] no-launch requested; hook files created only.") return 0 proc = launch(args, env) relay_thread = relay_output(proc) print(f"[+] launched pid: {proc.pid}") print("[*] use the app normally, then close it or press Ctrl+C here.") proc.wait() if relay_thread: relay_thread.join(timeout=2) print(f"[+] target exited with code: {proc.returncode}") return int(proc.returncode or 0) except KeyboardInterrupt: print("\n[!] interrupted; terminating target if it is still running.") return 130 finally: if args.patch_loader: restore_loader(loader, backup) print(f"[+] restored loader: {loader}") if __name__ == "__main__": raise SystemExit(main())