1350 lines
43 KiB
Python
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())
|