move wordArrayToBuffer to wtvshared
This commit is contained in:
@@ -77,7 +77,7 @@ class WTVSec {
|
|||||||
* @returns {CryptoJS.lib.WordArray}
|
* @returns {CryptoJS.lib.WordArray}
|
||||||
*/
|
*/
|
||||||
DuplicateWordArray(wordArray) {
|
DuplicateWordArray(wordArray) {
|
||||||
return CryptoJS.lib.WordArray.create(this.wordArrayToBuffer(wordArray));
|
return CryptoJS.lib.WordArray.create(this.wtvshared.wordArrayToBuffer(wordArray));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -274,16 +274,6 @@ class WTVSec {
|
|||||||
return challenge_b64;
|
return challenge_b64;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* convert a CryptoJS.lib.WordArray to a Javascript Buffer
|
|
||||||
* @param {CryptoJS.lib.WordArray} wordArray
|
|
||||||
* @returns {Buffer} JS Buffer object
|
|
||||||
*/
|
|
||||||
wordArrayToBuffer(wordArray) {
|
|
||||||
if (wordArray) return new Buffer.from(wordArray.toString(CryptoJS.enc.Hex), 'hex');
|
|
||||||
else return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts an encryption session
|
* Starts an encryption session
|
||||||
* @param {Number} rc4session Session Type (0 = enc k1, 1 = dec k1, 2 = enc k2, 3 = dec k2, default: all)
|
* @param {Number} rc4session Session Type (0 = enc k1, 1 = dec k1, 2 = enc k2, 3 = dec k2, default: all)
|
||||||
@@ -295,8 +285,8 @@ class WTVSec {
|
|||||||
endianness(buf, 4);
|
endianness(buf, 4);
|
||||||
this.hRC4_Key1 = CryptoJS.MD5(this.DuplicateWordArray(this.session_key1).concat(CryptoJS.lib.WordArray.create(buf).concat(this.DuplicateWordArray(this.session_key1))));
|
this.hRC4_Key1 = CryptoJS.MD5(this.DuplicateWordArray(this.session_key1).concat(CryptoJS.lib.WordArray.create(buf).concat(this.DuplicateWordArray(this.session_key1))));
|
||||||
this.hRC4_Key2 = CryptoJS.MD5(this.DuplicateWordArray(this.session_key2).concat(CryptoJS.lib.WordArray.create(buf).concat(this.DuplicateWordArray(this.session_key2))));
|
this.hRC4_Key2 = CryptoJS.MD5(this.DuplicateWordArray(this.session_key2).concat(CryptoJS.lib.WordArray.create(buf).concat(this.DuplicateWordArray(this.session_key2))));
|
||||||
var key1 = this.wordArrayToBuffer(this.hRC4_Key1);
|
var key1 = this.wtvshared.wordArrayToBuffer(this.hRC4_Key1);
|
||||||
var key2 = this.wordArrayToBuffer(this.hRC4_Key2);
|
var key2 = this.wtvshared.wordArrayToBuffer(this.hRC4_Key2);
|
||||||
const setRC4Session = (sessionIndex, key) => {
|
const setRC4Session = (sessionIndex, key) => {
|
||||||
this.RC4Session[sessionIndex] = new RC4.RC4(key);
|
this.RC4Session[sessionIndex] = new RC4.RC4(key);
|
||||||
};
|
};
|
||||||
@@ -338,7 +328,7 @@ class WTVSec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data.words) {
|
if (data.words) {
|
||||||
data = this.wordArrayToBuffer(data);
|
data = this.wtvshared.wordArrayToBuffer(data);
|
||||||
} else if (data instanceof ArrayBuffer || typeof data === 'string') {
|
} else if (data instanceof ArrayBuffer || typeof data === 'string') {
|
||||||
data = Buffer.from(data);
|
data = Buffer.from(data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,16 @@ class WTVShared {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert a CryptoJS.lib.WordArray to a Javascript Buffer
|
||||||
|
* @param {CryptoJS.lib.WordArray} wordArray
|
||||||
|
* @returns {Buffer} JS Buffer object
|
||||||
|
*/
|
||||||
|
wordArrayToBuffer(wordArray) {
|
||||||
|
if (wordArray) return new Buffer.from(wordArray.toString(CryptoJS.enc.Hex), 'hex');
|
||||||
|
else return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts an IP address to a hexadecimal string (WTV)
|
* Converts an IP address to a hexadecimal string (WTV)
|
||||||
* @param {string} ip The IP address to convert
|
* @param {string} ip The IP address to convert
|
||||||
|
|||||||
@@ -1,320 +1,258 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const pcap = require('pcap-parser');
|
const pcapParser = require('pcap-parser');
|
||||||
const WTVSec = require('./includes/classes/WTVSec.js');
|
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');
|
||||||
|
|
||||||
// A map to hold the state of each TCP connection.
|
var wtvsec = null;
|
||||||
const connections = new Map();
|
var wtv_challenge_response = null;
|
||||||
|
// A simple mock config, the initial_shared_key is populated dynamically.
|
||||||
/**
|
const minisrv_config = {
|
||||||
* A simple, resilient function to parse HTTP headers from a buffer.
|
|
||||||
* @param {Buffer} buffer - The buffer containing HTTP headers.
|
|
||||||
* @returns {object|null} An object containing the headers, or null if headers are incomplete.
|
|
||||||
*/
|
|
||||||
function parseHeaders(buffer) {
|
|
||||||
const headers = {};
|
|
||||||
const headerString = buffer.toString('utf8');
|
|
||||||
const headersEnd = headerString.indexOf('\r\n\r\n');
|
|
||||||
if (headersEnd === -1) {
|
|
||||||
return null; // Incomplete headers
|
|
||||||
}
|
|
||||||
|
|
||||||
const lines = headerString.slice(0, headersEnd).split('\r\n');
|
|
||||||
for (const line of lines) {
|
|
||||||
const parts = line.split(':');
|
|
||||||
if (parts.length > 1) {
|
|
||||||
const key = parts.shift().trim().toLowerCase();
|
|
||||||
const value = parts.join(':').trim();
|
|
||||||
headers[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents the state of a single direction of a TCP connection.
|
|
||||||
*/
|
|
||||||
class ConnectionState {
|
|
||||||
constructor() {
|
|
||||||
this.buffer = Buffer.alloc(0);
|
|
||||||
this.securityState = 'PLAINTEXT'; // PLAINTEXT, AWAITING_SECURE_RESPONSE, SECURE
|
|
||||||
this.wtvSec = null;
|
|
||||||
this.initialKey = null;
|
|
||||||
this.challenge = null;
|
|
||||||
this.incarnation = null;
|
|
||||||
this.isClient = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Appends new data to the connection's buffer.
|
|
||||||
* @param {Buffer} data - The raw TCP payload data.
|
|
||||||
*/
|
|
||||||
feed(data) {
|
|
||||||
this.buffer = Buffer.concat([this.buffer, data]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the WTVSec instance for this connection.
|
|
||||||
* @param {string} initialKey - The wtv-initial-key from the server.
|
|
||||||
* @param {string} challenge - The wtv-challenge from the server.
|
|
||||||
*/
|
|
||||||
initializeSecurity(initialKey, challenge) {
|
|
||||||
this.wtvSec = new WTVSec({
|
|
||||||
config: {
|
config: {
|
||||||
keys: { initial_shared_key: initialKey },
|
keys: {
|
||||||
debug_flags: { debug: false }
|
initial_shared_key: null
|
||||||
|
},
|
||||||
|
debug_flags: {
|
||||||
|
debug: false // Set to true for verbose logging from WTVSec
|
||||||
}
|
}
|
||||||
});
|
|
||||||
this.wtvSec.ProcessChallenge(challenge);
|
|
||||||
console.log("🔑 Security context initialized.");
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
// --- Main Execution ---
|
||||||
* Sets up the RC4 keys for an encrypted session.
|
const pcapFile = process.argv[2];
|
||||||
* @param {number} incarnation - The wtv-incarnation value.
|
if (!pcapFile) {
|
||||||
*/
|
console.error('Usage: node parse_wtvp_parser.js <path_to_pcap_file>');
|
||||||
setupEncryption(incarnation) {
|
|
||||||
if (this.wtvSec) {
|
|
||||||
this.incarnation = incarnation;
|
|
||||||
this.wtvSec.set_incarnation(this.incarnation);
|
|
||||||
console.log(`🔐 Encryption keys prepared for this stream (incarnation=${this.incarnation})`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decrypts data using the appropriate RC4 key.
|
|
||||||
* @param {Buffer} data - The data to decrypt.
|
|
||||||
* @returns {Buffer} The decrypted or original data.
|
|
||||||
*/
|
|
||||||
decrypt(data) {
|
|
||||||
if (this.wtvSec && data.length > 0) {
|
|
||||||
try {
|
|
||||||
// Client encrypts with key 0, server with key 1.
|
|
||||||
const keyNum = this.isClient ? 0 : 1;
|
|
||||||
const decrypted = this.wtvSec.Decrypt(keyNum, data);
|
|
||||||
console.log(`📦 Decrypted ${data.length} bytes for ${this.isClient ? 'client' : 'server'} stream.`);
|
|
||||||
return Buffer.from(decrypted);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Decryption failed:", e);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Processes the reassembled buffer for a connection, handling state transitions.
|
|
||||||
* @param {ConnectionState} state - The state object for the current connection direction.
|
|
||||||
* @param {ConnectionState} oppositeState - The state for the opposite direction of the connection.
|
|
||||||
*/
|
|
||||||
function processConnectionBuffer(state, oppositeState) {
|
|
||||||
while (state.buffer.length > 0) {
|
|
||||||
switch (state.securityState) {
|
|
||||||
case 'AWAITING_SECURE_RESPONSE': {
|
|
||||||
const headersEndIndex = state.buffer.indexOf('\r\n\r\n');
|
|
||||||
if (headersEndIndex === -1) {
|
|
||||||
return; // Wait for the full headers.
|
|
||||||
}
|
|
||||||
|
|
||||||
const bodyStartIndex = headersEndIndex + 4;
|
|
||||||
const plaintextHeaders = state.buffer.slice(0, bodyStartIndex);
|
|
||||||
const encryptedBody = state.buffer.slice(bodyStartIndex);
|
|
||||||
|
|
||||||
process.stdout.write(plaintextHeaders);
|
|
||||||
|
|
||||||
if (encryptedBody.length > 0) {
|
|
||||||
const decryptedBody = state.decrypt(encryptedBody);
|
|
||||||
process.stdout.write(decryptedBody);
|
|
||||||
}
|
|
||||||
|
|
||||||
state.buffer = Buffer.alloc(0);
|
|
||||||
state.securityState = 'SECURE';
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'SECURE': {
|
|
||||||
const output = state.decrypt(state.buffer);
|
|
||||||
process.stdout.write(output);
|
|
||||||
state.buffer = Buffer.alloc(0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'PLAINTEXT':
|
|
||||||
default: {
|
|
||||||
const headersEndIndex = state.buffer.indexOf('\r\n\r\n');
|
|
||||||
if (headersEndIndex === -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const headerSectionLength = headersEndIndex + 4;
|
|
||||||
const headerBuffer = state.buffer.slice(0, headerSectionLength);
|
|
||||||
const headers = parseHeaders(headerBuffer);
|
|
||||||
|
|
||||||
const requestLine = headerBuffer.toString('utf8').split('\r\n')[0];
|
|
||||||
if (state.isClient && requestLine.includes('SECURE ON')) {
|
|
||||||
console.log("▶️ Client sent SECURE ON. Transitioning to encrypted mode.");
|
|
||||||
let incarnation = headers['wtv-incarnation'] ? parseInt(headers['wtv-incarnation'].trim(), 10) : 1;
|
|
||||||
|
|
||||||
state.setupEncryption(incarnation);
|
|
||||||
state.securityState = 'SECURE';
|
|
||||||
if (oppositeState) {
|
|
||||||
oppositeState.setupEncryption(incarnation);
|
|
||||||
oppositeState.securityState = 'AWAITING_SECURE_RESPONSE';
|
|
||||||
}
|
|
||||||
|
|
||||||
process.stdout.write(headerBuffer);
|
|
||||||
const remainingData = state.buffer.slice(headerSectionLength);
|
|
||||||
|
|
||||||
if (remainingData.length > 0) {
|
|
||||||
const decryptedBody = state.decrypt(remainingData);
|
|
||||||
process.stdout.write(decryptedBody);
|
|
||||||
}
|
|
||||||
state.buffer = Buffer.alloc(0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.isClient && headers) {
|
|
||||||
if (headers['wtv-initial-key']) {
|
|
||||||
state.initialKey = headers['wtv-initial-key'];
|
|
||||||
if(oppositeState) oppositeState.initialKey = headers['wtv-initial-key'];
|
|
||||||
console.log("Found wtv-initial-key.");
|
|
||||||
}
|
|
||||||
if (headers['wtv-challenge']) {
|
|
||||||
state.challenge = headers['wtv-challenge'];
|
|
||||||
if(oppositeState) oppositeState.challenge = headers['wtv-challenge'];
|
|
||||||
console.log("Found wtv-challenge.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.initialKey && state.challenge && !state.wtvSec) {
|
|
||||||
state.initializeSecurity(state.initialKey, state.challenge);
|
|
||||||
if (oppositeState) oppositeState.initializeSecurity(state.initialKey, state.challenge);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let fullMessageLength = headerSectionLength;
|
|
||||||
if (headers && headers['content-length']) {
|
|
||||||
const bodyLength = parseInt(headers['content-length'], 10);
|
|
||||||
fullMessageLength += bodyLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.buffer.length < fullMessageLength) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullMessage = state.buffer.slice(0, fullMessageLength);
|
|
||||||
process.stdout.write(fullMessage);
|
|
||||||
state.buffer = state.buffer.slice(fullMessageLength);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// --- Main PCAP Processing Logic ---
|
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
|
||||||
const inputFile = args[args.indexOf('-i') + 1];
|
|
||||||
const serverIP = args[args.indexOf('-h') + 1];
|
|
||||||
|
|
||||||
if (!inputFile || !serverIP) {
|
|
||||||
console.error('Usage: node unroll_rc4.js -i <pcap_file> -h <server_ip>');
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parser = pcap.parse(fs.createReadStream(inputFile));
|
// A store for all active WTVP sessions, keyed by stream identifier.
|
||||||
console.log(`🚀 Starting pcap parser for ${inputFile} with server IP ${serverIP}`);
|
// The identifier is a sorted combination of src/dst ip:port pairs.
|
||||||
|
const wtvpSessions = {};
|
||||||
|
|
||||||
let totalPackets = 0;
|
const parser = pcapParser.parse(pcapFile);
|
||||||
let processedPackets = 0;
|
|
||||||
let linkLayerType = -1;
|
|
||||||
let ipHeaderOffset = 14;
|
|
||||||
|
|
||||||
parser.on('globalHeader', (globalHeader) => {
|
|
||||||
linkLayerType = globalHeader.linkLayerType;
|
|
||||||
console.log(`[INFO] PCAP Link-Layer Header Type: ${linkLayerType}. Adjusting offsets.`);
|
|
||||||
switch (linkLayerType) {
|
|
||||||
case 0: ipHeaderOffset = 4; break;
|
|
||||||
case 1: ipHeaderOffset = 14; break;
|
|
||||||
case 101: ipHeaderOffset = 0; break;
|
|
||||||
case 113: ipHeaderOffset = 16; break;
|
|
||||||
default:
|
|
||||||
console.warn(`[WARN] Unsupported link-layer type: ${linkLayerType}. Assuming Ethernet.`);
|
|
||||||
ipHeaderOffset = 14;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
parser.on('packet', (packet) => {
|
parser.on('packet', (packet) => {
|
||||||
totalPackets++;
|
|
||||||
|
|
||||||
let isIPv4 = false;
|
const data = packet.data;
|
||||||
switch (linkLayerType) {
|
const ethType = data.readUInt16BE(12);
|
||||||
case 0: isIPv4 = (packet.data.length > 4) && (packet.data.readUInt32LE(0) === 2); break;
|
if (ethType !== 0x0800) return; // Not IPv4
|
||||||
case 1: isIPv4 = packet.data.readUInt16BE(12) === 0x0800; break;
|
|
||||||
case 101: isIPv4 = (packet.data.length > 0) && ((packet.data[0] >> 4) === 4); break;
|
const ipHeader = data.slice(14, 34);
|
||||||
case 113: isIPv4 = packet.data.readUInt16BE(14) === 0x0800; break;
|
const protocol = ipHeader[9];
|
||||||
default: return;
|
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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (!isIPv4) return;
|
|
||||||
|
|
||||||
const protocolOffset = ipHeaderOffset + 9;
|
// Ignore packets without a payload
|
||||||
if (packet.data.length <= protocolOffset || packet.data[protocolOffset] !== 6) return;
|
if (!payload || payload.length === 0) {
|
||||||
|
|
||||||
const ipHeaderLength = (packet.data[ipHeaderOffset] & 0x0F) * 4;
|
|
||||||
const tcpHeaderBase = ipHeaderOffset + ipHeaderLength;
|
|
||||||
const tcpHeaderLength = (packet.data[tcpHeaderBase + 12] >> 4) * 4;
|
|
||||||
const payloadOffset = tcpHeaderBase + tcpHeaderLength;
|
|
||||||
if (packet.data.length <= payloadOffset) return;
|
|
||||||
const payload = packet.data.slice(payloadOffset);
|
|
||||||
if (payload.length === 0) return;
|
|
||||||
|
|
||||||
processedPackets++;
|
|
||||||
|
|
||||||
const srcIP = packet.data.slice(ipHeaderOffset + 12, ipHeaderOffset + 16).join('.');
|
|
||||||
const dstIP = packet.data.slice(ipHeaderOffset + 16, ipHeaderOffset + 20).join('.');
|
|
||||||
const srcPort = packet.data.readUInt16BE(tcpHeaderBase);
|
|
||||||
const dstPort = packet.data.readUInt16BE(tcpHeaderBase + 2);
|
|
||||||
|
|
||||||
if (srcIP !== serverIP && dstIP !== serverIP) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentKey = `${srcIP}:${srcPort}->${dstIP}:${dstPort}`;
|
const currentSession = wtvpSessions[sessionKey];
|
||||||
const oppositeKey = `${dstIP}:${dstPort}->${srcIP}:${srcPort}`;
|
const sourceAddr = `${srcIP}:${srcPort}`;
|
||||||
|
const payloadStr = payload.toString('utf8');
|
||||||
|
|
||||||
if (!connections.has(currentKey)) {
|
// 1. Identify Client and Server
|
||||||
let isClientToServer;
|
if (!currentSession.clientAddr) {
|
||||||
const payloadString = payload.toString('utf8');
|
if (payloadStr.startsWith('GET') || payloadStr.startsWith('POST') || payloadStr.startsWith('SECURE ON')) {
|
||||||
console.log(payloadString);
|
currentSession.clientAddr = sourceAddr;
|
||||||
if (srcIP === serverIP && dstIP === serverIP) {
|
currentSession.serverAddr = `${dstIP}:${dstPort}`;
|
||||||
isClientToServer = payloadString.startsWith('GET') || payloadString.startsWith('POST') || payloadString.startsWith('SECURE ON');
|
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 {
|
} else {
|
||||||
isClientToServer = dstIP === serverIP;
|
handleEncrypted(currentSession, payload, isClient);
|
||||||
}
|
}
|
||||||
console.log(`[INFO] New connection detected: ${currentKey} (Client to Server: ${isClientToServer})`);
|
|
||||||
connections.set(currentKey, new ConnectionState());
|
|
||||||
connections.set(oppositeKey, new ConnectionState());
|
|
||||||
connections.get(currentKey).isClient = isClientToServer;
|
|
||||||
connections.get(oppositeKey).isClient = !isClientToServer;
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = connections.get(currentKey);
|
|
||||||
const oppositeState = connections.get(oppositeKey);
|
|
||||||
|
|
||||||
state.feed(payload);
|
|
||||||
processConnectionBuffer(state, oppositeState);
|
|
||||||
processConnectionBuffer(oppositeState, state);
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
parser.on('end', () => {
|
parser.on('end', () => {
|
||||||
console.log('\n[INFO] End of PCAP file reached. Processing any remaining buffered data...');
|
console.log('\n[*] PCAP file processing complete.');
|
||||||
for(const [key, state] of connections.entries()){
|
|
||||||
const parts = key.split('->');
|
|
||||||
const oppositeKey = `${parts[1]}->${parts[0]}`;
|
|
||||||
if(connections.has(oppositeKey)) {
|
|
||||||
const oppositeState = connections.get(oppositeKey);
|
|
||||||
processConnectionBuffer(state, oppositeState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log(`\n✅ Done parsing PCAP. Processed ${processedPackets} out of ${totalPackets} total packets.`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user