initial commit
This commit is contained in:
@@ -0,0 +1,538 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user