""" AnyDesk Blind Protocol Fuzzer Hooks the RECEIVING side's TLS decryption and mutates incoming data to trigger crashes in codec/protocol parsing. Modes: --mode random : Random byte flips in all incoming data --mode dimensions : Target likely dimension fields (2-byte and 4-byte values 100-8192) --mode overflow : Replace small size values with large ones (integer overflow) --mode all : Cycle through all mutation strategies Run on the VICTIM side while connected to attacker. The attacker just needs to move the mouse / show screen content to generate frames. Usage: python 04_fuzzer.py --mode all --intensity medium """ import frida import sys import os import json import time import random import argparse from datetime import datetime FUZZER_AGENT = r""" 'use strict'; const FUZZ_MODE = '%FUZZ_MODE%'; const INTENSITY = %INTENSITY%; // 0.0-1.0, probability of mutating a packet const SKIP_FIRST_N = %SKIP_FIRST_N%; // Skip first N packets (handshake) let packetCount = 0; let mutationCount = 0; let crashDetected = false; // Mutation strategies const strategies = { // Flip random bytes random: function(arr, len) { const numFlips = Math.max(1, Math.floor(len * 0.01)); // 1% of bytes for (let i = 0; i < numFlips; i++) { const idx = Math.floor(Math.random() * len); arr[idx] = Math.floor(Math.random() * 256); } return `random_flip(${numFlips})`; }, // Target dimension-like fields: find 2/4-byte LE values between 100-8192 // and replace with edge cases dimensions: function(arr, len) { const edgeCases16 = [0, 1, 0x7FFF, 0x8000, 0xFFFF, 0xFFFE, 65535, 32768, 32767]; const edgeCases32 = [0, 1, 0x7FFFFFFF, 0x80000000, 0xFFFFFFFF, 0xFFFFFFFE, 0x10000, 0xFFFF, 65536, 2147483647]; let mutations = []; // Scan for 4-byte values that look like dimensions for (let i = 0; i < len - 4; i += 2) { const val = arr[i] | (arr[i+1] << 8) | (arr[i+2] << 16) | (arr[i+3] << 24); if (val >= 100 && val <= 8192) { // This might be a width/height — replace with edge case if (Math.random() < 0.3) { const edge = edgeCases32[Math.floor(Math.random() * edgeCases32.length)]; arr[i] = edge & 0xFF; arr[i+1] = (edge >> 8) & 0xFF; arr[i+2] = (edge >> 16) & 0xFF; arr[i+3] = (edge >> 24) & 0xFF; mutations.push(`dim32@${i}:${val}->${edge}`); } } // Also check 2-byte values const val16 = arr[i] | (arr[i+1] << 8); if (val16 >= 100 && val16 <= 4096) { if (Math.random() < 0.2) { const edge = edgeCases16[Math.floor(Math.random() * edgeCases16.length)]; arr[i] = edge & 0xFF; arr[i+1] = (edge >> 8) & 0xFF; mutations.push(`dim16@${i}:${val16}->${edge}`); } } } return mutations.length > 0 ? mutations.join(', ') : 'no_dims_found'; }, // Integer overflow: find size-like fields and make them huge overflow: function(arr, len) { let mutations = []; // Look for 4-byte values that could be sizes (reasonable range) for (let i = 0; i < len - 4; i += 4) { const val = arr[i] | (arr[i+1] << 8) | (arr[i+2] << 16) | (arr[i+3] << 24); // Sizes typically > 0 and < 10MB if (val > 0 && val < 10 * 1024 * 1024) { if (Math.random() < 0.15) { // Integer overflow payloads const overflows = [ 0xFFFFFFFF, // Max uint32 0x80000000, // Int32 sign flip val * 0x10001, // width*height overflow pattern 0x7FFFFFFF, // Max int32 val | 0xFF000000, // High bytes set (val << 16) | val, // Doubled 0x01000000, // 16MB (alloc stress) 0x10000000, // 256MB 0xFFFFFFF0, // Near-max aligned ]; const ov = overflows[Math.floor(Math.random() * overflows.length)]; arr[i] = ov & 0xFF; arr[i+1] = (ov >> 8) & 0xFF; arr[i+2] = (ov >> 16) & 0xFF; arr[i+3] = (ov >> 24) & 0xFF; mutations.push(`overflow@${i}:${val}->0x${(ov >>> 0).toString(16)}`); } } } return mutations.length > 0 ? mutations.join(', ') : 'no_sizes_found'; } }; function mutatePacket(buf) { const arr = new Uint8Array(buf); const len = arr.length; if (len < 8) return null; // Too small to fuzz meaningfully let strategy; if (FUZZ_MODE === 'all') { const modes = ['random', 'dimensions', 'overflow']; strategy = modes[mutationCount % modes.length]; } else { strategy = FUZZ_MODE; } const desc = strategies[strategy](arr, len); return {strategy: strategy, desc: desc}; } // ============================================================ // Hook DecryptMessage (SChannel) — mutate data AFTER decryption // ============================================================ function hookDecrypt() { const DecryptMessage = Module.findExportByName('sspicli.dll', 'DecryptMessage') || Module.findExportByName('secur32.dll', 'DecryptMessage'); if (DecryptMessage) { Interceptor.attach(DecryptMessage, { onEnter(args) { this.pMessage = args[1]; }, onLeave(retval) { if (retval.toInt32() !== 0) return; packetCount++; if (packetCount <= SKIP_FIRST_N) return; // Skip handshake if (Math.random() > INTENSITY) return; // Probabilistic try { const pBufDesc = this.pMessage; const cBuffers = pBufDesc.add(4).readU32(); const pBuffers = pBufDesc.add(8).readPointer(); for (let i = 0; i < cBuffers; i++) { const bufPtr = pBuffers.add(i * 16); const cbBuffer = bufPtr.readU32(); const bufType = bufPtr.add(4).readU32(); const pvBuffer = bufPtr.add(8).readPointer(); if (bufType === 1 && cbBuffer > 16 && cbBuffer < 1024 * 1024) { const data = pvBuffer.readByteArray(cbBuffer); const result = mutatePacket(data); if (result) { pvBuffer.writeByteArray(data); mutationCount++; if (mutationCount % 10 === 0 || mutationCount <= 5) { send({ type: 'mutation', seq: packetCount, size: cbBuffer, strategy: result.strategy, desc: result.desc, totalMutations: mutationCount, timestamp: Date.now() }); } } } } } catch(e) { send({type: 'error', msg: e.toString()}); } } }); return true; } return false; } // ============================================================ // Hook SSL_read (OpenSSL) — mutate after read // ============================================================ function hookSSLRead() { let hooked = false; const mods = Process.enumerateModules(); for (const m of mods) { if (m.name.toLowerCase().includes('ssl')) { try { const exports = m.enumerateExports(); for (const exp of exports) { if (exp.name === 'SSL_read') { Interceptor.attach(exp.address, { onEnter(args) { this.buf = args[1]; }, onLeave(retval) { const read = retval.toInt32(); if (read <= 0) return; packetCount++; if (packetCount <= SKIP_FIRST_N) return; if (Math.random() > INTENSITY) return; const data = this.buf.readByteArray(read); const result = mutatePacket(data); if (result) { this.buf.writeByteArray(data); mutationCount++; if (mutationCount % 10 === 0 || mutationCount <= 5) { send({ type: 'mutation', seq: packetCount, size: read, strategy: result.strategy, desc: result.desc, totalMutations: mutationCount, timestamp: Date.now() }); } } } }); hooked = true; } } } catch(e) {} } } return hooked; } // ============================================================ // Fallback: Hook ws2_32!recv and mutate raw (may break TLS) // ============================================================ function hookRawRecv() { const wsRecv = Module.findExportByName('ws2_32.dll', 'recv'); if (wsRecv) { Interceptor.attach(wsRecv, { onEnter(args) { this.buf = args[1]; }, onLeave(retval) { const received = retval.toInt32(); if (received <= 0) return; packetCount++; if (packetCount <= SKIP_FIRST_N) return; if (Math.random() > INTENSITY) return; const data = this.buf.readByteArray(received); const result = mutatePacket(data); if (result) { this.buf.writeByteArray(data); mutationCount++; if (mutationCount % 50 === 0 || mutationCount <= 3) { send({ type: 'mutation', seq: packetCount, size: received, strategy: result.strategy, desc: result.desc, totalMutations: mutationCount, note: 'RAW_SOCKET (may break TLS)', timestamp: Date.now() }); } } } }); return true; } return false; } // ============================================================ // Crash detection: hook common crash paths // ============================================================ function setupCrashDetection() { // Hook UnhandledExceptionFilter const uef = Module.findExportByName('kernel32.dll', 'UnhandledExceptionFilter'); if (uef) { Interceptor.attach(uef, { onEnter(args) { const exceptionRecord = args[0]; try { const exceptionCode = exceptionRecord.readU32(); const exceptionAddress = exceptionRecord.add(Process.pointerSize * 2).readPointer(); send({ type: 'crash', code: '0x' + (exceptionCode >>> 0).toString(16), address: exceptionAddress.toString(), totalPackets: packetCount, totalMutations: mutationCount, timestamp: Date.now() }); crashDetected = true; } catch(e) {} } }); } // Hook RtlReportException / RaiseException for earlier detection const raiseEx = Module.findExportByName('kernel32.dll', 'RaiseException'); if (raiseEx) { Interceptor.attach(raiseEx, { onEnter(args) { const code = args[0].toInt32() >>> 0; // Filter: only report access violations, heap corruption, stack overflow if (code === 0xC0000005 || code === 0xC0000374 || code === 0xC00000FD || code === 0xC0000409) { send({ type: 'exception', code: '0x' + code.toString(16), codeName: { 0xC0000005: 'ACCESS_VIOLATION', 0xC0000374: 'HEAP_CORRUPTION', 0xC00000FD: 'STACK_OVERFLOW', 0xC0000409: 'STACK_BUFFER_OVERRUN' }[code] || 'UNKNOWN', totalPackets: packetCount, totalMutations: mutationCount, stack: Thread.backtrace(this.context, Backtracer.ACCURATE) .map(DebugSymbol.fromAddress).join('\n'), timestamp: Date.now() }); } } }); } send({type: 'status', msg: 'Crash detection hooks installed'}); } // ============================================================ // Main // ============================================================ send({type: 'status', msg: `Fuzzer starting (mode: ${FUZZ_MODE}, intensity: ${INTENSITY})`}); send({type: 'status', msg: `Skipping first ${SKIP_FIRST_N} packets (handshake protection)`}); setupCrashDetection(); let hooked = hookDecrypt(); if (!hooked) hooked = hookSSLRead(); if (!hooked) { send({type: 'status', msg: 'WARNING: No TLS hooks available. Falling back to raw socket (unstable).'}); hookRawRecv(); } send({type: 'status', msg: 'Fuzzer active. Make sure attacker screen is visible / moving...'}); send({type: 'ready'}); // Periodic stats setInterval(function() { send({ type: 'stats', packets: packetCount, mutations: mutationCount, crashDetected: crashDetected, timestamp: Date.now() }); }, 5000); """; def find_anydesk_pid(): import subprocess try: output = subprocess.check_output( ['tasklist', '/FI', 'IMAGENAME eq AnyDesk.exe', '/FO', 'CSV', '/NH'], text=True, stderr=subprocess.DEVNULL ) for line in output.strip().split('\n'): if 'AnyDesk' in line: parts = line.strip().strip('"').split('","') if len(parts) >= 2: return int(parts[1].strip('"')) except: pass return None class FuzzerState: def __init__(self): self.ready = False self.mutations = 0 self.packets = 0 self.crashes = [] self.exceptions = [] self.start_time = time.time() def on_message(self, message, data): if message['type'] == 'send': payload = message['payload'] msg_type = payload.get('type', '') if msg_type == 'status': print(f" [*] {payload['msg']}") elif msg_type == 'ready': self.ready = True elif msg_type == 'mutation': self.mutations = payload.get('totalMutations', self.mutations + 1) print(f" [FUZZ #{self.mutations}] pkt#{payload['seq']} " f"{payload['size']}b {payload['strategy']}: {payload['desc']}") elif msg_type == 'stats': elapsed = time.time() - self.start_time self.packets = payload['packets'] self.mutations = payload['mutations'] rate = self.mutations / elapsed if elapsed > 0 else 0 print(f" [STATS] {self.packets} pkts, {self.mutations} mutations " f"({rate:.1f}/s), {len(self.crashes)} crashes, {len(self.exceptions)} exceptions") elif msg_type == 'crash': self.crashes.append(payload) print(f"\n {'!'*60}") print(f" [CRASH] Exception 0x{payload['code']} at {payload['address']}") print(f" [CRASH] After {payload['totalPackets']} packets, {payload['totalMutations']} mutations") print(f" {'!'*60}\n") elif msg_type == 'exception': self.exceptions.append(payload) print(f"\n [EXCEPTION] {payload['codeName']} (0x{payload['code']})") print(f" After {payload['totalPackets']} packets, {payload['totalMutations']} mutations") if payload.get('stack'): for frame in payload['stack'].split('\n')[:5]: print(f" {frame}") print() elif msg_type == 'error': print(f" [!] {payload['msg']}") elif message['type'] == 'error': print(f" [!] FRIDA ERROR: {message['description']}") def save_results(self, path): results = { 'duration': time.time() - self.start_time, 'total_packets': self.packets, 'total_mutations': self.mutations, 'crashes': self.crashes, 'exceptions': self.exceptions } with open(path, 'w') as f: json.dump(results, f, indent=2) print(f"[+] Results saved to {path}") def main(): parser = argparse.ArgumentParser(description='AnyDesk Blind Protocol Fuzzer') parser.add_argument('--mode', choices=['random', 'dimensions', 'overflow', 'all'], default='all', help='Mutation strategy') parser.add_argument('--intensity', choices=['low', 'medium', 'high', 'max'], default='medium', help='Mutation probability per packet') parser.add_argument('--skip', type=int, default=50, help='Skip first N packets (handshake protection)') args = parser.parse_args() intensity_map = {'low': 0.05, 'medium': 0.15, 'high': 0.4, 'max': 0.9} intensity = intensity_map[args.intensity] print("=" * 60) print(" AnyDesk Blind Protocol Fuzzer") print("=" * 60) print(f" Mode: {args.mode}") print(f" Intensity: {args.intensity} ({intensity*100:.0f}% of packets)") print(f" Skip first: {args.skip} packets") print() pid = find_anydesk_pid() if not pid: print("[!] AnyDesk.exe not found.") sys.exit(1) print(f"[+] Found AnyDesk.exe (PID: {pid})") state = FuzzerState() try: session = frida.attach(pid) except Exception as e: print(f"[!] Frida attach failed: {e}") print("[!] Run as Administrator.") sys.exit(1) agent_code = FUZZER_AGENT.replace('%FUZZ_MODE%', args.mode) agent_code = agent_code.replace('%INTENSITY%', str(intensity)) agent_code = agent_code.replace('%SKIP_FIRST_N%', str(args.skip)) script = session.create_script(agent_code, runtime='v8') script.on('message', state.on_message) script.load() for _ in range(100): if state.ready: break time.sleep(0.1) print(f"\n{'=' * 60}") print(" FUZZING — move mouse on attacker screen to generate frames") print(" Press Ctrl+C to stop and save results") print(f"{'=' * 60}\n") try: while True: time.sleep(0.1) except KeyboardInterrupt: print("\n\n[+] Stopping fuzzer...") timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') state.save_results(f"fuzz_results_{timestamp}.json") if state.crashes: print(f"\n[!!!] {len(state.crashes)} CRASHES DETECTED — review results file") if state.exceptions: print(f"[!!!] {len(state.exceptions)} EXCEPTIONS DETECTED — review results file") session.detach() if __name__ == '__main__': main()