From 8e75cd29693240368027ce29b6d612d2fba79ecf Mon Sep 17 00:00:00 2001 From: JJTech0130 Date: Thu, 19 Oct 2023 08:33:08 -0400 Subject: [PATCH] refactor gsa.py --- examples/openhaystack.py | 33 ++-- gsa.py | 376 +++++++++++++-------------------------- 2 files changed, 137 insertions(+), 272 deletions(-) diff --git a/examples/openhaystack.py b/examples/openhaystack.py index e19c84b..e5630c0 100644 --- a/examples/openhaystack.py +++ b/examples/openhaystack.py @@ -26,13 +26,8 @@ else: USERNAME = input("Username: ") PASSWORD = input("Password: ") - anisette = gsa.Anisette() - - print("Anisette headers:", anisette.generate_headers()) - - print("Authenticating with Grand Slam...") - g = gsa.authenticate(USERNAME, PASSWORD, anisette) + g = gsa.authenticate(USERNAME, PASSWORD) #print(g) pet = g["t"]["com.apple.gs.idms.pet"]["token"] print("Authenticated!") @@ -55,14 +50,13 @@ else: headers = { "X-Apple-ADSID": g["adsid"], - "X-Mme-Nas-Qualify": b64encode(v), - "User-Agent": "com.apple.iCloudHelper/282 CFNetwork/1408.0.4 Darwin/22.5.0" + "X-Mme-Nas-Qualify": b64encode(v).decode(), + "User-Agent": "com.apple.iCloudHelper/282 CFNetwork/1408.0.4 Darwin/22.5.0", + "X-Mme-Client-Info": gsa.build_client(emulated_app="accountsd") # Otherwise we get MOBILEME_TERMS_OF_SERVICE_UPDATE on some accounts } - headers.update(anisette.generate_headers()) - # Otherwise we get MOBILEME_TERMS_OF_SERVICE_UPDATE on some accounts - # Really should just change it in gsa.py - headers["X-Mme-Client-Info"]= " " - #print(headers) + headers.update(gsa.generate_anisette_headers()) + + print(headers) print("Logging in to iCloud...") r = requests.post( @@ -71,18 +65,18 @@ else: data=data, headers=headers, verify=False, - ) + + print(r) + print(r.headers) r = plistlib.loads(r.content) + print(r) search_party_token = r['delegates']['com.apple.mobileme']['service-data']['tokens']['searchPartyToken'] ds_prs_id = r['delegates']['com.apple.mobileme']['service-data']['appleAccountInfo']['dsPrsID'] # This can also be obtained from the grandslam response - #print(r) print("Logged in!") - # print("Search Party Token: ", search_party_token) - with open(CONFIG_PATH, "w") as f: json.dump({ "search_party_token": search_party_token, @@ -91,13 +85,10 @@ else: import time -#print("Search Party Token: ", search_party_token) - - r = requests.post( "https://gateway.icloud.com/acsnservice/fetch", auth=(ds_prs_id, search_party_token), - headers=gsa.Anisette().generate_headers(), + headers=gsa.generate_anisette_headers(), json={ "search": [ { diff --git a/gsa.py b/gsa.py index b50c21e..37ba8fd 100644 --- a/gsa.py +++ b/gsa.py @@ -17,15 +17,19 @@ import srp._pysrp as srp from cryptography.hazmat.primitives import padding from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes -# Constants -DEBUG = True # Allows using a proxy for debugging (disables SSL verification) + # Server to use for anisette generation +ANISETTE = False # Use local generation with AOSKit (macOS only) # ANISETTE = "https://sign.rheaa.xyz/" # ANISETTE = 'http://45.132.246.138:6969/' -ANISETTE = "https://ani.sidestore.io/" +#ANISETTE = "https://ani.sidestore.io/" # ANISETTE = 'https://sideloadly.io/anisette/irGb3Quww8zrhgqnzmrx' # ANISETTE = "http://jkcoxson.com:2052/" +# Created here so that it is consistent +USER_ID = uuid.uuid4() +DEVICE_ID = uuid.uuid4() + # Configure SRP library for compatibility with Apple's implementation srp.rfc5054_enable() srp.no_username_in_x() @@ -35,190 +39,7 @@ import urllib3 urllib3.disable_warnings() - -def generate_anisette() -> dict: - import objc - from Foundation import NSBundle, NSClassFromString # type: ignore - - AOSKitBundle = NSBundle.bundleWithPath_( - "/System/Library/PrivateFrameworks/AOSKit.framework" - ) - objc.loadBundleFunctions(AOSKitBundle, globals(), [("retrieveOTPHeadersForDSID", b"")]) # type: ignore - util = NSClassFromString("AOSUtilities") - - h = util.retrieveOTPHeadersForDSID_("-2") - - o = { - "X-Apple-I-MD": str(h["X-Apple-MD"]), - "X-Apple-I-MD-M": str(h["X-Apple-MD-M"]), - } - # h["X-Apple-I-MD"] = str(h["X-Apple-MD"]) - # h["X-Apple-I-MD-M"] = str(h["X-Apple-MD-M"]) - # print(o) - return o - # r = requests.get(ANISETTE, verify=False if DEBUG else True, timeout=5) - # r = json.loads(r.text) - # return r - - -class Anisette: - @staticmethod - def _fetch(url: str) -> dict: - """Fetches anisette data that we cannot calculate from a remote server""" - if url == False: - return generate_anisette() - r = requests.get(url, verify=False if DEBUG else True, timeout=5) - r = json.loads(r.text) - return r - - def __init__(self, url: str = ANISETTE, name: str = "") -> None: - self._name = name - self._url = url - self._anisette = self._fetch(self._url) - - # Generate a "user id": just a random UUID - # TODO: Figure out how to tie it to the user's account on the device - self._user_id = str(uuid.uuid4()).upper() - self._device_id = str(uuid.uuid4()).upper() - - # override string printing - def __str__(self) -> str: - return f"{self._name} ({self.backend})" - - @property - def url(self) -> str: - return self._url - - @property - def backend(self) -> str: - if ( - self._anisette["X-MMe-Client-Info"] - == " " - ): - return "AltServer" - elif ( - self._anisette["X-MMe-Client-Info"] - == " " - ): - return "Provision" - else: - return f"Unknown ({self._anisette['X-MMe-Client-Info']})" - - # Getters - @property - def timestamp(self) -> str: - """'Timestamp' - Current timestamp in ISO 8601 format - """ - - # We only want sencond precision, so we set the microseconds to 0 - # We also add 'Z' to the end to indicate UTC - # An alternate way to write this is strftime("%FT%T%zZ") - return datetime.utcnow().replace(microsecond=0).isoformat() + "Z" - - @property - def timezone(self) -> str: - """'Time Zone' - Abbreviation of the timezone of the device (e.g. EST)""" - - return str(datetime.utcnow().astimezone().tzinfo) - - @property - def locale(self) -> str: - """'Locale' - Locale of the device (e.g. en_US) - """ - - return locale.getdefaultlocale()[0] or "en_US" - - @property - def otp(self) -> str: - """'One Time Password' - A seemingly random base64 string containing 28 bytes - TODO: Figure out how to generate this - """ - - return self._anisette["X-Apple-I-MD"] - - @property - def local_user(self) -> str: - """'Local User ID' - There are 2 possible implementations of this value - 1. Uppercase hex of the SHA256 hash of some unknown value (used by Windows based servers) - 2. Base64 encoding of an uppercase UUID (used by android based servers) - I picked the second one because it's more fully understood. - """ - - return b64encode(self._user_id.encode()).decode() - - @property - def machine(self) -> str: - """'Machine ID' - This is a base64 encoded string of 60 'random' bytes - We're not sure how this is generated, we have to rely on the server - TODO: Figure out how to generate this - """ - - return self._anisette["X-Apple-I-MD-M"] - - @property - def router(self) -> str: - """'Routing Info' - This is a number, either 17106176 or 50660608 - It doesn't seem to matter which one we use, - 17106176 is used by Sideloadly and Provision (android) based servers - 50660608 is used by Windows iCloud based servers - """ - - return "17106176" - - @property - def serial(self) -> str: - """'Device Serial Number' - This is the serial number of the device - You can use a legitimate serial number, but Apple accepts '0' as well (for andriod devices) - See https://github.com/acidanthera/OpenCorePkg/blob/master/Utilities/macserial/macserial.c for how to generate a legit serial - """ - - return "0" - - @property - def device(self) -> str: - """'Device Unique Identifier' - This is just an uppercase UUID""" - - return self._device_id - - def _build_client(self, emulated_device: str, emulated_app: str) -> str: - # TODO: Update OS version and app versions - - model = emulated_device - if emulated_device == "PC": - # We're emulating a PC, so we run Windows (Vista?) - os = "Windows" - os_version = "6.2(0,0);9200" - else: - # We're emulating a Mac, so we run macOS (What is 15.6?) - os = "Mac OS X" - os_version = "10.15.6;19G2021" - - if emulated_app == "Xcode": - app_bundle = "com.apple.dt.Xcode" - app_version = "3594.4.19" - else: - app_bundle = "com.apple.iCloud" - app_version = "7.21" - - if os == "Windows": - authkit_bundle = "com.apple.AuthKitWin" - else: - authkit_bundle = "com.apple.AuthKit" - authkit_version = "1" - - return f"<{model}> <{os};{os_version}> <{authkit_bundle}/{authkit_version} ({app_bundle}/{app_version})>" - - @property - def client(self) -> str: +def build_client(emulated_device: str = "MacBookPro18,3", emulated_app: str = "accountsd") -> str: """'Client Information' String in the following format: <%MODEL%> <%OS%;%MAJOR%.%MINOR%(%SPMAJOR%,%SPMINOR%);%BUILD%> <%AUTHKIT_BUNDLE_ID%/%AUTHKIT_VERSION% (%APP_BUNDLE_ID%/%APP_VERSION%)> @@ -235,67 +56,117 @@ class Anisette: APP_BUNDLE_ID: The bundle ID of the app (e.g. com.apple.dt.Xcode) APP_VERSION: The version of the app (e.g. 3594.4.19) """ - return self._build_client("iMac11,3", "Xcode") - def generate_headers(self, client_info: bool = False) -> dict: - h = { - # Current Time - "X-Apple-I-Client-Time": self.timestamp, - "X-Apple-I-TimeZone": self.timezone, - # Locale - # Some implementations only use this for locale - "loc": self.locale, - "X-Apple-Locale": self.locale, - # Anisette - "X-Apple-I-MD": self.otp, # 'One Time Password' - # 'Local User ID' - "X-Apple-I-MD-LU": self.local_user, - "X-Apple-I-MD-M": self.machine, # 'Machine ID' - # 'Routing Info', some implementations convert this to an integer - "X-Apple-I-MD-RINFO": self.router, - # Device information - # 'Device Unique Identifier' - "X-Mme-Device-Id": self.device, - # 'Device Serial Number' - "X-Apple-I-SRL-NO": self.serial, - } + model = emulated_device + if emulated_device == "PC": + # We're emulating a PC, so we run Windows (Vista?) + os = "Windows" + os_version = "6.2(0,0);9200" + else: + # We're emulating a Mac, so we run macOS Ventura + os = "Mac OS X" + os_version = "13.4.1;22F8" - # Additional client information only used in some requests - if client_info: - h["X-Mme-Client-Info"] = self.client - h["X-Apple-App-Info"] = "com.apple.gs.xcode.auth" - h["X-Xcode-Version"] = "11.2 (11B41)" + if emulated_app == "Xcode": + app_bundle = "com.apple.dt.Xcode" + app_version = "3594.4.19" + elif emulated_app == "accountsd": + app_bundle = "com.apple.accountsd" + app_version = "113" + else: + app_bundle = "com.apple.iCloud" + app_version = "7.21" - return h + if os == "Windows": + authkit_bundle = "com.apple.AuthKitWin" + authkit_version = "1" + else: + authkit_bundle = "com.apple.AOSKit" + authkit_version = "282" - def generate_cpd(self) -> dict: - cpd = { - # Many of these values are not strictly necessary, but may be tracked by Apple - # I've chosen to match the AltServer implementation - # Not sure what these are for, needs some investigation - "bootstrap": True, # All implementations set this to true - "icscrec": True, # Only AltServer sets this to true - "pbe": False, # All implementations explicitly set this to false - "prkgen": True, # I've also seen ckgen - "svct": "iCloud", # In certian circumstances, this can be 'iTunes' or 'iCloud' - # Not included, but I've also seen: - # 'capp': 'AppStore', - # 'dc': '#d4c5b3', - # 'dec': '#e1e4e3', - # 'prtn': 'ME349', - } + return f"<{model}> <{os};{os_version}> <{authkit_bundle}/{authkit_version} ({app_bundle}/{app_version})>" + +def _generate_cpd() -> dict: + cpd = { + # Many of these values are not strictly necessary, but may be tracked by Apple + # I've chosen to match the AltServer implementation + # Not sure what these are for, needs some investigation + "bootstrap": True, # All implementations set this to true + "icscrec": True, # Only AltServer sets this to true + "pbe": False, # All implementations explicitly set this to false + "prkgen": True, # I've also seen ckgen + "svct": "iCloud", # In certian circumstances, this can be 'iTunes' or 'iCloud' + # Not included, but I've also seen: + # 'capp': 'AppStore', + # 'dc': '#d4c5b3', + # 'dec': '#e1e4e3', + # 'prtn': 'ME349', + } - cpd.update(self.generate_headers()) - return cpd + cpd.update(generate_anisette_headers()) + return cpd +def _generate_meta_headers(serial: str = "0", user_id: uuid = uuid.uuid4(), device_id: uuid = uuid.uuid4()) -> dict: + return { + "X-Apple-I-Client-Time": datetime.utcnow().replace(microsecond=0).isoformat() + "Z", # Current timestamp in ISO 8601 format + "X-Apple-I-TimeZone": str(datetime.utcnow().astimezone().tzinfo), # Abbreviation of the timezone of the device (e.g. EST) -def authenticated_request(parameters, anisette: Anisette) -> dict: + # Locale of the device (e.g. en_US) + "loc": locale.getdefaultlocale()[0] or "en_US", + "X-Apple-Locale": locale.getdefaultlocale()[0] or "en_US", + + "X-Apple-I-MD-RINFO": "17106176", # either 17106176 or 50660608 + + "X-Apple-I-MD-LU": b64encode(str(user_id).upper().encode()).decode(), # 'Local User ID': Base64 encoding of an uppercase UUID + "X-Mme-Device-Id": str(device_id).upper(), # 'Device Unique Identifier', uppercase UUID + "X-Apple-I-SRL-NO": serial, # Serial number + } + +def _generate_local_anisette() -> dict: + print("Using local anisette generation") + """Generates anisette data using AOSKit locally""" + + import objc + from Foundation import NSBundle, NSClassFromString # type: ignore + + AOSKitBundle = NSBundle.bundleWithPath_( + "/System/Library/PrivateFrameworks/AOSKit.framework" + ) + objc.loadBundleFunctions(AOSKitBundle, globals(), [("retrieveOTPHeadersForDSID", b"")]) # type: ignore + util = NSClassFromString("AOSUtilities") + + h = util.retrieveOTPHeadersForDSID_("-2") + + return { + "X-Apple-I-MD": str(h["X-Apple-MD"]), + "X-Apple-I-MD-M": str(h["X-Apple-MD-M"]), + } + +def _generate_remote_anisette(url: str) -> dict: + print("Using remote anisette generation: " + url) + h = json.loads(requests.get(url, timeout=5).text) + return { + "X-Apple-I-MD": h["X-Apple-I-MD"], + "X-Apple-I-MD-M": h["X-Apple-I-MD-M"], + } + +def generate_anisette_headers() -> dict: + if isinstance(ANISETTE, str) and ANISETTE.startswith("http"): + a = _generate_remote_anisette(ANISETTE) + else: + a =_generate_local_anisette() + + a.update(_generate_meta_headers(user_id=USER_ID, device_id=DEVICE_ID)) + return a + + +def authenticated_request(parameters) -> dict: body = { "Header": { "Version": "1.0.1", }, "Request": { - "cpd": anisette.generate_cpd(), + "cpd": _generate_cpd(), }, } body["Request"].update(parameters) @@ -305,11 +176,10 @@ def authenticated_request(parameters, anisette: Anisette) -> dict: "Content-Type": "text/x-xml-plist", "Accept": "*/*", "User-Agent": "akd/1.0 CFNetwork/978.0.7 Darwin/18.7.0", - "X-MMe-Client-Info": anisette.client, + "X-MMe-Client-Info": build_client(emulated_app="Xcode"), } resp = requests.post( - # "https://17.32.194.2/grandslam/GsService2", "https://gsa.apple.com/grandslam/GsService2", headers=headers, data=plist.dumps(body), @@ -361,7 +231,7 @@ def decrypt_cbc(usr: srp.User, data: bytes) -> bytes: return padder.update(data) + padder.finalize() -def trusted_second_factor(dsid, idms_token, anisette: Anisette): +def trusted_second_factor(dsid, idms_token): identity_token = b64encode((dsid + ":" + idms_token).encode()).decode() headers = { @@ -370,10 +240,13 @@ def trusted_second_factor(dsid, idms_token, anisette: Anisette): "Accept": "text/x-xml-plist", "Accept-Language": "en-us", "X-Apple-Identity-Token": identity_token, + "X-Apple-App-Info": "com.apple.gs.xcode.auth", + "X-Xcode-Version": "11.2 (11B41)", + "X-Mme-Client-Info": build_client(emulated_app="Xcode") } - headers.update(anisette.generate_headers(client_info=True)) - + headers.update(generate_anisette_headers()) + # This will trigger the 2FA prompt on trusted devices # We don't care about the response, it's just some HTML with a form for entering the code # Easier to just use a text prompt @@ -403,7 +276,7 @@ def trusted_second_factor(dsid, idms_token, anisette: Anisette): print("2FA successful") -def sms_second_factor(dsid, idms_token, anisette: Anisette): +def sms_second_factor(dsid, idms_token): # TODO: Figure out how to make SMS 2FA work correctly raise NotImplementedError("SMS 2FA is not yet implemented") identity_token = b64encode((dsid + ":" + idms_token).encode()).decode() @@ -415,9 +288,12 @@ def sms_second_factor(dsid, idms_token, anisette: Anisette): "Accept": "application/x-buddyml", "Accept-Language": "en-us", "X-Apple-Identity-Token": identity_token, + "X-Apple-App-Info": "com.apple.gs.xcode.auth", + "X-Xcode-Version": "11.2 (11B41)", + "X-Mme-Client-Info": build_client(emulated_app="Xcode") } - headers.update(anisette.generate_headers(client_info=True)) + headers.update(generate_anisette_headers()) body = {"serverInfo": {"phoneNumber.id": "1"}} @@ -457,7 +333,7 @@ def sms_second_factor(dsid, idms_token, anisette: Anisette): # print("2FA successful") -def authenticate(username, password, anisette: Anisette): +def authenticate(username, password): # Password is None as we'll provide it later usr = srp.User(username, bytes(), hash_alg=srp.SHA256, ng_type=srp.NG_2048) _, A = usr.start_authentication() @@ -469,8 +345,7 @@ def authenticate(username, password, anisette: Anisette): # "ps": ["s2k"], "u": username, "o": "init", - }, - anisette, + } ) # Check for an error code @@ -497,8 +372,7 @@ def authenticate(username, password, anisette: Anisette): "M1": M, "u": username, "o": "complete", - }, - anisette, + } ) if check_error(r): @@ -524,11 +398,11 @@ def authenticate(username, password, anisette: Anisette): for k, v in spd.items(): if isinstance(v, bytes): spd[k] = b64encode(v).decode() - trusted_second_factor(spd["adsid"], spd["GsIdmsToken"], anisette) - return authenticate(username, password, anisette) + trusted_second_factor(spd["adsid"], spd["GsIdmsToken"]) + return authenticate(username, password) elif "au" in r["Status"] and r["Status"]["au"] == "secondaryAuth": print("SMS authentication required") - sms_second_factor(spd["adsid"], spd["GsIdmsToken"], anisette) + sms_second_factor(spd["adsid"], spd["GsIdmsToken"]) elif "au" in r["Status"]: print(f"Unknown auth value {r['Status']['au']}") return