well, its closer

This commit is contained in:
zefie
2025-07-24 19:34:23 -04:00
parent 626ea611dc
commit 74ef4693fa

View File

@@ -12,7 +12,7 @@ var wtv_challenge_response = null;
const minisrv_config = { const minisrv_config = {
config: { config: {
keys: { keys: {
initial_shared_key: null initial_shared_key: null
}, },
debug_flags: { debug_flags: {
debug: false // Set to true for verbose logging from WTVSec debug: false // Set to true for verbose logging from WTVSec
@@ -28,37 +28,45 @@ if (!pcapFile) {
} }
// A store for all active WTVP sessions, keyed by stream identifier. // 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 wtvpSessions = {};
const parser = pcapParser.parse(pcapFile); const parser = pcapParser.parse(pcapFile);
parser.on('packet', (packet) => { parser.on('packet', (packet) => {
const data = packet.data; const data = packet.data;
const ethType = data.readUInt16BE(12); const ethType = data.readUInt16BE(12);
if (ethType !== 0x0800) return; // Not IPv4 if (ethType !== 0x0800) return; // Not IPv4
const ipHeader = data.slice(14, 34); // IP header parsing
const ipHeaderLength = (data[14] & 0x0F) * 4;
const ipHeader = data.slice(14, 14 + ipHeaderLength);
const protocol = ipHeader[9]; const protocol = ipHeader[9];
if (protocol !== 6) return; // Not TCP if (protocol !== 6) return; // Not TCP
const srcIP = ipHeader.slice(12, 16).join('.'); const srcIP = ipHeader.slice(12, 16).join('.');
const dstIP = ipHeader.slice(16, 20).join('.'); const dstIP = ipHeader.slice(16, 20).join('.');
const tcpHeaderStart = 34;
// TCP header parsing
const tcpHeaderStart = 14 + ipHeaderLength;
const tcpHeaderLen = (data[tcpHeaderStart + 12] >> 4) * 4;
const srcPort = data.readUInt16BE(tcpHeaderStart); const srcPort = data.readUInt16BE(tcpHeaderStart);
const dstPort = data.readUInt16BE(tcpHeaderStart + 2); const dstPort = data.readUInt16BE(tcpHeaderStart + 2);
const tcpHeaderLen = (data[tcpHeaderStart + 12] >> 4) * 4; const seq = data.readUInt32BE(tcpHeaderStart + 4);
const tcpPayloadOffset = tcpHeaderStart + tcpHeaderLen; const flags = data[tcpHeaderStart + 13];
const isSYN = (flags & 0x02) !== 0;
const isFIN = (flags & 0x01) !== 0;
const tcpPayloadOffset = tcpHeaderStart + tcpHeaderLen;
const payload = data.slice(tcpPayloadOffset); const payload = data.slice(tcpPayloadOffset);
const tcpPayloadLength = payload.length;
// Create a unique key for the TCP session, independent of direction
console.log(`[DEBUG] data.length=${data.length}, tcpPayloadOffset=${tcpPayloadOffset}`);
// Create a unique key for the TCP session
const src = `${srcIP}:${srcPort}`; const src = `${srcIP}:${srcPort}`;
const dst = `${dstIP}:${dstPort}`; const dst = `${dstIP}:${dstPort}`;
const sessionKey = [src, dst].sort().join('-'); const sessionKey = [src, dst].sort().join('-');
// If it's a new session, initialize its state // Initialize session state if new
if (!wtvpSessions[sessionKey]) { if (!wtvpSessions[sessionKey]) {
console.log(`[+] New TCP Session detected: ${sessionKey}`); console.log(`[+] New TCP Session detected: ${sessionKey}`);
wtvpSessions[sessionKey] = { wtvpSessions[sessionKey] = {
@@ -66,45 +74,152 @@ parser.on('packet', (packet) => {
serverAddr: null, serverAddr: null,
wtvsec: null, wtvsec: null,
secureMode: false, secureMode: false,
// TCP stream reassembly state, keyed by source ip:port
streams: {},
}; };
} }
// Ignore packets without a payload
if (!payload || payload.length === 0) {
return;
}
const currentSession = wtvpSessions[sessionKey]; const currentSession = wtvpSessions[sessionKey];
const sourceAddr = `${srcIP}:${srcPort}`;
const payloadStr = payload.toString('utf8');
// 1. Identify Client and Server // Ensure a stream object exists for the source of this packet
if (!currentSession.clientAddr) { if (!currentSession.streams[src]) {
currentSession.streams[src] = { nextSeq: -1, outOfOrder: {}, data: Buffer.alloc(0), isClient: false };
}
const stream = currentSession.streams[src];
// 1. Identify Client and Server (if not already done)
if (!currentSession.clientAddr && payload.length > 0) {
const payloadStr = payload.toString('utf8');
if (payloadStr.startsWith('GET') || payloadStr.startsWith('POST') || payloadStr.startsWith('SECURE ON')) { if (payloadStr.startsWith('GET') || payloadStr.startsWith('POST') || payloadStr.startsWith('SECURE ON')) {
currentSession.clientAddr = sourceAddr; console.log(`[*] Client identified as ${src}, Server as ${dst}`);
currentSession.serverAddr = `${dstIP}:${dstPort}`; currentSession.clientAddr = src;
console.log(`[*] Client identified as ${currentSession.clientAddr}`); currentSession.serverAddr = dst;
// Mark the current stream (from src) as the client
stream.isClient = true;
// Ensure the server's stream object exists as well
if (!currentSession.streams[dst]) {
currentSession.streams[dst] = { nextSeq: -1, outOfOrder: {}, data: Buffer.alloc(0), isClient: false };
}
} }
} }
// This check handles cases where the first packet didn't identify the client. // Set the isClient flag for every packet now that identification might have happened
if (!currentSession.clientAddr) { if(currentSession.clientAddr){
return; stream.isClient = src === currentSession.clientAddr;
} }
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 // This is the expected in-order packet. Append its payload.
if (!currentSession.secureMode) { stream.data = Buffer.concat([stream.data, payload]);
handlePlaintext(currentSession, payloadStr, isClient); stream.nextSeq += tcpPayloadLength;
} else { if (isSYN || isFIN) stream.nextSeq++;
handleEncrypted(currentSession, payload, isClient);
// Process any buffered out-of-order packets that are now in sequence
let nextSeqInChain = stream.nextSeq;
while (stream.outOfOrder[nextSeqInChain]) {
const bufferedPayload = stream.outOfOrder[nextSeqInChain];
const bufferedPayloadLength = bufferedPayload.length;
stream.data = Buffer.concat([stream.data, bufferedPayload]);
delete stream.outOfOrder[nextSeqInChain];
nextSeqInChain += bufferedPayloadLength;
}
stream.nextSeq = nextSeqInChain;
// Now that we have new contiguous data, try to process it as application messages
for (const addr in currentSession.streams) {
const s = currentSession.streams[addr];
if (s.data.length > 0) {
processStream(currentSession, addr);
}
} }
}); });
/**
* Processes the reassembled data buffer for a session, looking for complete messages.
* @param {object} session - The session state object.
* @param {string} sourceAddr - The source address (ip:port) of the stream being processed.
*/
function processStream(session, sourceAddr) {
const stream = session.streams[sourceAddr];
console.log(`[DEBUG] Processing stream: ${sourceAddr} isClient: ${stream.isClient}, buffer length: ${stream.data.length}`);
if (!stream || !session.clientAddr) return; // Don't process until client is identified
const isClient = stream.isClient;
const direction = isClient ? '[CLIENT -> SERVER]' : '[SERVER -> CLIENT]';
// Loop to process all complete messages currently in the buffer
while (true) {
let buffer = stream.data;
if (buffer.length === 0) break;
if (buffer.length === 6) {
// Special case: buffer is exactly 6 bytes (likely a keepalive or unknown control message)
// Remove the 6 bytes from the buffer and continue
stream.data = buffer.slice(6);
break;
}
const lfSeparator = Buffer.from('\n\n');
const crlfSeparator = Buffer.from('\r\n\r\n');
let separatorIndex = buffer.indexOf(lfSeparator);
let separatorLength = lfSeparator.length;
const crlfIndex = buffer.indexOf(crlfSeparator);
if (crlfIndex !== -1 && (separatorIndex === -1 || crlfIndex < separatorIndex)) {
separatorIndex = crlfIndex;
separatorLength = crlfSeparator.length;
}
if (separatorIndex === -1) {
// Incomplete message (no full headers yet), wait for more data.
break;
}
const headersPart = buffer.slice(0, separatorIndex);
const headers = parseHeaders(headersPart.toString('utf8'));
const headerBlockLength = separatorIndex + separatorLength;
let messageToProcess;
let consumedSize;
if (headers['content-length']) {
const contentLength = parseInt(headers['content-length'], 10);
const totalMessageSize = headerBlockLength + contentLength;
if (buffer.length < totalMessageSize) {
// We have headers, but the body is not fully here yet. Wait for more data.
break;
}
messageToProcess = buffer.slice(0, totalMessageSize);
consumedSize = totalMessageSize;
} else {
// No content-length. Assume the rest of the buffer is the message.
messageToProcess = buffer.slice(0, headerBlockLength);
consumedSize = headerBlockLength;
}
console.log(`\n${'='.repeat(20)} Processing Message: ${direction} (${messageToProcess.length} bytes) ${'='.repeat(20)}`);
if (!session.secureMode) {
handlePlaintext(session, messageToProcess.toString('utf8'), isClient);
} else {
handleEncrypted(session, messageToProcess, isClient);
}
// Slice the processed message from the front of the buffer
stream.data = buffer.slice(consumedSize);
}
}
parser.on('end', () => { parser.on('end', () => {
console.log('\n[*] PCAP file processing complete.'); console.log('\n[*] PCAP file processing complete.');
}); });
@@ -115,21 +230,24 @@ parser.on('error', (err) => {
/** /**
* Handles plaintext WTVP messages to set up the security context. * Handles a single complete plaintext WTVP message.
* @param {object} session - The session state object. * @param {object} session - The session state object.
* @param {string} payload - The plaintext payload. * @param {string} message - The plaintext message string.
* @param {boolean} isClient - True if the message is from the client. * @param {boolean} isClient - True if the message is from the client.
*/ */
function handlePlaintext(session, payload, isClient) { function handlePlaintext(session, message, isClient) {
console.log(payload); const headers = parseHeaders(message);
const headers = parseHeaders(payload); if (!headers['wtv-encrypted']) {
console.log('[PLAINTEXT MESSAGE]:');
console.log(message);
}
if (wtvsec && !session.wtvsec) { if (wtvsec && !session.wtvsec) {
session.wtvsec = wtvsec; session.wtvsec = wtvsec;
} }
if (isClient) { if (isClient) {
// Check for the SECURE ON command from the client if (message.includes('SECURE ON')) {
if (payload.startsWith('SECURE ON')) {
if (session.wtvsec) { if (session.wtvsec) {
console.log('[*] SECURE ON detected. Initializing RC4 session.'); console.log('[*] SECURE ON detected. Initializing RC4 session.');
session.wtvsec.SecureOn(); session.wtvsec.SecureOn();
@@ -138,7 +256,6 @@ function handlePlaintext(session, payload, isClient) {
console.error('[!] SECURE ON received before wtv-initial-key. Cannot proceed.'); console.error('[!] SECURE ON received before wtv-initial-key. Cannot proceed.');
} }
} }
// Check for wtv-incarnation header
if (headers['wtv-incarnation']) { if (headers['wtv-incarnation']) {
const incarnation = parseInt(headers['wtv-incarnation'], 10); const incarnation = parseInt(headers['wtv-incarnation'], 10);
if (session.wtvsec) { if (session.wtvsec) {
@@ -157,81 +274,72 @@ function handlePlaintext(session, payload, isClient) {
} }
} }
} else { // Server } else { // Server
// Look for the initial key to bootstrap the WTVSec instance
if (headers['wtv-initial-key']) { if (headers['wtv-initial-key']) {
const initialKey = headers['wtv-initial-key']; const initialKey = headers['wtv-initial-key'];
console.log(`[*] Captured wtv-initial-key: ${initialKey}`); console.log(`[*] Captured wtv-initial-key: ${initialKey}`);
minisrv_config.config.keys.initial_shared_key = initialKey; minisrv_config.config.keys.initial_shared_key = initialKey;
wtvsec = new WTVSec(minisrv_config); wtvsec = new WTVSec(minisrv_config);
} }
// Process the challenge from the server
if (headers['wtv-challenge'] && wtvsec) { if (headers['wtv-challenge'] && wtvsec) {
const challenge = headers['wtv-challenge']; const challenge = headers['wtv-challenge'];
console.log(`[*] Captured wtv-challenge. Processing...`); console.log(`[*] Captured wtv-challenge. Processing...`);
wtv_challenge_response = wtvsec.ProcessChallenge(challenge).toString(CryptoJS.enc.Base64) wtv_challenge_response = wtvsec.ProcessChallenge(challenge).toString(CryptoJS.enc.Base64)
session.wtvsec = wtvsec; // Ensure session has the WTVSec instance session.wtvsec = wtvsec; // Ensure session has the WTVSec instance
} }
if (headers['wtv-lzpf'] !== undefined) { if (typeof headers['wtv-lzpf'] !== 'undefined') {
session.lzpf = true; session.lzpf = true;
} }
if (headers['wtv-encrypted']) {
handleEncrypted(session, Buffer.from(message), isClient);
}
} }
} }
/** /**
* Handles encrypted WTVP messages. * Handles a single complete encrypted WTVP message.
* @param {object} session - The session state object. * @param {object} session - The session state object.
* @param {Buffer} data - The raw TCP data buffer. * @param {Buffer} message - The raw message buffer.
* @param {boolean} isClient - True if the message is from the client. * @param {boolean} isClient - True if the message is from the client.
*/ */
function handleEncrypted(session, data, isClient) { function handleEncrypted(session, message, isClient) {
// The encrypted data comes after the headers and a double newline. const lfSeparator = Buffer.from('\n\n');
var lzpf = false; const crlfSeparator = Buffer.from('\r\n\r\n');
const separator = '\n\n';
const dataStr = data.toString('binary'); let separatorIndex = message.indexOf(lfSeparator);
const separatorIndex = dataStr.indexOf(separator); let separatorLength = lfSeparator.length;
const crlfIndex = message.indexOf(crlfSeparator);
if (crlfIndex !== -1 && (separatorIndex === -1 || crlfIndex < separatorIndex)) {
separatorIndex = crlfIndex;
separatorLength = crlfSeparator.length;
}
if (separatorIndex === -1) { if (separatorIndex === -1) {
console.log('[!] Encrypted message without header separator found. Assuming entire payload is encrypted.'); console.log('[!] Encrypted message without header separator. This should not happen with reassembled streams.');
// 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; return;
} }
const headersPart = data.slice(0, separatorIndex).toString('utf8'); const headersPart = message.slice(0, separatorIndex).toString('utf8');
const encryptedBody = data.slice(separatorIndex + separator.length); const encryptedBody = message.slice(separatorIndex + separatorLength);
console.log('[HEADERS]:'); console.log('[ENCRYPTED HEADERS]:');
console.log(headersPart); console.log(headersPart);
if (encryptedBody.length > 0) { if (encryptedBody.length > 0) {
// Decrypt based on message direction const keyNum = isClient ? 0 : 1;
const keyNum = isClient ? 0 : 1; // 0 for client-to-server, 1 for server-to-client
try { try {
const decryptedBody = session.wtvsec.Decrypt(keyNum, encryptedBody); let decryptedBody = session.wtvsec.Decrypt(keyNum, encryptedBody);
if (session.lzpf) {
// Check for compression flag in the now-decrypted headers
const headers = parseHeaders(headersPart);
if (typeof headers['wtv-lzpf'] !== 'undefined') {
console.log('\n[DECRYPTED DECOMPRESSED PAYLOAD]:'); console.log('\n[DECRYPTED DECOMPRESSED PAYLOAD]:');
var lzpf = new LZPF(); var lzpfHandler = new LZPF();
decryptedBody = lzpf.decompress(decryptedBody); decryptedBody = lzpfHandler.expand(decryptedBody);
session.lzpf = false; // Reset after decompression
} else { } else {
console.log('\n[DECRYPTED PAYLOAD]:'); console.log('\n[DECRYPTED PAYLOAD]:');
} }
console.log(decryptedBody.toString('utf8')); console.log(decryptedBody.toString('utf8'));
} catch (e) { } catch (e) {
console.error(`[!] Decryption failed: ${e.message}`); console.error(`[!] Decryption failed: ${e.message}`);
} }
@@ -240,8 +348,9 @@ function handleEncrypted(session, data, isClient) {
} }
} }
/** /**
* A simple utility to parse HTTP-like headers into an object. * A utility to parse HTTP-like headers into an object.
* @param {string} payload - The raw text payload. * @param {string} payload - The raw text payload.
* @returns {object} A key-value map of the headers. * @returns {object} A key-value map of the headers.
*/ */
@@ -250,8 +359,8 @@ function parseHeaders(payload) {
const lines = payload.split(/\r?\n/); const lines = payload.split(/\r?\n/);
lines.forEach(line => { lines.forEach(line => {
const parts = line.split(':'); const parts = line.split(':');
if (parts.length === 2) { if (parts.length >= 2) {
headers[parts[0].toLowerCase()] = parts[1].trim(); headers[parts[0].toLowerCase().trim()] = parts.slice(1).join(':').trim();
} }
}); });
return headers; return headers;