commit 2f2a8d653afb33525c836018d3dc5d5e4792aeaf Author: test Date: Sun Jul 12 23:37:10 2026 +0800 单次上传深度档_明文CSV.py diff --git a/深度档_明文CSV.py b/深度档_明文CSV.py new file mode 100644 index 0000000..69d0e37 --- /dev/null +++ b/深度档_明文CSV.py @@ -0,0 +1,1349 @@ +# -*- 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())