/** * Binary File Analyzer with x86 Disassembler * This tool decodes binary files from Base64 and provides analysis functionality * including x86 disassembly, DOS function identification, and string extraction. */ // Function to decode Base64 to binary function base64ToArrayBuffer(base64) { // Remove any whitespace const cleanedBase64 = base64.replace(/\s/g, ''); // Create a binary string from the base64 const binaryString = atob(cleanedBase64); // Create a buffer to hold the binary data const bytes = new Uint8Array(binaryString.length); // Fill the buffer for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes; } // Find all instances of DOS function calls (B4 XX CD 21) function findDOSCalls(data) { let dosCallPatterns = []; for (let i = 0; i < data.length - 3; i++) { if (data[i] === 0xB4 && data[i+2] === 0xCD && data[i+3] === 0x21) { const functionValue = data[i+1]; dosCallPatterns.push({ position: i, ah_value: functionValue, hex_value: '0x' + functionValue.toString(16).padStart(2, '0').toUpperCase() }); } } return dosCallPatterns; } // Identify DOS function by AH value function getDOSFunctionName(ah_value) { const dosServices = { 0x00: "Terminate Program", 0x01: "Character Input", 0x02: "Character Output", 0x09: "Display String", 0x0A: "Buffered Keyboard Input", 0x0E: "Select Disk", 0x19: "Get Current Disk", 0x1A: "Set DTA Address", 0x25: "Set Interrupt Vector", 0x26: "Create New PSP", 0x2A: "Get Date", 0x2B: "Set Date", 0x2C: "Get Time", 0x2D: "Set Time", 0x2F: "Get DTA Address", 0x30: "Get DOS Version", 0x31: "Terminate and Stay Resident (Keep Process)", 0x35: "Get Interrupt Vector", 0x3B: "Change Directory", 0x3C: "Create File", 0x3D: "Open File", 0x3E: "Close File", 0x3F: "Read File or Device", 0x40: "Write File or Device", 0x41: "Delete File", 0x42: "Move File Pointer", 0x43: "Get/Set File Attributes", 0x44: "IOCTL", 0x47: "Get Current Directory", 0x48: "Allocate Memory", 0x49: "Free Allocated Memory", 0x4A: "Modify Allocated Memory Blocks", 0x4B: "Load and Execute Program", 0x4C: "Terminate Process with Return Code", 0x4D: "Get Return Code", 0x4E: "Find First Matching File", 0x4F: "Find Next Matching File", 0x56: "Rename File", 0x57: "Get/Set File Date and Time" }; return dosServices[ah_value] || "Unknown function"; } // Extract all readable strings from the binary function findStrings(data, minLength = 5) { const strings = []; let currentString = ""; for (let i = 0; i < data.length; i++) { const byte = data[i]; // Check if it's a printable ASCII character if (byte >= 32 && byte <= 126) { currentString += String.fromCharCode(byte); } else { // End of string if (currentString.length >= minLength) { strings.push({ position: i - currentString.length, text: currentString }); } currentString = ""; } } // Check if we have a string at the end of the file if (currentString.length >= minLength) { strings.push({ position: data.length - currentString.length, text: currentString }); } return strings; } // Simple x86 disassembler class X86Disassembler { constructor(data) { this.data = data; this.position = 0; this.instructions = []; } // Read a byte from the current position readByte() { if (this.position >= this.data.length) return null; return this.data[this.position++]; } // Read a word (2 bytes) from the current position readWord() { const low = this.readByte(); const high = this.readByte(); if (low === null || high === null) return null; return low + (high << 8); } // Peek at bytes without advancing position peekByte(offset = 0) { if (this.position + offset >= this.data.length) return null; return this.data[this.position + offset]; } // Get register name getRegName(regCode, wide = true) { const byteRegs = ['AL', 'CL', 'DL', 'BL', 'AH', 'CH', 'DH', 'BH']; const wordRegs = ['AX', 'CX', 'DX', 'BX', 'SP', 'BP', 'SI', 'DI']; return wide ? wordRegs[regCode] : byteRegs[regCode]; } // Format a hex value with 0x prefix formatHex(value, digits = 2) { return '0x' + value.toString(16).toUpperCase().padStart(digits, '0'); } // Disassemble next instruction disassembleNext() { const startPos = this.position; const opcode = this.readByte(); if (opcode === null) return null; let instruction = { offset: startPos, bytes: [opcode], text: "", comment: "" }; switch (opcode) { // MOV reg, imm case 0xB0: case 0xB1: case 0xB2: case 0xB3: case 0xB4: case 0xB5: case 0xB6: case 0xB7: { const imm = this.readByte(); instruction.bytes.push(imm); const reg = this.getRegName(opcode - 0xB0, false); instruction.text = `MOV ${reg}, ${this.formatHex(imm)}`; break; } case 0xB8: case 0xB9: case 0xBA: case 0xBB: case 0xBC: case 0xBD: case 0xBE: case 0xBF: { const imm = this.readWord(); instruction.bytes.push(imm & 0xFF); instruction.bytes.push((imm >> 8) & 0xFF); const reg = this.getRegName(opcode - 0xB8, true); instruction.text = `MOV ${reg}, ${this.formatHex(imm, 4)}`; break; } // INT instructions case 0xCD: { const intNum = this.readByte(); instruction.bytes.push(intNum); instruction.text = `INT ${this.formatHex(intNum)}`; // Add comment for known interrupts if (intNum === 0x21) { instruction.comment = "DOS function call"; } else if (intNum === 0x27) { instruction.comment = "TSR function (original method)"; } break; } // CALL instructions case 0xE8: { const offset = this.readWord(); instruction.bytes.push(offset & 0xFF); instruction.bytes.push((offset >> 8) & 0xFF); // Calculate the target address (relative to instruction end) const target = startPos + 3 + ((offset < 0x8000) ? offset : offset - 0x10000); instruction.text = `CALL ${this.formatHex(target, 4)}`; break; } // JMP instructions case 0xE9: { const offset = this.readWord(); instruction.bytes.push(offset & 0xFF); instruction.bytes.push((offset >> 8) & 0xFF); // Calculate the target address (relative to instruction end) const target = startPos + 3 + ((offset < 0x8000) ? offset : offset - 0x10000); instruction.text = `JMP ${this.formatHex(target, 4)}`; break; } case 0xEB: { const offset = this.readByte(); instruction.bytes.push(offset); // Calculate the target address (relative to instruction end) const target = startPos + 2 + ((offset < 0x80) ? offset : offset - 0x100); instruction.text = `JMP SHORT ${this.formatHex(target, 4)}`; break; } // RET instructions case 0xC3: { instruction.text = "RET"; break; } case 0xC2: { const imm = this.readWord(); instruction.bytes.push(imm & 0xFF); instruction.bytes.push((imm >> 8) & 0xFF); instruction.text = `RET ${this.formatHex(imm, 4)}`; break; } // PUSH/POP instructions case 0x50: case 0x51: case 0x52: case 0x53: case 0x54: case 0x55: case 0x56: case 0x57: { const reg = this.getRegName(opcode - 0x50); instruction.text = `PUSH ${reg}`; break; } case 0x58: case 0x59: case 0x5A: case 0x5B: case 0x5C: case 0x5D: case 0x5E: case 0x5F: { const reg = this.getRegName(opcode - 0x58); instruction.text = `POP ${reg}`; break; } // MOV between registers case 0x88: case 0x89: case 0x8A: case 0x8B: { const modrm = this.readByte(); instruction.bytes.push(modrm); const mod = (modrm >> 6) & 0x3; const reg = (modrm >> 3) & 0x7; const rm = modrm & 0x7; const isToReg = (opcode & 0x02) !== 0; const isWide = (opcode & 0x01) !== 0; let source, dest; // For simplicity, only handle direct register-to-register moves if (mod === 0x3) { const regName = this.getRegName(reg, isWide); const rmName = this.getRegName(rm, isWide); if (isToReg) { source = rmName; dest = regName; } else { source = regName; dest = rmName; } instruction.text = `MOV ${dest}, ${source}`; } else { // Memory-operand instructions are complex; simplified here instruction.text = "MOV with memory operand"; } break; } // Other common instructions case 0x90: instruction.text = "NOP"; break; case 0xF4: instruction.text = "HLT"; break; case 0xFC: instruction.text = "CLD"; break; case 0xFD: instruction.text = "STD"; break; case 0xFA: instruction.text = "CLI"; break; case 0xFB: instruction.text = "STI"; break; default: instruction.text = `DB ${this.formatHex(opcode)}`; instruction.comment = "Unknown/unhandled opcode"; break; } return instruction; } // Disassemble a range of code disassembleRange(startAddress, length) { this.position = startAddress; const endAddress = startAddress + length; const instructions = []; while (this.position < endAddress && this.position < this.data.length) { const instruction = this.disassembleNext(); if (instruction === null) break; instructions.push(instruction); } return instructions; } // Format instructions for display formatInstructions(instructions) { return instructions.map(instr => { // Format the bytes const bytesHex = instr.bytes.map(b => b.toString(16).toUpperCase().padStart(2, '0') ).join(' '); // Format the full line let line = instr.offset.toString(16).toUpperCase().padStart(4, '0') + ': '; line += bytesHex.padEnd(12, ' '); line += instr.text.padEnd(30, ' '); if (instr.comment) { line += '; ' + instr.comment; } return line; }).join('\n'); } } // Main analysis function async function analyzeBinary(base64Content) { // Decode the Base64 content to binary const binaryData = base64ToArrayBuffer(base64Content); console.log(`Successfully decoded binary data: ${binaryData.length} bytes`); // Find DOS function calls const dosCallPatterns = findDOSCalls(binaryData); console.log(`\nFound ${dosCallPatterns.length} DOS function calls (B4 XX CD 21 pattern)`); // Group DOS calls by function const functionCounts = {}; dosCallPatterns.forEach(pattern => { const key = pattern.hex_value; if (!functionCounts[key]) { functionCounts[key] = { count: 0, name: getDOSFunctionName(pattern.ah_value), positions: [] }; } functionCounts[key].count++; functionCounts[key].positions.push(pattern.position); }); // Print function usage summary console.log("DOS function calls summary:"); Object.entries(functionCounts) .sort((a, b) => b[1].count - a[1].count) .forEach(([func, info]) => { console.log(` AH=${func} (${info.name}): ${info.count} occurrences at positions 0x${info.positions.map(p => p.toString(16).toUpperCase()).join(', 0x')}`); }); // Look for TSR functions specifically const tsrCalls = dosCallPatterns.filter(pattern => pattern.ah_value === 0x31); const int27Calls = []; for (let i = 0; i < binaryData.length - 1; i++) { if (binaryData[i] === 0xCD && binaryData[i+1] === 0x27) { int27Calls.push(i); } } console.log(`\nTSR implementation details:`); console.log(` Modern TSR method (INT 21h/AH=31h): ${tsrCalls.length} occurrences`); console.log(` Original TSR method (INT 27h): ${int27Calls.length} occurrences`); // Disassemble the program const disassembler = new X86Disassembler(binaryData); // Disassemble entry point console.log("\nDisassembly of program entry point (first 50 bytes):"); const entryPointCode = disassembler.disassembleRange(0, 50); console.log(disassembler.formatInstructions(entryPointCode)); // Disassemble around the TSR call if it exists if (tsrCalls.length > 0) { const tsrPosition = tsrCalls[0].position; const startPos = Math.max(0, tsrPosition - 20); const length = Math.min(binaryData.length - startPos, 50); console.log(`\nDisassembly around TSR call at 0x${tsrPosition.toString(16).toUpperCase()} (20 bytes before, 30 bytes after):`); const tsrCode = disassembler.disassembleRange(startPos, length); console.log(disassembler.formatInstructions(tsrCode)); } // Extract strings const strings = findStrings(binaryData); console.log(`\nFound ${strings.length} strings in the binary`); // Filter interesting strings const interestingKeywords = [ 'XRAY', 'memory', 'monitor', 'hook', 'TSR', 'resident', 'shift', 'alt', 'segment', 'offset', 'scroll', 'window', 'size', 'address' ]; const interestingStrings = strings.filter(str => interestingKeywords.some(keyword => str.text.toLowerCase().includes(keyword.toLowerCase())) ); console.log(`\nInteresting program strings (${interestingStrings.length} matches):`); interestingStrings.forEach(str => { console.log(` 0x${str.position.toString(16).toUpperCase()}: ${str.text}`); }); // Return analysis results return { fileSize: binaryData.length, dosCalls: functionCounts, tsrImplementation: { modern: tsrCalls.length > 0, original: int27Calls.length > 0, positions: tsrCalls.map(call => call.position) }, entryPointDisassembly: entryPointCode, interestingStrings }; } // Example usage: // const base64Content = "..."; // Base64-encoded binary data // analyzeBinary(base64Content).then(results => console.log(JSON.stringify(results, null, 2)));