Files
minisrv/zefie_wtvp_minisrv/unroll_rc4.js
2025-07-24 15:29:56 -04:00

259 lines
9.4 KiB
JavaScript

const fs = require('fs');
const pcapParser = require('pcap-parser');
const WTVSec = require('./includes/classes/WTVSec.js');
const LZPF = require('./includes/classes/LZPF.js');
const WTVShared = require('./includes/classes/WTVShared.js')['WTVShared'];
const wtvshared = new WTVShared();
const CryptoJS = require('crypto-js');
var wtvsec = null;
var wtv_challenge_response = null;
// A simple mock config, the initial_shared_key is populated dynamically.
const minisrv_config = {
config: {
keys: {
initial_shared_key: null
},
debug_flags: {
debug: false // Set to true for verbose logging from WTVSec
}
}
};
// --- Main Execution ---
const pcapFile = process.argv[2];
if (!pcapFile) {
console.error('Usage: node parse_wtvp_parser.js <path_to_pcap_file>');
process.exit(1);
}
// A store for all active WTVP sessions, keyed by stream identifier.
// The identifier is a sorted combination of src/dst ip:port pairs.
const wtvpSessions = {};
const parser = pcapParser.parse(pcapFile);
parser.on('packet', (packet) => {
const data = packet.data;
const ethType = data.readUInt16BE(12);
if (ethType !== 0x0800) return; // Not IPv4
const ipHeader = data.slice(14, 34);
const protocol = ipHeader[9];
if (protocol !== 6) return; // Not TCP
const srcIP = ipHeader.slice(12, 16).join('.');
const dstIP = ipHeader.slice(16, 20).join('.');
const tcpHeaderStart = 34;
const srcPort = data.readUInt16BE(tcpHeaderStart);
const dstPort = data.readUInt16BE(tcpHeaderStart + 2);
const tcpHeaderLen = (data[tcpHeaderStart + 12] >> 4) * 4;
const tcpPayloadOffset = tcpHeaderStart + tcpHeaderLen;
const payload = data.slice(tcpPayloadOffset);
// Create a unique key for the TCP session, independent of direction
const src = `${srcIP}:${srcPort}`;
const dst = `${dstIP}:${dstPort}`;
const sessionKey = [src, dst].sort().join('-');
// If it's a new session, initialize its state
if (!wtvpSessions[sessionKey]) {
console.log(`[+] New TCP Session detected: ${sessionKey}`);
wtvpSessions[sessionKey] = {
clientAddr: null,
serverAddr: null,
wtvsec: null,
secureMode: false,
};
}
// Ignore packets without a payload
if (!payload || payload.length === 0) {
return;
}
const currentSession = wtvpSessions[sessionKey];
const sourceAddr = `${srcIP}:${srcPort}`;
const payloadStr = payload.toString('utf8');
// 1. Identify Client and Server
if (!currentSession.clientAddr) {
if (payloadStr.startsWith('GET') || payloadStr.startsWith('POST') || payloadStr.startsWith('SECURE ON')) {
currentSession.clientAddr = sourceAddr;
currentSession.serverAddr = `${dstIP}:${dstPort}`;
console.log(`[*] Client identified as ${currentSession.clientAddr}`);
}
}
// This check handles cases where the first packet didn't identify the client.
if (!currentSession.clientAddr) {
return;
}
const isClient = sourceAddr === currentSession.clientAddr;
const direction = isClient ? '[CLIENT -> SERVER]' : '[SERVER -> CLIENT]';
console.log(`\n${'='.repeat(20)} ${direction} (${payload.length} bytes) ${'='.repeat(20)}`);
// 2. Process data based on whether we are in secure mode or not
if (!currentSession.secureMode) {
handlePlaintext(currentSession, payloadStr, isClient);
} else {
handleEncrypted(currentSession, payload, isClient);
}
});
parser.on('end', () => {
console.log('\n[*] PCAP file processing complete.');
});
parser.on('error', (err) => {
console.error(`[!] An error occurred: ${err.message}`);
});
/**
* Handles plaintext WTVP messages to set up the security context.
* @param {object} session - The session state object.
* @param {string} payload - The plaintext payload.
* @param {boolean} isClient - True if the message is from the client.
*/
function handlePlaintext(session, payload, isClient) {
console.log(payload);
const headers = parseHeaders(payload);
if (wtvsec && !session.wtvsec) {
session.wtvsec = wtvsec;
}
if (isClient) {
// Check for the SECURE ON command from the client
if (payload.startsWith('SECURE ON')) {
if (session.wtvsec) {
console.log('[*] SECURE ON detected. Initializing RC4 session.');
session.wtvsec.SecureOn();
session.secureMode = true;
} else {
console.error('[!] SECURE ON received before wtv-initial-key. Cannot proceed.');
}
}
// Check for wtv-incarnation header
if (headers['wtv-incarnation']) {
const incarnation = parseInt(headers['wtv-incarnation'], 10);
if (session.wtvsec) {
console.log(`[*] Client sent wtv-incarnation: ${incarnation}`);
session.wtvsec.set_incarnation(incarnation);
}
}
if (headers['wtv-challenge-response']) {
const challengeResponse = headers['wtv-challenge-response'];
console.log(`[*] Client sent wtv-challenge-response: ${challengeResponse}`);
if (wtv_challenge_response != challengeResponse) {
console.error('[!] Mismatched wtv-challenge-response. Expected:', wtv_challenge_response);
process.exit(1);
} else {
console.log('[*] wtv-challenge-response matches expected value.');
}
}
} else { // Server
// Look for the initial key to bootstrap the WTVSec instance
if (headers['wtv-initial-key']) {
const initialKey = headers['wtv-initial-key'];
console.log(`[*] Captured wtv-initial-key: ${initialKey}`);
minisrv_config.config.keys.initial_shared_key = initialKey;
wtvsec = new WTVSec(minisrv_config);
}
// Process the challenge from the server
if (headers['wtv-challenge'] && wtvsec) {
const challenge = headers['wtv-challenge'];
console.log(`[*] Captured wtv-challenge. Processing...`);
wtv_challenge_response = wtvsec.ProcessChallenge(challenge).toString(CryptoJS.enc.Base64)
session.wtvsec = wtvsec; // Ensure session has the WTVSec instance
}
if (headers['wtv-lzpf'] !== undefined) {
session.lzpf = true;
}
}
}
/**
* Handles encrypted WTVP messages.
* @param {object} session - The session state object.
* @param {Buffer} data - The raw TCP data buffer.
* @param {boolean} isClient - True if the message is from the client.
*/
function handleEncrypted(session, data, isClient) {
// The encrypted data comes after the headers and a double newline.
var lzpf = false;
const separator = '\n\n';
const dataStr = data.toString('binary');
const separatorIndex = dataStr.indexOf(separator);
if (separatorIndex === -1) {
console.log('[!] Encrypted message without header separator found. Assuming entire payload is encrypted.');
// This can happen if headers are in a separate packet from the body.
// For simplicity, we try to decrypt the whole payload.
// A more robust solution would buffer data across packets.
try {
const keyNum = isClient ? 0 : 1;
const decryptedBody = session.wtvsec.Decrypt(keyNum, data);
if (session.lzpf) {
console.log('\n[DECRYPTED DECOMPRESSED PAYLOAD (ASSUMED)]:\n' + decryptedBody.toString('utf8'));
var lzpf = new LZPF();
decryptedBody = lzpf.decompress(decryptedBody);
session.lzpf = false; // Reset after decompression
} else {
console.log('\n[DECRYPTED PAYLOAD (ASSUMED)]:\n' + decryptedBody.toString('utf8'));
}
} catch (e) {
console.error(`[!] Decryption failed: ${e.message}`);
}
return;
}
const headersPart = data.slice(0, separatorIndex).toString('utf8');
const encryptedBody = data.slice(separatorIndex + separator.length);
console.log('[HEADERS]:');
console.log(headersPart);
if (encryptedBody.length > 0) {
// Decrypt based on message direction
const keyNum = isClient ? 0 : 1; // 0 for client-to-server, 1 for server-to-client
try {
const decryptedBody = session.wtvsec.Decrypt(keyNum, encryptedBody);
if (session.lzpf) {
console.log('\n[DECRYPTED DECOMPRESSED PAYLOAD]:');
var lzpf = new LZPF();
decryptedBody = lzpf.decompress(decryptedBody);
session.lzpf = false; // Reset after decompression
} else {
console.log('\n[DECRYPTED PAYLOAD]:');
}
console.log(decryptedBody.toString('utf8'));
} catch (e) {
console.error(`[!] Decryption failed: ${e.message}`);
}
} else {
console.log('\n[Encrypted message with no body]');
}
}
/**
* A simple utility to parse HTTP-like headers into an object.
* @param {string} payload - The raw text payload.
* @returns {object} A key-value map of the headers.
*/
function parseHeaders(payload) {
const headers = {};
const lines = payload.split(/\r?\n/);
lines.forEach(line => {
const parts = line.split(':');
if (parts.length === 2) {
headers[parts[0].toLowerCase()] = parts[1].trim();
}
});
return headers;
}