Files
test/深度档_明文CSV.py
T
2026-07-12 23:37:10 +08:00

1350 lines
43 KiB
Python

# -*- 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_<timestamp>/;
- 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())