diff --git a/.gitignore b/.gitignore index 6a93e4e..6be3dd6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ config.json IMDAppleServices +pypush/_version.py + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/README.md b/README.md index bc7aa92..1a7d237 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ > [!WARNING] -> Pypush is broken at the moment. We thank you for your continued support of the project! The Pypush demo will not work without significant modification to the code to remediate challenges posed as a response to third party iMessage clients growing in popularity. +> `pypush` is undergoing a major rewrite. The current version is not stable and may not work as expected. # pypush `pypush` is a POC demo of my recent iMessage reverse-engineering. @@ -8,33 +8,18 @@ It can currently register as a new device on an Apple ID, set up encryption keys `pypush` is completely platform-independent, and does not require a Mac or other Apple device to use! ## Installation -It's pretty self explanatory: -1. `git clone https://github.com/JJTech0130/pypush` -2. If on a Mac, ensure `cmake` is installed. Otherwise, run `brew install cmake` -3. `pip3 install -r requirements.txt` -4. `python3 ./demo.py` - -## Troubleshooting -If you have any issues, please join [the Discord](https://discord.gg/BVvNukmfTC) and ask for help. - -## Operation -`pypush` will generate a `config.json` in the repository when you run demo.py. DO NOT SHARE THIS FILE. -It contains all the encryption keys necessary to log into you Apple ID and send iMessages as you. - -Once it loads, it should prompt you with `>>`. Type `help` and press enter for a list of supported commands. - -## Special Notes -### Unicorn dependency -`pypush` currently uses the Unicorn CPU emulator and a custom MachO loader to load a framework from an old version of macOS, -in order to call some obfuscated functions. - -This is only necessary during initial registration, so theoretically you can register on one device, and then copy the `config.json` -to another device that doesn't support the Unicorn emulator. Or you could switch out the emulator for another x86 emulator if you really wanted to. - -## "data.plist" and Mac serial numbers -This repository contains a sample [`data.plist`](https://github.com/JJTech0130/pypush/blob/main/emulated/data.plist), which contains the serial number and several other identifiers from a real Mac device. If you run into issues related to rate-limiting or messages failing to deliver, you may regenerate this file by cloning [nacserver](https://github.com/JJTech0130/nacserver) and running `build.sh` on a non-M1 Mac. It should place the generated file in the current directory, which you can then copy to the emulated/ folder in pypush. +Simple installation: +```bash +pip install git+https://github.com/JJTech0130/pypush +``` +Editable installation (for development): +```bash +git clone https://github.com/JJTech0130/pypush +cd pypush +pip install -e . +``` ## Licensing -This project is licensed under the terms of the [SSPL](https://www.mongodb.com/licensing/server-side-public-license). Portions of this project are based on [macholibre by Aaron Stephens](https://github.com/aaronst/macholibre/blob/master/LICENSE) under the Apache 2.0 license. +This project is licensed under the terms of the [SSPL](https://www.mongodb.com/licensing/server-side-public-license) This project has been purchased by [Beeper](https://github.com/beeper), please contact them with any questions about licensing. diff --git a/apns.py b/apns.py deleted file mode 100644 index 755abf6..0000000 --- a/apns.py +++ /dev/null @@ -1,323 +0,0 @@ -from __future__ import annotations - -import random -import socket -import threading -import time -from hashlib import sha1 -from base64 import b64encode, b64decode -import logging -logger = logging.getLogger("apns") - -import tlslite -if tlslite.__version__ != "0.8.0-alpha43": - logger.warning("tlslite-ng is not the correct version!") - logger.warning("Please install tlslite-ng==0.8.0a43 or you will experience issues!") - -import albert -import bags - -#COURIER_HOST = "windows.courier.push.apple.com" # TODO: Get this from config -# Pick a random courier server from 01 to APNSCourierHostcount -COURIER_HOST = f"{random.randint(1, bags.apns_init_bag()['APNSCourierHostcount'])}-{bags.apns_init_bag()['APNSCourierHostname']}" -COURIER_PORT = 5223 -ALPN = [b"apns-security-v2"] - - -# Connect to the courier server -def _connect(private_key: str, cert: str) -> tlslite.TLSConnection: - # Connect to the courier server - sock = socket.create_connection((COURIER_HOST, COURIER_PORT)) - # Wrap the socket in TLS - sock = tlslite.TLSConnection(sock) - # Parse the certificate and private key - cert = tlslite.X509CertChain([tlslite.X509().parse(cert)]) - private_key = tlslite.parsePEMKey(private_key, private=True) - # Handshake with the server - sock.handshakeClientCert(cert, private_key, alpn=ALPN) - - logger.info(f"Connected to APNs ({COURIER_HOST})") - - return sock - - -class IncomingQueue: - def __init__(self): - self.queue = [] - self.lock = threading.Lock() - - def append(self, item): - with self.lock: - self.queue.append(item) - - def pop(self, index = -1): - with self.lock: - return self.queue.pop(index) - - def __getitem__(self, index): - with self.lock: - return self.queue[index] - - def __len__(self): - with self.lock: - return len(self.queue) - - def find(self, finder): - with self.lock: - return next((i for i in self.queue if finder(i)), None) - - def pop_find(self, finder): - with self.lock: - found = next((i for i in self.queue if finder(i)), None) - if found is not None: - # We have the lock, so we can safely remove it - self.queue.remove(found) - return found - - def remove_all(self, id): - with self.lock: - self.queue = [i for i in self.queue if i[0] != id] - - def wait_pop_find(self, finder, delay=0.1): - found = None - while found is None: - found = self.pop_find(finder) - if found is None: - time.sleep(delay) - return found - - -class APNSConnection: - incoming_queue = IncomingQueue() - - # Sink everything in the queue - def sink(self): - self.incoming_queue = IncomingQueue() - - def _queue_filler(self): - while True and not self.sock.closed: - payload = _deserialize_payload(self.sock) - - if payload is not None: - # Automatically ACK incoming notifications to prevent APNs from getting mad at us - if payload[0] == 0x0A: - logger.debug("Sending automatic ACK") - self._send_ack(_get_field(payload[1], 4)) - logger.debug(f"Received payload: {payload}") - self.incoming_queue.append(payload) - logger.debug(f"Queue length: {len(self.incoming_queue)}") - - def _keep_alive_loop(self): - while True and not self.sock.closed: - time.sleep(300) - self._keep_alive() - - def __init__(self, private_key=None, cert=None): - # Generate the private key and certificate if they're not provided - if private_key is None or cert is None: - logger.debug("APNs needs a new push certificate") - self.private_key, self.cert = albert.generate_push_cert() - else: - self.private_key, self.cert = private_key, cert - - self.sock = _connect(self.private_key, self.cert) - - self.queue_filler_thread = threading.Thread( - target=self._queue_filler, daemon=True - ) - self.queue_filler_thread.start() - - self.keep_alive_thread = threading.Thread( - target=self._keep_alive_loop, daemon=True - ) - self.keep_alive_thread.start() - - - def connect(self, root: bool = True, token: bytes = None): - if token is None: - logger.debug(f"Sending connect message without token (root={root})") - else: - logger.debug(f"Sending connect message with token {b64encode(token).decode()} (root={root})") - flags = 0b01000001 - if root: - flags |= 0b0100 - - if token is None: - payload = _serialize_payload( - 7, [(2, 0x01.to_bytes(1, "big")), (5, flags.to_bytes(4, "big"))] - ) - else: - payload = _serialize_payload( - 7, - [ - (1, token), - (2, 0x01.to_bytes(1, "big")), - (5, flags.to_bytes(4, "big")), - ], - ) - - self.sock.write(payload) - - payload = self.incoming_queue.wait_pop_find(lambda i: i[0] == 8) - - if ( - payload == None - or payload[0] != 8 - or _get_field(payload[1], 1) != 0x00.to_bytes(1, "big") - ): - raise Exception("Failed to connect") - - new_token = _get_field(payload[1], 3) - if new_token is not None: - self.token = new_token - elif token is not None: - self.token = token - else: - raise Exception("No token") - - logger.debug(f"Recieved connect response with token {b64encode(self.token).decode()}") - - return self.token - - def filter(self, topics: list[str]): - logger.debug(f"Sending filter message with topics {topics}") - fields = [(1, self.token)] - - for topic in topics: - fields.append((2, sha1(topic.encode()).digest())) - - payload = _serialize_payload(9, fields) - - self.sock.write(payload) - - def send_message(self, topic: str, payload: str, id=None): - logger.debug(f"Sending message to topic {topic} with payload {payload}") - if id is None: - id = random.randbytes(4) - - payload = _serialize_payload( - 0x0A, - [ - (4, id), - (1, sha1(topic.encode()).digest()), - (2, self.token), - (3, payload), - ], - ) - - self.sock.write(payload) - - # Wait for ACK - payload = self.incoming_queue.wait_pop_find(lambda i: i[0] == 0x0B) - - if payload[1][0][1] != 0x00.to_bytes(1, "big"): - raise Exception("Failed to send message") - - def set_state(self, state: int): - logger.debug(f"Sending state message with state {state}") - self.sock.write( - _serialize_payload( - 0x14, - [(1, state.to_bytes(1, "big")), (2, 0x7FFFFFFF.to_bytes(4, "big"))], - ) - ) - - def _keep_alive(self): - logger.debug("Sending keep alive message") - self.sock.write(_serialize_payload(0x0C, [])) - # Remove any keep alive responses we have or missed - self.incoming_queue.remove_all(0x0D) - - - def _send_ack(self, id: bytes): - logger.debug(f"Sending ACK for message {id}") - payload = _serialize_payload(0x0B, [(1, self.token), (4, id), (8, b"\x00")]) - self.sock.write(payload) - # #self.sock.write(_serialize_payload(0x0B, [(4, id)]) - # #pass - - # def recieve_message(self): - # payload = self.incoming_queue.wait_pop_find(lambda i: i[0] == 0x0A) - # # Send ACK - # self._send_ack(_get_field(payload[1], 4)) - # return _get_field(payload[1], 3) - - # TODO: Find a way to make this non-blocking - # def expect_message(self) -> tuple[int, list[tuple[int, bytes]]] | None: - # return _deserialize_payload(self.sock) - - -def _serialize_field(id: int, value: bytes) -> bytes: - return id.to_bytes(1, "big") + len(value).to_bytes(2, "big") + value - - -def _serialize_payload(id: int, fields: list[(int, bytes)]) -> bytes: - payload = b"" - - for fid, value in fields: - if fid is not None: - payload += _serialize_field(fid, value) - - return id.to_bytes(1, "big") + len(payload).to_bytes(4, "big") + payload - - -def _deserialize_field(stream: bytes) -> tuple[int, bytes]: - id = int.from_bytes(stream[:1], "big") - length = int.from_bytes(stream[1:3], "big") - value = stream[3 : 3 + length] - return id, value - - -# Note: Takes a stream, not a buffer, as we do not know the length of the payload -# WILL BLOCK IF THE STREAM IS EMPTY -def _deserialize_payload(stream) -> tuple[int, list[tuple[int, bytes]]] | None: - id = int.from_bytes(stream.read(1), "big") - - if id == 0x0: - return None - - length = int.from_bytes(stream.read(4), "big") - - buffer = stream.read(length) - - fields = [] - - while len(buffer) > 0: - fid, value = _deserialize_field(buffer) - fields.append((fid, value)) - buffer = buffer[3 + len(value) :] - - return id, fields - - -def _deserialize_payload_from_buffer( - buffer: bytes, -) -> tuple[int, list[tuple[int, bytes]]] | None: - id = int.from_bytes(buffer[:1], "big") - - if id == 0x0: - return None - - length = int.from_bytes(buffer[1:5], "big") - - buffer = buffer[5:] - - if len(buffer) < length: - raise Exception("Buffer is too short") - - fields = [] - - while len(buffer) > 0: - fid, value = _deserialize_field(buffer) - fields.append((fid, value)) - buffer = buffer[3 + len(value) :] - - return id, fields - - -# Returns the value of the first field with the given id -def _get_field(fields: list[tuple[int, bytes]], id: int) -> bytes: - for field_id, value in fields: - if field_id == id: - return value - return None diff --git a/bags.py b/bags.py deleted file mode 100644 index b1468ee..0000000 --- a/bags.py +++ /dev/null @@ -1,86 +0,0 @@ -import plistlib - -import requests -import logging -logger = logging.getLogger("bags") - -OLD_APNS_BAG = None -def apns_init_bag_old(): - global OLD_APNS_BAG - - if OLD_APNS_BAG is not None: - return OLD_APNS_BAG - - r = requests.get("https://init.push.apple.com/bag", verify=False) - if r.status_code != 200: - raise Exception("Failed to get APNs init bag") - - # Parse the config as a plist - bag = plistlib.loads(r.content) - - logger.debug("Received APNs old-style init bag") - - OLD_APNS_BAG = bag - - return bag - - -# This is the same as the above, but the response has a signature which we unwrap -APNS_BAG = None -def apns_init_bag(): - global APNS_BAG - - if APNS_BAG is not None: - return APNS_BAG - - r = requests.get("http://init-p01st.push.apple.com/bag", verify=False) - if r.status_code != 200: - raise Exception("Failed to get APNs init bag 2") - - content = plistlib.loads(r.content) - bag = plistlib.loads(content["bag"]) - - logger.debug("Received APNs new init bag") - - APNS_BAG = bag - - return bag - - -IDS_BAG = None -def ids_bag(): - global IDS_BAG - - if IDS_BAG is not None: - return IDS_BAG - - r = requests.get( - "https://init.ess.apple.com/WebObjects/VCInit.woa/wa/getBag?ix=3", verify=False - ) - if r.status_code != 200: - raise Exception("Failed to get IDS bag") - - # Parse the config as a plist - content = plistlib.loads(r.content) - # Load the inner bag - bag = plistlib.loads(content["bag"]) - - logger.debug("Recieved IDS bag") - - IDS_BAG = bag - - return bag - - -if __name__ == "__main__": - # config = get_config() - # print(config) - # print(apns_init_bag_2()) - # print(apns_init_bag_2() == apns_init_bag()) - bag = ids_bag() - for key in bag: - # print(key) - # print(bag[key]) - if type(bag[key]) == str: - if "http" in bag[key]: - print(key, bag[key]) diff --git a/demo.py b/demo.py deleted file mode 100644 index ca48ad3..0000000 --- a/demo.py +++ /dev/null @@ -1,248 +0,0 @@ -#!/usr/bin/env python - -import json -import logging -import os -import threading -import time -from base64 import b64decode, b64encode -from getpass import getpass - -from rich.logging import RichHandler - -import apns -import ids -import imessage - -logging.basicConfig( - level=logging.NOTSET, format="%(message)s", datefmt="[%X]", handlers=[RichHandler()] -) - -# Set sane log levels -logging.getLogger("urllib3").setLevel(logging.WARNING) -logging.getLogger("py.warnings").setLevel(logging.ERROR) # Ignore warnings from urllib3 -logging.getLogger("asyncio").setLevel(logging.WARNING) -logging.getLogger("jelly").setLevel(logging.INFO) -logging.getLogger("nac").setLevel(logging.INFO) -logging.getLogger("apns").setLevel(logging.INFO) -logging.getLogger("albert").setLevel(logging.INFO) -logging.getLogger("ids").setLevel(logging.DEBUG) -logging.getLogger("bags").setLevel(logging.INFO) -logging.getLogger("imessage").setLevel(logging.DEBUG) - -logging.captureWarnings(True) - -# Try and load config.json -try: - with open("config.json", "r") as f: - CONFIG = json.load(f) -except FileNotFoundError: - CONFIG = {} - -conn = apns.APNSConnection( - CONFIG.get("push", {}).get("key"), CONFIG.get("push", {}).get("cert") -) - - -def safe_b64decode(s): - try: - return b64decode(s) - except: - return None - - -conn.connect(token=safe_b64decode(CONFIG.get("push", {}).get("token"))) -conn.set_state(1) -conn.filter(["com.apple.madrid"]) - -user = ids.IDSUser(conn) - -if CONFIG.get("auth", {}).get("cert") is not None: - auth_keypair = ids._helpers.KeyPair(CONFIG["auth"]["key"], CONFIG["auth"]["cert"]) - user_id = CONFIG["auth"]["user_id"] - handles = CONFIG["auth"]["handles"] - user.restore_authentication(auth_keypair, user_id, handles) -else: - username = input("Username: ") - password = getpass("Password: ") - - user.authenticate(username, password) - -user.encryption_identity = ids.identity.IDSIdentity( - encryption_key=CONFIG.get("encryption", {}).get("rsa_key"), - signing_key=CONFIG.get("encryption", {}).get("ec_key"), -) - -if ( - CONFIG.get("id", {}).get("cert") is not None - and user.encryption_identity is not None -): - id_keypair = ids._helpers.KeyPair(CONFIG["id"]["key"], CONFIG["id"]["cert"]) - user.restore_identity(id_keypair) -else: - logging.info("Registering new identity...") - import emulated.nac - - vd = emulated.nac.generate_validation_data() - vd = b64encode(vd).decode() - - user.register(vd) - -logging.info("Waiting for incoming messages...") - -# Write config.json -CONFIG["encryption"] = { - "rsa_key": user.encryption_identity.encryption_key, - "ec_key": user.encryption_identity.signing_key, -} -CONFIG["id"] = { - "key": user._id_keypair.key, - "cert": user._id_keypair.cert, -} -CONFIG["auth"] = { - "key": user._auth_keypair.key, - "cert": user._auth_keypair.cert, - "user_id": user.user_id, - "handles": user.handles, -} -CONFIG["push"] = { - "token": b64encode(user.push_connection.token).decode(), - "key": user.push_connection.private_key, - "cert": user.push_connection.cert, -} - -with open("config.json", "w") as f: - json.dump(CONFIG, f, indent=4) - -im = imessage.iMessageUser(conn, user) - -INPUT_QUEUE = apns.IncomingQueue() - -def input_thread(): - from prompt_toolkit import prompt - while True: - - try: - msg = prompt('>> ') - except: - msg = 'quit' - INPUT_QUEUE.append(msg) - -threading.Thread(target=input_thread, daemon=True).start() - -print("Type 'help' for help") - -def fixup_handle(handle): - if handle.startswith('tel:+'): - return handle - elif handle.startswith('mailto:'): - return handle - elif handle.startswith('tel:'): - return 'tel:+' + handle[4:] - elif handle.startswith('+'): - return 'tel:' + handle - # If the handle starts with a number - elif handle[0].isdigit(): - # If the handle is 10 digits, assume it's a US number - if len(handle) == 10: - return 'tel:+1' + handle - # If the handle is 11 digits, assume it's a US number with country code - elif len(handle) == 11: - return 'tel:+' + handle - else: # Assume it's an email - return 'mailto:' + handle - -current_participants = [] -current_effect = None -while True: - msg = im.receive() - if msg is not None: - # print(f'[{msg.sender}] {msg.text}') - print(msg.to_string()) - - attachments = msg.attachments() - if len(attachments) > 0: - attachments_path = f"attachments/{msg.id}/" - os.makedirs(attachments_path, exist_ok=True) - - for attachment in attachments: - with open(attachments_path + attachment.name, "wb") as attachment_file: - attachment_file.write(attachment.versions[0].data()) - - print(f"({len(attachments)} attachment{'s have' if len(attachments) != 1 else ' has'} been downloaded and put " - f"in {attachments_path})") - - if len(INPUT_QUEUE) > 0: - msg = INPUT_QUEUE.pop() - if msg == '': continue - if msg == 'help' or msg == 'h': - print('help (h): show this message') - print('quit (q): quit') - #print('send (s) [recipient] [message]: send a message') - print('filter (f) [recipient]: set the current chat') - print('effect (e): adds an iMessage effect to the next sent message') - print('note: recipient must start with tel: or mailto: and include the country code') - print('handle : set the current handle (for sending messages)') - print('\\: escape commands (will be removed from message)') - elif msg == 'quit' or msg == 'q': - break - elif msg == 'effect' or msg == 'e' or msg.startswith("effect ") or msg.startswith("e "): - msg = msg.split(" ") - if len(msg) < 2 or msg[1] == "": - print("effect [effect namespace]") - else: - print(f"next message will be sent with [{msg[1]}]") - current_effect = msg[1] - elif msg == 'filter' or msg == 'f' or msg.startswith('filter ') or msg.startswith('f '): - # Set the curernt chat - msg = msg.split(' ') - if len(msg) < 2 or msg[1] == '': - print('filter [recipients]') - else: - print(f'Filtering to {[fixup_handle(h) for h in msg[1:]]}') - current_participants = [fixup_handle(h) for h in msg[1:]] - elif msg == 'handle' or msg.startswith('handle '): - msg = msg.split(' ') - if len(msg) < 2 or msg[1] == '': - print('handle [handle]') - print('Available handles:') - for h in user.handles: - if h == user.current_handle: - print(f'\t{h} (current)') - else: - print(f'\t{h}') - else: - h = msg[1] - h = fixup_handle(h) - if h in user.handles: - print(f'Using {h} as handle') - user.current_handle = h - else: - print(f'Handle {h} not found') - - elif current_participants != []: - if msg.startswith('\\'): - msg = msg[1:] - im.send(imessage.iMessage( - text=msg, - participants=current_participants, - sender=user.current_handle, - effect=current_effect - )) - current_effect = None - else: - print('No chat selected, use help for help') - - time.sleep(0.1) - - # elif msg.startswith('send') or msg.startswith('s'): - # msg = msg.split(' ') - # if len(msg) < 3: - # print('send [recipient] [message]') - # else: - # im.send(imessage.iMessage( - # text=' '.join(msg[2:]), - # participants=[msg[1], user.handles[0]], - # #sender=user.handles[0] - # )) - diff --git a/development/demo.py b/development/demo.py deleted file mode 100644 index 730ae9e..0000000 --- a/development/demo.py +++ /dev/null @@ -1,17 +0,0 @@ -import apns -import ids - -conn1 = apns.APNSConnection() -conn1.connect() - -# Uncomment these for greater parity with apsd -# conn1.keep_alive() -# conn1.set_state(0x01) -# conn1.filter([]) -# conn1.connect(False) - -conn1.filter(["com.apple.madrid"]) - -# print(ids.lookup(conn1, ["mailto:jjtech@jjtech.dev"])) - -#print(ids.register(conn1, "user_test2@icloud.com", "wowSecure1")) diff --git a/development/printer.py b/development/printer.py deleted file mode 100644 index 46711e6..0000000 --- a/development/printer.py +++ /dev/null @@ -1,296 +0,0 @@ -import plistlib -import zlib -from base64 import b64decode, b64encode -from hashlib import sha1 - -# Taken from debug logs of apsd -enabled_topics = "(com.apple.icloud-container.com.apple.avatarsd, com.icloud.askpermission, com.apple.icloud-container.com.apple.Safari, com.apple.itunesstored, com.apple.icloud-container.clouddocs.com.apple.CloudDocs.health, com.apple.passd.usernotifications, com.apple.icloud-container.com.apple.donotdisturbd, com.apple.icloud-container.clouddocs.iCloud.com.reddit.reddit, com.apple.mobileme.fmf3, com.apple.icloud-container.com.apple.cloudpaird, com.apple.icloud-container.clouddocs.com.apple.Pages, com.apple.appstored-testflight, com.apple.askpermissiond, com.apple.icloud-container.com.apple.willowd, com.me.cal, com.apple.icloud-container.com.apple.suggestd, com.apple.icloud-container.clouddocs.F3LWYJ7GM7.com.apple.garageband10, com.apple.icloud-container.clouddocs.com.apple.CloudDocs.container-metadata, com.apple.icloud-container.com.apple.callhistory.sync-helper, com.apple.icloud-container.com.apple.syncdefaultsd, com.apple.icloud-container.com.apple.SafariShared.Settings, com.apple.pay.services.products.prod, com.apple.icloud-container.com.apple.StatusKitAgent, com.apple.icloud-container.com.apple.siriknowledged, com.me.contacts, com.apple.icloud-container.com.apple.TrustedPeersHelper, com.apple.icloud-container.clouddocs.iCloud.com.apple.iBooks, com.apple.icloud-container.clouddocs.iCloud.dk.simonbs.Scriptable, com.apple.icloud-container.clouddocs.com.apple.ScriptEditor2, com.icloud.family, com.apple.idmsauth, com.apple.watchList, com.apple.icloud-container.clouddocs.com.apple.TextEdit, com.apple.icloud-container.com.apple.VoiceMemos, com.apple.sharedstreams, com.apple.pay.services.apply.prod, com.apple.icloud-container.com.apple.SafariShared.CloudTabs, com.apple.wallet.sharing.qa, com.apple.appstored, com.apple.icloud-container.clouddocs.3L68KQB4HG.com.readdle.CommonDocuments, com.apple.icloud-container.clouddocs.com.apple.CloudDocs.pp-metadata, com.me.setupservice, com.apple.icloud-container.com.apple.amsengagementd, com.apple.icloud-container.com.apple.appleaccount.beneficiary.private, com.apple.icloud-container.com.apple.appleaccount.beneficiary, com.apple.icloud-container.clouddocs.com.apple.mail, com.apple.icloud-container.com.apple.appleaccount.custodian, com.apple.icloud-container.com.apple.securityd, com.apple.icloud-container.com.apple.iBooksX, com.apple.icloud-container.clouddocs.com.apple.QuickTimePlayerX, com.apple.icloud-container.clouddocs.com.apple.TextInput, com.apple.icloud-container.com.apple.icloud.fmfd, com.apple.tv.favoriteTeams, com.apple.pay.services.ownershipTokens.prod, com.apple.icloud-container.com.apple.passd, com.apple.amsaccountsd, com.apple.pay.services.devicecheckin.prod.us, com.apple.storekit, com.apple.icloud-container.com.apple.keyboardservicesd, paymentpass.com.apple, com.apple.aa.setupservice, com.apple.icloud-container.clouddocs.com.apple.shoebox, com.apple.icloud-container.clouddocs.F3LWYJ7GM7.com.apple.mobilegarageband, com.apple.icloud-container.com.apple.icloud.searchpartyuseragent, com.apple.icloud-container.clouddocs.iCloud.com.apple.configurator.ui, com.apple.icloud-container.com.apple.gamed, com.apple.icloud-container.clouddocs.com.apple.Keynote, com.apple.icloud-container.com.apple.willowd.homekit, com.apple.amsengagementd.notifications, com.apple.icloud.presence.mode.status, com.apple.aa.idms, com.apple.icloud-container.clouddocs.iCloud.com.apple.MobileSMS, com.apple.gamed, com.apple.icloud-container.clouddocs.iCloud.is.workflow.my.workflows, com.apple.icloud-container.clouddocs.iCloud.md.obsidian, com.apple.icloud-container.clouddocs.com.apple.CloudDocs, com.apple.wallet.sharing, com.apple.icloud-container.clouddocs.iCloud.com.apple.iBooks.iTunesU, com.apple.icloud.presence.shared.experience, com.apple.icloud-container.com.apple.imagent, com.apple.icloud-container.com.apple.financed, com.apple.pay.services.account.prod, com.apple.icloud-container.com.apple.assistant.assistantd, com.apple.pay.services.ck.zone.prod, com.apple.icloud-container.com.apple.security.cuttlefish, com.apple.icloud-container.clouddocs.com.apple.iBooks.cloudData, com.apple.peerpayment, com.icloud.quota, com.apple.pay.provision, com.apple.icloud-container.com.apple.upload-request-proxy.com.apple.photos.cloud, com.apple.icloud-container.com.apple.appleaccount.custodian.private, com.apple.icloud-container.clouddocs.com.apple.Preview, com.apple.maps.icloud, com.apple.icloud-container.com.apple.reminders, com.apple.icloud-container.com.apple.SafariShared.WBSCloudBookmarksStore, com.apple.idmsauthagent, com.apple.icloud-container.clouddocs.com.apple.Numbers, com.apple.bookassetd, com.apple.pay.auxiliary.registration.requirement.prod, com.apple.icloud.fmip.voiceassistantsync)" -opportunistic_topics = "(com.apple.madrid, com.apple.icloud-container.com.apple.photos.cloud, com.apple.private.alloy.ded, com.apple.private.ac, com.apple.private.alloy.coreduet.sync, com.apple.private.alloy.sms, com.apple.private.alloy.ids.cloudmessaging, com.apple.private.alloy.maps, com.apple.private.alloy.facetime.mw, com.apple.private.alloy.facetime.sync, com.apple.private.alloy.maps.eta, com.apple.private.alloy.thumper.keys, com.apple.private.alloy.phonecontinuity, com.apple.private.alloy.continuity.tethering, com.apple.private.alloy.biz, com.apple.private.alloy.tips, com.apple.private.alloy.keytransparency.accountkey.pinning, com.apple.private.alloy.nearby, com.apple.private.alloy.itunes, com.apple.private.alloy.status.keysharing, com.apple.private.alloy.facetime.video, com.apple.private.alloy.screentime.invite, com.apple.private.alloy.proxiedcrashcopier.icloud, com.apple.private.alloy.classroom, com.apple.private.alloy.carmelsync, com.apple.ess, com.apple.private.alloy.facetime.lp, com.apple.private.alloy.icloudpairing, com.apple.private.alloy.accounts.representative, com.apple.private.alloy.gamecenter.imessage, com.apple.private.alloy.photostream, com.apple.private.alloy.electrictouch, com.apple.private.alloy.messagenotification, com.apple.private.alloy.avconference.icloud, com.apple.private.alloy.fmd, com.apple.private.alloy.usagetracking, com.apple.private.alloy.fmf, com.apple.private.alloy.home.invite, com.apple.private.alloy.phone.auth, com.apple.private.alloy.quickrelay, com.apple.private.alloy.copresence, com.apple.private.alloy.home, com.apple.private.alloy.digitalhealth, com.apple.private.alloy.multiplex1, com.apple.private.alloy.screensharing.qr, com.apple.private.alloy.contextsync, com.apple.private.alloy.facetime.audio, com.apple.private.alloy.willow.stream, com.apple.private.ids, com.apple.private.alloy.continuity.activity, com.apple.private.alloy.gamecenter, com.apple.private.alloy.clockface.sharing, com.apple.private.alloy.safeview, com.apple.private.alloy.continuity.unlock, com.apple.private.alloy.continuity.encryption, com.apple.private.alloy.facetime.multi, com.apple.private.alloy.notes, com.apple.private.alloy.screentime, com.apple.private.alloy.willow, com.apple.private.alloy.accessibility.switchcontrol, com.apple.private.alloy.status.personal, com.apple.triald, com.apple.private.alloy.screensharing, com.apple.private.alloy.gelato, com.apple.private.alloy.safari.groupactivities, com.apple.private.alloy.applepay, com.apple.private.alloy.amp.potluck, com.apple.private.alloy.sleep.icloud, com.apple.icloud-container.com.apple.knowledge-agent)" -paused_topics = "(com.apple.icloud-container.company.thebrowser.Browser, com.apple.icloud-container.com.apple.sociallayerd, com.apple.icloud.fmip.app.push, com.apple.iBooksX, company.thebrowser.Browser, com.apple.icloud-container.com.apple.Maps, com.apple.mobileme.fmf2, com.apple.findmy, com.apple.iWork.Numbers, com.apple.jalisco, com.apple.iWork.Pages, com.apple.Notes, com.apple.Maps, com.apple.icloud-container.com.apple.Notes, com.apple.dt.Xcode, com.apple.sagad, com.apple.icloud.presence.channel.management, com.apple.icloud-container.com.apple.protectedcloudstorage.protectedcloudkeysyncing, com.apple.TestFlight, com.apple.icloud-container.com.apple.news, com.apple.music.social, com.apple.icloud-container.com.apple.iWork.Numbers, com.apple.news, com.apple.tilt, com.apple.icloud-container.com.apple.findmy, com.apple.icloud-container.com.apple.iWork.Pages)" - -# Parse the topics into a list -enabled_topics = enabled_topics[1:-1].split(", ") -opportunistic_topics = opportunistic_topics[1:-1].split(", ") -paused_topics = paused_topics[1:-1].split(", ") - -topics = enabled_topics + opportunistic_topics + paused_topics - -# Calculate the SHA1 hash of each topic -topics_lookup = [(topic, sha1(topic.encode()).digest()) for topic in topics] - - -class bcolors: - HEADER = "\033[95m" - OKBLUE = "\033[94m" - OKCYAN = "\033[96m" - OKGREEN = "\033[92m" - WARNING = "\033[93m" - FAIL = "\033[91m" - ENDC = "\033[0m" - BOLD = "\033[1m" - UNDERLINE = "\033[4m" - - -def _lookup_topic(hash: bytes): - for topic_lookup in topics_lookup: - if topic_lookup[1] == hash: - return topic_lookup[0] - return None - - -# Returns the value of the first field with the given id -def _get_field(fields: list[tuple[int, bytes]], id: int) -> bytes: - for field_id, value in fields: - if field_id == id: - return value - return None - - -def _p_filter(prefix, fields: list[tuple[int, bytes]]): - enabled = [] - ignored = [] - oppertunistic = [] - paused = [] - - token = "" - - for field in fields: - if field[0] == 1: - token = b64encode(field[1]) - # print(f"Push Token: {b64encode(field[1])}") - elif field[0] == 2: - enabled.append(_lookup_topic(field[1])) - elif field[0] == 3: - ignored.append(_lookup_topic(field[1])) - elif field[0] == 4: - oppertunistic.append(_lookup_topic(field[1])) - elif field[0] == 5: - paused.append(_lookup_topic(field[1])) - else: - pass # whatever, there's a 6 but it's not documented - # print(f"Unknown field ID: {field[0]}") - - # Remove None values - enabled = [topic.strip() for topic in enabled if topic is not None] - ignored = [topic.strip() for topic in ignored if topic is not None] - oppertunistic = [topic.strip() for topic in oppertunistic if topic is not None] - paused = [topic.strip() for topic in paused if topic is not None] - - enabled = ", ".join(enabled) - ignored = ", ".join(ignored) - oppertunistic = ", ".join(oppertunistic) - paused = ", ".join(paused) - - if not enabled: - enabled = "None" - if not ignored: - ignored = "None" - if not oppertunistic: - oppertunistic = "None" - if not paused: - paused = "None" - - # Trim the list of topics - if len(enabled) > 100: - enabled = enabled[:100] + "..." - if len(ignored) > 100: - ignored = ignored[:100] + "..." - if len(oppertunistic) > 100: - oppertunistic = oppertunistic[:100] + "..." - if len(paused) > 100: - paused = paused[:100] + "..." - # (Token: {token.decode()}) - print( - f"{bcolors.OKGREEN}{prefix}{bcolors.ENDC}: {bcolors.OKCYAN}Filter{bcolors.ENDC} {bcolors.WARNING}Enabled{bcolors.ENDC}: {enabled} {bcolors.FAIL}Ignored{bcolors.ENDC}: {ignored} {bcolors.OKBLUE}Oppertunistic{bcolors.ENDC}: {oppertunistic} {bcolors.OKGREEN}Paused{bcolors.ENDC}: {paused}" - ) - - -import apns - - -def pretty_print_payload( - prefix, payload: tuple[int, list[tuple[int, bytes]]] -) -> bytes | None: - id = payload[0] - - if id == 9: - _p_filter(prefix, payload[1]) - elif id == 8: - token_str = "" - if _get_field(payload[1], 3): - token_str = f"{bcolors.WARNING}Token{bcolors.ENDC}: {b64encode(_get_field(payload[1], 3)).decode()}" - print( - f"{bcolors.OKGREEN}{prefix}{bcolors.ENDC}: {bcolors.OKCYAN}Connected{bcolors.ENDC} {token_str} {bcolors.OKBLUE}{_get_field(payload[1], 1).hex()}{bcolors.ENDC}" - ) - elif id == 7: - print( - f"{bcolors.OKGREEN}{prefix}{bcolors.ENDC}: {bcolors.OKCYAN}Connect Request{bcolors.ENDC}", - end="", - ) - if _get_field(payload[1], 1): - print( - f" {bcolors.WARNING}Token{bcolors.ENDC}: {b64encode(_get_field(payload[1], 1)).decode()}", - end="", - ) - if _get_field(payload[1], 0x0C): - print(f" {bcolors.OKBLUE}SIGNED{bcolors.ENDC}", end="") - if ( - _get_field(payload[1], 0x5) - and int.from_bytes(_get_field(payload[1], 0x5)) & 0x4 - ): - print(f" {bcolors.FAIL}ROOT{bcolors.ENDC}", end="") - print() - - # for field in payload[1]: - # print(f"Field ID: {field[0]}") - # print(f"Field Value: {field[1]}") - - # 65 (user) or 69 (root) - - for i in range(len(payload[1])): - # if payload[1][i][0] == 5: - # if payload[1][i][1] == b'\x00\x00\x00A': # user - # payload[1][i][1] = b'\x00\x00\x00E' - # elif payload[1][i][1] == b'\x00\x00\x00E': # root - # payload[1][i][1] = b'\x00\x00\x00A' - # else: - # print("Unknown field value: ", payload[1][i][1]) - if payload[1][i][0] == 1: - pass - # payload[1][i] = (None, None) - # payload[1][i] = (1, b64decode("D3MtN3e18QE8rve3n92wp+CwK7u/bWk/5WjQUOBN640=")) - - out = apns._serialize_payload(payload[0], payload[1]) - # return out - elif id == 0xC: - print( - f"{bcolors.OKGREEN}{prefix}{bcolors.ENDC}: {bcolors.OKCYAN}Keep Alive{bcolors.ENDC}" - ) - elif id == 0xD: - print( - f"{bcolors.OKGREEN}{prefix}{bcolors.ENDC}: {bcolors.OKCYAN}Keep Alive Ack{bcolors.ENDC}" - ) - elif id == 0x14: - print( - f"{bcolors.OKGREEN}{prefix}{bcolors.ENDC}: {bcolors.OKCYAN}Set State{bcolors.ENDC}: {_get_field(payload[1], 1).hex()}" - ) - elif id == 0x1D or id == 0x20: - print( - f"{bcolors.OKGREEN}{prefix}{bcolors.ENDC}: {bcolors.WARNING}PubSub ??{bcolors.ENDC}" - ) - elif id == 0xE: - print( - f"{bcolors.OKGREEN}{prefix}{bcolors.ENDC}: {bcolors.WARNING}Token Confirmation{bcolors.ENDC}" - ) - elif id == 0xA: - topic = "" - # topic = _lookup_topic(_get_field(payload[1], 1)) - # if it has apsd -> APNs in the prefix, it's an outgoing notification - if "apsd -> APNs" in prefix: - print( - f"{bcolors.OKGREEN}{prefix}{bcolors.ENDC}: {bcolors.OKBLUE}OUTGOING Notification{bcolors.ENDC}", - end="", - ) - topic = _lookup_topic(_get_field(payload[1], 1)) - # topic = _lookup_topic(_get_field(payload[1], 1)) - # if b"bplist" in _get_field(payload[1], 3): - # print(f" {bcolors.OKCYAN}Binary{bcolors.ENDC}", end="") - # if topic == "com.apple.madrid": - # print(f" {bcolors.FAIL}Madrid{bcolors.ENDC}", end="") - # import plistlib - # plist = plistlib.loads(_get_field(payload[1], 3)) - # #payload = plist["P"] - # #print(f" {bcolors.WARNING}Payload{bcolors.ENDC}: {payload}", end="") - - # for key in plist: - # print(f" {bcolors.OKBLUE}{key}{bcolors.ENDC}: {plist[key]}", end="") - - else: - print( - f"{bcolors.OKGREEN}{prefix}{bcolors.ENDC}: {bcolors.OKCYAN}Notification{bcolors.ENDC}", - end="", - ) - topic = _lookup_topic(_get_field(payload[1], 2)) - # if b"bplist" in _get_field(payload[1], 3): - # print(f" {bcolors.OKBLUE}Binary{bcolors.ENDC}", end="") - # print(f" {bcolors.WARNING}Topic{bcolors.ENDC}: {_lookup_topic(_get_field(payload[1], 2))}") - - print(f" {bcolors.WARNING}Topic{bcolors.ENDC}: {topic}", end="") - - if topic == "com.apple.madrid": - print(f" {bcolors.FAIL}Madrid{bcolors.ENDC}", end="") - orig_payload = payload - payload = plistlib.loads(_get_field(payload[1], 3)) - - # print(payload) - if "cT" in payload and False: - # It's HTTP over APNs - if "hs" in payload: - print( - f" {bcolors.WARNING}HTTP Response{bcolors.ENDC}: {payload['hs']}", - end="", - ) - else: - print(f" {bcolors.WARNING}HTTP Request{bcolors.ENDC}", end="") - # print(f" {bcolors.WARNING}HTTP{bcolors.ENDC} {payload['hs']}", end="") - if "u" in payload: - print(f" {bcolors.OKCYAN}URL{bcolors.ENDC}: {payload['u']}", end="") - print( - f" {bcolors.FAIL}Content Type{bcolors.ENDC}: {payload['cT']}", - end="", - ) - if "h" in payload: - print( - f" {bcolors.FAIL}Headers{bcolors.ENDC}: {payload['h']}", end="" - ) - if "b" in payload: - # What am I really supposed to put in WBITS? Got this from a random SO answer - # print(payload["b"]) - body = zlib.decompress(payload["b"], 16 + zlib.MAX_WBITS) - if b"plist" in body: - body = plistlib.loads(body) - print(f" {bcolors.FAIL}Body{bcolors.ENDC}: {body}", end="") - #if not "cT" in payload: - for key in payload: - print(f" {bcolors.OKBLUE}{key}{bcolors.ENDC}: {payload[key]}") - - if 'dtl' in payload: - print("OVERRIDE DTL") - payload['dtl'][0].update({'sT': b64decode("jJ86jTYbv1mGVwO44PyfuZ9lh3o56QjOE39Jk8Z99N8=")}) - - # Re-serialize the payload - payload = plistlib.dumps(payload, fmt=plistlib.FMT_BINARY) - # Construct APNS message - # Get the original fields except 3 - fields = orig_payload[1] - fields = [field for field in fields if field[0] != 3] - # Add the new field - fields.append((3, payload)) - payload = apns._serialize_payload(0xA, fields) - - # Use the override payload - - #print(payload, orig_payload) - #print(payload == orig_payload) - return payload - - print() - - # print(f" {bcolors.WARNING}{bcolors.ENDC}: {payload['cT']}") - - # for field in payload[1]: - # print(f"Field ID: {field[0]}") - # print(f"Field Value: {field[1]}") - elif id == 0xB: - print( - f"{bcolors.OKGREEN}{prefix}{bcolors.ENDC}: {bcolors.OKCYAN}Notification Ack{bcolors.ENDC} {bcolors.OKBLUE}{_get_field(payload[1], 8).hex()}{bcolors.ENDC}" - ) - else: - print(prefix, f"Payload ID: {hex(payload[0])}") - for field in payload[1]: - print(f"Field ID: {field[0]}") - print(f"Field Value: {field[1]}") - - -if __name__ == "__main__": - print(f"{bcolors.OKGREEN}Enabled:{bcolors.ENDC}") diff --git a/development/proxy/generate.sh b/development/proxy/generate.sh deleted file mode 100755 index c06eb7a..0000000 --- a/development/proxy/generate.sh +++ /dev/null @@ -1,32 +0,0 @@ -set -e -# Use brew's openssl -export PATH="/opt/homebrew/opt/openssl@3/bin:$PATH" - -openssl req -newkey rsa:2048 -nodes -keyout root_key.pem -x509 -days 3650 -out root_certificate.pem \ - -subj "/C=US/O=Apple Inc./OU=Apple Certification Authority/CN=Apple Root CA" \ - -addext "basicConstraints=critical, CA:true" -addext "keyUsage=critical, digitalSignature, keyCertSign, cRLSign" - -openssl req -newkey rsa:2048 -nodes -keyout intermediate_key.pem -out intermediate_certificate.csr \ - -subj "/CN=Apple Server Authentication CA/OU=Certification Authority/O=Apple Inc./C=US" \ - -addext "basicConstraints=critical, CA:true" -addext "keyUsage=critical, keyCertSign, cRLSign" - # Need 1.2.840.113635.100.6.2.12? - -openssl x509 -req -CAkey root_key.pem -CA root_certificate.pem -days 3650 \ - -in intermediate_certificate.csr -out intermediate_certificate.pem -CAcreateserial -copy_extensions copyall - -openssl req -newkey rsa:2048 -nodes -keyout push_key.pem -out push_certificate.csr \ - -subj "/CN=courier.push.apple.com/O=Apple Inc./ST=California/C=US" \ - -addext "basicConstraints=critical, CA:false" \ - -addext "subjectAltName = DNS:courier.push.apple.com, DNS:courier2.push.apple.com" \ - -addext "keyUsage = critical, digitalSignature, keyEncipherment" \ - -addext "extendedKeyUsage = serverAuth" - -openssl x509 -req -CAkey intermediate_key.pem -CA intermediate_certificate.pem -days 365 \ - -in push_certificate.csr -out push_certificate.pem -CAcreateserial -copy_extensions copyall - -cat push_certificate.pem intermediate_certificate.pem root_certificate.pem > push_certificate_chain.pem - -# Remove the leftover files -rm intermediate_certificate.csr intermediate_certificate.pem intermediate_key.pem intermediate_certificate.srl -rm push_certificate.csr push_certificate.pem -rm root_certificate.pem root_key.pem root_certificate.srl \ No newline at end of file diff --git a/development/proxy/hosts.py b/development/proxy/hosts.py deleted file mode 100644 index c998dec..0000000 --- a/development/proxy/hosts.py +++ /dev/null @@ -1,5 +0,0 @@ -# Print out the additions to /etc/hosts for the proxy - -for i in range(1, 50): - print(f"127.0.0.1 {i}-courier.push.apple.com") - print(f"127.0.0.1 {i}.courier.push.apple.com") diff --git a/development/proxy/proxy.py b/development/proxy/proxy.py deleted file mode 100644 index 0ca16ac..0000000 --- a/development/proxy/proxy.py +++ /dev/null @@ -1,197 +0,0 @@ -# TLS server to proxy APNs traffic - -import socket -import sys -import threading - -import tlslite - -# setting path -sys.path.append("../") -sys.path.append("../../") - -# APNs server to proxy traffic to -APNS_HOST = "windows.courier.push.apple.com" -APNS_PORT = 5223 -ALPN = b"apns-security-v3" -# ALPN = b"apns-security-v2" -# ALPN = b"apns-pack-v1" - -global_cnt = 0 - - -# Connect to the APNs server -def connect() -> tlslite.TLSConnection: - # Connect to the APNs server - sock = socket.create_connection((APNS_HOST, APNS_PORT)) - # Wrap the socket in TLS - ssock = tlslite.TLSConnection(sock) - # print("Handshaking with APNs") - # Handshake with the server - if ALPN == b"apns-security-v3": - print("Using v3") - ssock.handshakeClientCert(alpn=[ALPN]) - else: - import albert - - private_key, cert = albert.generate_push_cert() - cert = tlslite.X509CertChain([tlslite.X509().parse(cert)]) - private_key = tlslite.parsePEMKey(private_key, private=True) - # Handshake with the server - ssock.handshakeClientCert(cert, private_key, alpn=[ALPN]) - - return ssock - - -cert: str = None -key: str = None - - -import printer - -import apns - -outgoing_list = [] -incoming_list = [] -# last_outgoing = b"" - - -def proxy(conn1: tlslite.TLSConnection, conn2: tlslite.TLSConnection, prefix: str = ""): - try: - while True: - # Read data from the first connection - data = conn1.read() - # print(prefix, "data: ", data) - # If there is no data, the connection has closed - if not data: - print(prefix, "Connection closed due to no data") - break - - try: - override = printer.pretty_print_payload( - prefix, apns._deserialize_payload_from_buffer(data) - ) - except Exception as e: - print(e) # Can't crash the proxy over parsing errors - if override is not None: - data = override - print("OVERRIDE: ", end="") - printer.pretty_print_payload( - prefix, apns._deserialize_payload_from_buffer(data) - ) - - if "apsd -> APNs" in prefix: - global outgoing_list - outgoing_list.insert(0, data) - if len(outgoing_list) > 100: - outgoing_list.pop() - elif "APNs -> apsd" in prefix: - global incoming_list - incoming_list.insert(0, data) - if len(incoming_list) > 100: - incoming_list.pop() - - # print(prefix, data) - # Write the data to the second connection - conn2.write(data) - except OSError as e: - if e.errno == 9: - print(prefix, "Connection closed due to OSError 9") - pass # Probably a connection closed error - else: - raise e - except tlslite.TLSAbruptCloseError as e: - print(prefix, "Connection closed abruptly: ", e) - print("Connection closed") - # Close the connections - conn1.close() - conn2.close() - - -repl_lock = False - - -def repl(conn1: tlslite.TLSConnection, conn2: tlslite.TLSConnection): - global repl_lock - if repl_lock: - print("REPL already running") - return - repl_lock = True - import IPython - - IPython.embed() - - -def handle(conn: socket.socket): - # Wrap the socket in TLS - s_conn = tlslite.TLSConnection(conn) - global cert, key - chain = tlslite.X509CertChain() - chain.parsePemList(cert) - # print(chain) - # cert = tlslite.X509CertChain([tlslite.X509().parse(cert)]) - key_parsed = tlslite.parsePEMKey(key, private=True) - # print(key_parsed) - s_conn.handshakeServer( - certChain=chain, privateKey=key_parsed, reqCert=False, alpn=[ALPN] - ) - - print("Handling connection") - # Connect to the APNs server - apns = connect() - print("Connected to APNs") - - threading.Thread(target=repl, args=(s_conn, apns)).start() - - global global_cnt - global_cnt += 1 - # Proxy data between the connections - # Create a thread to proxy data from the APNs server to the client - threading.Thread( - target=proxy, args=(s_conn, apns, f"{global_cnt} apsd -> APNs") - ).start() - # Just proxy data from the client to the APNs server in this thread - proxy(apns, s_conn, f"{global_cnt} APNs -> apsd") - - -def serve(): - # Create a socket to listen for connections - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - # Allow the socket to be reused - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.bind(("localhost", 5223)) - sock.listen() - - print("Listening for connections...") - - # Handshake with the client - # Read the certificate and private key from the config - with open("push_certificate_chain.pem", "r") as f: - global cert - cert = f.read() - - # NEED TO USE OPENSSL, SEE CORETRUST CMD, MIMIC ENTRUST? OR AT LEAST SEE PUSHPROXY FOR EXTRACTION & REPLACEMENT - with open("push_key.pem", "r") as f: - global key - key = f.read() - - conns = [] - # Accept connections - try: - while True: - # Accept a connection - conn, addr = sock.accept() - conns.append(conn) - # Create a thread to handle the connection - # handle(conn) - thread = threading.Thread(target=handle, args=(conn,)) - thread.start() - except KeyboardInterrupt: - print("Keyboard interrupt, closing sockets") - for conn in conns: - conn.close() - sock.close() - - -if __name__ == "__main__": - serve() diff --git a/development/proxy/push_certificate_chain.pem b/development/proxy/push_certificate_chain.pem deleted file mode 100644 index 1ddfc2c..0000000 --- a/development/proxy/push_certificate_chain.pem +++ /dev/null @@ -1,69 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEBjCCAu6gAwIBAgIULoswBSnhsTUwq4AYoAdnf46gKa0wDQYJKoZIhvcNAQEL -BQAwbTEnMCUGA1UEAwweQXBwbGUgU2VydmVyIEF1dGhlbnRpY2F0aW9uIENBMSAw -HgYDVQQLDBdDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUg -SW5jLjELMAkGA1UEBhMCVVMwHhcNMjMwNDA3MDU0NjUwWhcNMjQwNDA2MDU0NjUw -WjBYMR8wHQYDVQQDDBZjb3VyaWVyLnB1c2guYXBwbGUuY29tMRMwEQYDVQQKDApB -cHBsZSBJbmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMQswCQYDVQQGEwJVUzCCASIw -DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALMnRj3/NBqeFDE+ZYALffJRcNtn -Z0vMZLbFwAv5k9ybXKUY5OMHM74urjHgbvSz1VU73dqI0x1yTwDp+5v4bySb7TCY -jhXznMNfw6mpbSTHfucheNgtYQcFZlcgzMeDjrjFBphiuV8IekvzhwUhye5XtCC4 -9n0cNU3xPqYTwBSjmYD9Dabfn175RSJWnSiS+NzbBEopZQNB5exoWG/G+LvdIwXv -6p3xWYZsTEqYKi1F/AXkwmzKOPiCeP6Nxa7GkzWE0BjfB2YLqOxPojm6ldI1a2Je -Hi/OkzElsNVV4OmJlVa5c5B9ahfDREPsjUug9zskjIs5mowBVGUoxwt+Kn8CAwEA -AaOBsjCBrzAMBgNVHRMBAf8EAjAAMDoGA1UdEQQzMDGCFmNvdXJpZXIucHVzaC5h -cHBsZS5jb22CF2NvdXJpZXIyLnB1c2guYXBwbGUuY29tMA4GA1UdDwEB/wQEAwIF -oDATBgNVHSUEDDAKBggrBgEFBQcDATAdBgNVHQ4EFgQUscg6bogx1eW0qxjpHvXd -QtCE+HgwHwYDVR0jBBgwFoAULCD7ZnF0OUO5w+TLs+dO1H+LnOwwDQYJKoZIhvcN -AQELBQADggEBAH3R+ErzeiRqp57hMzIw4FUntvt4JbN4PBnLekbaD582QJb6rccn -21ZaTvVRw0rlwfWXxEZXG4iSqVe5ZUI55DqqnI5P5WU9q1h58nwJv1vBVp0a5/AQ -FczbMfG+d3Fdh88Oic47WRHSg6gghrY2Vi326FOeKPyy+DnoY1Qx1zSyF9Zh7/Gz -EA/V7hczn8io8Qyr5xapFuCtH+z5W0IvX3ConBfogbozHM/z1ZF0R4vmCl/YbWPY -qF87w6w2Ov0tfjipOYLZhssRaf4U7Ppq2K8KRCmJ7ha73otsqJDZ2on9dMnVq6b+ -I5JFOB5GYNhivmmH+pkpP7WjAhyAZu1byuo= ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIDwDCCAqigAwIBAgIUZZzOEefzPPJIshhkspF84IGOf60wDQYJKoZIhvcNAQEL -BQAwYjELMAkGA1UEBhMCVVMxEzARBgNVBAoMCkFwcGxlIEluYy4xJjAkBgNVBAsM -HUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRYwFAYDVQQDDA1BcHBsZSBS -b290IENBMB4XDTIzMDQwNzA1NDY1MFoXDTMzMDQwNDA1NDY1MFowbTEnMCUGA1UE -AwweQXBwbGUgU2VydmVyIEF1dGhlbnRpY2F0aW9uIENBMSAwHgYDVQQLDBdDZXJ0 -aWZpY2F0aW9uIEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UE -BhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC34BqN3F/RUi2x -XFKBTbYj+6z1mqifV+j4+PpXUawAb8/jiVomDKACOz+dtoUmKtZrGfKQT/8jmA90 -atZwG/l7UD+BV4R5CTFODgJ5w1qisf+fNU1XAEiakLVRTUGO2IEGCUx3BrX+OLbV -8H1t9jqp/+bCE9mpkgiAfJHhs3/90JdKwx8Rw9rR0mvR8gpygicBw4yaKSSjgQaz -rVW4Li++Mk3dLipIAkSNf/3zne3Rjurxz87GsQ17M4zZ8AkzyvYptUoQhNkzhlo6 -iBWw0yrYIce1XLKo+2ykvT56WG0hqcupNskHXD0/KfHfoGiJPSrZrU67eTBfIYFr -X98+K/JXAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEG -MB0GA1UdDgQWBBQsIPtmcXQ5Q7nD5Muz507Uf4uc7DAfBgNVHSMEGDAWgBSlIMsP -LlzUQiFAbd4WdR9DQmaSYjANBgkqhkiG9w0BAQsFAAOCAQEAduyTbo18KSykgxH0 -433x/UKxp88AvQpweG6VNrXhFkCAKwrxRPPY9/m5OasHpJuL6/hZF5LMMzKXPNlX -sFitEAoFmCkSDGCK/mItu43E2XPyfyVBg7vIi74lypmmVJEExqk4W0SSi6KSz6k4 -8H28ezyS7K2x0ESnm8H9raE4mNdwq3wLERO4dQCv+qBXdRVBf2gmjLB1t1zIOwzF -GNGl+zkZ0cXN9XOpJwoQQBlsP8J6fyMFGXCcWtTRTJQKsTtebx2rY1ydDhWPmnhX -tkSV1N1ktfbjqv8KuUaw15seHXkWjKqszXl6BbqFsdPqB13fBmigB+51PPSWhCPF -Ocu/tg== ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIDtTCCAp2gAwIBAgIUM/+kpUeTCAkr5w84cRYALQ7XXt0wDQYJKoZIhvcNAQEL -BQAwYjELMAkGA1UEBhMCVVMxEzARBgNVBAoMCkFwcGxlIEluYy4xJjAkBgNVBAsM -HUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRYwFAYDVQQDDA1BcHBsZSBS -b290IENBMB4XDTIzMDQwNzA1NDY1MFoXDTMzMDQwNDA1NDY1MFowYjELMAkGA1UE -BhMCVVMxEzARBgNVBAoMCkFwcGxlIEluYy4xJjAkBgNVBAsMHUFwcGxlIENlcnRp -ZmljYXRpb24gQXV0aG9yaXR5MRYwFAYDVQQDDA1BcHBsZSBSb290IENBMIIBIjAN -BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlCcyUv6XI39lXHzAkI03l7QDzVFl -Jhw7FrG6eyjh1YXUEQcDzp0v5m5BamM0gazXYfYRIpzXbg5vV9bdZrTUF2DOMh0N -xCCMuEUYy3PWOPILj3SvWiL+v3ReXgX1RT97UzOrZwSthacNGTbEUHnWAoSMbOrG -r8nX/mfpYqMNcNjwzKrooLetSOD6RobM5BdRHqFu7H/UKLDhPK/qftSsgesroT1N -l+rzgxkrNjiiYgB8KLTxYTQHMmFKDpjD0YteTtSZfZvkn03p5pnLAr33sewFuOiQ -+pCDJh0J4zJhmpH3m67LzN6MSUpX6Bgqr2TOa+7vnFocU6slg9aKN0PkhwIDAQAB -o2MwYTAdBgNVHQ4EFgQUpSDLDy5c1EIhQG3eFnUfQ0JmkmIwHwYDVR0jBBgwFoAU -pSDLDy5c1EIhQG3eFnUfQ0JmkmIwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E -BAMCAYYwDQYJKoZIhvcNAQELBQADggEBAFXtllENQx2qu3LvYkJzzKyTA+DKzjOk -j6vvqTnEPcjJRd01Iluy+EIvj5jFdJYTIeEoERRa0e0/kXY+SKMrhCe64w6U3SnO -xIagSfTXR5eBmQqL05dDOcx7Us7JBPG+QHxN8bIqVYD5lqJDUcjYCGE6svGD02Tv -VViG+ATDqPRuJcviuIjBLPlySW8L6uuU398aLZs9s/TuvRslPUASul8kxHdjjQk+ -ruwRE4vi1zGaORl9Mk7jeAEaYUHoF4v4aP9lQU/H1L0R/JIRFVBNJpoT+NY58Wah -tpfvVem4xGMi0gsi71mvLFJF+M44iAeN2NqTidVJvuvUPha/tJCNd3s= ------END CERTIFICATE----- diff --git a/development/proxy/push_key.pem b/development/proxy/push_key.pem deleted file mode 100644 index dca74cb..0000000 --- a/development/proxy/push_key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCzJ0Y9/zQanhQx -PmWAC33yUXDbZ2dLzGS2xcAL+ZPcm1ylGOTjBzO+Lq4x4G70s9VVO93aiNMdck8A -6fub+G8km+0wmI4V85zDX8OpqW0kx37nIXjYLWEHBWZXIMzHg464xQaYYrlfCHpL -84cFIcnuV7QguPZ9HDVN8T6mE8AUo5mA/Q2m359e+UUiVp0okvjc2wRKKWUDQeXs -aFhvxvi73SMF7+qd8VmGbExKmCotRfwF5MJsyjj4gnj+jcWuxpM1hNAY3wdmC6js -T6I5upXSNWtiXh4vzpMxJbDVVeDpiZVWuXOQfWoXw0RD7I1LoPc7JIyLOZqMAVRl -KMcLfip/AgMBAAECggEAHygN5btLzvM99MByu32EJk+2jlmh08NUopqqhwqdBek5 -B/dX2wnGHGWW4tKyyTi7OasDLr3L5VubVL/cg6gFgDrj2ac2Uqf+09WEPC3cbuCI -995Q21us+6EbRrzMEEiXWhfdyUOmFFpvlmTgTuqs6Rf0mhpAt8pflNIHQK+/oHb1 -tHxdHTkfaRhuePZkkXSXhXQBL7CBnbTzH1y55v6k78lyuY0mppHApPnZMk3r2R8z -Q7ujt40XNAPwJwtkgIpaqoO5ltyNGY7V8RB//o3mrWCHHDG4barzEnhWF5wBqOVx -Rn/P2NYfxZ06IY6IuAxYTZj9/iCYb7oWu+ia9UJ5lQKBgQC1PqIgOXT+GahmS/EJ -WZsAroLfPVw9KgIrtNg8xtqy7lY5+WqBsgV2LIWYCLagz+rOrQgGdej0rv3EVjAH -CC9f74e301QS/CCL32wVVNrTFnm9o+W8YcT7DYGhlShkrvH1yV4hdRRFmWE0OQn/ -uDHR3w1ezkVgFEeVbpul8uXYWwKBgQD9C9RTx9EmhwG/9LYFYAHi9E3pG7jx7/HB -9YfhoHFeBHVHNKlu0AnHamvk9fLfg1v0YEmr6WKaY6jufs0Mq7cIwe5nJyk6PiSr -gwvvHXSx3rvtEWlaU+teFySxVLoyoyswSAdXnjDgCcmlyLjhrfWRSVQ0Dua3dM4Y -t7KWCurvrQKBgBREwdq6sjOsaOGvwm/aPnRQD8CpWwRYC1AZ9EivsEtsTeZD1z5T -yzqQ9guvhcTsG4UrYodebkPWwfhP7yUKLEodPl83WeKZoYYi4TJtf981Vz328z0i -7eVpeEK6rH699b8X2tgBANhbs7wkWNLpJWEkAZnFlYqOGgdDDFL9m+ibAoGBANIJ -hZ5dFy/BlCQ+/O8NN8OoQRsilarMHYF/SeBwCmmdjbMi9Rgzuko5/YC43XXFVQhM -/BmjyOKZolkECzNfPLgaMMaSbNgZSNrMFZXOf1Q8qwQmFaqSSaV51E0VJNBvO6QB -xYZmme78cNIY/8zjVhfYI/pp+/t/Cqrdn3CHc2apAoGANPGDJNbmux4EQCszW8fB -Zjv1K1wm0XRXo7UGTe56sTDyW+d6TUj2q5Jmk2+VegstpofoQKGBJJeM9+ssn6Xw -f284Ug1e+90QG9luiRIirFYGagTEHCmxZZknio4EcduPbqxl+G+5xHJLSgvZReYp -aeio1XrlmyIdivwT7bj6QqM= ------END PRIVATE KEY----- diff --git a/development/test.py b/development/test.py deleted file mode 100644 index 41bbc45..0000000 --- a/development/test.py +++ /dev/null @@ -1,85 +0,0 @@ -import hashlib -import plistlib -import zlib -from base64 import b64decode - -import ids - -with open("body.txt", "r") as f: - BODY = f.read() - -CERT = "MIIIKTCCBxGgAwIBAgIRAMRS4zTbARHt//////////8wDQYJKoZIhvcNAQEFBQAwbjELMAkGA1UEBhMCVVMxEzARBgNVBAoMCkFwcGxlIEluYy4xEjAQBgNVBAsMCUFwcGxlIElEUzEOMAwGA1UEDwwFZHMtaWQxJjAkBgNVBAMMHUFwcGxlIElEUyBEUy1JRCBSZWFsbSBDQSAtIFIxMB4XDTIzMDQxNDIwMjAxNVoXDTMzMDQxMTIwMjAxNVowXDELMAkGA1UEBhMCVVMxEzARBgNVBAoMCkFwcGxlIEluYy4xCTAHBgNVBAsMADEOMAwGA1UEDwwFZHMtaWQxHTAbBgoJkiaJk/IsZAEBDA1EOjEwMTA0MjI0OTc0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAteCyewpP4kvYA3Yb8T5Pt2S1Y8T1BRStnM4pZelzN+61sQvgFgbnO+5cs0swDKxexRpbHQ4Lo7FrVQhHry0AhxI4FAw7L4dilRH9XAvWvt+VrOiDY6V2ha+DQwpLjZpgLJ0Zgofh35CxGg5A/uUmeNhldGfo8DxdnR6t8FvE/qkkePNYZDMtk9X9xa3XcQypH89iG7AqIDnueTGReZ0IOPwOWb6AQ2HUQz2Ihz3PwfHxknQcYMMnm9iRFsDGeit/hByYTKvGzpcsd+2A5jRg5jeiPYi7olNOi2qaDEGaOa4vsJV3Z9aJpFPGTxXFDzSM+5sSP9XZtrfQ9WxExeW1FwIDAQABo4IE0jCCBM4wggRKBgNVHREEggRBMIIEPaAgBgoqhkiG92NkBgQEAxIAAwAAAAEAAAAAAAAGXgAAAACgggQXBgoqhkiG92NkBgQHA4IEBwBGVVNQAPM8GcTGrDQ7T/92lgt2SSgzrnmJhCZ8Ix6ahDnaNY+VMvm1sfFUziTt6fS18G9QDdNTHKpBuB4Ond4gCgIWBroGzmCvjFpRR8/dGkY+Ho80Q0wGLX/Au9ITmeWdk8xtkdaQ65n+ICVyfQXaMCI0J+kpC33hrytrMz/LPZ6c2tfKcykBR4Gp9RuwwUc1V+PsNSFeNqiLszBR1c95n4LLqoc4j2IC3vX+3QCfIJPc/zQqPaw6CWlKS/DJM2vGVhwlahGJyVZsc6bIKVftHoG4Jmzq25Itqg0V8PlJiqHAMhYdgCCy7s++L2NhkVecpKzyDW3CP0RPE2oJeinkxNxEA+V++4myoYBi4xsejhOLYMIOS21msqgmHKhi98xtkMUXD3tsLfymqwlL+EluurfzetV/baRqL88stFHakmlEspGuwaoTSsMisJ0B4HADw5digUH/tpUhFeaB2dfD+PRzqzp055V8JcpXoBN548oBA7IMbDjMH9TSdB0ZkexaB3v0TWpsTagxy0oNnSM3MdhoyGGFUB81vulo5YrO5kz/t3EC1BDuoVFBFIcLY2V5549UNyYksg7YxSzgVUHDclSpA/+TUWrT7SQS5dcvXXVktO0s05hxc7Itpb+FiGqhY+9zcrOGUKy1hKWEk4XAXYSIVORJdu7cXBgyGJ1RSubNl8+1dgZLhA45vSKyhhQWVPY0R8HBMb7Rg7NGjO29xsjD9jA3/03bxvM+X4vXpfO5sJtolyMxvlM4X3vIyEsHGtPLrwDjB0yuYJmqlTdQQZLWL8fi93XqKdt+xaCN8M+ATOUlBIhwr7SNNLIlZ38LsX5hwHUkGONuxiaU57kY9GhvRr7Tw0m8Hu2xjD1KkE0iAQEOcOkN6UcO9QbfCi1JQIV6vDpzuIuiNasQXOmnHYrkXYNf/JFZt4BAIFa1qoxHHLQ8aljz9vAyc7dIwEg6AIPOhcBHsb23GLFKVZ0Q2tQf9ci+r23iKFWhDP9RFEm/B6E7FcW5DIFifR9cYEBnRTtI2BlO49k3jGbHVj5L16VN8eY5HRSYXYpgpTmmDgIbD191nhtpMhKpKMrk8k8wJdL1YAYSVA2alC374y5hlm3p6F9ciYBoZBYUiP5npnt79HpmnQt2tiN41obyQ2SUShhjdm+Nhbr4qvYwafsBUHPDwxniArCs7Orek3gAjpP8Jq7QFMG/nlvN55STKKG01+4eTdggZkSeSbEAySY/b35/Ip98jhyICEDrsIPcv8UAnq1fgzDnRvvIJqEqZC9J0f+aylhNsWytLHECPIMBMM9lRNU2HWAI+pFI+J2QEWl8AkM8RAIniACVRbW0BbfWg3ZTb/NQgKWlkUQqT3xYHSsSgruxMB8GA1UdIwQYMBaAFITTbIZYMHdiREysh4kURPIcsTtjMB0GA1UdDgQWBBQea+P2ao26hYm1WZ9AcyBfo4VdlzAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwID+DAgBgNVHSUBAf8EFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDQYJKoZIhvcNAQEFBQADggEBADK7x9QZfg2vtK0IUYiI0OiTHgXYAlLuYjos6qLgSAtARoEzzRuA8sGlJ5JRYsWZqkUj2ERoOzq4S88heXlD+Dlj07RAMXsB0guxiwpsIzxZ7M/S2zOmRtlvCKxxdfKtg8ohNfbQfC/SmfhL+I9X7rm4hJOj+NkpgmhRfgPOWIbHHguaDhPIXmhgqLwAODpvYBBKjuMLSlkZZsOrpxfS79f5NcObnBKlTkmiKTb2NXeEZ8n6+qnaNJdN3moRN2Mp1IB5gEXD//ZT+9K1O4ge/r9p+TRInjyBuCwGo7y8bXVhShwjXvpqtAWmElwpQ9MMDt1BxAxGBk7Otc8f5G7ewkA=" -SIG = "AQEZs/u9Ptb8AmFpCv5XgzUsskvcleZDBxYTe5JOoshFCxpnByTwFA0mxplklHqT2rTEeF+Bu0Bo0vEPlh9KslmIoQLo6ej25bbtFN07dnHNwd84xzQzWBa4VHLQE1gNjSpcorppxpAUon/eFRu5yRxmwQVmqo+XmmxSuFCzxUaAZAPFPDna+8tvRwd0q3kuK9b0w/kuT16X1SL166fFNzmsQGcBqob9C9xX0VlYGqSd4K975gWdYsPo/kiY0ni4Q130oc6oAANr8ATN0bEeAO6/AfVM2aqHJTGlYlekBFWf8Tp8AJLUc4cm676346IEBST+l4rYGxYYStV2PEmp9cZ+" -TOKEN = "5V7AY+ikHr4DiSfq1W2UBa71G3FLGkpUSKTrOLg81yk=" -NONCE = "AQAAAYeBb0XwKDMBW5PfAPM=" - - -def extract_hash(sig: bytes, cert: str) -> str: - # sig = b64decode(SIG)[2:] - sig = int.from_bytes(sig) - - # Get the correct hash - from cryptography.hazmat.backends import default_backend - from cryptography.x509 import load_pem_x509_certificate - - # key, cert = load_keys() - cert = "-----BEGIN CERTIFICATE-----\n" + cert + "\n-----END CERTIFICATE-----" - cert = load_pem_x509_certificate(cert.encode(), default_backend()) - modulus = cert.public_key().public_numbers().n - power = cert.public_key().public_numbers().e - # print(hex(pow(sig, power, modulus))) - - return pow(sig, power, modulus) - # from cryptography import x509 - - # cert = "-----BEGIN CERTIFICATE-----\n" + cert + "\n-----END CERTIFICATE-----" - # cert = x509.load_pem_x509_certificate(cert.encode()) - # pub = cert.public_key() - # modulus = pub.public_numbers().n - # # Get the power - # power = pub.public_numbers().e - # # Get the hash from the sig - # sig = b64decode(sig) - # sig = sig[2:] - # sig = int.from_bytes(sig) - # hash = pow(sig, power, modulus) - - # print(hex(hash)) - - # return hash - - -body = plistlib.dumps(plistlib.loads(BODY.encode())) -body = zlib.compress(BODY.encode(), wbits=16 + zlib.MAX_WBITS) - -p = ids._create_payload("id-register", "", TOKEN, body, b64decode(NONCE))[0] -s = hashlib.sha1(p).digest() -print(s.hex()) -# extract_hash(SIG, CERT) - -# Loop through all POSSIBLE ranges -# sig = b64decode(SIG) -# print(sig[:2]) -# for i in range(len(sig)): -# for j in range(i, len(sig)): -# h = extract_hash(sig[i:j], CERT) -# t = hex(h) -# if 'ffffff' in t: -# print(i, j, t) -# sig = b64decode(SIG)[2:] -# for i in range(len(sig)): -# h = extract_hash(sig[:i], CERT) -# t = hex(h) -# if 'ffff' in t: -# print(i, t) - -# #print(hex(extract_hash(SIG, CERT))) - -# CERT2 = "MIICnjCCAgegAwIBAgIKBAr40/DyW42YxjANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJVUzETMBEGA1UEChMKQXBwbGUgSW5jLjEVMBMGA1UECxMMQXBwbGUgaVBob25lMR8wHQYDVQQDExZBcHBsZSBpUGhvbmUgRGV2aWNlIENBMB4XDTIzMDQwNzE0MTUwNVoXDTI0MDQwNzE0MjAwNVowLzEtMCsGA1UEAxYkOUNCOTkzMTYtNkJERi00REYzLUFCRjUtNzcxNDU5MjFFQkY1MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCwsLiv8cifPdQZJQZtWvoD0WoTekSGwRj7KhxOi+AC1EUTdByWna8l7DDnixqww01FyA9pCBwottv0Xk9lOsrJrK05RXS+A7IieycejnUMdmRkgS7AsHIXOUSjtlkg2sfz5eYV9cqemTJnhdOvKtbqb9lYVN/8EehXD5JuogN+vwIDAQABo4GVMIGSMB8GA1UdIwQYMBaAFLL+ISNEhpVqedWBJo5zENinTI50MB0GA1UdDgQWBBQCl798/NQ3s5KywbJjoCjfjvWvmDAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIFoDAgBgNVHSUBAf8EFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwEAYKKoZIhvdjZAYKBAQCBQAwDQYJKoZIhvcNAQEFBQADgYEAfBwkujrswCn+wtu0eKCa39Cv58YC3AhK24Aj2iwXbddHaj9B9ye6HDy1BHPG21LKNGqm4X/XEtJQ3ZY/hGr4eenmtYjOI4a/oi127mrSt7uZmoib9x5S6w68eCCKkO+DD2JqDbMr2ATUhVNUxMegrzYdju8LofYqXBKzkoZ0/nk=" -# SIG2 = "AQGOTyyRBWMxoGWqEUl5bZJXssL6bkK4acxIDOCJTUy0MMavNEwtFThZkqVpQFqjB7eXNBM6PxwPtmwHmf/5IWgIkBUthIwhGJV3pLUkhDHTVX5YjbUSF7Z4y+Y39BQ2hhYjfcz1bw2KH40MByt+bnk28Xv2XaKWYuBinH9PVajp3g==" - - -SIG3 = "AQEZs/u9Ptb8AmFpCv5XgzUsskvcleZDBxYTe5JOoshFCxpnByTwFA0mxplklHqT2rTEeF+Bu0Bo0vEPlh9KslmIoQLo6ej25bbtFN07dnHNwd84xzQzWBa4VHLQE1gNjSpcorppxpAUon/eFRu5yRxmwQVmqo+XmmxSuFCzxUaAZAPFPDna+8tvRwd0q3kuK9b0w/kuT16X1SL166fFNzmsQGcBqob9C9xX0VlYGqSd4K975gWdYsPo/kiY0ni4Q130oc6oAANr8ATN0bEeAO6/AfVM2aqHJTGlYlekBFWf8Tp8AJLUc4cm676346IEBST+l4rYGxYYStV2PEmp9cZ+" -CERT3 = "MIIIKTCCBxGgAwIBAgIRAMRS4zTbARHt//////////8wDQYJKoZIhvcNAQEFBQAwbjELMAkGA1UEBhMCVVMxEzARBgNVBAoMCkFwcGxlIEluYy4xEjAQBgNVBAsMCUFwcGxlIElEUzEOMAwGA1UEDwwFZHMtaWQxJjAkBgNVBAMMHUFwcGxlIElEUyBEUy1JRCBSZWFsbSBDQSAtIFIxMB4XDTIzMDQxNDIwMjAxNVoXDTMzMDQxMTIwMjAxNVowXDELMAkGA1UEBhMCVVMxEzARBgNVBAoMCkFwcGxlIEluYy4xCTAHBgNVBAsMADEOMAwGA1UEDwwFZHMtaWQxHTAbBgoJkiaJk/IsZAEBDA1EOjEwMTA0MjI0OTc0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAteCyewpP4kvYA3Yb8T5Pt2S1Y8T1BRStnM4pZelzN+61sQvgFgbnO+5cs0swDKxexRpbHQ4Lo7FrVQhHry0AhxI4FAw7L4dilRH9XAvWvt+VrOiDY6V2ha+DQwpLjZpgLJ0Zgofh35CxGg5A/uUmeNhldGfo8DxdnR6t8FvE/qkkePNYZDMtk9X9xa3XcQypH89iG7AqIDnueTGReZ0IOPwOWb6AQ2HUQz2Ihz3PwfHxknQcYMMnm9iRFsDGeit/hByYTKvGzpcsd+2A5jRg5jeiPYi7olNOi2qaDEGaOa4vsJV3Z9aJpFPGTxXFDzSM+5sSP9XZtrfQ9WxExeW1FwIDAQABo4IE0jCCBM4wggRKBgNVHREEggRBMIIEPaAgBgoqhkiG92NkBgQEAxIAAwAAAAEAAAAAAAAGXgAAAACgggQXBgoqhkiG92NkBgQHA4IEBwBGVVNQAPM8GcTGrDQ7T/92lgt2SSgzrnmJhCZ8Ix6ahDnaNY+VMvm1sfFUziTt6fS18G9QDdNTHKpBuB4Ond4gCgIWBroGzmCvjFpRR8/dGkY+Ho80Q0wGLX/Au9ITmeWdk8xtkdaQ65n+ICVyfQXaMCI0J+kpC33hrytrMz/LPZ6c2tfKcykBR4Gp9RuwwUc1V+PsNSFeNqiLszBR1c95n4LLqoc4j2IC3vX+3QCfIJPc/zQqPaw6CWlKS/DJM2vGVhwlahGJyVZsc6bIKVftHoG4Jmzq25Itqg0V8PlJiqHAMhYdgCCy7s++L2NhkVecpKzyDW3CP0RPE2oJeinkxNxEA+V++4myoYBi4xsejhOLYMIOS21msqgmHKhi98xtkMUXD3tsLfymqwlL+EluurfzetV/baRqL88stFHakmlEspGuwaoTSsMisJ0B4HADw5digUH/tpUhFeaB2dfD+PRzqzp055V8JcpXoBN548oBA7IMbDjMH9TSdB0ZkexaB3v0TWpsTagxy0oNnSM3MdhoyGGFUB81vulo5YrO5kz/t3EC1BDuoVFBFIcLY2V5549UNyYksg7YxSzgVUHDclSpA/+TUWrT7SQS5dcvXXVktO0s05hxc7Itpb+FiGqhY+9zcrOGUKy1hKWEk4XAXYSIVORJdu7cXBgyGJ1RSubNl8+1dgZLhA45vSKyhhQWVPY0R8HBMb7Rg7NGjO29xsjD9jA3/03bxvM+X4vXpfO5sJtolyMxvlM4X3vIyEsHGtPLrwDjB0yuYJmqlTdQQZLWL8fi93XqKdt+xaCN8M+ATOUlBIhwr7SNNLIlZ38LsX5hwHUkGONuxiaU57kY9GhvRr7Tw0m8Hu2xjD1KkE0iAQEOcOkN6UcO9QbfCi1JQIV6vDpzuIuiNasQXOmnHYrkXYNf/JFZt4BAIFa1qoxHHLQ8aljz9vAyc7dIwEg6AIPOhcBHsb23GLFKVZ0Q2tQf9ci+r23iKFWhDP9RFEm/B6E7FcW5DIFifR9cYEBnRTtI2BlO49k3jGbHVj5L16VN8eY5HRSYXYpgpTmmDgIbD191nhtpMhKpKMrk8k8wJdL1YAYSVA2alC374y5hlm3p6F9ciYBoZBYUiP5npnt79HpmnQt2tiN41obyQ2SUShhjdm+Nhbr4qvYwafsBUHPDwxniArCs7Orek3gAjpP8Jq7QFMG/nlvN55STKKG01+4eTdggZkSeSbEAySY/b35/Ip98jhyICEDrsIPcv8UAnq1fgzDnRvvIJqEqZC9J0f+aylhNsWytLHECPIMBMM9lRNU2HWAI+pFI+J2QEWl8AkM8RAIniACVRbW0BbfWg3ZTb/NQgKWlkUQqT3xYHSsSgruxMB8GA1UdIwQYMBaAFITTbIZYMHdiREysh4kURPIcsTtjMB0GA1UdDgQWBBQea+P2ao26hYm1WZ9AcyBfo4VdlzAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwID+DAgBgNVHSUBAf8EFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDQYJKoZIhvcNAQEFBQADggEBADK7x9QZfg2vtK0IUYiI0OiTHgXYAlLuYjos6qLgSAtARoEzzRuA8sGlJ5JRYsWZqkUj2ERoOzq4S88heXlD+Dlj07RAMXsB0guxiwpsIzxZ7M/S2zOmRtlvCKxxdfKtg8ohNfbQfC/SmfhL+I9X7rm4hJOj+NkpgmhRfgPOWIbHHguaDhPIXmhgqLwAODpvYBBKjuMLSlkZZsOrpxfS79f5NcObnBKlTkmiKTb2NXeEZ8n6+qnaNJdN3moRN2Mp1IB5gEXD//ZT+9K1O4ge/r9p+TRInjyBuCwGo7y8bXVhShwjXvpqtAWmElwpQ9MMDt1BxAxGBk7Otc8f5G7ewkA=" - -print(hex(extract_hash(b64decode(SIG)[2:], CERT))) diff --git a/emulated/data.plist b/emulated/data.plist deleted file mode 100644 index e1c8f85..0000000 --- a/emulated/data.plist +++ /dev/null @@ -1,55 +0,0 @@ - - - - - iokit - - 4D1EDE05-38C7-4A6A-9CC6-4BCCA8B38C14:MLB - - QzA3NjQ5NzAwN0ZHVjZYRFc= - - 4D1EDE05-38C7-4A6A-9CC6-4BCCA8B38C14:ROM - - qGC26soK - - Fyp98tpgj - - N7rgxPQOa8kAoaUFzrgOoJI= - - Gq3489ugfi - - zxsZESjSTtPw9SyBsELYoEM= - - IOMACAddress - - qGC2H1Am - - IOPlatformSerialNumber - C07SW06DG1J1 - IOPlatformUUID - 9CA28140-E736-59AB-9AC6-792E507D2184 - abKPld1EcMni - - mqJmiIGO40uj6irTvezB4K4= - - board-id - - TWFjLTM1QzVFMDgxMjBDN0VFQUYA - - kbjfrfpoJU - - RMD8Rrsnoq/nRCd5qG5pxvc= - - oycqAZloTNDm - - oygSjpinM48Z7vkz7Ttqaek= - - product-name - - TWFjbWluaTcsMQA= - - - root_disk_uuid - 2CB26D1C-D73C-4F92-970B-F87341D4144E - - \ No newline at end of file diff --git a/emulated/jelly.py b/emulated/jelly.py deleted file mode 100644 index 5bf6668..0000000 --- a/emulated/jelly.py +++ /dev/null @@ -1,356 +0,0 @@ -from io import BytesIO -import unicorn -from . import mparser as macholibre -import logging -logger = logging.getLogger("jelly") - -STOP_ADDRESS = 0x00900000 # Used as a return address when calling functions - -ARG_REGISTERS = [ - unicorn.x86_const.UC_X86_REG_RDI, - unicorn.x86_const.UC_X86_REG_RSI, - unicorn.x86_const.UC_X86_REG_RDX, - unicorn.x86_const.UC_X86_REG_RCX, - unicorn.x86_const.UC_X86_REG_R8, - unicorn.x86_const.UC_X86_REG_R9 -] - -class VirtualInstructions: - def __init__(self, uc: unicorn.Uc): - self.uc = uc - - def push(self, value: int): - self.uc.reg_write(unicorn.x86_const.UC_X86_REG_ESP, self.uc.reg_read(unicorn.x86_const.UC_X86_REG_ESP) - 8) - self.uc.mem_write(self.uc.reg_read(unicorn.x86_const.UC_X86_REG_ESP), value.to_bytes(8, byteorder='little')) - - def pop(self) -> int: - value = int.from_bytes(self.uc.mem_read(self.uc.reg_read(unicorn.x86_const.UC_X86_REG_ESP), 8), byteorder='little') - self.uc.reg_write(unicorn.x86_const.UC_X86_REG_ESP, self.uc.reg_read(unicorn.x86_const.UC_X86_REG_ESP) + 8) - return value - - def _set_args(self, args: list[int]): - for i in range(len(args)): - if i < 6: - self.uc.reg_write(ARG_REGISTERS[i], args[i]) - else: - self.push(args[i]) - - - def call(self, address: int, args: list[int] = []): - logger.debug(f"Calling {hex(address)} with args {args}") - self.push(STOP_ADDRESS) - self._set_args(args) - self.uc.emu_start(address, STOP_ADDRESS) - return self.uc.reg_read(unicorn.x86_const.UC_X86_REG_RAX) - - -class Jelly: - # Constants - UC_ARCH = unicorn.UC_ARCH_X86 - UC_MODE = unicorn.UC_MODE_64 - - BINARY_BASE = 0x0 - - HOOK_BASE = 0xD00000 - HOOK_SIZE = 0x1000 - - STACK_BASE = 0x00300000 - STACK_SIZE = 0x00100000 - - HEAP_BASE = 0x00400000 - HEAP_SIZE = 0x00100000 - - STOP_ADDRESS = 0x00900000 - - # Public variables - _hooks: dict[str, callable] = {} - """Symbol name to hook function mapping""" - - instr: VirtualInstructions = None - - uc: unicorn.Uc = None - - # Private variables - _binary: bytes = b"" - - _heap_use: int = 0 - - def __init__(self, binary: bytes): - self._binary = binary - - def setup(self, hooks: dict[str, callable] = {}): - self._hooks = hooks - self._setup_unicorn() - self.instr = VirtualInstructions(self.uc) - self._setup_hooks() - self._map_binary() - self._setup_stack() - self._setup_heap() - self._setup_stop() - - - def _setup_unicorn(self): - self.uc = unicorn.Uc(self.UC_ARCH, self.UC_MODE) - - def _setup_stack(self): - self.uc.mem_map(self.STACK_BASE, self.STACK_SIZE) - self.uc.mem_write(self.STACK_BASE, b"\x00" * self.STACK_SIZE) - - self.uc.reg_write(unicorn.x86_const.UC_X86_REG_ESP, self.STACK_BASE + self.STACK_SIZE) - self.uc.reg_write(unicorn.x86_const.UC_X86_REG_EBP, self.STACK_BASE + self.STACK_SIZE) - - def _setup_heap(self): - self.uc.mem_map(self.HEAP_BASE, self.HEAP_SIZE) - self.uc.mem_write(self.HEAP_BASE, b"\x00" * self.HEAP_SIZE) - - def debug_registers(self): - logger.debug(f""" - RAX: {hex(self.uc.reg_read(unicorn.x86_const.UC_X86_REG_RAX))} - RBX: {hex(self.uc.reg_read(unicorn.x86_const.UC_X86_REG_RBX))} - RCX: {hex(self.uc.reg_read(unicorn.x86_const.UC_X86_REG_RCX))} - RDX: {hex(self.uc.reg_read(unicorn.x86_const.UC_X86_REG_RDX))} - RSI: {hex(self.uc.reg_read(unicorn.x86_const.UC_X86_REG_RSI))} - RDI: {hex(self.uc.reg_read(unicorn.x86_const.UC_X86_REG_RDI))} - RSP: {hex(self.uc.reg_read(unicorn.x86_const.UC_X86_REG_RSP))} - RBP: {hex(self.uc.reg_read(unicorn.x86_const.UC_X86_REG_RBP))} - RIP: {hex(self.uc.reg_read(unicorn.x86_const.UC_X86_REG_RIP))} - R8: {hex(self.uc.reg_read(unicorn.x86_const.UC_X86_REG_R8))} - R9: {hex(self.uc.reg_read(unicorn.x86_const.UC_X86_REG_R9))} - """) - def wrap_hook(self, func: callable) -> callable: - # Get the number of arguments the function takes - arg_count = func.__code__.co_argcount - #print(f"Wrapping {arg_count} argument function {func.__name__}") - # Create a wrapper function that reads the arguments from registers and the stack - def wrapper(self: 'Jelly'): - args = [] - for i in range(1, arg_count): - if i < 6: - args.append(self.uc.reg_read(ARG_REGISTERS[i-1])) - else: - args.append(self.instr.pop()) - #print(ARG_REGISTERS[1]) - #self.debug_registers() - logger.debug(f"calling {func.__name__}") - if args != []: - logger.debug(f" with args: {args}") - ret = func(self, *args) - if ret is not None: - self.uc.reg_write(unicorn.x86_const.UC_X86_REG_RAX, ret) - return - return wrapper - - - def malloc(self, size: int) -> int: - # Very naive malloc implementation - addr = self.HEAP_BASE + self._heap_use - self._heap_use += size - return addr - - def _setup_stop(self): - self.uc.mem_map(self.STOP_ADDRESS, 0x1000) - self.uc.mem_write(self.STOP_ADDRESS, b"\xc3" * 0x1000) - - def _resolve_hook(uc: unicorn.Uc, address: int, size: int, self: 'Jelly'): - for name, addr in self._resolved_hooks.items(): - if addr == address: - logger.debug(f"{name}: ") - self._hooks[name](self) - - def _setup_hooks(self): - # Wrap all hooks - for name, func in self._hooks.items(): - self._hooks[name] = self.wrap_hook(func) - - self.uc.mem_map(self.HOOK_BASE, self.HOOK_SIZE) - # Write 'ret' instruction to all hook addresses - self.uc.mem_write(self.HOOK_BASE, b"\xc3" * self.HOOK_SIZE) - # Assign address in hook space to each hook - current_address = self.HOOK_BASE - self._resolved_hooks = {} - for hook in self._hooks: - self._resolved_hooks[hook] = current_address - current_address += 1 - # Add unicorn instruction hook to entire hook space - self.uc.hook_add(unicorn.UC_HOOK_CODE, Jelly._resolve_hook, begin=self.HOOK_BASE, end=self.HOOK_BASE + self.HOOK_SIZE, user_data=self) - - def _map_binary(self): - self.uc.mem_map(self.BINARY_BASE, round_to_page_size(len(self._binary), self.uc.ctl_get_page_size())) - self.uc.mem_write(self.BINARY_BASE, self._binary) - - # Unmap the first page so we can catch NULL derefs - self.uc.mem_unmap(0x0, self.uc.ctl_get_page_size()) - - # Parse the binary so we can process binds - p = macholibre.Parser(self._binary) - p.parse() - - for seg in p.segments: - for section in seg['sects']: - if section['type'] == 'LAZY_SYMBOL_POINTERS' or section['type'] == 'NON_LAZY_SYMBOL_POINTERS': - self._parse_lazy_binds(self.uc, section['r1'], section, self._binary[p.dysymtab['indirectsymoff']:], self._binary[p.symtab['stroff']:], self._binary[p.symtab['symoff']:]) - - self._parse_binds(self.uc, self._binary[p.dyld_info['bind_off']:p.dyld_info['bind_off']+p.dyld_info['bind_size']], p.segments) - - def _do_bind(self, mu: unicorn.Uc, type, location, name): - if type == 1: # BIND_TYPE_POINTER - if name in self._hooks: - #print(f"Hooking {name} at {hex(location)}") - mu.mem_write(location, self._resolved_hooks[name].to_bytes(8, byteorder='little')) - else: - #print(f"Unknown symbol {name}") - pass - else: - raise NotImplementedError(f"Unknown bind type {type}") - - def _parse_lazy_binds(self, mu: unicorn.Uc, indirect_offset, section, dysimtab, strtab, symtab): - logger.debug(f"Doing binds for {section['name']}") - for i in range(0, int(section['size']/8)): - # Parse into proper list? - dysym = dysimtab[(indirect_offset + i)*4:(indirect_offset + i)*4+4] - dysym = int.from_bytes(dysym, 'little') - index = dysym & 0x3fffffff - - # Proper list too? - symbol = symtab[index * 16:(index * 16) + 4] - strx = int.from_bytes(symbol, 'little') - - name = c_string(strtab, strx) # Remove _ at beginning - #print(f"Lazy bind for {hex(section['offset'] + (i * 8))} : {name}") - self._do_bind(mu, 1, section['offset'] + (i * 8), name) - - def _parse_binds(self, mu: unicorn.Uc, binds: bytes, segments): - blen = len(binds) - binds: BytesIO = BytesIO(binds) - - ordinal = 0 - symbolName = '' - type = BIND_TYPE_POINTER - addend = 0 - segIndex = 0 - segOffset = 0 - - while binds.tell() < blen: - current = binds.read(1)[0] - opcode = current & BIND_OPCODE_MASK - immediate = current & BIND_IMMEDIATE_MASK - - #print(f"{hex(offset)}: {hex(opcode)} {hex(immediate)}") - - if opcode == BIND_OPCODE_DONE: - logger.debug("BIND_OPCODE_DONE") - break - elif opcode == BIND_OPCODE_SET_DYLIB_ORDINAL_IMM: - ordinal = immediate - elif opcode == BIND_OPCODE_SET_DYLIB_ORDINAL_ULEB: - #ordinal = uLEB128(&p); - ordinal = decodeULEB128(binds) - #raise NotImplementedError("BIND_OPCODE_SET_DYLIB_ORDINAL_ULEB") - elif opcode == BIND_OPCODE_SET_DYLIB_SPECIAL_IMM: - if (immediate == 0): - ordinal = 0 - else: - ordinal = BIND_OPCODE_MASK | immediate - elif opcode == BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM: - # Parse string until null terminator - symbolName = '' - while True: - b = binds.read(1)[0] - if b == 0: - break - symbolName += chr(b) - #while binds[offset] != 0: - # symbolName += chr(binds[offset]) - # offset += 1 - #offset += 1 - #print(f"Symbol name: {symbolName}") - elif opcode == BIND_OPCODE_SET_TYPE_IMM: - type = immediate - elif opcode == BIND_OPCODE_SET_ADDEND_SLEB: - #addend = sLEB128(&p); - raise NotImplementedError("BIND_OPCODE_SET_ADDEND_SLEB") - elif opcode == BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB: - segIndex = immediate - segOffset = decodeULEB128(binds) - #raise NotImplementedError("BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB") - elif opcode == BIND_OPCODE_ADD_ADDR_ULEB: - segOffset += decodeULEB128(binds) - #segOffset += uLEB128(&p); - #raise NotImplementedError("BIND_OPCODE_ADD_ADDR_ULEB") - elif opcode == BIND_OPCODE_DO_BIND: - self._do_bind(mu, type, segments[segIndex]['offset'] + segOffset, symbolName) - segOffset += 8 - elif opcode == BIND_OPCODE_DO_BIND_ADD_ADDR_ULEB: - self._do_bind(mu, type, segments[segIndex]['offset'] + segOffset, symbolName) - segOffset += decodeULEB128(binds) + 8 - #bind(type, (cast(void**) &segments[segIndex][segOffset]), symbolName, addend, generateFallback); - #segOffset += uLEB128(&p) + size_t.sizeof; - #raise NotImplementedError("BIND_OPCODE_DO_BIND_ADD_ADDR_ULEB") - elif opcode == BIND_OPCODE_DO_BIND_ADD_ADDR_IMM_SCALED: - #bind(type, (cast(void**) &segments[segIndex][segOffset]), symbolName, addend, generateFallback); - self._do_bind(mu, type, segments[segIndex]['offset'] + segOffset, symbolName) - segOffset += immediate * 8 + 8 - elif opcode == BIND_OPCODE_DO_BIND_ULEB_TIMES_SKIPPING_ULEB: - count = decodeULEB128(binds) - skip = decodeULEB128(binds) - for i in range(count): - self._do_bind(mu, type, segments[segIndex]['offset'] + segOffset, symbolName) - segOffset += skip + 8 - # uint64_t count = uLEB128(&p); - # uint64_t skip = uLEB128(&p); - # for (uint64_t i = 0; i < count; i++) { - # bind(type, (cast(void**) &segments[segIndex][segOffset]), symbolName, addend, generateFallback); - # segOffset += skip + size_t.sizeof; - # } - #raise NotImplementedError("BIND_OPCODE_DO_BIND_ULEB_TIMES_SKIPPING_ULEB") - else: - logger.error(f"Unknown bind opcode {opcode}") - -# Mach-O defines -BIND_OPCODE_DONE = 0x00 -BIND_OPCODE_SET_DYLIB_ORDINAL_IMM = 0x10 -BIND_OPCODE_SET_DYLIB_ORDINAL_ULEB = 0x20 -BIND_OPCODE_SET_DYLIB_SPECIAL_IMM = 0x30 -BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM = 0x40 -BIND_OPCODE_SET_TYPE_IMM = 0x50 -BIND_OPCODE_SET_ADDEND_SLEB = 0x60 -BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB = 0x70 -BIND_OPCODE_ADD_ADDR_ULEB = 0x80 -BIND_OPCODE_DO_BIND = 0x90 -BIND_OPCODE_DO_BIND_ADD_ADDR_ULEB = 0xA0 -BIND_OPCODE_DO_BIND_ADD_ADDR_IMM_SCALED = 0xB0 -BIND_OPCODE_DO_BIND_ULEB_TIMES_SKIPPING_ULEB = 0xC0 -BIND_OPCODE_THREADED = 0xD0 - -BIND_TYPE_POINTER = 1 - -BIND_OPCODE_MASK = 0xF0 -BIND_IMMEDIATE_MASK = 0x0F - -# Helper functions -def round_to_page_size(size: int, page_size: int) -> int: - return (size + page_size - 1) & ~(page_size - 1) - -def decodeULEB128(bytes: BytesIO) -> int: - result = 0 - shift = 0 - while True: - b = bytes.read(1)[0] - result |= (b & 0x7F) << shift - if (b & 0x80) == 0: - break - shift += 7 - return result - -def c_string(bytes, start: int = 0) -> str: - out = '' - i = start - - while True: - if i > len(bytes) or bytes[i] == 0: - break - out += chr(bytes[i]) - #print(start) - #print(chr(bytes[i])) - i += 1 - return out \ No newline at end of file diff --git a/emulated/mparser.py b/emulated/mparser.py deleted file mode 100644 index ba34b14..0000000 --- a/emulated/mparser.py +++ /dev/null @@ -1,2301 +0,0 @@ -""" -Modified from https://github.com/aaronst/macholibre/blob/master/macholibre/parser.py - -This file is Copyright 2016 Aaron Stephens - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" - -import hashlib - -from collections import Counter -from datetime import datetime -from json import dump -from math import exp, log -from os import SEEK_END -from re import split -from struct import unpack -from uuid import UUID - -#from asn1crypto.cms import ContentInfo -#from asn1crypto.x509 import DirectoryString -from plistlib import loads - -#import mdictionary as mdictionary - -from io import BytesIO - -import logging -logger = logging.getLogger("jelly") - - -class Parser(): - """Main object containing all the necessary functions to parse - a mach-o binary. - """ - - def __init__(self, file): - """Initialize instance variables and flags.""" - - self.__extract_certs = False - self.__file = BytesIO(file) - self.__is_64_bit = True # default place-holder - self.__is_little_endian = True # ^^ - self.__macho = {} - self.__output = { - 'name': 'IMDAppleServices' - } - - self.file = self.__file - - def add_abnormality(self, abnormality): - """Add abnormality to output.""" - - if 'abnormalities' not in self.__output: - self.__output['abnormalities'] = [] - - self.__output['abnormalities'].append(abnormality) - - def calc_entropy(self, b): - """Calculate byte entropy for given bytes.""" - - byte_counts = Counter() - - entropy = 0 - - for i in b: - byte_counts[i] += 1 - - total = float(sum(byte_counts.values())) - - for count in byte_counts.values(): - p = float(count) / total - entropy -= p * log(p, 256) - - return entropy - - def get_string(self): - """Read a null-terminated string from macho.""" - - string = bytearray() - - c = self.__file.read(1) - - while c not in (b'\x00', ''): - string += c - c = self.__file.read(1) - - return string.decode('utf-8', errors='replace') - - def get_int(self, ignore_endian=False): - """Read a 4-byte integer from macho, account for endian-ness.""" - - integer = self.__file.read(4) - - if self.__is_little_endian and not ignore_endian: - return int.from_bytes(integer, byteorder='little') - - return int.from_bytes(integer, byteorder='big') - - def get_ll(self): - """Read an 8-byte long long from macho, account for endian-ness.""" - - longlong = self.__file.read(8) - - if self.__is_little_endian: - return int.from_bytes(longlong, byteorder='little') - - return int.from_bytes(longlong, byteorder='big') - - def make_version(self, version): - """Construct a version number from given bytes.""" - - vx = version >> 16 - vy = (version >> 8) & 0xff - vz = version & 0xff - - return '{}.{}.{}'.format(vx, vy, vz) - - def identify_file(self): - """Identify if the given file is a single Mach-O or a - Universal binary.""" - - magic = self.get_int(ignore_endian=True) - - if magic in mdictionary.machos: - return mdictionary.machos[magic] - else: - raise ValueError('Provided file has unrecognized magic: {}'.format( - magic)) - - def parse_macho_flags(self, flags): - """Parse ``flags`` into list of readable flags.""" - - output = [] - - i = 0 - - while i < 28: - if (0x1 & (flags >> i)) == 0x1: - if 2 ** i in mdictionary.flags: - output.append(mdictionary.flags[2 ** i]) - else: - self.add_abnormality('Unknown mach-o flag "{}".'.format( - 2 ** i)) - - i += 1 - - return output - - def get_segment_entropy(self, m_offset, offset, size): - """Determine byte-entropy for this segment.""" - - old = self.__file.tell() - - self.__file.seek(m_offset + offset) - #print("seeking to: " + str(m_offset + offset)) - - entropy = self.calc_entropy(self.__file.read(size)) - - self.__file.seek(old) - - return entropy - - def parse_section_attrs(self, attrs): - """Parse section attributes.""" - - output = [] - - for a in mdictionary.section_attrs: - if attrs & a == a: - output.append(mdictionary.section_attrs[a]) - - return output - - def parse_section_flags(self, output, flags): - """Parse section flags into section type and attributes.""" - - output['type'] = mdictionary.section_types[flags & 0xff] - - attrs = flags & 0xffffff00 - - output['attrs'] = self.parse_section_attrs(attrs) - - def parse_section(self): - """Parse section.""" - - name = self.__file.read(16).decode().rstrip('\u0000') - segname = self.__file.read(16).decode().rstrip('\u0000') - addr = self.get_ll() if self.__is_64_bit else self.get_int() - size = self.get_ll() if self.__is_64_bit else self.get_int() - offset = self.get_int() - align = self.get_int() - reloff = self.get_int() - nreloc = self.get_int() - flags = self.get_int() - r1 = self.get_int() - r2 = self.get_int() - r3 = self.get_int() - - #self.__file.read(12) if self.__is_64_bit else self.__file.read(8) - - output = { - 'name': name, - 'segname': segname, - 'addr': addr, - 'offset': offset, - 'align': align, - 'reloff': reloff, - 'nreloc': nreloc, - 'size': size, - 'r1': r1, - 'r2': r2, - 'r3': r3 - } - - self.parse_section_flags(output, flags) - - return output - - def parse_segment_flags(self, flags): - """Parse segment flags into readable list.""" - - output = [] - - i = 1 - - while i < 9: - if flags & i == i: - output.append(mdictionary.segment_flags[i]) - i <<= 1 - - return output - - def parse_segment(self, m_offset, m_size, cmd, cmd_size): - """Parse segment command.""" - - name = self.__file.read(16).decode().rstrip('\u0000') - vmaddr = self.get_ll() if self.__is_64_bit else self.get_int() - vmsize = self.get_ll() if self.__is_64_bit else self.get_int() - offset = self.get_ll() if self.__is_64_bit else self.get_int() - segsize = self.get_ll() if self.__is_64_bit else self.get_int() - maxprot = self.get_int() - initprot = self.get_int() - nsects = self.get_int() - flags = self.get_int() - - maxprot = mdictionary.protections[maxprot & 0b111] - initprot = mdictionary.protections[initprot & 0b111] - - entropy = self.get_segment_entropy(m_offset, offset, segsize) - - output = { - 'm_offset': m_offset, - 'cmd': cmd, - 'size': cmd_size, - 'name': name, - 'vmaddr': vmaddr, - 'vmsize': vmsize, - 'offset': offset, - 'segsize': segsize, - 'maxprot': maxprot, - 'initprot': initprot, - 'nsects': nsects, - 'entropy': entropy, - 'sects': [] - } - - sect_size = 80 if self.__is_64_bit else 68 - - for _ in range(nsects): - if self.__file.tell() + sect_size > m_offset + m_size: - self.add_abnormality('Section at offset "{}" with size "{}" ' - 'greater than mach-o size.'.format( - self.__file.tell(), sect_size)) - - break - - output['sects'].append(self.parse_section()) - - output['flags'] = self.parse_segment_flags(flags) - - return output - - def parse_symtab(self, cmd, cmd_size): - """Parse symbol table load command.""" - - symoff = self.get_int() - nsyms = self.get_int() - stroff = self.get_int() - strsize = self.get_int() - - output = { - 'cmd': cmd, - 'cmd_size': cmd_size, - 'symoff': symoff, - 'nsyms': nsyms, - 'stroff': stroff, - 'strsize': strsize - } - - return output - - def parse_symseg(self, cmd, cmd_size): - """Parse link-edit gdb symbol table info (obsolete).""" - - offset = self.get_int() - size = self.get_int() - - output = { - 'cmd': cmd, - 'cmd_size': cmd_size, - 'offset': offset, - 'size': size - } - - return output - - def parse_thread(self, cmd, cmd_size): - """Parse thread load command.""" - - state = self.get_int() - count = self.get_int() - - self.__file.read(cmd_size - 16) # skip thread_state objects. - # TODO: parse them, definitions in - - if state in mdictionary.thread_states: - state = mdictionary.thread_states[state] - else: - self.add_abnormality('Invalid THREAD STATE FLAVOR "{}" at offset ' - '"{}".'.format(state, self.__file.tell() - 8)) - - output = { - 'cmd': cmd, - 'cmd_size': cmd_size, - 'state': state, - 'count': count - } - - return output - - def parse_fvmlib(self, cmd, cmd_size): - """Parse fvmlib load command.""" - - offset = self.__file.tell() - 8 - - self.__file.read(4) # skip name offset - - minor_version = self.get_int() - header_addr = self.get_int() - name = self.get_string() - - output = { - 'cmd': cmd, - 'cmd_size': cmd_size, - 'name': name, - 'minor_version': self.make_version(minor_version), - 'header_addr': header_addr - } - - self.__file.read(cmd_size - (self.__file.tell() - offset)) - - return output - - def parse_ident(self, cmd, cmd_size): - """Parse object identification info (obsolete).""" - - output = { - 'cmd': cmd, - 'cmd_size': cmd_size, - 'strings': [] - } - - end = self.__file.tell() - 8 + cmd_size - - while self.__file.tell() < end: - string = self.get_string() - - if string != '': - output['strings'].append(string) - - return output - - def parse_fvmfile(self, cmd, cmd_size): - """Parse fixed VM file inclusion (internal use).""" - - name = self.get_string() - header_addr = self.get_int() - - output = { - 'cmd': cmd, - 'cmd_size': cmd_size, - 'name': name, - 'header_addr': header_addr - } - - return output - - def parse_prepage(self, cmd, cmd_size): - """Parse prepage command (internal use). Load command structure not - found. - """ - - self.__file.read(cmd_size - 8) - - output = { - 'cmd': cmd, - 'cmd_size': cmd_size - } - - return output - - def parse_dysymtab(self, cmd, cmd_size): - """Parse dynamic link-edit symbol table info.""" - - ilocalsym = self.get_int() # index to local symbols - nlocalsym = self.get_int() # number of local symbols - iextdefsym = self.get_int() # index to externally defined sybmols - nextdefsym = self.get_int() # number of externally defined symbols - iundefsym = self.get_int() # index to undefined symbols - nundefsym = self.get_int() # number of externally defined symbols - tocoff = self.get_int() # file offset to table of contents - ntoc = self.get_int() # number of module table entries - modtaboff = self.get_int() # file offset to module table - nmodtab = self.get_int() # number of module table entries - extrefsymoff = self.get_int() # offset to referenced symbol table - nextrefsyms = self.get_int() # number of referenced symbol table entries - indirectsymoff = self.get_int() # file offset to the indirect symbol table - nindirectsyms = self.get_int() # number of indirect symbol table entries - extreloff = self.get_int() # offset to external relocation entries - nextrel = self.get_int() # number of external relocation entries - locreloff = self.get_int() # offset to local relocation entries - nlocrel = self.get_int() # number of local relocation entries - - output = { - 'cmd': cmd, - 'cmd_size': cmd_size, - 'ilocalsym': ilocalsym, - 'nlocalsym': nlocalsym, - 'iextdefsym': iextdefsym, - 'nextdefsym': nextdefsym, - 'iundefsym': iundefsym, - 'nundefsym': nundefsym, - 'tocoff': tocoff, - 'ntoc': ntoc, - 'modtaboff': modtaboff, - 'nmodtab': nmodtab, - 'extrefsymoff': extrefsymoff, - 'nextrefsyms': nextrefsyms, - 'indirectsymoff': indirectsymoff, - 'nindirectsyms': nindirectsyms, - 'extreloff': extreloff, - 'nextrel': nextrel, - 'locreloff': locreloff, - 'nlocrel': nlocrel - } - - return output - - def parse_load_dylib(self, cmd, cmd_size): - """Parse dylib load command.""" - - offset = self.__file.tell() - 8 - - self.__file.read(4) # skip name offset - - timestamp = self.get_int() - current_version = self.get_int() - compatibility_version = self.get_int() - name = self.get_string() - - output = { - 'cmd': cmd, - 'cmd_size': cmd_size, - 'name': name, - 'timestamp': datetime.fromtimestamp(timestamp).strftime( - '%Y-%m-%d %H:%M:%S'), - 'current_version': self.make_version(current_version), - 'compatability_version': self.make_version(compatibility_version) - } - - # skip padding - self.__file.read(cmd_size - (self.__file.tell() - offset)) - - return output - - def parse_load_dylinker(self, cmd, cmd_size): - """Parse dylinker load command.""" - - offset = self.__file.tell() - 8 - - self.__file.read(4) # skip name offset - - output = { - 'cmd': cmd, - 'cmd_size': cmd_size, - 'name': self.get_string() - } - - # skip padding - self.__file.read(cmd_size - (self.__file.tell() - offset)) - - return output - - def parse_prebound_dylib(self, cmd, cmd_size): - """Parse prebound dylib load command. An executable that is prebound to - its dynamic libraries will have one of these for each library that the - static linker used in prebinding. - """ - - name = self.get_string() - nmodules = self.get_int() - linked_modules = self.get_string() - - output = { - 'cmd': cmd, - 'cmd_size': cmd_size, - 'name': name, - 'nmodules': nmodules, - 'linked_modules': linked_modules - } - - return output - - def parse_routines(self, cmd, cmd_size): - """Parse routines load command. The routines command contains the - address of the dynamic shared library initialization routine and an - index into the module table for the module that defines the routine. - """ - - init_address = self.get_ll() if self.__is_64_bit else self.get_int() - init_module = self.get_ll() if self.__is_64_bit else self.get_int() - - self.__file.read(48) if self.__is_64_bit else self.__file.read(24) - - output = { - 'cmd': cmd, - 'cmd_size': cmd_size, - 'init_address': init_address, - 'init_module': init_module - } - - return output - - def parse_sub_stuff(self, cmd, cmd_size): - """Parse sub_* load command.""" - - output = { - 'cmd': cmd, - 'cmd_size': cmd_size, - 'name': self.get_string() - } - - return output - - def parse_twolevel_hints(self, cmd, cmd_size): - """Parse two-level hints load command.""" - - offset = self.get_int() - nhints = self.get_int() - - output = { - 'cmd': cmd, - 'cmd_size': cmd_size, - 'offset': offset, - 'nhints': nhints - } - - return output - - def parse_prebind_cksum(self, cmd, cmd_size): - """Parse prebind checksum load command.""" - - cksum = self.get_int() - - output = { - 'cmd': cmd, - 'cmd_size': cmd_size, - 'cksum': cksum - } - - return output - - def parse_uuid(self, cmd, cmd_size): - """Parse UUID load command.""" - - uuid = self.__file.read(16) - - if self.__is_little_endian: - uuid = unpack('<16s', uuid)[0] - - output = { - 'cmd': cmd, - 'cmd_size': cmd_size, - 'uuid': UUID(bytes=uuid).hex - } - - return output - - def parse_linkedit_data(self, cmd, cmd_size): - """Parse link-edit data load command.""" - - dataoff = self.get_int() # file offset of data in __LINKEDIT segment - datasize = self.get_int() # file size of data in __LINKEDIT segment - - output = { - 'cmd': cmd, - 'cmd_size': cmd_size, - 'dataoff': dataoff, - 'datasize': datasize - } - - return output - - def parse_encryption_info(self, cmd, cmd_size): - """Parse encryption info load command. Contains the file offset and size - of an encrypted segment. - """ - - cryptoff = self.get_int() - cryptsize = self.get_int() - cryptid = self.get_int() - - if cmd.endswith('64'): - self.__file.read(4) # skip padding - - output = { - 'cmd': cmd, - 'cmd_size': cmd_size, - 'cryptoff': cryptoff, - 'cryptsize': cryptsize, - 'cryptid': cryptid - } - - return output - - def parse_dyld_info(self, cmd, cmd_size): - """Parse dyld info load command. contains the file offsets and sizes of - the new compressed form of the information dyld needs to load the - image. This information is used by dyld on Mac OS X 10.6 and later. All - information pointed to by this command is encoded using byte streams, - so no endian swapping is needed to interpret it. - """ - - rebase_off = self.get_int() # file offset to rebase info - rebase_size = self.get_int() # size of rebase info - bind_off = self.get_int() # file offset to binding info - bind_size = self.get_int() # size of binding info - weak_bind_off = self.get_int() # file offset to weak binding info - weak_bind_size = self.get_int() # size of weak binding info - lazy_bind_off = self.get_int() # file offset to lazy binding info - lazy_bind_size = self.get_int() # size of lazy binding info - export_off = self.get_int() # file offset to export info - export_size = self.get_int() # size of offset info - - output = { - 'cmd': cmd, - 'cmd_size': cmd_size, - 'rebase_off': rebase_off, - 'rebase_size': rebase_size, - 'bind_off': bind_off, - 'bind_size': bind_size, - 'weak_bind_off': weak_bind_off, - 'weak_bind_size': weak_bind_size, - 'lazy_bind_off': lazy_bind_off, - 'lazy_bind_size': lazy_bind_size, - 'export_off': export_off, - 'export_size': export_size - } - - return output - - def parse_version_min_os(self, cmd, cmd_size): - """Parse minimum OS version load command.""" - - version = self.get_int() - sdk = self.get_int() - - output = { - 'cmd': cmd, - 'cmd_size': cmd_size, - 'version': self.make_version(version), - 'sdk': self.make_version(sdk) - } - - return output - - def parse_source_version(self, cmd, cmd_size): - """Parse source version load command.""" - - version = self.get_ll() # A.B.C.D.E packed as a24.b10.c10.d10.e10 - - mask = 0b1111111111 # 10 bit mask for B, C, D, and E - - a = version >> 40 - b = (version >> 30) & mask - c = (version >> 20) & mask - d = (version >> 10) & mask - e = version & mask - - output = { - 'cmd': cmd, - 'cmd_size': cmd_size, - 'version': '{}.{}.{}.{}.{}'.format(a, b, c, d, e) - } - - return output - - def parse_linker_option(self, cmd, cmd_size): - """Parse linker options load command.""" - - start = self.__file.tell() - 8 - - count = self.get_int() - - linker_options = [] - - for _ in range(count): - linker_options.append(self.get_string()) - - self.__file.read(cmd_size - (self.__file.tell() - start)) - - output = { - 'cmd': cmd, - 'cmd_size': cmd_size, - 'count': count, - 'linker_options': linker_options - } - - return output - - def parse_rpath(self, cmd, cmd_size): - """Parse rpath load command.""" - - offset = self.__file.tell() - 8 - - self.__file.read(4) # skip path offset - - path = self.get_string() - - output = { - 'cmd': cmd, - 'cmd_size': cmd_size, - 'path': path - } - - self.__file.read(cmd_size - (self.__file.tell() - offset)) - - return output - - def parse_main(self, cmd, cmd_size): - """Parse main load command.""" - - entryoff = self.get_ll() # file (__TEXT) offset of main() - stacksize = self.get_ll() # if not zero, initialize stack size - - output = { - 'cmd': cmd, - 'cmd_size': cmd_size, - 'entryoff': entryoff, - 'stacksize': stacksize - } - - return output - - def parse_lcs(self, offset, size, nlcs, slcs): - """Determine which load commands are present and parse each one - accordingly. Return as a list. - - Load command structures found in '/usr/include/mach-o/loader.h'. - """ - - self.__macho['lcs'] = [] - self.segments = [] - - for _ in range(nlcs): - cmd = self.get_int() # Load command type - cmd_size = self.get_int() # Size of load command - - if self.__is_64_bit and cmd_size % 8 != 0: - raise ValueError('Load command size "{}" for 64-bit mach-o at ' - 'offset "{}" is not divisible by 8.'.format( - cmd_size, self.__file.tell() - 4)) - elif cmd_size % 4 != 0: - raise ValueError('Load command size "{}" for 32-bit mach-o at ' - 'offset "{}" is not divisible by 4.'.format( - cmd_size, self.__file.tell() - 4)) - - if cmd in mdictionary.loadcommands: - cmd = mdictionary.loadcommands[cmd] - else: - self.add_abnormality('Unknown load command "{}" at offset ' - '"{}".'.format( - cmd, self.__file.tell() - 8)) - - self.__file.read(cmd_size - 8) # skip load command - - if cmd == 'SEGMENT' or cmd == 'SEGMENT_64': - #self.segments.append((offset, size, cmd, cmd_size)) - #self.__macho['lcs'].append( - - parsed = self.parse_segment(offset, size, cmd, cmd_size) - self.__macho['lcs'].append(parsed) - self.segments.append(parsed) - elif cmd == 'SYMTAB': - self.symtab = self.parse_symtab(cmd, cmd_size) - self.__macho['lcs'].append(self.symtab) - elif cmd == 'SYMSEG': - self.__macho['lcs'].append(self.parse_symseg(cmd, cmd_size)) - elif cmd in ('THREAD', 'UNIXTHREAD'): - self.__macho['lcs'].append(self.parse_thread(cmd, cmd_size)) - elif cmd in ('LOADFVMLIB', 'IDFVMLIB'): - self.__macho['lcs'].append(self.parse_fvmlib(cmd, cmd_size)) - elif cmd == 'IDENT': - self.__macho['lcs'].append(self.parse_ident(cmd, cmd_size)) - elif cmd == 'FVMFILE': - self.__macho['lcs'].append(self.parse_fvmfile(cmd, cmd_size)) - elif cmd == 'PREPAGE': - self.__macho['lcs'].append(self.parse_prepage(cmd, cmd_size)) - elif cmd == 'DYSYMTAB': - self.__macho['lcs'].append(self.parse_dysymtab(cmd, cmd_size)) - elif cmd in ('LOAD_DYLIB', 'ID_DYLIB', 'LAZY_LOAD_DYLIB', - 'LOAD_WEAK_DYLIB', 'REEXPORT_DYLIB', - 'LOAD_UPWARD_DYLIB'): - self.__macho['lcs'].append( - self.parse_load_dylib(cmd, cmd_size)) - elif cmd in ('LOAD_DYLINKER', 'ID_DYLINKER', 'DYLD_ENVIRONMENT'): - self.__macho['lcs'].append( - self.parse_load_dylinker(cmd, cmd_size)) - elif cmd == 'PREBOUND_DYLIB': - self.__macho['lcs'].append( - self.parse_prebound_dylib(cmd, cmd_size)) - elif cmd in ('ROUTINES', 'ROUTINES_64'): - self.__macho['lcs'].append(self.parse_routines(cmd, cmd_size)) - elif cmd in ('SUB_FRAMEWORK', 'SUB_UMBRELLA', 'SUB_CLIENT', - 'SUB_LIBRARY'): - self.__macho['lcs'].append(self.parse_sub_stuff(cmd, cmd_size)) - elif cmd == 'TWOLEVEL_HINTS': - self.__macho['lcs'].append( - self.parse_twolevel_hints(cmd, cmd_size)) - elif cmd == 'PREBIND_CKSUM': - self.__macho['lcs'].append( - self.parse_prebind_cksum(cmd, cmd_size)) - elif cmd == 'UUID': - self.__macho['lcs'].append(self.parse_uuid(cmd, cmd_size)) - elif cmd in ('CODE_SIGNATURE', 'SEGMENT_SPLIT_INFO', - 'FUNCTION_STARTS', 'DATA_IN_CODE', - 'DYLIB_CODE_SIGN_DRS', 'LINKER_OPTIMIZATION_HINT'): - self.__macho['lcs'].append( - self.parse_linkedit_data(cmd, cmd_size)) - elif cmd in ('ENCRYPTION_INFO', 'ENCRYPTION_INFO_64'): - self.__macho['lcs'].append( - self.parse_encryption_info(cmd, cmd_size)) - elif cmd in ('DYLD_INFO', 'DYLD_INFO_ONLY'): - self.dyld_info = self.parse_dyld_info(cmd, cmd_size) - self.__macho['lcs'].append(self.dyld_info) - elif cmd in ('VERSION_MIN_MACOSX', 'VERSION_MIN_IPHONEOS', - 'VERSION_MIN_WATCHOS', 'VERSION_MIN_TVOS'): - self.__macho['lcs'].append( - self.parse_version_min_os(cmd, cmd_size)) - elif cmd == 'SOURCE_VERSION': - self.__macho['lcs'].append( - self.parse_source_version(cmd, cmd_size)) - elif cmd == 'LINKER_OPTION': - self.__macho['lcs'].append( - self.parse_linker_option(cmd, cmd_size)) - elif cmd == 'RPATH': - self.__macho['lcs'].append(self.parse_rpath(cmd, cmd_size)) - elif cmd == 'MAIN': - self.__macho['lcs'].append(self.parse_main(cmd, cmd_size)) - - def parse_syms(self, offset, size, lc_symtab): - """Parse symbol and string tables. - - Symbol table format found in: - /usr/include/mach-o/nlist.h - /usr/include/mach-o/stab.h - """ - - # Check if symbol table offset is within mach-o - if lc_symtab['symoff'] > size: - self.add_abnormality('Symbol table at offset "{}" out of ' - 'bounds.'.format( - offset + lc_symtab['symoff'])) - - return - - true_offset = offset + lc_symtab['symoff'] # beginning of symbol table - - symbol_size = 16 if self.__is_64_bit else 12 - - self.__file.seek(true_offset) - - entropy = self.calc_entropy(self.__file.read( - lc_symtab['nsyms'] * symbol_size)) - - if entropy >= 0.8: - self.add_abnormality('Symbol table with entropy of "{}" is ' - 'probably packed. Not attempting to ' - 'parse.'.format(entropy)) - - return - - if lc_symtab['symoff'] + lc_symtab['nsyms'] * symbol_size > size: - self.add_abnormality('Symbol table at offset "{}" partially out ' - 'of bounds. Attempting to parse as many ' - 'symbols as possible.'.format(true_offset)) - - self.__file.seek(true_offset) # jump to beginning of symbol table - - self.__macho['symtab'] = [] - - for _ in range(lc_symtab['nsyms']): - if self.__file.tell() + symbol_size > offset + size: - break - - n_strx = self.get_int() - n_type = int(self.__file.read(1).hex(), 16) - n_sect = int(self.__file.read(1).hex(), 16) - n_desc = int(self.__file.read(2).hex(), 16) - - n_value = self.get_ll() if self.__is_64_bit else self.get_int() - - symbol = { - 'n_strx': n_strx, - 'n_sect': n_sect, - 'n_desc': n_desc, - 'n_value': n_value - } - - if n_type >= 32: - if n_type in mdictionary.stabs: - symbol['stab'] = mdictionary.stabs[n_type] - else: - self.add_abnormality( - 'Unknown stab type "{}" at offset "{}".'.format( - n_type, self.__file.tell() - symbol_size + 4)) - else: - n_pext = n_type & 0x10 # private external symbol flag - n_ext = n_type & 0x01 # external symbol flag - n_type = n_type & 0x0e # symbol type - - if n_type in mdictionary.n_types: - n_type = mdictionary.n_types[n_type] - else: - self.add_abnormality( - 'Unknown N_TYPE "{}" at offset "{}".'.format( - n_type, self.__file.tell() - symbol_size + 4)) - - if self.__is_little_endian: - dylib = n_desc & 0x0f - ref = (n_desc >> 8) & 0xff - else: - dylib = (n_desc >> 8) & 0xff - ref = n_desc & 0x0f - - symbol['pext'] = n_pext - symbol['n_type'] = n_type - symbol['ext'] = n_ext - symbol['dylib'] = dylib - symbol['ref'] = ref - - self.__macho['symtab'].append(symbol) - - def parse_strings(self, offset, size, lc_symtab): - """Parse string table.""" - - # Check is string table offset is within mach-o - if lc_symtab['stroff'] > size: - self.add_abnormality( - 'String table at offset "{}" greater than mach-o size.'.format( - offset + lc_symtab['stroff'])) - - return - - true_offset = offset + lc_symtab['stroff'] - - self.__file.seek(true_offset) - #self.strtab = bytes(self.__file.read(lc_symtab['strsize'])) - #self.__file.seek(true_offset) - - entropy = self.calc_entropy(self.__file.read(lc_symtab['strsize'])) - - if entropy >= 0.8: - self.add_abnormality('String table with entropy of "{}" is ' - 'probably packed. Not attempting to ' - 'parse.'.format(entropy)) - - return - - if true_offset + lc_symtab['strsize'] > offset + size: - self.add_abnormality('String Table at offset "{}" partially out ' - 'of bounds. Attempting to parse as many ' - 'strings as possible.'.format(true_offset)) - - self.__macho['strtab'] = [] - - self.__file.seek(true_offset) - - while self.__file.tell() < true_offset + lc_symtab['strsize']: - try: - string = self.get_string() - - if string != '': - self.__macho['strtab'].append(string) - except: - break - - def parse_imports(self, offset, size, lc_symtab, lc_dysymtab=None, - lc_dylibs=None): - """Parse undefined external symbols (imports) out of the symbol and - string tables. - """ - - self.__macho['imports'] = [] - - true_offset = offset + lc_symtab['stroff'] - - undef_syms = None - - if lc_dysymtab is not None: # Use symtab layout info from DYSYMTAB - i_undef = lc_dysymtab['nlocalsym'] + lc_dysymtab['nextdefsym'] - 1 - j_undef = i_undef + lc_dysymtab['nundefsym'] - - undef_syms = self.__macho['symtab'][i_undef:j_undef] - else: # Find undefined symbols manually by checking n_type - undef_syms = filter(lambda sym: sym['n_type'] in ('UNDF', 'PBUD'), - self.__macho['symtab']) - - for sym in undef_syms: - self.__file.seek(true_offset + sym['n_strx']) - - value = self.get_string() - - if lc_dylibs is not None: # If created with two-level namespace - dylib = sym['dylib'] - - if dylib == 0: - dylib = 'SELF_LIBRARY' - elif dylib == 254: - dylib = 'DYNAMIC_LOOKUP' - elif dylib == 255: - dylib = 'EXECUTABLE' - elif dylib > len(lc_dylibs): - dylib = f'{dylib} (OUT_OF_RANGE)' - else: - dylib = lc_dylibs[dylib - 1]['name'] - - self.__macho['imports'].append((value, dylib)) - else: - self.__macho['imports'].append(value) - - def parse_certs(self, sig_offset, index_offset): - """Parse X509 certificates out of code signature.""" - - prev = self.__file.tell() - - true_offset = sig_offset + index_offset - - self.__file.seek(true_offset) - - magic = self.get_int(ignore_endian=True) - - if magic != mdictionary.signatures['BLOBWRAPPER']: - self.add_abnormality('Bad magic "{}" for certificate blob wrapper ' - 'at offset "{}".'.format(magic, true_offset)) - - return [] - - # subtract 8 to ignore magic and size fields - size = self.get_int(ignore_endian=True) - 8 - - if size <= 0: - self.add_abnormality('Non-positive CMS size "{}" at offset ' - '"{}".'.format(size, self.__file.tell() - 4)) - - return [] - - signed_data = ContentInfo.load(self.__file.read(size))['content'] - - self.__macho['code_signature']['certs'] = [] - - for cert in signed_data['certificates']: - cert = cert.chosen - - if self.__extract_certs: - c_bytes = cert.dump() - open(hashlib.md5(c_bytes).hexdigest(), 'wb').write(c_bytes) - - subject = {} - - for rdn in cert.subject.chosen: - name = rdn[0]['type'].human_friendly - value = rdn[0]['value'] - - if name == 'Country': - subject['country'] = str(value.chosen) - elif name == 'Organization': - subject['org'] = str(value.chosen) - elif name == 'Organizational Unit': - subject['org_unit'] = str(value.chosen) - elif name == 'Common Name': - subject['common_name'] = str(value.chosen) - else: - if isinstance(value, DirectoryString): - subject[name] = str(value.chosen) - else: - subject[name] = str(value.parsed) - - issuer = {} - - for rdn in cert.issuer.chosen: - name = rdn[0]['type'].human_friendly - value = rdn[0]['value'] - - if name == 'Country': - issuer['country'] = str(value.chosen) - elif name == 'Organization': - issuer['org'] = str(value.chosen) - elif name == 'Organizational Unit': - issuer['org_unit'] = str(value.chosen) - elif name == 'Common Name': - issuer['common_name'] = str(value.chosen) - else: - if isinstance(value, DirectoryString): - issuer[name] = str(value.chosen) - else: - issuer[name] = str(value.parsed) - - certificate = { - 'subject': subject, - 'issuer': issuer, - 'serial': cert.serial_number, - 'is_ca': cert.ca - } - - self.__macho['code_signature']['certs'].append(certificate) - - self.__file.seek(prev) - - def parse_codedirectory(self, sig_offset, index_offset): - """Parse code directory from code signature.""" - - prev = self.__file.tell() - - true_offset = sig_offset + index_offset - - self.__file.seek(true_offset) - - magic = self.get_int(ignore_endian=True) - - if magic != mdictionary.signatures['CODEDIRECTORY']: - self.add_abnormality('Bad magic "{}" for code directory at offset ' - '"{}".'.format(magic, self.__file.tell() - 4)) - - return - - size = self.get_int(ignore_endian=True) - version = self.get_int(ignore_endian=True) - # TODO: not sure how to parse flags yet... - flags = self.get_int(ignore_endian=True) - hash_offset = self.get_int(ignore_endian=True) - ident_offset = self.get_int(ignore_endian=True) - n_special_slots = self.get_int(ignore_endian=True) - n_code_slots = self.get_int(ignore_endian=True) - code_limit = self.get_int(ignore_endian=True) - hash_size = int(self.__file.read(1).hex(), 16) - hash_type = mdictionary.hashes[int(self.__file.read(1).hex(), 16)] - - if version >= 0x20200: - platform = int(self.__file.read(1).hex(), 16) - else: - self.__file.read(1) # skip spare1 - - page_size = int(round(exp( - int(self.__file.read(1).hex(), 16) * log(2)))) - - self.__file.read(4) # skip spare2 - - if version >= 0x20100: - scatter_offset = self.get_int(ignore_endian=True) - if version >= 0x20200: - team_id_offset = self.get_int(ignore_endian=True) - self.__file.seek(true_offset + team_id_offset) - team_id = self.get_string() - - self.__file.seek(true_offset + ident_offset) - - identity = self.get_string() - - self.__macho['code_signature']['codedirectory'] = { - 'size': size, - 'version': version, - 'flags': flags, - 'hash_offset': hash_offset, - 'n_special_slots': n_special_slots, - 'n_code_slots': n_code_slots, - 'code_limit': code_limit, - 'hash_size': hash_size, - 'hash_type': hash_type, - 'page_size': page_size, - 'identity': identity, - 'hashes': [] - } - - if version >= 0x20100: - self.__macho['code_signature']['codedirectory']['scatter_offset'] = scatter_offset - if version >= 0x20200: - self.__macho['code_signature']['codedirectory']['platform'] = platform - self.__macho['code_signature']['codedirectory']['team_id_offset'] = team_id_offset - self.__macho['code_signature']['codedirectory']['team_id'] = team_id - - self.__file.seek( - true_offset + hash_offset - n_special_slots * hash_size) - - count = n_special_slots + n_code_slots - - for _ in range(count): - self.__macho['code_signature']['codedirectory']['hashes'].append( - self.__file.read(hash_size).hex()) - - self.__file.seek(prev) - - def get_oid(self, db, p): - """OID parser implementation from: - - http://opensource.apple.com/source/Security/Security-57337.20.44/ - OSX/libsecurity_cdsa_utilities/lib/cssmdata.cpp - """ - - q = 0 - - while True: - q = q * 128 + (db[p] & ~0x80) - - if p < len(db) and db[p] & 0x80: - p += 1 - else: - p += 1 - break - - return q, p - - def to_oid(self, length): - """Convert bytes to correct OID.""" - - if length == 0: - return '' - - data_bytes = [ - int(self.__file.read(1).hex(), 16) for i in range(length) - ] - - p = 0 - - # first byte is composite (q1, q2) - oid1, p = self.get_oid(data_bytes, p) - - q1 = min(oid1 / 40, 2) - - data = str(q1) + '.' + str(oid1 - q1 * 40) - - while p < len(data_bytes): - d, p = self.get_oid(data_bytes, p) - data += '.' + str(d) - - self.__file.read(-length & 3) - - return data - - def parse_entitlement(self, sig_offset, index_offset): - """Parse entitlement from code signature.""" - - prev = self.__file.tell() - - true_offset = sig_offset + index_offset - - self.__file.seek(true_offset) - - magic = self.get_int(ignore_endian=True) - - if magic != mdictionary.signatures['ENTITLEMENT']: - self.add_abnormality('Bad magic "{}" for entitlement at offset ' - '"{}".'.format(magic, self.__file.tell() - 4)) - - return - - # size of plist minus magic and size values - size = self.get_int(ignore_endian=True) - 8 - - try: - plist = loads(self.__file.read(size)) - except Exception as exc: - plist = {} - self.add_abnormality('Unable to parse plist at offset "{}". ' - '{}.'.format(self.__file.tell() - size, exc)) - - if 'entitlements' not in self.__macho['code_signature']: - self.__macho['code_signature']['entitlements'] = [] - - self.__macho['code_signature']['entitlements'].append({ - 'size': size, - 'plist': plist - }) - - self.__file.seek(prev) - - def parse_data(self): - """Parse data for requirement expression.""" - - length = self.get_int(ignore_endian=True) - - data = self.__file.read(length) - - self.__file.read(-length & 3) # skip padding - - return data - - def parse_match(self): - """Parse match for requirement expression.""" - - match_type = self.get_int(ignore_endian=True) - - if match_type in mdictionary.matches: - match_type = mdictionary.matches[match_type] - - if match_type == 'matchExists': - return ' /* exists */' - elif match_type == 'matchEqual': - return ' = "{}"'.format(self.parse_data().decode()) - elif match_type == 'matchContains': - return ' ~ "{}"'.format(self.parse_data().decode()) - elif match_type == 'matchBeginsWith': - return ' = "{}*"'.format(self.parse_data().decode()) - elif match_type == 'matchEndsWith': - return ' = "*{}"'.format(self.parse_data().decode()) - elif match_type == 'matchLessThan': - return ' < {}'.format(int(self.parse_data(), 16)) - elif match_type == 'matchGreaterThan': - return ' > {}'.format(int(self.parse_data(), 16)) - elif match_type == 'matchLessEqual': - return ' <= {}'.format(int(self.parse_data(), 16)) - elif match_type == 'matchGreaterEqual': - return ' >= {}'.format(int(self.parse_data(), 16)) - else: - return ' UNKNOWN MATCH TYPE "{}"'.format(match_type) - - def parse_expression(self, in_or=False): - """Parse requirement expression. Recurse if necessary""" - - # Zero out flags in high byte (TODO: Look into flags field) - operator = self.get_int(ignore_endian=True) - operator = mdictionary.operators[operator & 0xfff] - - expression = '' - - if operator == 'False': - expression += 'never' - elif operator == 'True': - expression += 'always' - elif operator == 'Ident': - expression += 'identity "{}"'.format(self.parse_data().decode()) - elif operator == 'AppleAnchor': - expression += 'anchor apple' - elif operator == 'AppleGenericAnchor': - expression += 'anchor apple generic' - elif operator == 'AnchorHash': - cert_slot = self.get_int(ignore_endian=True) - - if cert_slot in mdictionary.cert_slots: - cert_slot = mdictionary.cert_slots[cert_slot] - - expression += 'certificate {} = {}'.format( - cert_slot, self.parse_data().decode()) - elif operator == 'InfoKeyValue': - expression += 'info[{}] = "{}"'.format( - self.parse_data().decode(), self.parse_data().decode()) - elif operator == 'And': - if in_or: - expression += '({} and {})'.format( - self.parse_expression(), self.parse_expression()) - else: - expression += '{} and {}'.format( - self.parse_expression(), self.parse_expression()) - elif operator == 'Or': - if in_or: - expression += '({} or {})'.format( - self.parse_expression(in_or=True), - self.parse_expression(in_or=True)) - else: - expression += '{} or {}'.format( - self.parse_expression(in_or=True), - self.parse_expression(in_or=True)) - elif operator == 'Not': - expression += '! {}'.format(self.parse_expression()) - elif operator == 'CDHash': - expression += 'cdhash {}'.format(self.parse_data().decode()) - elif operator == 'InfoKeyField': - expression += 'info[{}]{}'.format( - self.parse_data().decode(), self.parse_match()) - elif operator == 'EntitlementField': - expression += 'entitlement[{}]{}'.format( - self.parse_data().decode(), self.parse_match()) - elif operator == 'CertField': - cert_slot = self.get_int(ignore_endian=True) - - if cert_slot in mdictionary.cert_slots: - cert_slot = mdictionary.cert_slots[cert_slot] - - expression += 'certificate {}[{}]{}'.format( - cert_slot, self.parse_data().decode(), self.parse_match()) - elif operator == 'CertGeneric': - cert_slot = self.get_int(ignore_endian=True) - - if cert_slot in mdictionary.cert_slots: - cert_slot = mdictionary.cert_slots[cert_slot] - - length = self.get_int(ignore_endian=True) - - expression += 'certificate {}[field.{}]{}'.format( - cert_slot, self.to_oid(length), self.parse_match()) - elif operator == 'CertPolicy': - cert_slot = self.get_int(ignore_endian=True) - - if cert_slot in mdictionary.cert_slots: - cert_slot = mdictionary.cert_slots[cert_slot] - - expression += 'certificate {}[policy.{}]{}'.format( - cert_slot, self.parse_data().decode(), self.parse_match()) - elif operator == 'TrustedCert': - cert_slot = self.get_int(ignore_endian=True) - - if cert_slot in mdictionary.cert_slots: - cert_slot = mdictionary.cert_slots[cert_slot] - - expression += 'certificate {} trusted'.format(cert_slot) - elif operator == 'TrustedCerts': - expression += 'anchor trusted' - elif operator == 'NamedAnchor': - expression += 'anchor apple {}'.format(self.parse_data().decode()) - elif operator == 'NamedCode': - expression += '({})'.format(self.parse_data().decode()) - elif operator == 'Platform': - platform = self.get_int(ignore_endian=True) - expression += 'platform = {}'.format(platform) - - return expression - - def parse_requirement(self, reqs_offset, req_type, req_offset): - """Parse single requirement from code signature.""" - - prev = self.__file.tell() - - true_offset = reqs_offset + req_offset - - self.__file.seek(true_offset) - - magic = self.get_int(ignore_endian=True) - - if magic != mdictionary.signatures['REQUIREMENT']: - self.add_abnormality('Bad magic "{}" for requirement at offset ' - '"{}".'.format(magic, self.__file.tell() - 4)) - - return - - self.__file.read(8) # skip size and kind fields - # (TODO: look into ``kind`` field) - - self.__macho['code_signature']['requirements'].append({ - 'req_type': req_type, - 'req_offset': req_offset, - 'expression': self.parse_expression() - }) - - self.__file.seek(prev) - - def parse_requirements(self, sig_offset, index_offset): - """Parse requirements from code signature.""" - - prev = self.__file.tell() - - true_offset = sig_offset + index_offset - - self.__file.seek(true_offset) - - magic = self.get_int(ignore_endian=True) - - if magic != mdictionary.signatures['REQUIREMENTS']: - self.add_abnormality('Bad magic "{}" for requirements at offset ' - '"{}".'.format(magic, self.__file.tell() - 4)) - - return - - self.__file.read(4) # skip size field - - count = self.get_int(ignore_endian=True) - - self.__macho['code_signature']['requirements'] = [] - - for _ in range(count): - req_type = self.get_int(ignore_endian=True) - req_type = mdictionary.requirements[req_type] - - req_offset = self.get_int(ignore_endian=True) - - self.parse_requirement(true_offset, req_type, req_offset) - - self.__file.seek(prev) - - def parse_sig(self, offset, size, lc_codesig): - """Parse code signature in its entirety.""" - - if lc_codesig['dataoff'] + lc_codesig['datasize'] > size: - self.add_abnormality('CODE_SIGNATURE at offset "{}" with size ' - '"{}" greater than mach-o size.'.format( - offset + lc_codesig['dataoff'], - lc_codesig['datasize'])) - - return - - true_offset = offset + lc_codesig['dataoff'] - - self.__file.seek(true_offset) - - magic = self.get_int(ignore_endian=True) - - if magic != mdictionary.signatures['EMBEDDED_SIGNATURE']: - self.add_abnormality('Bad magic "{}" for embedded signature at ' - 'offset "{}".'.format(magic, true_offset)) - - return - - self.__macho['code_signature'] = {} - - size = self.get_int(ignore_endian=True) - count = self.get_int(ignore_endian=True) - - for _ in range(count): - index_type = self.get_int(ignore_endian=True) - - if index_type in mdictionary.indeces: - index_type = mdictionary.indeces[index_type] - else: - self.add_abnormality('Unknown code signature index type "{}" ' - 'at offset "{}".'.format( - index_type, self.__file.tell() - 4)) - - self.__file.read(4) # skip offset - continue - - index_offset = self.get_int(ignore_endian=True) - - if index_type == 'SignatureSlot': - self.parse_certs(true_offset, index_offset) - elif index_type == 'CodeDirectorySlot': - self.parse_codedirectory(true_offset, index_offset) - elif index_type == 'EntitlementSlot': - self.parse_entitlement(true_offset, index_offset) - elif index_type == 'RequirementsSlot': - self.parse_requirements(true_offset, index_offset) - - def parse_macho(self, offset, size): - """Parse mach-o binary, possibly contained within a - universal binary. - """ - - if size is None: - self.__file.seek(0, SEEK_END) # find the end of the file - size = self.__file.tell() - - # jump to the location of this mach-o within the file - self.__file.seek(offset) - - identity = self.identify_file() - self.__is_64_bit = identity[0] - self.__is_little_endian = identity[1] - - cputype = self.get_int() # CPU type - subtype = self.get_int() # CPU sub-type - filetype = self.get_int() # Mach-o file type - nlcs = self.get_int() # Number of load commands - slcs = self.get_int() # Size of load commands - flags = self.get_int() # Mach-o flags - - if self.__is_64_bit: - self.__file.read(4) # skip padding - - if cputype in mdictionary.cputypes: - if subtype in mdictionary.cputypes[cputype]: - subtype = mdictionary.cputypes[cputype][subtype] - else: - self.add_abnormality('Unknown SUBTYPE "{}" for CPUTYPE "{}" ' - 'at offset "{}".'.format( - subtype, cputype, offset + 8)) - - cputype = mdictionary.cputypes[cputype][-2] - else: - raise ValueError('Unknown or unsupported CPUTYPE "{}" at offset ' - '"{}".'.format(cputype, offset + 4)) - - if filetype in mdictionary.filetypes: - filetype = mdictionary.filetypes[filetype] - else: - self.add_abnormality('Unknown FILETYPE "{}" at offset ' - '"{}".'.format(filetype, offset + 12)) - - flags = self.parse_macho_flags(flags) - - self.__macho['cputype'] = cputype - self.__macho['subtype'] = subtype - self.__macho['filetype'] = filetype - self.__macho['nlcs'] = nlcs - self.__macho['slcs'] = slcs - self.__macho['flags'] = flags - - # Parse load commands - self.parse_lcs(offset, size, nlcs, slcs) - - lcs = list(map(lambda x: x['cmd'], self.__macho['lcs'])) - - # Check for symbol and strings tables and parse if present - if 'SYMTAB' in lcs: - lc_symtab = self.__macho['lcs'][lcs.index('SYMTAB')] - - self.parse_syms(offset, size, lc_symtab) - self.parse_strings(offset, size, lc_symtab) - - # If symbol and strings tables were parsed, parse imports - if 'symtab' in self.__macho and 'strtab' in self.__macho: - lc_dysymtab = None - lc_dylibs = None - - # Check for presence of DYSYMTAB load command and, if present, use - # it to parse undefined external symbols (imports). Otherwise, find - # imports manually. - if 'DYSYMTAB' in lcs: - lc_dysymtab = self.__macho['lcs'][lcs.index('DYSYMTAB')] - self.dysymtab = lc_dysymtab - - # Check if the static linker used the two-level namespace feature. - # If so, pass in the list of dynamic libraries (dylibs) given in - # the 'DYLIB' load commands. - if 'TWOLEVEL' in self.__macho['flags']: - lc_dylibs = list(filter(lambda x: x['cmd'].endswith('DYLIB'), - self.__macho['lcs'])) - - self.parse_imports(offset, size, lc_symtab, - lc_dysymtab=lc_dysymtab, lc_dylibs=lc_dylibs) - - # Check for a code signature and parse if present - if 'CODE_SIGNATURE' in lcs: - lc_codesig = self.__macho['lcs'][lcs.index('CODE_SIGNATURE')] - - #self.parse_sig(offset, size, lc_codesig) - - #self.__macho['strtab'] = None - #self.__macho['symtab'] = None - self.__macho['imports'] = None - - - return self.__macho - - def parse_universal(self): - """Parses universal binary.""" - - self.__output['universal'] = { - 'machos': [] - } - - # number of mach-o's contained in this binary - n_machos = self.get_int(ignore_endian=True) - - for i in range(n_machos): - self.__file.read(8) # skip cputype and subtype fields - - offset = self.get_int(ignore_endian=True) - size = self.get_int(ignore_endian=True) - - self.__file.read(4) # skip align field - - prev = self.__file.tell() - self.parse_macho(offset, size) - self.__file.seek(prev) - - self.__output['universal']['machos'].append(self.__macho.copy()) - self.__macho.clear() - - - def u_get_offset(self, cpu_type = None, uni_index = None): - self.__file.seek(0) # return to beginning of file - - if self.__file.read(4) != b'\xca\xfe\xba\xbe': - # Throw a fit - logger.critical("Wrong magic for universal binary?") - - n_machos = self.get_int(ignore_endian=True) - - for i in range(n_machos): - self.__file.read(8) # skip cputype and subtype fields - - offset = self.get_int(ignore_endian=True) - size = self.get_int(ignore_endian=True) - - self.__file.read(4) # skip align field - - # Read the cpu type and subtype in the macho - old = self.__file.tell() - self.__file.seek(offset) - identity = self.identify_file() - self.__is_64_bit = identity[0] - self.__is_little_endian = identity[1] - - cputype = self.get_int() # CPU type - subtype = self.get_int() # CPU sub-type - - if cputype in mdictionary.cputypes: - if subtype in mdictionary.cputypes[cputype]: - subtype = mdictionary.cputypes[cputype][subtype] - else: - logger.debug("UNKNOWN CPU TYPE: " + str(cputype)) - - cputype = mdictionary.cputypes[cputype][-2] - - #print(f"CPU TYPE: {cputype} SUBTYPE: {subtype}") - - self.__file.seek(old) - if i == uni_index or cpu_type == cputype: - return offset, size - - #prev = self.__file.tell() - - #self.parse_macho(offset, size) - #self.__file.seek(prev) - - #self.__output['universal']['machos'].append(self.__macho.copy()) - #self.__macho.clear() - - - def parse_file(self): - """Determines characteristics about the entire file and begins - to parse. - """ - - contents = self.__file.read() - - self.__output['size'] = len(contents) - - self.__output['hashes'] = { - 'md5': hashlib.md5(contents).hexdigest(), - 'sha1': hashlib.sha1(contents).hexdigest(), - 'sha256': hashlib.sha256(contents).hexdigest() - } - - self.__file.seek(0) # return to beginning of file - - if self.__file.read(4) == b'\xca\xfe\xba\xbe': - self.parse_universal() - else: - self.parse_macho(0, self.__output['size']) - self.__output['macho'] = self.__macho - - def parse(self, certs: bool=False, out=None): - """Parse Mach-O file at given path, and either return a dict - or write output to provided file. - """ - - self.__extract_certs = certs - - self.parse_file() - - if out is None: - return self.__output - - dump(self.__output, out) - -class mdictionary: - cert_slots = { - -1: 'root', - 0: 'leaf' - } - - hashes = { - 0: 'No Hash', - 1: 'SHA-1', - 2: 'SHA-256' - } - - segment_flags = { - 1: 'HIGHVM', - 2: 'FVMLIB', - 4: 'NORELOC', - 8: 'PROTECTED_VERSION_1' - } - - n_types = { - 0x0: 'UNDF', - 0x2: 'ABS', - 0xe: 'SECT', - 0xc: 'PBUD', - 0xa: 'INDR' - } - - machos = { - 4277009102: (False, False), # 32 bit, big endian - 4277009103: (True, False), # 64 bit, big endian - 3472551422: (False, True), # 32 bit, little endian - 3489328638: (True, True) # 64 bit, little endian - } - - requirements = { - 1: 'HostRequirementType', - 2: 'GuestRequirementType', - 3: 'DesignatedRequirementType', - 4: 'LibraryRequirementType', - 5: 'PluginRequirementType', - } - - indeces = { - 0: 'CodeDirectorySlot', - 1: 'InfoSlot', - 2: 'RequirementsSlot', - 3: 'ResourceDirSlot', - 4: 'ApplicationSlot', - 5: 'EntitlementSlot', - 0x10000: 'SignatureSlot' - } - - matches = { - 0: 'matchExists', - 1: 'matchEqual', - 2: 'matchContains', - 3: 'matchBeginsWith', - 4: 'matchEndsWith', - 5: 'matchLessThan', - 6: 'matchGreaterThan', - 7: 'matchLessEqual', - 8: 'matchGreaterEqual' - } - - protections = { - 0b000: '---', - 0b001: 'r--', - 0b010: '-w-', - 0b011: 'rw-', - 0b100: '--x', - 0b101: 'r-x', - 0b110: '-wx', - 0b111: 'rwx' - } - - signatures = { - 'REQUIREMENT': 0xfade0c00, - 'REQUIREMENTS': 0xfade0c01, - 'CODEDIRECTORY': 0xfade0c02, - 'ENTITLEMENT': 0xfade7171, - 'BLOBWRAPPER': 0xfade0b01, - 'EMBEDDED_SIGNATURE': 0xfade0cc0, - 'DETACHED_SIGNATURE': 0xfade0cc1, - 'CODE_SIGN_DRS': 0xfade0c05 - } - - section_attrs = { - 0x80000000: 'PURE_INSTRUCTIONS', - 0x40000000: 'NO_TOC', - 0x20000000: 'STRIP_STATIC_SYMS', - 0x10000000: 'NO_DEAD_STRIP', - 0x08000000: 'LIVE_SUPPORT', - 0x04000000: 'SELF_MODIFYING_CODE', - 0x02000000: 'DEBUG', - 0x00000400: 'SOME_INSTRUCTIONS', - 0x00000200: 'EXT_RELOC', - 0x00000100: 'LOC_RELOC' - } - - filetypes = { - 1: 'OBJECT', - 2: 'EXECUTE', - 3: 'FVMLIB', - 4: 'CORE', - 5: 'PRELOAD', - 6: 'DYLIB', - 7: 'DYLINKER', - 8: 'BUNDLE', - 9: 'DYLIB_STUB', - 10: 'DSYM', - 11: 'KEXT_BUNDLE' - } - - section_types = { - 0x0: 'REGULAR', - 0x1: 'ZEROFILL', - 0x2: 'CSTRING_LITERALS', - 0x3: '4BYTE_LITERALS', - 0x4: '8BYTE_LITERALS', - 0x5: 'LITERAL_POINTERS', - 0x6: 'NON_LAZY_SYMBOL_POINTERS', - 0x7: 'LAZY_SYMBOL_POINTERS', - 0x8: 'SYMBOL_STUBS', - 0x9: 'MOD_INIT_FUNC_POINTERS', - 0xa: 'MOD_TERM_FUNC_POINTERS', - 0xb: 'COALESCED', - 0xc: 'GB_ZEROFILL', - 0xd: 'INTERPOSING', - 0xe: '16BYTE_LITERALS', - 0xf: 'DTRACE_DOF', - 0x10: 'LAZY_DYLIB_SYMBOL_POINTERS', - 0x11: 'THREAD_LOCAL_REGULAR', - 0x12: 'THREAD_LOCAL_ZEROFILL', - 0x13: 'THREAD_LOCAL_VARIABLES', - 0x14: 'THREAD_LOCAL_VARIABLE_POINTERS', - 0x15: 'THREAD_LOCAL_INIT_FUNCTION_POINTERS' - } - - operators = { - 0: 'False', - 1: 'True', - 2: 'Ident', - 3: 'AppleAnchor', - 4: 'AnchorHash', - 5: 'InfoKeyValue', - 6: 'And', - 7: 'Or', - 8: 'CDHash', - 9: 'Not', - 10: 'InfoKeyField', - 11: 'CertField', - 12: 'TrustedCert', - 13: 'TrustedCerts', - 14: 'CertGeneric', - 15: 'AppleGenericAnchor', - 16: 'EntitlementField', - 17: 'CertPolicy', - 18: 'NamedAnchor', - 19: 'NamedCode', - 20: 'Platform' - } - - thread_states = { - 1: 'x86_THREAD_STATE32', - 2: 'x86_FLOAT_STATE32', - 3: 'x86_EXCEPTION_STATE32', - 4: 'x86_THREAD_STATE64', - 5: 'x86_FLOAT_STATE64', - 6: 'x86_EXCEPTION_STATE64', - 7: 'x86_THREAD_STATE', - 8: 'x86_FLOAT_STATE', - 9: 'x86_EXCEPTION_STATE', - 10: 'x86_DEBUG_STATE32', - 11: 'x86_DEBUG_STATE64', - 12: 'x86_DEBUG_STATE', - 13: 'THREAD_STATE_NONE', - 14: 'x86_SAVED_STATE_1 (INTERNAL ONLY)', - 15: 'x86_SAVED_STATE_2 (INTERNAL ONLY)', - 16: 'x86_AVX_STATE32', - 17: 'x86_AVX_STATE64', - 18: 'x86_AVX_STATE' - } - - flags = { - 1: 'NOUNDEFS', - 2: 'INCRLINK', - 4: 'DYLDLINK', - 8: 'BINDATLOAD', - 16: 'PREBOUND', - 32: 'SPLIT_SEGS', - 64: 'LAZY_INIT', - 128: 'TWOLEVEL', - 256: 'FORCE_FLAT', - 512: 'NOMULTIDEFS', - 1024: 'NOFIXPREBINDING', - 2048: 'PREBINDABLE', - 4096: 'ALLMODSBOUND', - 8192: 'SUBSECTIONS_VIA_SYMBOLS', - 16384: 'CANONICAL', - 32768: 'WEAK_DEFINES', - 65536: 'BINDS_TO_WEAK', - 131072: 'ALLOW_STACK_EXECUTION', - 262144: 'ROOT_SAFE', - 524288: 'SETUID_SAFE', - 1048576: 'NOREEXPORTED_DYLIBS', - 2097152: 'PIE', - 4194304: 'DEAD_STRIPPABLE_DYLIB', - 8388608: 'HAS_TLV_DESCRIPTORS', - 16777216: 'NO_HEAP_EXECUTION', - 33554432: 'APP_EXTENSION_SAFE' - } - - stabs = { - 0x20: 'GSYM', - 0x22: 'FNAME', - 0x24: 'FUN', - 0x26: 'STSYM', - 0x28: 'LCSYM', - 0x2a: 'MAIN', - 0x2e: 'BNSYM', - 0x30: 'PC', - 0x32: 'AST', - 0x3a: 'MAC_UNDEF', - 0x3c: 'OPT', - 0x40: 'RSYM', - 0x44: 'SLINE', - 0x46: 'DSLINE', - 0x48: 'BSLINE', - 0x4e: 'ENSYM', - 0x60: 'SSYM', - 0x64: 'SO', - 0x66: 'OSO', - 0x80: 'LSYM', - 0x82: 'BINCL', - 0x84: 'SOL', - 0x86: 'PARAMS', - 0x88: 'VERSION', - 0x8a: 'OLEVEL', - 0xa0: 'PSYM', - 0xa2: 'EINCL', - 0xa4: 'ENTRY', - 0xc0: 'LBRAC', - 0xc2: 'EXCL', - 0xe0: 'RBRAC', - 0xe2: 'BCOMM', - 0xe4: 'ECOMM', - 0xe8: 'ECOML', - 0xfe: 'LENG' - } - - loadcommands = { - 1: 'SEGMENT', - 2: 'SYMTAB', - 3: 'SYMSEG', - 4: 'THREAD', - 5: 'UNIXTHREAD', - 6: 'LOADFVMLIB', - 7: 'IDFVMLIB', - 8: 'IDENT', - 9: 'FVMFILE', - 10: 'PREPAGE', - 11: 'DYSYMTAB', - 12: 'LOAD_DYLIB', - 13: 'ID_DYLIB', - 14: 'LOAD_DYLINKER', - 15: 'ID_DYLINKER', - 16: 'PREBOUND_DYLIB', - 17: 'ROUTINES', - 18: 'SUB_FRAMEWORK', - 19: 'SUB_UMBRELLA', - 20: 'SUB_CLIENT', - 21: 'SUB_LIBRARY', - 22: 'TWOLEVEL_HINTS', - 23: 'PREBIND_CKSUM', - 25: 'SEGMENT_64', - 26: 'ROUTINES_64', - 27: 'UUID', - 29: 'CODE_SIGNATURE', - 30: 'SEGMENT_SPLIT_INFO', - 32: 'LAZY_LOAD_DYLIB', - 33: 'ENCRYPTION_INFO', - 34: 'DYLD_INFO', - 36: 'VERSION_MIN_MACOSX', - 37: 'VERSION_MIN_IPHONEOS', - 38: 'FUNCTION_STARTS', - 39: 'DYLD_ENVIRONMENT', - 41: 'DATA_IN_CODE', - 42: 'SOURCE_VERSION', - 43: 'DYLIB_CODE_SIGN_DRS', - 44: 'ENCRYPTION_INFO_64', - 45: 'LINKER_OPTION', - 46: 'LINKER_OPTIMIZATION_HINT', - 47: 'VERSION_MIN_TVOS', - 48: 'VERSION_MIN_WATCHOS', - 49: 'NOTE', - 50: 'BUILD_VERSION', - 2147483672: 'LOAD_WEAK_DYLIB', - 2147483676: 'RPATH', - 2147483679: 'REEXPORT_DYLIB', - 2147483682: 'DYLD_INFO_ONLY', - 2147483683: 'LOAD_UPWARD_DYLIB', - 2147483688: 'MAIN', - } - - # CPU Types & Subtypes as defined in - # http://opensource.apple.com/source/cctools/cctools-822/include/mach/machine.h - cputypes = { - -1: { - -2: 'ANY', - -1: 'MULTIPLE', - 0: 'LITTLE_ENDIAN', - 1: 'BIG_ENDIAN' - }, - 1: { - -2: 'VAX', - -1: 'MULTIPLE', - 0: 'VAX_ALL', - 1: 'VAX780', - 2: 'VAX785', - 3: 'VAX750', - 4: 'VAX730', - 5: 'UVAXI', - 6: 'UVAXII', - 7: 'VAX8200', - 8: 'VAX8500', - 9: 'VAX8600', - 10: 'VAX8650', - 11: 'VAX8800', - 12: 'UVAXIII' - }, - 6: { - -2: 'MC680x0', - -1: 'MULTIPLE', - 1: 'MC680x0_ALL or MC68030', - 2: 'MC68040', - 3: 'MC68030_ONLY' - }, - 7: {-2: 'X86 (I386)', - -1: 'MULITPLE', - 0: 'INTEL_MODEL_ALL', - 3: 'X86_ALL, X86_64_ALL, I386_ALL, or 386', - 4: 'X86_ARCH1 or 486', - 5: '586 or PENT', - 8: 'X86_64_H or PENTIUM_3', - 9: 'PENTIUM_M', - 10: 'PENTIUM_4', - 11: 'ITANIUM', - 12: 'XEON', - 15: 'INTEL_FAMILY_MAX', - 22: 'PENTPRO', - 24: 'PENTIUM_3_M', - 26: 'PENTIUM_4_M', - 27: 'ITANIUM_2', - 28: 'XEON_MP', - 40: 'PENTIUM_3_XEON', - 54: 'PENTII_M3', - 86: 'PENTII_M5', - 103: 'CELERON', - 119: 'CELERON_MOBILE', - 132: '486SX' - }, - 10: { - -2: 'MC98000', - -1: 'MULTIPLE', - 0: 'MC98000_ALL', - 1: 'MC98601' - }, - 11: { - -2: 'HPPA', - -1: 'MULITPLE', - 0: 'HPPA_ALL or HPPA_7100', - 1: 'HPPA_7100LC' - }, - 12: { - -2: 'ARM', - -1: 'MULTIPLE', - 0: 'ARM_ALL', - 1: 'ARM_A500_ARCH', - 2: 'ARM_A500', - 3: 'ARM_A440', - 4: 'ARM_M4', - 5: 'ARM_V4T', - 6: 'ARM_V6', - 7: 'ARM_V5TEJ', - 8: 'ARM_XSCALE', - 9: 'ARM_V7', - 10: 'ARM_V7F', - 11: 'ARM_V7S', - 12: 'ARM_V7K', - 13: 'ARM_V8', - 14: 'ARM_V6M', - 15: 'ARM_V7M', - 16: 'ARM_V7EM' - }, - 13: { - -2: 'MC88000', - -1: 'MULTIPLE', - 0: 'MC88000_ALL', - 1: 'MMAX_JPC or MC88100', - 2: 'MC88110' - }, - 14: { - -2: 'SPARC', - -1: 'MULTIPLE', - 0: 'SPARC_ALL or SUN4_ALL', - 1: 'SUN4_260', - 2: 'SUN4_110' - }, - 15: { - -2: 'I860 (big-endian)', - -1: 'MULTIPLE', - 0: 'I860_ALL', - 1: 'I860_860' - }, - 18: { - -2: 'POWERPC', - -1: 'MULTIPLE', - 0: 'POWERPC_ALL', - 1: 'POWERPC_601', - 2: 'POWERPC_602', - 3: 'POWERPC_603', - 4: 'POWERPC_603e', - 5: 'POWERPC_603ev', - 6: 'POWERPC_604', - 7: 'POWERPC_604e', - 8: 'POWERPC_620', - 9: 'POWERPC_750', - 10: 'POWERPC_7400', - 11: 'POWERPC_7450', - 100: 'POWERPC_970' - }, - 16777223: { - -2: 'X86_64', - -1: 'MULTIPLE', - 0: 'INTEL_MODEL_ALL', - 3: 'X86_ALL, X86_64_ALL, I386_ALL, or 386', - 4: 'X86_ARCH1 or 486', - 5: '586 or PENT', - 8: 'X86_64_H or PENTIUM_3', - 9: 'PENTIUM_M', - 10: 'PENTIUM_4', - 11: 'ITANIUM', - 12: 'XEON', - 15: 'INTEL_FAMILY_MAX', - 22: 'PENTPRO', - 24: 'PENTIUM_3_M', - 26: 'PENTIUM_4_M', - 27: 'ITANIUM_2', - 28: 'XEON_MP', - 40: 'PENTIUM_3_XEON', - 54: 'PENTII_M3', - 86: 'PENTII_M5', - 103: 'CELERON', - 119: 'CELERON_MOBILE', - 132: '486SX', - 2147483648 + 0: 'INTEL_MODEL_ALL', - 2147483648 + 3: 'X86_ALL, X86_64_ALL, I386_ALL, or 386', - 2147483648 + 4: 'X86_ARCH1 or 486', - 2147483648 + 5: '586 or PENT', - 2147483648 + 8: 'X86_64_H or PENTIUM_3', - 2147483648 + 9: 'PENTIUM_M', - 2147483648 + 10: 'PENTIUM_4', - 2147483648 + 11: 'ITANIUM', - 2147483648 + 12: 'XEON', - 2147483648 + 15: 'INTEL_FAMILY_MAX', - 2147483648 + 22: 'PENTPRO', - 2147483648 + 24: 'PENTIUM_3_M', - 2147483648 + 26: 'PENTIUM_4_M', - 2147483648 + 27: 'ITANIUM_2', - 2147483648 + 28: 'XEON_MP', - 2147483648 + 40: 'PENTIUM_3_XEON', - 2147483648 + 54: 'PENTII_M3', - 2147483648 + 86: 'PENTII_M5', - 2147483648 + 103: 'CELERON', - 2147483648 + 119: 'CELERON_MOBILE', - 2147483648 + 132: '486SX' - }, - 16777228: { - -2: 'ARM64', - -1: 'MULTIPLE', - 0: 'ARM64_ALL', - 1: 'ARM64_V8', - 2147483648 + 0: 'ARM64_ALL', - 2147483648 + 1: 'ARM64_V8' - }, - 16777234: { - -2: 'POWERPC64', - -1: 'MULTIPLE', - 0: 'POWERPC_ALL', - 1: 'POWERPC_601', - 2: 'POWERPC_602', - 3: 'POWERPC_603', - 4: 'POWERPC_603e', - 5: 'POWERPC_603ev', - 6: 'POWERPC_604', - 7: 'POWERPC_604e', - 8: 'POWERPC_620', - 9: 'POWERPC_750', - 10: 'POWERPC_7400', - 11: 'POWERPC_7450', - 100: 'POWERPC_970', - 2147483648 + 0: 'POWERPC_ALL (LIB64)', - 2147483648 + 1: 'POWERPC_601 (LIB64)', - 2147483648 + 2: 'POWERPC_602 (LIB64)', - 2147483648 + 3: 'POWERPC_603 (LIB64)', - 2147483648 + 4: 'POWERPC_603e (LIB64)', - 2147483648 + 5: 'POWERPC_603ev (LIB64)', - 2147483648 + 6: 'POWERPC_604 (LIB64)', - 2147483648 + 7: 'POWERPC_604e (LIB64)', - 2147483648 + 8: 'POWERPC_620 (LIB64)', - 2147483648 + 9: 'POWERPC_750 (LIB64)', - 2147483648 + 10: 'POWERPC_7400 (LIB64)', - 2147483648 + 11: 'POWERPC_7450 (LIB64)', - 2147483648 + 100: 'POWERPC_970 (LIB64)' - } - } \ No newline at end of file diff --git a/emulated/nac.py b/emulated/nac.py deleted file mode 100644 index 4eeadc6..0000000 --- a/emulated/nac.py +++ /dev/null @@ -1,422 +0,0 @@ -import hashlib -from . import mparser as macholibre -from .jelly import Jelly -import plistlib -import logging -logger = logging.getLogger("nac") - -BINARY_HASH = "e1181ccad82e6629d52c6a006645ad87ee59bd13" -BINARY_PATH = "emulated/IMDAppleServices" -BINARY_URL = "https://github.com/JJTech0130/nacserver/raw/main/IMDAppleServices" - -FAKE_DATA = plistlib.load(open("emulated/data.plist", "rb")) - -def load_binary() -> bytes: - # Open the file at BINARY_PATH, check the hash, and return the binary - # If the hash doesn't match, raise an exception - # Download the binary if it doesn't exist - import os, requests - if not os.path.exists(BINARY_PATH): - logger.info("Downloading IMDAppleServices") - resp = requests.get(BINARY_URL) - b = resp.content - # Save the binary - open(BINARY_PATH, "wb").write(b) - else: - logger.debug("Using already downloaded IMDAppleServices") - b = open(BINARY_PATH, "rb").read() - if hashlib.sha1(b).hexdigest() != BINARY_HASH: - raise Exception("Hashes don't match") - return b - - -def get_x64_slice(binary: bytes) -> bytes: - # Get the x64 slice of the binary - # If there is no x64 slice, raise an exception - p = macholibre.Parser(binary) - # Parse the binary to find the x64 slice - off, size = p.u_get_offset(cpu_type="X86_64") - return binary[off : off + size] - - -def nac_init(j: Jelly, cert: bytes): - # Allocate memory for the cert - cert_addr = j.malloc(len(cert)) - j.uc.mem_write(cert_addr, cert) - - # Allocate memory for the outputs - out_validation_ctx_addr = j.malloc(8) - out_request_bytes_addr = j.malloc(8) - out_request_len_addr = j.malloc(8) - - # Call the function - ret = j.instr.call( - 0xB1DB0, - [ - cert_addr, - len(cert), - out_validation_ctx_addr, - out_request_bytes_addr, - out_request_len_addr, - ], - ) - - #print(hex(ret)) - - if ret != 0: - n = ret & 0xffffffff - n = (n ^ 0x80000000) - 0x80000000 - raise Exception(f"Error calling nac_init: {n}") - - # Get the outputs - validation_ctx_addr = j.uc.mem_read(out_validation_ctx_addr, 8) - request_bytes_addr = j.uc.mem_read(out_request_bytes_addr, 8) - request_len = j.uc.mem_read(out_request_len_addr, 8) - - request_bytes_addr = int.from_bytes(request_bytes_addr, 'little') - request_len = int.from_bytes(request_len, 'little') - - logger.debug(f"Request @ {hex(request_bytes_addr)} : {hex(request_len)}") - - request = j.uc.mem_read(request_bytes_addr, request_len) - - validation_ctx_addr = int.from_bytes(validation_ctx_addr, 'little') - return validation_ctx_addr, request - -def nac_key_establishment(j: Jelly, validation_ctx: int, response: bytes): - response_addr = j.malloc(len(response)) - j.uc.mem_write(response_addr, response) - - ret = j.instr.call( - 0xB1DD0, - [ - validation_ctx, - response_addr, - len(response), - ], - ) - - if ret != 0: - n = ret & 0xffffffff - n = (n ^ 0x80000000) - 0x80000000 - raise Exception(f"Error calling nac_submit: {n}") - -def nac_sign(j: Jelly, validation_ctx: int): - #void *validation_ctx, void *unk_bytes, int unk_len, - # void **validation_data, int *validation_data_len - - out_validation_data_addr = j.malloc(8) - out_validation_data_len_addr = j.malloc(8) - - ret = j.instr.call( - 0xB1DF0, - [ - validation_ctx, - 0, - 0, - out_validation_data_addr, - out_validation_data_len_addr, - ], - ) - - if ret != 0: - n = ret & 0xffffffff - n = (n ^ 0x80000000) - 0x80000000 - raise Exception(f"Error calling nac_generate: {n}") - - validation_data_addr = j.uc.mem_read(out_validation_data_addr, 8) - validation_data_len = j.uc.mem_read(out_validation_data_len_addr, 8) - - validation_data_addr = int.from_bytes(validation_data_addr, 'little') - validation_data_len = int.from_bytes(validation_data_len, 'little') - - validation_data = j.uc.mem_read(validation_data_addr, validation_data_len) - - return validation_data - - -def hook_code(uc, address: int, size: int, user_data): - logger.debug(">>> Tracing instruction at 0x%x, instruction size = 0x%x" % (address, size)) - - -def malloc(j: Jelly, len: int) -> int: - # Hook malloc - # Return the address of the allocated memory - #print("malloc hook called with len = %d" % len) - return j.malloc(len) - - -def memset_chk(j: Jelly, dest: int, c: int, len: int, destlen: int): - logger.debug( - "memset_chk called with dest = 0x%x, c = 0x%x, len = 0x%x, destlen = 0x%x" - % (dest, c, len, destlen) - ) - j.uc.mem_write(dest, bytes([c]) * len) - return 0 - - -def sysctlbyname(j: Jelly): - return 0 # The output is not checked - - -def memcpy(j: Jelly, dest: int, src: int, len: int): - logger.debug("memcpy called with dest = 0x%x, src = 0x%x, len = 0x%x" % (dest, src, len)) - orig = j.uc.mem_read(src, len) - j.uc.mem_write(dest, bytes(orig)) - return 0 - -CF_OBJECTS = [] - -# struct __builtin_CFString { -# int *isa; // point to __CFConstantStringClassReference -# int flags; -# const char *str; -# long length; -# } -import struct - -def _parse_cfstr_ptr(j: Jelly, ptr: int) -> str: - size = struct.calcsize(" str: - data = j.uc.mem_read(ptr, 256) # Lazy way to do it - return data.split(b"\x00")[0].decode("utf-8") - -def IORegistryEntryCreateCFProperty(j: Jelly, entry: int, key: int, allocator: int, options: int): - key_str = _parse_cfstr_ptr(j, key) - if key_str in FAKE_DATA["iokit"]: - fake = FAKE_DATA["iokit"][key_str] - logger.debug(f"IOKit Entry: {key_str} -> {fake}") - # Return the index of the fake data in CF_OBJECTS - CF_OBJECTS.append(fake) - return len(CF_OBJECTS) # NOTE: We will have to subtract 1 from this later, can't return 0 here since that means NULL - else: - logger.debug(f"IOKit Entry: {key_str} -> None") - return 0 - -def CFGetTypeID(j: Jelly, obj: int): - obj = CF_OBJECTS[obj - 1] - if isinstance(obj, bytes): - return 1 - elif isinstance(obj, str): - return 2 - else: - raise Exception("Unknown CF object type") - -def CFDataGetLength(j: Jelly, obj: int): - obj = CF_OBJECTS[obj - 1] - if isinstance(obj, bytes): - return len(obj) - else: - raise Exception("Unknown CF object type") - -def CFDataGetBytes(j: Jelly, obj: int, range_start: int, range_end: int, buf: int): - obj = CF_OBJECTS[obj - 1] - if isinstance(obj, bytes): - data = obj[range_start:range_end] - j.uc.mem_write(buf, data) - logger.debug(f"CFDataGetBytes: {hex(range_start)}-{hex(range_end)} -> {hex(buf)}") - return len(data) - else: - raise Exception("Unknown CF object type") - -def CFDictionaryCreateMutable(j: Jelly) -> int: - CF_OBJECTS.append({}) - return len(CF_OBJECTS) - -def maybe_object_maybe_string(j: Jelly, obj: int): - # If it's already a str - if isinstance(obj, str): - return obj - elif obj > len(CF_OBJECTS): - return obj - #raise Exception(f"WTF: {hex(obj)}") - # This is probably a CFString - # return _parse_cfstr_ptr(j, obj) - else: - return CF_OBJECTS[obj - 1] - -def CFDictionaryGetValue(j: Jelly, d: int, key: int) -> int: - logger.debug(f"CFDictionaryGetValue: {d} {hex(key)}") - d = CF_OBJECTS[d - 1] - if key == 0xc3c3c3c3c3c3c3c3: - key = "DADiskDescriptionVolumeUUIDKey" # Weirdness, this is a hack - key = maybe_object_maybe_string(j, key) - if isinstance(d, dict): - if key in d: - val = d[key] - logger.debug(f"CFDictionaryGetValue: {key} -> {val}") - CF_OBJECTS.append(val) - return len(CF_OBJECTS) - else: - raise Exception("Key not found") - return 0 - else: - raise Exception("Unknown CF object type") - -def CFDictionarySetValue(j: Jelly, d: int, key: int, val: int): - d = CF_OBJECTS[d - 1] - key = maybe_object_maybe_string(j, key) - val = maybe_object_maybe_string(j, val) - if isinstance(d, dict): - d[key] = val - else: - raise Exception("Unknown CF object type") - -def DADiskCopyDescription(j: Jelly) -> int: - description = CFDictionaryCreateMutable(j) - CFDictionarySetValue(j, description, "DADiskDescriptionVolumeUUIDKey", FAKE_DATA["root_disk_uuid"]) - return description - -def CFStringCreate(j: Jelly, string: str) -> int: - CF_OBJECTS.append(string) - return len(CF_OBJECTS) - -def CFStringGetLength(j: Jelly, string: int) -> int: - string = CF_OBJECTS[string - 1] - if isinstance(string, str): - return len(string) - else: - raise Exception("Unknown CF object type") - -def CFStringGetCString(j: Jelly, string: int, buf: int, buf_len: int, encoding: int) -> int: - string = CF_OBJECTS[string - 1] - if isinstance(string, str): - data = string.encode("utf-8") - j.uc.mem_write(buf, data) - logger.debug(f"CFStringGetCString: {string} -> {hex(buf)}") - return len(data) - else: - raise Exception("Unknown CF object type") - -def IOServiceMatching(j: Jelly, name: int) -> int: - # Read the raw c string pointed to by name - name = _parse_cstr_ptr(j, name) - logger.debug(f"IOServiceMatching: {name}") - # Create a CFString from the name - name = CFStringCreate(j, name) - # Create a dictionary - d = CFDictionaryCreateMutable(j) - # Set the key "IOProviderClass" to the name - CFDictionarySetValue(j, d, "IOProviderClass", name) - # Return the dictionary - return d - -def IOServiceGetMatchingService(j: Jelly) -> int: - return 92 - -ETH_ITERATOR_HACK = False -def IOServiceGetMatchingServices(j: Jelly, port, match, existing) -> int: - global ETH_ITERATOR_HACK - ETH_ITERATOR_HACK = True - # Write 93 to existing - j.uc.mem_write(existing, bytes([93])) - return 0 - -def IOIteratorNext(j: Jelly, iterator: int) -> int: - global ETH_ITERATOR_HACK - if ETH_ITERATOR_HACK: - ETH_ITERATOR_HACK = False - return 94 - else: - return 0 - -def bzero(j: Jelly, ptr: int, len: int): - j.uc.mem_write(ptr, bytes([0]) * len) - return 0 - -def IORegistryEntryGetParentEntry(j: Jelly, entry: int, _, parent: int) -> int: - j.uc.mem_write(parent, bytes([entry + 100])) - return 0 - -import requests, plistlib -def get_cert(): - resp = requests.get("http://static.ess.apple.com/identity/validation/cert-1.0.plist") - resp = plistlib.loads(resp.content) - return resp["cert"] - -def get_session_info(req: bytes) -> bytes: - body = { - 'session-info-request': req, - } - body = plistlib.dumps(body) - resp = requests.post("https://identity.ess.apple.com/WebObjects/TDIdentityService.woa/wa/initializeValidation", data=body, verify=False) - resp = plistlib.loads(resp.content) - return resp["session-info"] - -def arc4random(j: Jelly) -> int: - import random - return random.randint(0, 0xFFFFFFFF) - #return 0 - -def load_nac() -> Jelly: - binary = load_binary() - binary = get_x64_slice(binary) - # Create a Jelly object from the binary - j = Jelly(binary) - - hooks = { - "_malloc": malloc, - "___stack_chk_guard": lambda: 0, - "___memset_chk": memset_chk, - "_sysctlbyname": lambda _: 0, - "_memcpy": memcpy, - "_kIOMasterPortDefault": lambda: 0, - "_IORegistryEntryFromPath": lambda _: 1, - "_kCFAllocatorDefault": lambda: 0, - "_IORegistryEntryCreateCFProperty": IORegistryEntryCreateCFProperty, - "_CFGetTypeID": CFGetTypeID, - "_CFStringGetTypeID": lambda _: 2, - "_CFDataGetTypeID": lambda _: 1, - "_CFDataGetLength": CFDataGetLength, - "_CFDataGetBytes": CFDataGetBytes, - "_CFRelease": lambda _: 0, - "_IOObjectRelease": lambda _: 0, - "_statfs$INODE64": lambda _: 0, - "_DASessionCreate": lambda _: 201, - "_DADiskCreateFromBSDName": lambda _: 202, - "_kDADiskDescriptionVolumeUUIDKey": lambda: 0, - "_DADiskCopyDescription": DADiskCopyDescription, - "_CFDictionaryGetValue": CFDictionaryGetValue, - "_CFUUIDCreateString": lambda _, __, uuid: uuid, - "_CFStringGetLength": CFStringGetLength, - "_CFStringGetMaximumSizeForEncoding": lambda _, length, __: length, - "_CFStringGetCString": CFStringGetCString, - "_free": lambda _: 0, - "_IOServiceMatching": IOServiceMatching, - "_IOServiceGetMatchingService": IOServiceGetMatchingService, - "_CFDictionaryCreateMutable": CFDictionaryCreateMutable, - "_kCFBooleanTrue": lambda: 0, - "_CFDictionarySetValue": CFDictionarySetValue, - "_IOServiceGetMatchingServices": IOServiceGetMatchingServices, - "_IOIteratorNext": IOIteratorNext, - "___bzero": bzero, - "_IORegistryEntryGetParentEntry": IORegistryEntryGetParentEntry, - "_arc4random": arc4random - } - j.setup(hooks) - - return j - -def generate_validation_data() -> bytes: - j = load_nac() - logger.debug("Loaded NAC library") - val_ctx, req = nac_init(j,get_cert()) - logger.debug("Initialized NAC") - session_info = get_session_info(req) - logger.debug("Got session info") - nac_key_establishment(j, val_ctx, session_info) - logger.debug("Submitted session info") - val_data = nac_sign(j, val_ctx) - logger.info("Generated validation data") - return bytes(val_data) - -if __name__ == "__main__": - from base64 import b64encode - val_data = generate_validation_data() - logger.info(f"Validation Data: {b64encode(val_data).decode()}") - #main() \ No newline at end of file diff --git a/generatenac.py b/generatenac.py deleted file mode 100644 index 9ceef18..0000000 --- a/generatenac.py +++ /dev/null @@ -1,6 +0,0 @@ -from base64 import b64encode -import emulated.nac - -vd = emulated.nac.generate_validation_data() -vd = b64encode(vd).decode() -print(vd) \ No newline at end of file diff --git a/ids/__init__.py b/ids/__init__.py deleted file mode 100644 index 7e561c4..0000000 --- a/ids/__init__.py +++ /dev/null @@ -1,86 +0,0 @@ -from base64 import b64encode - -import apns - -from . import _helpers, identity, profile, query - - -class IDSUser: - # Sets self.user_id and self._auth_token - def _authenticate_for_token( - self, username: str, password: str, factor_callback: callable = None - ): - self.user_id, self._auth_token = profile.get_auth_token( - username, password, factor_callback - ) - - # Sets self._auth_keypair using self.user_id and self._auth_token - def _authenticate_for_cert(self): - self._auth_keypair = profile.get_auth_cert(self.user_id, self._auth_token) - - # Factor callback will be called if a 2FA code is necessary - def __init__( - self, - push_connection: apns.APNSConnection, - ): - self.push_connection = push_connection - self._push_keypair = _helpers.KeyPair( - self.push_connection.private_key, self.push_connection.cert - ) - - self.ec_key = self.rsa_key = None - - def __str__(self): - return f"IDSUser(user_id={self.user_id}, handles={self.handles}, push_token={b64encode(self.push_connection.token).decode()})" - - # Authenticates with a username and password, to create a brand new authentication keypair - def authenticate( - self, username: str, password: str, factor_callback: callable = None - ): - self._authenticate_for_token(username, password, factor_callback) - self._authenticate_for_cert() - self.handles = profile.get_handles( - b64encode(self.push_connection.token), - self.user_id, - self._auth_keypair, - self._push_keypair, - ) - self.current_handle = self.handles[0] - - - # Uses an existing authentication keypair - def restore_authentication( - self, auth_keypair: _helpers.KeyPair, user_id: str, handles: dict - ): - self._auth_keypair = auth_keypair - self.user_id = user_id - self.handles = handles - self.current_handle = self.handles[0] - - # This is a separate call so that the user can make sure the first part succeeds before asking for validation data - def register(self, validation_data: str): - """ - self.ec_key, self.rsa_key will be set to a randomly gnenerated EC and RSA keypair - if they are not already set - """ - if self.encryption_identity is None: - self.encryption_identity = identity.IDSIdentity() - - - cert = identity.register( - b64encode(self.push_connection.token), - self.handles, - self.user_id, - self._auth_keypair, - self._push_keypair, - self.encryption_identity, - validation_data, - ) - self._id_keypair = _helpers.KeyPair(self._auth_keypair.key, cert) - - def restore_identity(self, id_keypair: _helpers.KeyPair): - self._id_keypair = id_keypair - - def lookup(self, uris: list[str], topic: str = "com.apple.madrid") -> any: - return query.lookup(self.push_connection, self.current_handle, self._id_keypair, uris, topic) - diff --git a/ids/_helpers.py b/ids/_helpers.py deleted file mode 100644 index a10fd79..0000000 --- a/ids/_helpers.py +++ /dev/null @@ -1,39 +0,0 @@ -from collections import namedtuple - -USER_AGENT = "com.apple.madrid-lookup [macOS,13.2.1,22D68,MacBookPro18,3]" -PROTOCOL_VERSION = "1640" - -# KeyPair is a named tuple that holds a key and a certificate in PEM form -KeyPair = namedtuple("KeyPair", ["key", "cert"]) - - -def dearmour(armoured: str) -> str: - import re - - # Use a regex to remove the header and footer (generic so it work on more than just certificates) - return re.sub(r"-----BEGIN .*-----|-----END .*-----", "", armoured).replace( - "\n", "" - ) - -from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import ec, rsa -def parse_key(key: str): - # Check if it is a public or private key - if "PUBLIC" in key: - return serialization.load_pem_public_key(key.encode()) - else: - return serialization.load_pem_private_key(key.encode(), None) - -def serialize_key(key): - if isinstance(key, ec.EllipticCurvePrivateKey) or isinstance(key, rsa.RSAPrivateKey): - return key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption(), - ).decode("utf-8").strip() - else: - return key.public_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PublicFormat.SubjectPublicKeyInfo, - ).decode("utf-8").strip() - \ No newline at end of file diff --git a/ids/identity.py b/ids/identity.py deleted file mode 100644 index 7e5890a..0000000 --- a/ids/identity.py +++ /dev/null @@ -1,177 +0,0 @@ -import plistlib -from base64 import b64decode -from typing import Union - -import requests - -from ._helpers import PROTOCOL_VERSION, USER_AGENT, KeyPair, parse_key, serialize_key -from .signing import add_auth_signature, armour_cert - -from io import BytesIO - -from cryptography.hazmat.primitives.asymmetric import ec, rsa - -import logging -logger = logging.getLogger("ids") - -class IDSIdentity: - def __init__( - self, - signing_key: Union[str, None] = None, - encryption_key: Union[str, None] = None, - signing_public_key: Union[str, None] = None, - encryption_public_key: Union[str, None] = None): - if signing_key is not None: - self.signing_key = signing_key - self.signing_public_key = serialize_key(parse_key(signing_key).public_key()) - elif signing_public_key is not None: - self.signing_key = None - self.signing_public_key = signing_public_key - else: - # Generate a new key - self.signing_key = serialize_key(ec.generate_private_key(ec.SECP256R1())) - self.signing_public_key = serialize_key(parse_key(self.signing_key).public_key()) - - if encryption_key is not None: - self.encryption_key = encryption_key - self.encryption_public_key = serialize_key(parse_key(encryption_key).public_key()) - elif encryption_public_key is not None: - self.encryption_key = None - self.encryption_public_key = encryption_public_key - else: - self.encryption_key = serialize_key(rsa.generate_private_key(65537, 1280)) - self.encryption_public_key = serialize_key(parse_key(self.encryption_key).public_key()) - - def decode(input: bytes) -> 'IDSIdentity': - input = BytesIO(input) - - assert input.read(5) == b'\x30\x81\xF6\x81\x43' # DER header - raw_ecdsa = input.read(67) - assert input.read(3) == b'\x82\x81\xAE' # DER header - raw_rsa = input.read(174) - - # Parse the RSA key - raw_rsa = BytesIO(raw_rsa) - assert raw_rsa.read(2) == b'\x00\xAC' # Not sure what this is - assert raw_rsa.read(3) == b'\x30\x81\xA9' # Inner DER header - assert raw_rsa.read(3) == b'\x02\x81\xA1' - rsa_modulus = raw_rsa.read(161) - rsa_modulus = int.from_bytes(rsa_modulus, "big") - assert raw_rsa.read(5) == b'\x02\x03\x01\x00\x01' # Exponent, should always be 65537 - - # Parse the EC key - assert raw_ecdsa[:3] == b'\x00\x41\x04' - raw_ecdsa = raw_ecdsa[3:] - ec_x = int.from_bytes(raw_ecdsa[:32], "big") - ec_y = int.from_bytes(raw_ecdsa[32:], "big") - - ec_key = ec.EllipticCurvePublicNumbers(ec_x, ec_y, ec.SECP256R1()) - ec_key = ec_key.public_key() - - rsa_key = rsa.RSAPublicNumbers(e=65537, n=rsa_modulus) - rsa_key = rsa_key.public_key() - - return IDSIdentity(signing_public_key=serialize_key(ec_key), encryption_public_key=serialize_key(rsa_key)) - - - def encode(self) -> bytes: - output = BytesIO() - - raw_rsa = BytesIO() - raw_rsa.write(b'\x00\xAC') - raw_rsa.write(b'\x30\x81\xA9') - raw_rsa.write(b'\x02\x81\xA1') - raw_rsa.write(parse_key(self.encryption_public_key).public_numbers().n.to_bytes(161, "big")) - raw_rsa.write(b'\x02\x03\x01\x00\x01') # Hardcode the exponent - - output.write(b'\x30\x81\xF6\x81\x43') - output.write(b'\x00\x41\x04') - output.write(parse_key(self.signing_public_key).public_numbers().x.to_bytes(32, "big")) - output.write(parse_key(self.signing_public_key).public_numbers().y.to_bytes(32, "big")) - - output.write(b'\x82\x81\xAE') - output.write(raw_rsa.getvalue()) - - return output.getvalue() - -def register( - push_token, handles, user_id, auth_key: KeyPair, push_key: KeyPair, identity: IDSIdentity, validation_data -): - logger.debug(f"Registering IDS identity for {handles}") - uris = [{"uri": handle} for handle in handles] - - body = { - "hardware-version": "MacBookPro18,3", - "language": "en-US", - "os-version": "macOS,13.2.1,22D68", - "software-version": "22D68", - "services": [ - { - "capabilities": [{"flags": 17, "name": "Messenger", "version": 1}], - "service": "com.apple.madrid", - "users": [ - { - "client-data": { - 'is-c2k-equipment': True, - 'optionally-receive-typing-indicators': True, - 'public-message-identity-key': identity.encode(), - 'public-message-identity-version':2, - 'show-peer-errors': True, - 'supports-ack-v1': True, - 'supports-activity-sharing-v1': True, - 'supports-audio-messaging-v2': True, - "supports-autoloopvideo-v1": True, - 'supports-be-v1': True, - 'supports-ca-v1': True, - 'supports-fsm-v1': True, - 'supports-fsm-v2': True, - 'supports-fsm-v3': True, - 'supports-ii-v1': True, - 'supports-impact-v1': True, - 'supports-inline-attachments': True, - 'supports-keep-receipts': True, - "supports-location-sharing": True, - 'supports-media-v2': True, - 'supports-photos-extension-v1': True, - 'supports-st-v1': True, - 'supports-update-attachments-v1': True, - }, - "uris": uris, - "user-id": user_id, - } - ], - } - ], - "validation-data": b64decode(validation_data), - } - - body = plistlib.dumps(body) - - headers = { - "x-protocol-version": PROTOCOL_VERSION, - "x-auth-user-id-0": user_id, - } - add_auth_signature(headers, body, "id-register", auth_key, push_key, push_token, 0) - - r = requests.post( - "https://identity.ess.apple.com/WebObjects/TDIdentityService.woa/wa/register", - headers=headers, - data=body, - verify=False, - ) - r = plistlib.loads(r.content) - #print(f'Response code: {r["status"]}') - logger.debug(f"Recieved response to IDS registration: {r}") - if "status" in r and r["status"] == 6004: - raise Exception("Validation data expired!") - # TODO: Do validation of nested statuses - if "status" in r and r["status"] != 0: - raise Exception(f"Failed to register: {r}") - if not "services" in r: - raise Exception(f"No services in response: {r}") - if not "users" in r["services"][0]: - raise Exception(f"No users in response: {r}") - if not "cert" in r["services"][0]["users"][0]: - raise Exception(f"No cert in response: {r}") - - return armour_cert(r["services"][0]["users"][0]["cert"]) diff --git a/ids/profile.py b/ids/profile.py deleted file mode 100644 index 3af9d73..0000000 --- a/ids/profile.py +++ /dev/null @@ -1,172 +0,0 @@ -import plistlib -import random -import uuid -from base64 import b64decode - -import requests -from cryptography import x509 -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import padding, rsa -from cryptography.x509.oid import NameOID - -import bags - -from . import signing -from ._helpers import PROTOCOL_VERSION, USER_AGENT, KeyPair - -import logging -logger = logging.getLogger("ids") - - -def _auth_token_request(username: str, password: str) -> any: - # Turn the PET into an auth token - data = { - "username": username, - #"client-id": str(uuid.uuid4()), - #"delegates": {"com.apple.private.ids": {"protocol-version": "4"}}, - "password": password, - } - data = plistlib.dumps(data) - - r = requests.post( - # TODO: Figure out which URL bag we can get this from - "https://profile.ess.apple.com/WebObjects/VCProfileService.woa/wa/authenticateUser", - #"https://setup.icloud.com/setup/prefpane/loginDelegates", - #auth=(username, password), - data=data, - verify=False, - ) - r = plistlib.loads(r.content) - return r - - -# Gets an IDS auth token for the given username and password -# Will use native Grand Slam on macOS -# If factor_gen is not None, it will be called to get the 2FA code, otherwise it will be prompted -# Returns (realm user id, auth token) -def get_auth_token( - username: str, password: str, factor_gen: callable = None -) -> tuple[str, str]: - from sys import platform - - result = _auth_token_request(username, password) - if result["status"] != 0: - if result["status"] == 5000: - if factor_gen is None: - password = password + input("Enter 2FA code: ") - else: - password = password + factor_gen() - result = _auth_token_request(username, password) - if result["status"] != 0: - raise Exception(f"Error: {result}") - - auth_token = result["auth-token"] - realm_user_id = result["profile-id"] - # else: - # logger.debug("Using old-style authentication") - # # Make the request without the 2FA code to make the prompt appear - # _auth_token_request(username, password) - # # TODO: Make sure we actually need the second request, some rare accounts don't have 2FA - # # Now make the request with the 2FA code - # if factor_gen is None: - # pet = password + input("Enter 2FA code: ") - # else: - # pet = password + factor_gen() - # r = _auth_token_request(username, pet) - # # print(r) - # if "description" in r: - # raise Exception(f"Error: {r['description']}") - # service_data = r["delegates"]["com.apple.private.ids"]["service-data"] - # realm_user_id = service_data["realm-user-id"] - # auth_token = service_data["auth-token"] - # print(f"Auth token for {realm_user_id}: {auth_token}") - logger.debug(f"Got auth token for IDS: {auth_token}") - return realm_user_id, auth_token - - -def _generate_csr(private_key: rsa.RSAPrivateKey) -> str: - csr = ( - x509.CertificateSigningRequestBuilder() - .subject_name( - x509.Name( - [ - x509.NameAttribute(NameOID.COMMON_NAME, random.randbytes(20).hex()), - ] - ) - ) - .sign(private_key, hashes.SHA256()) - ) - - csr = csr.public_bytes(serialization.Encoding.PEM).decode("utf-8") - return ( - csr.replace("-----BEGIN CERTIFICATE REQUEST-----", "") - .replace("-----END CERTIFICATE REQUEST-----", "") - .replace("\n", "") - ) - - -# Gets an IDS auth cert for the given user id and auth token -# Returns [private key PEM, certificate PEM] -def get_auth_cert(user_id, token) -> KeyPair: - BAG_KEY = "id-authenticate-ds-id" - - private_key = rsa.generate_private_key( - public_exponent=65537, key_size=2048, backend=default_backend() - ) - body = { - "authentication-data": {"auth-token": token}, - "csr": b64decode(_generate_csr(private_key)), - "realm-user-id": user_id, - } - - body = plistlib.dumps(body) - - r = requests.post( - bags.ids_bag()[BAG_KEY], - #"https://profile.ess.apple.com/WebObjects/VCProfileService.woa/wa/authenticateDS", - data=body, - headers={"x-protocol-version": "1630"}, - verify=False, - ) - r = plistlib.loads(r.content) - if r["status"] != 0: - raise (Exception(f"Failed to get auth cert: {r}")) - cert = x509.load_der_x509_certificate(r["cert"]) - logger.debug("Got auth cert from token") - return KeyPair( - private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption(), - ) - .decode("utf-8") - .strip(), - cert.public_bytes(serialization.Encoding.PEM).decode("utf-8").strip(), - ) - - -def get_handles(push_token, user_id: str, auth_key: KeyPair, push_key: KeyPair): - BAG_KEY = "id-get-handles" - - headers = { - "x-protocol-version": PROTOCOL_VERSION, - "x-auth-user-id": user_id, - } - signing.add_auth_signature( - headers, None, BAG_KEY, auth_key, push_key, push_token - ) - - r = requests.get( - bags.ids_bag()[BAG_KEY], - headers=headers, - verify=False, - ) - - r = plistlib.loads(r.content) - - if not "handles" in r: - raise Exception("No handles in response: " + str(r)) - - logger.debug(f"User {user_id} has handles {r['handles']}") - return [handle["uri"] for handle in r["handles"]] diff --git a/ids/query.py b/ids/query.py deleted file mode 100644 index 875372b..0000000 --- a/ids/query.py +++ /dev/null @@ -1,71 +0,0 @@ -import gzip -import plistlib -import random -from base64 import b64encode - -import apns -import bags - -from ._helpers import KeyPair, PROTOCOL_VERSION -from . import signing - - -def lookup( - conn: apns.APNSConnection, - self_uri: str, - id_keypair: KeyPair, - query: list[str], - topic, -) -> bytes: - BAG_KEY = "id-query" - - conn.filter([topic]) - - body = plistlib.dumps({"uris": query}) - body = gzip.compress(body, mtime=0) - - push_token = b64encode(conn.token).decode() - - headers = { - "x-id-self-uri": self_uri, - "x-protocol-version": PROTOCOL_VERSION, - } - signing.add_id_signature(headers, body, BAG_KEY, id_keypair, push_token) - - msg_id = random.randbytes(16) - - req = { - "cT": "application/x-apple-plist", - "U": msg_id, - "c": 96, - "u": bags.ids_bag()[BAG_KEY], - "h": headers, - "v": 2, - "b": body, - } - - conn.send_message(topic, plistlib.dumps(req, fmt=plistlib.FMT_BINARY)) - - def check_response(x): - if x[0] != 0x0A: - return False - resp_body = apns._get_field(x[1], 3) - if resp_body is None: - return False - resp_body = plistlib.loads(resp_body) - return resp_body.get('U') == msg_id - - # Lambda to check if the response is the one we want - payload = conn.incoming_queue.wait_pop_find(check_response) - resp = apns._get_field(payload[1], 3) - resp = plistlib.loads(resp) - resp = gzip.decompress(resp["b"]) - resp = plistlib.loads(resp) - # Acknowledge the message - #conn._send_ack(apns._get_field(payload[1], 4)) - - if resp['status'] != 0: - raise Exception(f'Query failed: {resp}') - if not 'results' in resp: - raise Exception(f'No results in response: {resp}') - return resp['results'] diff --git a/ids/signing.py b/ids/signing.py deleted file mode 100644 index c5f24ce..0000000 --- a/ids/signing.py +++ /dev/null @@ -1,125 +0,0 @@ -import random -from base64 import b64decode, b64encode -from datetime import datetime - -from cryptography import x509 -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import padding, rsa -from cryptography.x509.oid import NameOID - -from ._helpers import KeyPair, dearmour - - -# TODO: Move this helper somewhere else -def armour_cert(cert: bytes) -> str: - cert = x509.load_der_x509_certificate(cert) - return cert.public_bytes(serialization.Encoding.PEM).decode("utf-8").strip() - - -""" -Generates a nonce in this format: -01000001876bd0a2c0e571093967fce3d7 -01 # version - 000001876d008cc5 # unix time - r1r2r3r4r5r6r7r8 # random bytes -""" - - -def generate_nonce() -> bytes: - return ( - b"\x01" - + int(datetime.now().timestamp() * 1000).to_bytes(8, "big") - + random.randbytes(8) - ) - - -import typing - - -# Creates a payload from individual parts for signing -def _create_payload( - bag_key: str, - query_string: str, - push_token: typing.Union[str, bytes], - payload: bytes, - nonce: typing.Union[bytes, None] = None, -) -> tuple[bytes, bytes]: - # Generate the nonce - if nonce is None: - nonce = generate_nonce() - - push_token = b64decode(push_token) - - if payload is None: - payload = b"" - - return ( - nonce - + len(bag_key).to_bytes(4, "big") - + bag_key.encode() - + len(query_string).to_bytes(4, "big") - + query_string.encode() - + len(payload).to_bytes(4, "big") - + payload - + len(push_token).to_bytes(4, "big") - + push_token, - nonce, - ) - - -# Returns signature, nonce -def _sign_payload( - private_key: str, bag_key: str, query_string: str, push_token: str, payload: bytes, nonce = None -) -> tuple[str, bytes]: - # Load the private key - key = serialization.load_pem_private_key( - private_key.encode(), password=None, backend=default_backend() - ) - - payload, nonce = _create_payload(bag_key, query_string, push_token, payload, nonce) - - sig = key.sign(payload, padding.PKCS1v15(), hashes.SHA1()) # type: ignore - - sig = b"\x01\x01" + sig - sig = b64encode(sig).decode() - - return sig, nonce - - -# Add headers for x-push-sig and x-auth-sig stuff -def add_auth_signature( - headers: dict, - body: bytes, - bag_key: str, - auth_key: KeyPair, - push_key: KeyPair, - push_token: str, - auth_number=None, -): - push_sig, push_nonce = _sign_payload(push_key.key, bag_key, "", push_token, body) - headers["x-push-sig"] = push_sig - headers["x-push-nonce"] = b64encode(push_nonce) - headers["x-push-cert"] = dearmour(push_key.cert) - headers["x-push-token"] = push_token - - auth_sig, auth_nonce = _sign_payload(auth_key.key, bag_key, "", push_token, body) - auth_postfix = "-" + str(auth_number) if auth_number is not None else "" - headers["x-auth-sig" + auth_postfix] = auth_sig - headers["x-auth-nonce" + auth_postfix] = b64encode(auth_nonce) - headers["x-auth-cert" + auth_postfix] = dearmour(auth_key.cert) - - -def add_id_signature( - headers: dict, - body: bytes, - bag_key: str, - id_key: KeyPair, - push_token: str, - nonce=None, -): - id_sig, id_nonce = _sign_payload(id_key.key, bag_key, "", push_token, body, nonce) - headers["x-id-sig"] = id_sig - headers["x-id-nonce"] = b64encode(id_nonce).decode() - headers["x-id-cert"] = dearmour(id_key.cert) - headers["x-push-token"] = push_token diff --git a/imessage.py b/imessage.py deleted file mode 100644 index fefabb2..0000000 --- a/imessage.py +++ /dev/null @@ -1,565 +0,0 @@ -# LOW LEVEL imessage function, decryption etc -# Don't handle APNS etc, accept it already setup - -## HAVE ANOTHER FILE TO SETUP EVERYTHING AUTOMATICALLY, etc -# JSON parsing of keys, don't pass around strs?? - -import base64 -import gzip -import logging -import plistlib -import random -from typing import Union -import uuid -from dataclasses import dataclass, field -from hashlib import sha1, sha256 -from io import BytesIO - -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.asymmetric import ec, padding -from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes - -from xml.etree import ElementTree - -import apns -import ids - -logger = logging.getLogger("imessage") - -NORMAL_NONCE = b"\x00" * 15 + b"\x01" # This is always used as the AES nonce - - -class BalloonBody: - """Represents the special parts of message extensions etc.""" - - def __init__(self, type: str, data: bytes): - self.type = type - self.data = data - - # TODO : Register handlers based on type id - - -class AttachmentFile: - def data(self) -> bytes: - raise NotImplementedError() - - -@dataclass -class MMCSFile(AttachmentFile): - url: Union[str, None] = None - size: Union[int, None] = None - owner: Union[str, None] = None - signature: Union[bytes, None] = None - decryption_key: Union[bytes, None] = None - - def data(self) -> bytes: - import requests - logger.info(requests.get( - url=self.url, - headers={ - "User-Agent": f"IMTransferAgent/900 CFNetwork/596.2.3 Darwin/12.2.0 (x86_64) (Macmini5,1)", - # "MMCS-Url": self.url, - # "MMCS-Signature": str(base64.encodebytes(self.signature)), - # "MMCS-Owner": self.owner - }, - ).headers) - return b"" - - -@dataclass -class InlineFile(AttachmentFile): - _data: bytes - - def data(self) -> bytes: - return self._data - - -@dataclass -class Attachment: - name: str - mime_type: str - versions: list[AttachmentFile] - - def __init__(self, message_raw_content: dict, xml_element: ElementTree.Element): - attrs = xml_element.attrib - - self.name = attrs["name"] if "name" in attrs else None - self.mime_type = attrs["mime-type"] if "mime-type" in attrs else None - - if "inline-attachment" in attrs: - # just grab the inline attachment ! - self.versions = [InlineFile(message_raw_content[attrs["inline-attachment"]])] - else: - # suffer - versions = [] - for attribute in attrs: - if attribute.startswith("mmcs") or \ - attribute.startswith("decryption-key") or \ - attribute.startswith("file-size"): - segments = attribute.split('-') - if segments[-1].isnumeric(): - index = int(segments[-1]) - attribute_name = segments[:-1] - else: - index = 0 - attribute_name = attribute - - while index >= len(versions): - versions.append(MMCSFile()) - - val = attrs[attribute_name] - if attribute_name == "mmcs-url": - versions[index].url = val - elif attribute_name == "mmcs-owner": - versions[index].owner = val - elif attribute_name == "mmcs-signature-hex": - versions[index].signature = base64.b16decode(val) - elif attribute_name == "file-size": - versions[index].size = int(val) - elif attribute_name == "decryption-key": - versions[index].decryption_key = base64.b16decode(val)[1:] - - self.versions = versions - - def __repr__(self): - return f'' - - -@dataclass -class iMessage: - """Represents an iMessage""" - - text: str = "" - """Plain text of message, always required, may be an empty string""" - xml: Union[str, None] = None - """XML portion of message, may be None""" - participants: list[str] = field(default_factory=list) - """List of participants in the message, including the sender""" - sender: Union[str, None] = None - """Sender of the message""" - id: Union[uuid.UUID, None] = None - """ID of the message, will be randomly generated if not provided""" - group_id: Union[uuid.UUID, None] = None - """Group ID of the message, will be randomly generated if not provided""" - body: Union[BalloonBody, None] = None - """BalloonBody, may be None""" - effect: Union[str, None] = None - """iMessage effect sent with this message, may be None""" - - _compressed: bool = True - """Internal property representing whether the message should be compressed""" - - _raw: Union[dict, None] = None - """Internal property representing the original raw message, may be None""" - - def attachments(self) -> list[Attachment]: - if self.xml is not None: - return [Attachment(self._raw, elem) for elem in ElementTree.fromstring(self.xml)[0] if elem.tag == "FILE"] - else: - return [] - - def sanity_check(self): - """Corrects any missing fields""" - if self.id is None: - self.id = uuid.uuid4() - - if self.group_id is None: - self.group_id = uuid.uuid4() - - if self.sender is None: - if len(self.participants) > 1: - self.sender = self.participants[-1] - else: - logger.warning( - "Message has no sender, and only one participant, sanity check failed" - ) - return False - - if self.sender not in self.participants: - self.participants.append(self.sender) - - if self.xml != None: - self._compressed = False # XML is never compressed for some reason - - return True - - def from_raw(message: bytes, sender: Union[str, None] = None) -> "iMessage": - """Create an `iMessage` from raw message bytes""" - compressed = False - try: - message = gzip.decompress(message) - compressed = True - except: - pass - - message = plistlib.loads(message) - - return iMessage( - text=message.get("t", ""), - xml=message.get("x"), - participants=message.get("p", []), - sender=sender if sender is not None else message.get("p", [])[-1] if "p" in message else None, - id=uuid.UUID(message.get("r")) if "r" in message else None, - group_id=uuid.UUID(message.get("gid")) if "gid" in message else None, - body=BalloonBody(message["bid"], message["b"]) if "bid" in message and "b" in message else None, - effect=message["iid"] if "iid" in message else None, - _compressed=compressed, - _raw=message, - ) - - def to_raw(self) -> bytes: - """Convert an `iMessage` to raw message bytes""" - if not self.sanity_check(): - raise ValueError("Message failed sanity check") - - d = { - "t": self.text, - "x": self.xml, - "p": self.participants, - "r": str(self.id).upper(), - "gid": str(self.group_id).upper(), - "pv": 0, - "gv": "8", - "v": "1", - "iid": self.effect - } - - # Remove keys that are None - d = {k: v for k, v in d.items() if v is not None} - - # Serialize as a plist - d = plistlib.dumps(d, fmt=plistlib.FMT_BINARY) - - # Compression - if self._compressed: - d = gzip.compress(d, mtime=0) - - return d - - def to_string(self) -> str: - message_str = f"[{self.sender}] '{self.text}'" - if self.effect is not None: - message_str += f" with effect [{self.effect}]" - return message_str - - -class iMessageUser: - """Represents a logged in and connected iMessage user. - This abstraction should probably be reworked into IDS some time...""" - - def __init__(self, connection: apns.APNSConnection, user: ids.IDSUser): - self.connection = connection - self.user = user - - def _get_raw_message(self): - """ - Returns a raw APNs message corresponding to the next conforming notification in the queue - Returns None if no conforming notification is found - """ - - def check_response(x): - if x[0] != 0x0A: - return False - if apns._get_field(x[1], 2) != sha1("com.apple.madrid".encode()).digest(): - return False - resp_body = apns._get_field(x[1], 3) - if resp_body is None: - # logger.debug("Rejecting madrid message with no body") - return False - resp_body = plistlib.loads(resp_body) - if "P" not in resp_body: - # logger.debug(f"Rejecting madrid message with no payload : {resp_body}") - return False - return True - - payload = self.connection.incoming_queue.pop_find(check_response) - if payload is None: - return None - id = apns._get_field(payload[1], 4) - - return payload - - def _parse_payload(payload: bytes) -> tuple[bytes, bytes]: - payload = BytesIO(payload) - - tag = payload.read(1) - #print("TAG", tag) - body_length = int.from_bytes(payload.read(2), "big") - body = payload.read(body_length) - - signature_len = payload.read(1)[0] - signature = payload.read(signature_len) - - return (body, signature) - - def _construct_payload(body: bytes, signature: bytes) -> bytes: - payload = ( - b"\x02" - + len(body).to_bytes(2, "big") - + body - + len(signature).to_bytes(1, "big") - + signature - ) - return payload - - def _hash_identity(id: bytes) -> bytes: - iden = ids.identity.IDSIdentity.decode(id) - - # TODO: Combine this with serialization code in ids.identity - output = BytesIO() - output.write(b"\x00\x41\x04") - output.write( - ids._helpers.parse_key(iden.signing_public_key) - .public_numbers() - .x.to_bytes(32, "big") - ) - output.write( - ids._helpers.parse_key(iden.signing_public_key) - .public_numbers() - .y.to_bytes(32, "big") - ) - - output.write(b"\x00\xAC") - output.write(b"\x30\x81\xA9") - output.write(b"\x02\x81\xA1") - output.write( - ids._helpers.parse_key(iden.encryption_public_key) - .public_numbers() - .n.to_bytes(161, "big") - ) - output.write(b"\x02\x03\x01\x00\x01") - - return sha256(output.getvalue()).digest() - - def _encrypt_sign_payload( - self, key: ids.identity.IDSIdentity, message: bytes - ) -> bytes: - # Generate a random AES key - random_seed = random.randbytes(11) - # Create the HMAC - import hmac - - hm = hmac.new( - random_seed, - message - + b"\x02" - + iMessageUser._hash_identity(self.user.encryption_identity.encode()) - + iMessageUser._hash_identity(key.encode()), - sha256, - ).digest() - - aes_key = random_seed + hm[:5] - - # print(len(aes_key)) - - # Encrypt the message with the AES key - cipher = Cipher(algorithms.AES(aes_key), modes.CTR(NORMAL_NONCE)) - encrypted = cipher.encryptor().update(message) - - # Encrypt the AES key with the public key of the recipient - recipient_key = ids._helpers.parse_key(key.encryption_public_key) - rsa_body = recipient_key.encrypt( - aes_key + encrypted[:100], - padding.OAEP( - mgf=padding.MGF1(algorithm=hashes.SHA1()), - algorithm=hashes.SHA1(), - label=None, - ), - ) - - # Construct the payload - body = rsa_body + encrypted[100:] - sig = ids._helpers.parse_key(self.user.encryption_identity.signing_key).sign( - body, ec.ECDSA(hashes.SHA1()) - ) - payload = iMessageUser._construct_payload(body, sig) - - return payload - - def _decrypt_payload(self, payload: bytes) -> dict: - payload = iMessageUser._parse_payload(payload) - - body = BytesIO(payload[0]) - rsa_body = ids._helpers.parse_key( - self.user.encryption_identity.encryption_key - ).decrypt( - body.read(160), - padding.OAEP( - mgf=padding.MGF1(algorithm=hashes.SHA1()), - algorithm=hashes.SHA1(), - label=None, - ), - ) - - cipher = Cipher(algorithms.AES(rsa_body[:16]), modes.CTR(NORMAL_NONCE)) - decrypted = cipher.decryptor().update(rsa_body[16:] + body.read()) - - return decrypted - - def _verify_payload(self, payload: bytes, sender: str, sender_token: str) -> bool: - # Get the public key for the sender - self._cache_keys([sender]) - - if not sender_token in self.KEY_CACHE: - logger.warning("Unable to find the public key of the sender, cannot verify") - return False - - identity_keys = ids.identity.IDSIdentity.decode(self.KEY_CACHE[sender_token][0]) - sender_ec_key = ids._helpers.parse_key(identity_keys.signing_public_key) - - payload = iMessageUser._parse_payload(payload) - - try: - # Verify the signature (will throw an exception if it fails) - sender_ec_key.verify( - payload[1], - payload[0], - ec.ECDSA(hashes.SHA1()), - ) - return True - except: - return False - - def receive(self) -> Union[iMessage, None]: - """ - Will return the next iMessage in the queue, or None if there are no messages - """ - raw = self._get_raw_message() - if raw is None: - return None - body = apns._get_field(raw[1], 3) - body = plistlib.loads(body) - #print(f"Got body message {body}") - payload = body["P"] - - if not self._verify_payload(payload, body['sP'], body["t"]): - raise Exception("Failed to verify payload") - - decrypted = self._decrypt_payload(payload) - - return iMessage.from_raw(decrypted, body['sP']) - - KEY_CACHE_HANDLE: str = "" - KEY_CACHE: dict[bytes, tuple[bytes, bytes]] = {} - """Mapping of push token : (public key, session token)""" - USER_CACHE: dict[str, list[bytes]] = {} - """Mapping of handle : [push tokens]""" - - def _cache_keys(self, participants: list[str]): - # Clear the cache if the handle has changed - if self.KEY_CACHE_HANDLE != self.user.current_handle: - self.KEY_CACHE_HANDLE = self.user.current_handle - self.KEY_CACHE = {} - self.USER_CACHE = {} - - # Check to see if we have cached the keys for all of the participants - if all([p in self.USER_CACHE for p in participants]): - return - - # Look up the public keys for the participants, and cache a token : public key mapping - lookup = self.user.lookup(participants) - - for key, participant in lookup.items(): - if not key in self.USER_CACHE: - self.USER_CACHE[key] = [] - - for identity in participant["identities"]: - if not "client-data" in identity: - continue - if not "public-message-identity-key" in identity["client-data"]: - continue - if not "push-token" in identity: - continue - if not "session-token" in identity: - continue - - self.USER_CACHE[key].append(identity["push-token"]) - - # print(identity) - - self.KEY_CACHE[identity["push-token"]] = ( - identity["client-data"]["public-message-identity-key"], - identity["session-token"], - ) - - def send(self, message: iMessage): - # Set the sender, if it isn't already - if message.sender is None: - message.sender = self.user.handles[0] # TODO : Which handle to use? - - message.sanity_check() # Sanity check MUST be called before caching keys, so that the sender is added to the list of participants - self._cache_keys(message.participants) - - # Turn the message into a raw message - raw = message.to_raw() - import base64 - - bundled_payloads = [] - for participant in message.participants: - participant = participant.lower() - for push_token in self.USER_CACHE[participant]: - if push_token == self.connection.token: - continue # Don't send to ourselves - - identity_keys = ids.identity.IDSIdentity.decode( - self.KEY_CACHE[push_token][0] - ) - payload = self._encrypt_sign_payload(identity_keys, raw) - - bundled_payloads.append( - { - "tP": participant, - "D": not participant - == message.sender, # TODO: Should this be false sometimes? For self messages? - "sT": self.KEY_CACHE[push_token][1], - "P": payload, - "t": push_token, - } - ) - - msg_id = random.randbytes(4) - body = { - "fcn": 1, - "c": 100, - "E": "pair", - "ua": "[macOS,13.4.1,22F82,MacBookPro18,3]", - "v": 8, - "i": int.from_bytes(msg_id, "big"), - "U": message.id.bytes, - "dtl": bundled_payloads, - "sP": message.sender, - } - - body = plistlib.dumps(body, fmt=plistlib.FMT_BINARY) - - self.connection.send_message("com.apple.madrid", body, msg_id) - - # This code can check to make sure we got a success response, but waiting for the response is annoying, - # so for now we just YOLO it and assume it worked - - # def check_response(x): - # if x[0] != 0x0A: - # return False - # if apns._get_field(x[1], 2) != sha1("com.apple.madrid".encode()).digest(): - # return False - # resp_body = apns._get_field(x[1], 3) - # if resp_body is None: - # return False - # resp_body = plistlib.loads(resp_body) - # if "c" not in resp_body or resp_body["c"] != 255: - # return False - # return True - - - # num_recv = 0 - # while True: - # if num_recv == len(bundled_payloads): - # break - # payload = self.connection.incoming_queue.wait_pop_find(check_response) - # if payload is None: - # continue - - # resp_body = apns._get_field(payload[1], 3) - # resp_body = plistlib.loads(resp_body) - # logger.error(resp_body) - # num_recv += 1 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..05602b2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,37 @@ +[build-system] +requires = ["setuptools", "setuptools_scm"] +build-backend = "setuptools.build_meta" + +[project] +name = "pypush" +dynamic = ["version"] +dependencies = [ + "anyio", + "httpx", + "cryptography", + "typing-extensions", + "exceptiongroup", + 'importlib_metadata; python_version>="3.9"', +] + +[project.scripts] +pypush = "pypush.cli:main" + +[project.optional-dependencies] +test = [ + "pytest", + "pytest-asyncio", +] +cli = [ + "frida", + "rich", + "typer" +] + +[tool.setuptools_scm] +version_file = "pypush/_version.py" + +[tool.pytest.ini_options] +minversion = "6.0" +addopts = ["-ra", "-q"] +testpaths = ["tests"] \ No newline at end of file diff --git a/pypush/apns/__init__.py b/pypush/apns/__init__.py new file mode 100644 index 0000000..ff6398a --- /dev/null +++ b/pypush/apns/__init__.py @@ -0,0 +1,5 @@ +__all__ = ["protocol", "create_apns_connection", "activate"] + +from . import protocol +from .lifecycle import create_apns_connection +from .albert import activate diff --git a/pypush/apns/_protocol.py b/pypush/apns/_protocol.py new file mode 100644 index 0000000..140d9e3 --- /dev/null +++ b/pypush/apns/_protocol.py @@ -0,0 +1,142 @@ +from __future__ import annotations + +import logging +from dataclasses import MISSING, field +from dataclasses import fields as dataclass_fields +from typing import Any, TypeVar, get_origin, get_args, Union + +from pypush.apns.transport import Packet + +T = TypeVar("T") + + +def command(cls: T) -> T: + """ + Automatically add from_packet and to_packet methods to a dataclass + """ + + def from_packet(cls, packet: Packet): + assert packet.id == cls.PacketType + field_values = {} + for current_field in dataclass_fields(cls): + if ( + current_field.metadata is None + or "packet_id" not in current_field.metadata + ): + # This isn't meant for us, just skip it + continue + + packet_value = packet.fields_for_id(current_field.metadata["packet_id"]) + + current_field_type = current_field.type + + if get_origin(current_field_type) is Union and type(None) in get_args( + current_field_type + ): # Optional + if not packet_value: + field_values[current_field.name] = None + continue + current_field_type = get_args(current_field.type)[0] + else: + # If the field is not optional, it must be present + if not packet_value: + raise ValueError( + f"Field with packet ID {current_field.metadata['packet_id']} not found in packet" + ) + + if get_origin(current_field_type) is list: + assert get_args(current_field_type) == (bytes,) + field_values[current_field.name] = packet_value + else: + # If it's not supposed to be a list, assume that there is only 1 field with this ID + assert len(packet_value) == 1 + packet_value = packet_value[0] + + if current_field_type == int: + assert len(packet_value) == current_field.metadata["packet_bytes"] + field_values[current_field.name] = int.from_bytes( + packet_value, "big" + ) + elif current_field_type == str: + field_values[current_field.name] = packet_value.decode() + elif current_field_type == bytes: + field_values[current_field.name] = packet_value + else: + raise TypeError( + f"Unsupported field type: {repr(current_field_type)} for field '{current_field.name}' in {cls.__name__}" + ) + + # Check for extra fields + for field in packet.fields: + if field.id not in [ + f.metadata["packet_id"] + for f in dataclass_fields(cls) + if f.metadata is not None and "packet_id" in f.metadata + ]: + logging.warning( + f"Unexpected field with packet ID {field.id} in packet {packet}" + ) + return cls(**field_values) + + def to_packet(self) -> Packet: + packet_fields = [] + for f in dataclass_fields(self): + if f.metadata is None or "packet_id" not in f.metadata: + continue + value = getattr(self, f.name) + if isinstance(value, int): + packet_value = value.to_bytes(f.metadata["packet_bytes"], "big") + elif isinstance(value, str): + packet_value = value.encode() + elif isinstance(value, bytes): + packet_value = value + elif value is None: + continue + elif isinstance(value, list): + for v in value: + packet_fields.append( + Packet.Field(id=f.metadata["packet_id"], value=v) + ) + continue + else: + raise TypeError(f"Unsupported field type: {f.type}") + packet_fields.append( + Packet.Field(id=f.metadata["packet_id"], value=packet_value) + ) + return Packet(id=self.PacketType, fields=packet_fields) + + setattr(cls, "from_packet", classmethod(from_packet)) + setattr(cls, "to_packet", to_packet) + return cls + + +def fid( + packet_id: int, + byte_len: int = 1, + default: Any = MISSING, + default_factory: Any = MISSING, + repr: bool = True, +): + """ + :param packet_id: The packet ID of the field + :param byte_len: The length of the field in bytes (for int fields) + :param default: The default value of the field + """ + if not default == MISSING and not default_factory == MISSING: + raise ValueError("Cannot specify both default and default_factory") + if not default == MISSING: + return field( + metadata={"packet_id": packet_id, "packet_bytes": byte_len}, + default=default, + repr=repr, + ) + if not default_factory == MISSING: + return field( + metadata={"packet_id": packet_id, "packet_bytes": byte_len}, + default_factory=default_factory, + repr=repr, + ) + else: + return field( + metadata={"packet_id": packet_id, "packet_bytes": byte_len}, repr=repr + ) diff --git a/pypush/apns/_util.py b/pypush/apns/_util.py new file mode 100644 index 0000000..09e9574 --- /dev/null +++ b/pypush/apns/_util.py @@ -0,0 +1,45 @@ +import logging +from contextlib import asynccontextmanager +from typing import Generic, TypeVar + +import anyio +from anyio.abc import ObjectSendStream + +T = TypeVar("T") + + +class BroadcastStream(Generic[T]): + def __init__(self): + self.streams: list[ObjectSendStream[T]] = [] + + async def broadcast(self, packet): + for stream in self.streams: + try: + await stream.send(packet) + except anyio.BrokenResourceError: + self.streams.remove(stream) + + @asynccontextmanager + async def open_stream(self): + send, recv = anyio.create_memory_object_stream[T]() + self.streams.append(send) + async with recv: + yield recv + self.streams.remove(send) + await send.aclose() + + +def exponential_backoff(f): + async def wrapper(*args, **kwargs): + backoff = 1 + while True: + try: + return await f(*args, **kwargs) + except Exception as e: + logging.warning( + f"Error in {f.__name__}: {e}, retrying in {backoff} seconds" + ) + await anyio.sleep(backoff) + backoff *= 2 + + return wrapper diff --git a/albert.py b/pypush/apns/albert.py similarity index 76% rename from albert.py rename to pypush/apns/albert.py index ca447ed..024e449 100644 --- a/albert.py +++ b/pypush/apns/albert.py @@ -1,20 +1,18 @@ +__all__ = ["activate"] + import plistlib import re import uuid -from base64 import b64decode, b64encode +from base64 import b64decode +from typing import Tuple, Optional -import requests +import httpx from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding, rsa from cryptography.x509.oid import NameOID -import logging -logger = logging.getLogger("albert") - -# These keys are from https://github.com/MiUnlockCode/albertsimlockapple/blob/main/ALBERTBUGBYMIUNLOCK.php, which is licensed under the MIT license -# If this becomes a problem, I know how to generate new keys FAIRPLAY_PRIVATE_KEY = b64decode( "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlDV3dJQkFBS0JnUUMzQktyTFBJQmFiaHByKzRTdnVRSG5iRjBzc3FSSVE2Ny8xYlRmQXJWdVVGNnA5c2RjdjcwTityOHlGeGVzRG1wVG1LaXRMUDA2c3pLTkFPMWs1SlZrOS9QMWVqejA4Qk1lOWVBYjRqdUFoVldkZkFJeWFKN3NHRmplU0wwMTVtQXZyeFRGY09NMTBGL3FTbEFSQmljY3hIalBYdHVXVnIwZkxHcmhNKy9BTVFJREFRQUJBb0dBQ0dXM2JISFBOZGI5Y1Z6dC9wNFBmMDNTakoxNXVqTVkwWFk5d1VtL2gxczZyTE84Ky8xME1ETUVHTWxFZGNtSGlXUmt3T1ZpalJIeHpOUnhFQU1JODdBcnVvZmhqZGRiTlZMdDZwcFcybkxDSzdjRURRSkZhaFRXOUdRRnpwVlJRWFhmeHI0Y3MxWDNrdXRsQjZ1WTJWR2x0eFFGWXNqNWRqdjdEK0E3MkEwQ1FRRFpqMVJHZHhiZU9vNFh6eGZBNm40MkdwWmF2VGxNM1F6R0ZvQkpnQ3FxVnUxSlFPem9vQU1SVCtOUGZnb0U4K3VzSVZWQjRJbzBiQ1VUV0xwa0V5dFRBa0VBMTFyeklwR0loRmtQdE5jLzMzZnZCRmd3VWJzalRzMVY1RzZ6NWx5L1huRzlFTmZMYmxnRW9iTG1TbXozaXJ2QlJXQURpd1V4NXpZNkZOL0RtdGk1NndKQWRpU2Nha3VmY255dnp3UVo3UndwLzYxK2VyWUpHTkZ0YjJDbXQ4Tk82QU9laGNvcEhNWlFCQ1d5MWVjbS83dUovb1ozYXZmSmRXQkkzZkd2L2twZW13SkFHTVh5b0RCanB1M2oyNmJEUno2eHRTczc2N3IrVmN0VExTTDYrTzRFYWFYbDNQRW1DcngvVSthVGpVNDVyN0RuaThaK3dkaElKRlBkbkpjZEZrd0dId0pBUFErd1ZxUmpjNGgzSHd1OEk2bGxrOXdocEs5TzcwRkxvMUZNVmRheXRFbE15cXpRMi8wNWZNYjdGNnlhV2h1K1EyR0dYdmRsVVJpQTN0WTBDc2ZNMHc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQ==" ) @@ -23,7 +21,7 @@ FAIRPLAY_CERT_CHAIN = b64decode( ) -def _generate_csr(private_key: rsa.RSAPrivateKey) -> str: +def _generate_csr(private_key: rsa.RSAPrivateKey, name: str = str(uuid.uuid4())) -> str: csr = ( x509.CertificateSigningRequestBuilder() .subject_name( @@ -34,7 +32,7 @@ def _generate_csr(private_key: rsa.RSAPrivateKey) -> str: x509.NameAttribute(NameOID.LOCALITY_NAME, "Cupertino"), x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Apple Inc."), x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "iPhone"), - x509.NameAttribute(NameOID.COMMON_NAME, str(uuid.uuid4())), + x509.NameAttribute(NameOID.COMMON_NAME, name), ] ) ) @@ -43,71 +41,71 @@ def _generate_csr(private_key: rsa.RSAPrivateKey) -> str: return csr.public_bytes(serialization.Encoding.PEM).decode("utf-8") -# Generates an APNs push certificate by talking to Albert -# Returns [private key PEM, certificate PEM] -def generate_push_cert() -> tuple[str, str]: +async def activate( + http_client: Optional[httpx.AsyncClient] = None, + device_class: str = "Windows", + udid: str = str(uuid.uuid4()), + serial: str = "WindowSerial", + version: str = "10.6.4", + build: str = "10.6.4", + model: str = "windows1,1", +) -> Tuple[x509.Certificate, rsa.RSAPrivateKey]: + if http_client is None: + # Do this here to ensure the client is not accidentally reused during tests + http_client = httpx.AsyncClient() + private_key = rsa.generate_private_key( - public_exponent=65537, key_size=2048, backend=default_backend() + public_exponent=65537, key_size=1024, backend=default_backend() ) csr = _generate_csr(private_key) - activation_info = { - "ActivationRandomness": str(uuid.uuid4()), - "ActivationState": "Unactivated", - "BuildVersion": "10.6.4", - "DeviceCertRequest": csr.encode("utf-8"), - "DeviceClass": "Windows", - "ProductType": "windows1,1", - "ProductVersion": "10.6.4", - "SerialNumber": "WindowSerial", - "UniqueDeviceID": str(uuid.uuid4()), - } + activation_info = plistlib.dumps( + { + "ActivationRandomness": str(uuid.uuid4()), + "ActivationState": "Unactivated", + "BuildVersion": build, + "DeviceCertRequest": csr.encode("utf-8"), + "DeviceClass": device_class, + "ProductType": model, + "ProductVersion": version, + "SerialNumber": serial, + "UniqueDeviceID": udid, + } + ) - logger.debug(f"Generated activation info (with UUID: {activation_info['UniqueDeviceID']})") - - activation_info = plistlib.dumps(activation_info) - - # Load the private key fairplay_key = serialization.load_pem_private_key( FAIRPLAY_PRIVATE_KEY, password=None, backend=default_backend() ) + assert isinstance(fairplay_key, rsa.RSAPrivateKey) - # Sign the activation info - signature = fairplay_key.sign(activation_info, padding.PKCS1v15(), hashes.SHA1()) # type: ignore + signature = fairplay_key.sign(activation_info, padding.PKCS1v15(), hashes.SHA1()) - body = { - "ActivationInfoComplete": True, - "ActivationInfoXML": activation_info, - "FairPlayCertChain": FAIRPLAY_CERT_CHAIN, - "FairPlaySignature": signature, - } - - resp = requests.post( - "https://albert.apple.com/WebObjects/ALUnbrick.woa/wa/deviceActivation?device=Windows", - data={"activation-info": plistlib.dumps(body)}, - verify=False, + resp = await http_client.post( + f"https://albert.apple.com/deviceservices/deviceActivation?device={device_class}", + data={ + "activation-info": plistlib.dumps( + { + "ActivationInfoComplete": True, + "ActivationInfoXML": activation_info, + "FairPlayCertChain": FAIRPLAY_CERT_CHAIN, + "FairPlaySignature": signature, + } + ).decode() + }, ) - protocol = re.search("(.*)", resp.text).group(1) # type: ignore + try: + protocol = re.search("(.*)", resp.text).group(1) # type: ignore + except AttributeError: + # Search for error text between and + error = re.search("(.*)", resp.text).group(1) # type: ignore + raise Exception(f"Failed to get certificate from Albert: {error}") + protocol = plistlib.loads(protocol.encode("utf-8")) - logger.debug("Recieved push certificate from Albert") - return ( - private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption(), - ) - .decode("utf-8") - .strip(), - protocol["device-activation"]["activation-record"]["DeviceCertificate"] - .decode("utf-8") - .strip(), + x509.load_pem_x509_certificate( + protocol["device-activation"]["activation-record"]["DeviceCertificate"] + ), + private_key, ) - - -if __name__ == "__main__": - private_key, cert = generate_push_cert() - print(private_key) - print(cert) diff --git a/pypush/apns/lifecycle.py b/pypush/apns/lifecycle.py new file mode 100644 index 0000000..49b4fcf --- /dev/null +++ b/pypush/apns/lifecycle.py @@ -0,0 +1,141 @@ +# Lifecycle management, reconnection, etc +from __future__ import annotations + +import logging +import random +import time +import typing +from contextlib import asynccontextmanager + +import anyio +from anyio.abc import TaskGroup +from cryptography import x509 +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import padding, rsa + +from . import protocol, transport, _util + + +@asynccontextmanager +async def create_apns_connection( + certificate: x509.Certificate, + private_key: rsa.RSAPrivateKey, + token: typing.Optional[bytes] = None, + courier: typing.Optional[str] = None, +): + async with anyio.create_task_group() as tg: + conn = Connection(tg, certificate, private_key, token, courier) + yield conn + tg.cancel_scope.cancel() # Cancel the task group when the context manager exits + await conn.aclose() # Make sure to close the connection after the task group is cancelled + + +class Connection: + def __init__( + self, + task_group: TaskGroup, + certificate: x509.Certificate, + private_key: rsa.RSAPrivateKey, + token: typing.Optional[bytes] = None, + courier: typing.Optional[str] = None, + ): + + self.certificate = certificate + self.private_key = private_key + self.base_token = token + + self._conn = None + self._tg = task_group + self._broadcast = _util.BroadcastStream[protocol.Command]() + self._reconnect_lock = anyio.Lock() + + if courier is None: + # Pick a random courier server from 1 to 50 + courier = f"{random.randint(1, 50)}-courier.push.apple.com" + self.courier = courier + + self._tg.start_soon(self.reconnect) + self._tg.start_soon(self._ping_task) + + async def _receive_task(self): + assert self._conn is not None + async for command in self._conn: + logging.debug(f"Received command: {command}") + await self._broadcast.broadcast(command) + logging.warning("Receive task ended") + + async def _ping_task(self): + while True: + await anyio.sleep(30) + logging.debug("Sending keepalive") + await self.send(protocol.KeepAliveCommand()) + await self.receive(protocol.KeepAliveAck) + + @_util.exponential_backoff + async def reconnect(self): + async with self._reconnect_lock: # Prevent weird situations where multiple reconnects are happening at once + if self._conn is not None: + logging.warning("Closing existing connection") + await self._conn.aclose() + self._conn = protocol.CommandStream( + await transport.create_courier_connection(courier=self.courier) + ) + cert = self.certificate.public_bytes(serialization.Encoding.DER) + nonce = ( + b"\x00" + + int(time.time() * 1000).to_bytes(8, "big") + + random.randbytes(8) + ) + signature = b"\x01\x01" + self.private_key.sign( + nonce, padding.PKCS1v15(), hashes.SHA1() + ) + await self._conn.send( + protocol.ConnectCommand( + push_token=self.base_token, + state=1, + flags=69, + certificate=cert, + nonce=nonce, + signature=signature, + ) + ) + self._tg.start_soon(self._receive_task) + ack = await self.receive(protocol.ConnectAck) + logging.debug(f"Connected with ack: {ack}") + assert ack.status == 0 + if self.base_token is None: + self.base_token = ack.token + else: + assert ack.token == self.base_token + + async def aclose(self): + if self._conn is not None: + await self._conn.aclose() + # Note: Will be reopened if task group is still running and ping task is still running + + T = typing.TypeVar("T", bound=protocol.Command) + + async def receive_stream( + self, filter: typing.Type[T], max: int = -1 + ) -> typing.AsyncIterator[T]: + async with self._broadcast.open_stream() as stream: + async for command in stream: + if isinstance(command, filter): + yield command + max -= 1 + if max == 0: + break + + async def receive(self, filter: typing.Type[T]) -> T: + async for command in self.receive_stream(filter, 1): + return command + raise ValueError("No matching command received") + + async def send(self, command: protocol.Command): + try: + assert self._conn is not None + await self._conn.send(command) + except Exception as e: + logging.warning(f"Error sending command, reconnecting") + await self.reconnect() + await self.send(command) diff --git a/pypush/apns/protocol.py b/pypush/apns/protocol.py new file mode 100644 index 0000000..ea0f7d3 --- /dev/null +++ b/pypush/apns/protocol.py @@ -0,0 +1,285 @@ +from dataclasses import dataclass +from hashlib import sha1 +from typing import Optional, Union + +from anyio.abc import ByteStream, ObjectStream + +from pypush.apns._protocol import command, fid +from pypush.apns.transport import Packet + +# fmt: off +KNOWN_TOPICS = {'dev.jjtech.pypush.tests', 'com.apple.private.alloy.notes', 'com.apple.icloud-container.clouddocs.F3LWYJ7GM7.com.apple.garageband10', 'com.apple.private.alloy.screentime', 'com.apple.icloud-container.com.apple.appleaccount.custodian', 'com.apple.icloud-container.clouddocs.iCloud.com.apple.configurator.ui', 'com.apple.icloud-container.com.apple.VoiceMemos', 'com.apple.icloud-container.com.apple.SafariShared.Settings', 'com.apple.private.alloy.status.keysharing', 'com.apple.private.alloy.electrictouch', 'com.apple.private.alloy.icloudpairing', 'com.apple.icloud.presence.shared.experience', 'com.apple.icloud-container.com.apple.knowledge-agent', 'com.apple.private.alloy.thumper.keys', 'com.apple.pay.services.ck.zone.prod', 'com.apple.sharedstreams', 'com.apple.jalisco', 'com.apple.private.alloy.ded', 'com.apple.icloud-container.com.apple.cloudpaird', 'com.apple.private.alloy.multiplex1', 'com.apple.private.alloy.nearby', 'com.me.contacts', 'com.apple.TestFlight', 'com.icloud.family', 'com.apple.icloud-container.com.apple.iWork.Pages', 'com.apple.bookassetd', 'com.apple.tv.favoriteTeams', 'com.apple.icloud-container.com.apple.Safari', 'com.apple.mobileme.fmf3', 'com.apple.icloud-container.clouddocs.iCloud.com.apple.iBooks.iTunesU', 'com.apple.private.alloy.applepay', 'com.apple.private.alloy.willow', 'com.apple.idmsauth', 'com.apple.icloud-container.com.apple.iWork.Numbers', 'com.apple.icloud-container.clouddocs.F3LWYJ7GM7.com.apple.mobilegarageband', 'com.apple.private.alloy.maps', 'com.apple.private.alloy.phonecontinuity', 'com.apple.private.alloy.avconference.icloud', 'com.apple.pay.services.apply.prod', 'com.apple.private.alloy.facetime.multi', 'com.apple.icloud-container.clouddocs.com.apple.TextInput', 'com.apple.icloud-container.clouddocs.iCloud.com.reddit.reddit', 'com.apple.icloud-container.clouddocs.com.apple.Numbers', 'com.apple.icloud.fmip.voiceassistantsync', 'com.apple.icloud-container.com.apple.avatarsd', 'com.apple.private.ac', 'company.thebrowser.Browser', 'com.apple.itunesstored', 'com.apple.icloud-container.com.apple.icloud.fmfd', 'com.apple.private.alloy.screentime.invite', 'com.apple.icloud-container.com.apple.donotdisturbd', 'com.apple.icloud-container.clouddocs.com.apple.TextEdit', 'com.apple.appstored', 'com.apple.icloud-container.clouddocs.com.apple.CloudDocs.container-metadata', 'com.apple.private.alloy.screensharing', 'com.apple.private.alloy.accessibility.switchcontrol', 'com.apple.private.alloy.screensharing.qr', 'com.apple.private.alloy.amp.potluck', 'com.apple.icloud-container.com.apple.siriknowledged', 'com.apple.private.alloy.gamecenter', 'com.apple.appstored-testflight', 'com.apple.private.alloy.messagenotification', 'com.apple.passd.usernotifications', 'com.apple.icloud-container.clouddocs.com.apple.Pages', 'com.apple.private.alloy.safeview', 'com.apple.findmy', 'com.apple.pay.auxiliary.registration.requirement.prod', 'com.apple.aa.idms', 'com.apple.private.alloy.ids.cloudmessaging', 'com.apple.icloud-container.com.apple.icloud.searchpartyuseragent', 'com.icloud.quota', 'com.apple.icloud-container.com.apple.upload-request-proxy.com.apple.photos.cloud', 'com.apple.private.alloy.usagetracking', 'com.apple.icloud-container.com.apple.syncdefaultsd', 'com.apple.private.alloy.continuity.tethering', 'com.apple.idmsauthagent', 'com.apple.sagad', 'com.apple.pay.services.ownershipTokens.prod', 'com.apple.private.alloy.sms', 'com.apple.Notes', 'com.apple.icloud-container.com.apple.SafariShared.WBSCloudBookmarksStore', 'com.apple.icloud-container.com.apple.reminders', 'com.apple.private.alloy.classroom', 'com.apple.news', 'com.apple.icloud-container.com.apple.imagent', 'com.apple.pay.services.products.prod', 'com.apple.private.alloy.fmf', 'com.apple.amsaccountsd', 'com.apple.private.alloy.itunes', 'com.apple.icloud-container.clouddocs.iCloud.com.apple.iBooks', 'com.apple.private.alloy.gelato', 'com.apple.icloud-container.com.apple.willowd', 'com.apple.icloud-container.clouddocs.com.apple.CloudDocs', 'com.apple.icloud-container.com.apple.protectedcloudstorage.protectedcloudkeysyncing', 'com.apple.icloud-container.com.apple.Notes', 'com.me.cal', 'com.apple.peerpayment', 'com.apple.icloud-container.clouddocs.iCloud.is.workflow.my.workflows', 'com.apple.private.alloy.facetime.sync', 'com.apple.icloud-container.com.apple.news', 'com.apple.icloud-container.com.apple.TrustedPeersHelper', 'com.apple.private.alloy.home.invite', 'com.apple.private.alloy.coreduet.sync', 'com.apple.private.alloy.contextsync', 'com.apple.private.alloy.fmd', 'com.apple.private.alloy.status.personal', 'com.apple.icloud-container.com.apple.assistant.assistantd', 'com.apple.private.alloy.sleep.icloud', 'com.apple.icloud-container.com.apple.security.cuttlefish', 'com.apple.wallet.sharing', 'com.apple.icloud-container.clouddocs.3L68KQB4HG.com.readdle.CommonDocuments', 'com.apple.pay.provision', 'com.apple.icloud-container.com.apple.StatusKitAgent', 'com.apple.icloud-container.clouddocs.com.apple.Preview', 'com.apple.icloud-container.com.apple.gamed', 'com.apple.askpermissiond', 'com.apple.private.alloy.gamecenter.imessage', 'com.apple.private.alloy.safari.groupactivities', 'com.apple.icloud-container.com.apple.Maps', 'com.apple.private.alloy.willow.stream', 'com.apple.pay.services.devicecheckin.prod.us', 'com.apple.icloud.presence.mode.status', 'com.apple.ess', 'com.apple.private.alloy.accounts.representative', 'com.apple.icloud-container.clouddocs.com.apple.QuickTimePlayerX', 'com.apple.private.alloy.facetime.audio', 'com.apple.private.alloy.continuity.unlock', 'com.apple.icloud-container.clouddocs.iCloud.md.obsidian', 'com.apple.icloud-container.clouddocs.iCloud.com.apple.MobileSMS', 'com.apple.iWork.Numbers', 'com.apple.pay.services.account.prod', 'com.apple.private.alloy.quickrelay', 'com.apple.iBooksX', 'com.apple.madrid', 'com.apple.private.alloy.continuity.activity', 'com.apple.icloud-container.com.apple.keyboardservicesd', 'com.apple.icloud-container.clouddocs.com.apple.CloudDocs.health', 'com.apple.icloud-container.com.apple.suggestd', 'com.apple.icloud-container.clouddocs.com.apple.Keynote', 'com.apple.private.alloy.home', 'com.apple.private.alloy.photostream', 'com.apple.icloud-container.com.apple.iBooksX', 'com.apple.private.alloy.digitalhealth', 'com.apple.icloud-container.clouddocs.iCloud.dk.simonbs.Scriptable', 'com.apple.private.alloy.copresence', 'com.apple.private.alloy.continuity.encryption', 'com.apple.icloud-container.com.apple.passd', 'com.apple.icloud-container.com.apple.findmy', 'com.apple.icloud-container.com.apple.financed', 'com.apple.icloud-container.com.apple.photos.cloud', 'com.apple.private.alloy.proxiedcrashcopier.icloud', 'com.apple.private.alloy.tips', 'com.apple.icloud-container.com.apple.appleaccount.beneficiary.private', 'com.apple.watchList', 'com.apple.icloud-container.com.apple.willowd.homekit', 'com.apple.icloud-container.clouddocs.com.apple.CloudDocs.pp-metadata', 'com.apple.icloud-container.com.apple.SafariShared.CloudTabs', 'com.apple.private.alloy.facetime.lp', 'com.apple.icloud-container.com.apple.appleaccount.beneficiary', 'com.apple.aa.setupservice', 'com.apple.icloud.fmip.app.push', 'com.apple.icloud.presence.channel.management', 'com.apple.icloud-container.clouddocs.com.apple.ScriptEditor2', 'com.apple.private.alloy.facetime.mw', 'com.apple.Maps', 'com.apple.icloud-container.clouddocs.com.apple.mail', 'com.apple.mobileme.fmf2', 'com.me.setupservice', 'paymentpass.com.apple', 'com.apple.music.social', 'com.apple.icloud-container.clouddocs.com.apple.iBooks.cloudData', 'com.apple.iWork.Pages', 'com.apple.private.alloy.carmelsync', 'com.apple.private.alloy.maps.eta', 'com.apple.icloud-container.clouddocs.com.apple.shoebox', 'com.apple.dt.Xcode', 'com.apple.private.alloy.facetime.video', 'com.apple.icloud-container.com.apple.sociallayerd', 'com.apple.private.alloy.keytransparency.accountkey.pinning', 'com.apple.wallet.sharing.qa', 'com.apple.icloud-container.com.apple.appleaccount.custodian.private', 'com.apple.private.alloy.phone.auth', 'com.apple.icloud-container.com.apple.amsengagementd', 'com.apple.amsengagementd.notifications', 'com.apple.maps.icloud', 'com.apple.storekit', 'com.apple.triald', 'com.icloud.askpermission', 'com.apple.private.alloy.biz', 'com.apple.tilt', 'com.apple.icloud-container.com.apple.callhistory.sync-helper', 'com.apple.private.ids', 'com.apple.private.alloy.clockface.sharing', 'com.apple.gamed', 'com.apple.icloud-container.company.thebrowser.Browser', 'com.apple.icloud-container.com.apple.securityd'} +KNOWN_TOPICS_LOOKUP = {sha1(topic.encode()).digest():topic for topic in KNOWN_TOPICS} +# fmt: on + + +@dataclass +class Command: + @classmethod + def from_packet(cls, packet: Packet): + raise NotImplementedError + + def to_packet(self) -> Packet: + raise NotImplementedError + + +@command +@dataclass +class ConnectCommand(Command): + PacketType = Packet.Type.Connect + + push_token: Optional[bytes] = fid(1) + state: Optional[int] = fid(2) + flags: int = fid(5, byte_len=4) + certificate: Optional[bytes] = fid(12) + nonce: Optional[bytes] = fid(13) + signature: Optional[bytes] = fid(14) + + interface: Optional[int] = fid(6, default=None) + carrier_name: Optional[str] = fid(8, default=None) + os_version: Optional[str] = fid(9, default=None) + os_build: Optional[str] = fid(10, default=None) + hardware_version: Optional[str] = fid(11, default=None) + protocol_version: Optional[int] = fid(16, default=11, byte_len=2) + redirect_count: Optional[int] = fid(17, default=None, byte_len=2) + dns_resolve_time: Optional[int] = fid(19, default=None, byte_len=2) + tls_handshake_time: Optional[int] = fid(20, default=None, byte_len=2) + timestamp: Optional[int] = fid(22, default=None, byte_len=8) + unknown26: Optional[bytes] = fid(26, default=None) + + +@command +@dataclass +class ConnectAck(Command): + PacketType = Packet.Type.ConnectAck + + status: int = fid(1) + token: Optional[bytes] = fid(3) + max_message_size: int = fid(4, byte_len=2) + unknown5: bytes = fid(5) + capabilities: bytes = fid(6) + large_message_size: Optional[int] = fid(8, byte_len=2) + timestamp: int = fid(10, byte_len=8) + region: Optional[str] = fid(11) + timestamp2: Optional[int] = fid(12, byte_len=8) + unknown19: Optional[bytes] = fid(19) + + +@command +@dataclass +class NoStorageCommand(Command): + PacketType = Packet.Type.NoStorage + token: bytes = fid(1) + + +@command +@dataclass(repr=False) +class FilterCommand(Command): + PacketType = Packet.Type.FilterTopics + + token: bytes = fid(1) + enabled_topic_hashes: Optional[list[bytes]] = fid(2) + ignored_topic_hashes: Optional[list[bytes]] = fid(3, default=None) + opportunistic_topic_hashes: Optional[list[bytes]] = fid(4, default=None) + paused_topic_hashes: Optional[list[bytes]] = fid(5, default=None) + non_waking_topic_hashes: Optional[list[bytes]] = fid(6, default=None) + unknown12: Optional[bytes] = fid(12, default=None) + + def _lookup_hashes(self, hashes: Optional[list[bytes]]): + return ( + [ + KNOWN_TOPICS_LOOKUP[hash] if hash in KNOWN_TOPICS_LOOKUP else hash + for hash in hashes + ] + if hashes + else [] + ) + + @property + def enabled_topics(self): + return self._lookup_hashes(self.enabled_topic_hashes) + + @property + def ignored_topics(self): + return self._lookup_hashes(self.ignored_topic_hashes) + + @property + def opportunistic_topics(self): + return self._lookup_hashes(self.opportunistic_topic_hashes) + + @property + def paused_topics(self): + return self._lookup_hashes(self.paused_topic_hashes) + + @property + def non_waking_topics(self): + return self._lookup_hashes(self.non_waking_topic_hashes) + + def __repr__(self): + return f"FilterCommand(token={self.token}, enabled_topics={self.enabled_topics}, ignored_topics={self.ignored_topics}, opportunistic_topics={self.opportunistic_topics}, paused_topics={self.paused_topics}, non_waking_topics={self.non_waking_topics})" + + +@command +@dataclass +class KeepAliveCommand(Command): + PacketType = Packet.Type.KeepAlive + + connection_method: Optional[str] = fid(1, default=None) + ios_version: Optional[str] = fid(2, default=None) + ios_build: Optional[str] = fid(3, default=None) + device_model: Optional[str] = fid(4, default=None) + unknown5: Optional[int] = fid(5, default=None, byte_len=2) + unknown6: Optional[str] = fid(6, default=None) + unknown9: Optional[int] = fid(9, default=None, byte_len=1) + unknown10: Optional[int] = fid(10, default=None, byte_len=1) + + +@command +@dataclass +class KeepAliveAck(Command): + PacketType = Packet.Type.KeepAliveAck + unknown: Optional[int] = fid(1) + +@command +@dataclass +class SetStateCommand(Command): + PacketType = Packet.Type.SetState + + state: int = fid(1) + unknown2: int = fid(2, byte_len=4) + + +@command +@dataclass +class SendMessageCommand(Command): + PacketType = Packet.Type.SendMessage + + payload: bytes = fid(3) + id: bytes = fid(4) + + topic: Optional[Union[str, bytes]] = None + token: Optional[bytes] = None + outgoing: Optional[bool] = None + + expiry: Optional[int] = fid(5, byte_len=4, default=None) + timestamp: Optional[int] = fid(6, byte_len=8, default=None) + unknown7: Optional[bytes] = fid(7, default=None) + unknown9: Optional[int] = fid(9, byte_len=1, default=None) + unknown13: Optional[int] = fid(13, byte_len=1, default=None) + unknown15: Optional[bytes] = fid(15, default=None) + unknown21: Optional[bytes] = fid(21, default=None) + unknown28: Optional[bytes] = fid(28, default=None) + unknown29: Optional[bytes] = fid(29, default=None) + + _token_topic_1: bytes = fid(1, default=None, repr=False) + _token_topic_2: bytes = fid(2, default=None, repr=False) + + def __post_init__(self): + if not ( + self.topic is not None + and self.token is not None + and self.outgoing is not None + ) and not (self._token_topic_1 is not None and self._token_topic_2 is not None): + raise ValueError("topic, token, and outgoing must be set.") + + if self.outgoing == True: + assert self.topic and self.token + self._token_topic_1 = ( + sha1(self.topic.encode()).digest() + if isinstance(self.topic, str) + else self.topic + ) + self._token_topic_2 = self.token + elif self.outgoing == False: + assert self.topic and self.token + self._token_topic_1 = self.token + self._token_topic_2 = ( + sha1(self.topic.encode()).digest() + if isinstance(self.topic, str) + else self.topic + ) + else: + assert self._token_topic_1 and self._token_topic_2 + if len(self._token_topic_1) == 20: # SHA1 hash, topic + self.topic = ( + KNOWN_TOPICS_LOOKUP[self._token_topic_1] + if self._token_topic_1 in KNOWN_TOPICS_LOOKUP + else self._token_topic_1 + ) + self.token = self._token_topic_2 + self.outgoing = True + else: + self.topic = ( + KNOWN_TOPICS_LOOKUP[self._token_topic_2] + if self._token_topic_2 in KNOWN_TOPICS_LOOKUP + else self._token_topic_2 + ) + self.token = self._token_topic_1 + self.outgoing = False + + +@command +@dataclass +class SendMessageAck(Command): + PacketType = Packet.Type.SendMessageAck + + id: bytes = fid(4) + status: int = fid(8) + token: Optional[bytes] = fid(1, default=None) + unknown6: Optional[bytes] = fid(6, default=None) + + +@dataclass +class UnknownCommand(Command): + id: Packet.Type + fields: list[Packet.Field] + + @classmethod + def from_packet(cls, packet: Packet): + return cls(id=packet.id, fields=packet.fields) + + def to_packet(self) -> Packet: + return Packet(id=self.id, fields=self.fields) + + def __repr__(self): + if self.id.value in [29, 30, 32]: + return f"UnknownCommand(id={self.id}, fields=[SUPPRESSED])" + return f"UnknownCommand(id={self.id}, fields={self.fields})" + + +# Factory function to create Command instances from Packets +def command_from_packet(packet: Packet) -> Command: + command_classes: dict[Packet.Type, type[Command]] = { + Packet.Type.Connect: ConnectCommand, + Packet.Type.KeepAlive: KeepAliveCommand, + Packet.Type.ConnectAck: ConnectAck, + Packet.Type.FilterTopics: FilterCommand, + Packet.Type.NoStorage: NoStorageCommand, + Packet.Type.KeepAliveAck: KeepAliveAck, + Packet.Type.SetState: SetStateCommand, + Packet.Type.SendMessage: SendMessageCommand, + Packet.Type.SendMessageAck: SendMessageAck, + # Add other mappings here... + } + command_class = command_classes.get(packet.id, None) + if command_class: + return command_class.from_packet(packet) + else: + return UnknownCommand.from_packet(packet) + + +@dataclass +class CommandStream(ObjectStream[Command]): + transport_stream: ObjectStream[Packet] + + async def send(self, item: Command) -> None: + await self.transport_stream.send(item.to_packet()) + + async def receive(self) -> Command: + return command_from_packet(await self.transport_stream.receive()) + + async def aclose(self) -> None: + await self.transport_stream.aclose() + + async def send_eof(self) -> None: + await self.transport_stream.send_eof() diff --git a/pypush/apns/transport.py b/pypush/apns/transport.py new file mode 100644 index 0000000..864f3eb --- /dev/null +++ b/pypush/apns/transport.py @@ -0,0 +1,132 @@ +from __future__ import annotations + +import ssl +from dataclasses import dataclass +from enum import Enum + +import anyio +from anyio.abc import ByteStream, ObjectStream + +# Pick a random courier server from 01 to APNSCourierHostcount +COURIER_PORT = 5223 +ALPN = ["apns-security-v3"] + +# Manages TLS connection to courier, parses into raw packets + + +@dataclass +class Packet: + @dataclass + class Field: + id: int + value: bytes + + class Type(Enum): + Connect = 7 + ConnectAck = 8 + FilterTopics = 9 + SendMessage = 10 + SendMessageAck = 11 + KeepAlive = 12 + KeepAliveAck = 13 + NoStorage = 14 + SetState = 20 + UNKNOWN = "Unknown" + + def __new__(cls, value): + # Create a new instance of Enum + obj = object.__new__(cls) + obj._value_ = value + return obj + + @classmethod + def _missing_(cls, value): + # Handle unknown values + instance = cls.UNKNOWN + instance._value_ = value # Assign the unknown value + return instance + + def __str__(self): + if self is Packet.Type.UNKNOWN: + return f"Unknown({self._value_})" + return self.name + + + id: Type + fields: list[Field] + + def fields_for_id(self, id: int) -> list[bytes]: + return [field.value for field in self.fields if field.id == id] + + +async def create_courier_connection( + courier: str = "1-courier.push.apple.com", +) -> PacketStream: + context = ssl.create_default_context() + context.set_alpn_protocols(ALPN) + + # TODO: Verify courier certificate + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + + return PacketStream( + await anyio.connect_tcp( + courier, COURIER_PORT, ssl_context=context, tls_standard_compatible=False + ) + ) + + +async def receive_exact(stream: ByteStream, length: int) -> bytes: + buffer = b"" + while len(buffer) < length: + buffer += await stream.receive(length - len(buffer)) + return buffer + + +@dataclass +class PacketStream(ObjectStream[Packet]): + transport_stream: ByteStream + + def _serialize_field(self, field: Packet.Field) -> bytes: + return ( + field.id.to_bytes(1, "big") + + len(field.value).to_bytes(2, "big") + + field.value + ) + + def _serialize_packet(self, packet: Packet) -> bytes: + payload = b"" + for field in packet.fields: + payload += self._serialize_field(field) + return ( + packet.id.value.to_bytes(1, "big") + + len(payload).to_bytes(4, "big") + + payload + ) + + async def send(self, item: Packet) -> None: + await self.transport_stream.send(self._serialize_packet(item)) + + async def receive(self) -> Packet: + packet_id = int.from_bytes(await receive_exact(self.transport_stream, 1), "big") + packet_length = int.from_bytes( + await receive_exact(self.transport_stream, 4), "big" + ) + if packet_length == 0: + return Packet(Packet.Type(packet_id), []) + payload = await receive_exact(self.transport_stream, packet_length) + assert len(payload) == packet_length + fields = [] + while len(payload) > 0: + field_id = int.from_bytes(payload[:1], "big") + field_length = int.from_bytes(payload[1:3], "big") + field_value = payload[3 : 3 + field_length] + fields.append(Packet.Field(field_id, field_value)) + payload = payload[3 + field_length :] + return Packet(Packet.Type(packet_id), fields) + + async def aclose(self) -> None: + await self.transport_stream.aclose() + + async def send_eof(self) -> None: + await self.transport_stream.send_eof() diff --git a/pypush/cli/__init__.py b/pypush/cli/__init__.py new file mode 100644 index 0000000..83e70a0 --- /dev/null +++ b/pypush/cli/__init__.py @@ -0,0 +1,48 @@ +import logging + +import typer +from rich.logging import RichHandler +from typing_extensions import Annotated + +from . import proxy as _proxy + +logging.basicConfig(level=logging.DEBUG, handlers=[RichHandler()], format="%(message)s") + +app = typer.Typer() + + +@app.command() +def proxy( + attach: Annotated[ + bool, typer.Option(help="Use Frida to attach to the running `apsd`") + ] = True, +): + """ + Proxy APNs traffic between the local machine and the APNs courier + + Attach requires SIP to be disabled and to be running as root + """ + + _proxy.main(attach) + + +@app.command() +def client( + topic: Annotated[str, typer.Argument(help="app topic to listen on")], + sandbox: Annotated[ + bool, typer.Option("--sandbox/--production", help="APNs courier to use") + ] = True, +): + """ + Connect to the APNs courier and listen for app notifications on the given topic + """ + typer.echo("Running APNs client") + raise NotImplementedError("Not implemented yet") + + +def main(): + app() + + +if __name__ == "__main__": + main() diff --git a/pypush/cli/_frida.py b/pypush/cli/_frida.py new file mode 100644 index 0000000..dc30ce5 --- /dev/null +++ b/pypush/cli/_frida.py @@ -0,0 +1,74 @@ +import frida +import logging + + +def attach_to_apsd() -> frida.core.Session: + frida.kill("apsd") + while True: + try: + return frida.attach("apsd") + except frida.ProcessNotFoundError: + pass + + +def redirect_courier( + session: frida.core.Session, courier="courier.push.apple.com", to="localhost" +): + script = session.create_script( + """ + var getaddrinfo_handle = Module.findExportByName(null, 'getaddrinfo'); + if (getaddrinfo_handle) { + Interceptor.attach(getaddrinfo_handle, { + onEnter: function(args) { + var node = Memory.readUtf8String(args[0]); + var service = Memory.readUtf8String(args[1]); + //this.res_ptr = args[3] + //console.log('[*] getaddrinfo("' + node + '", "' + service + '", ...)'); + // Check for "courier.push.apple.com" in name + if (node.indexOf('""" + + courier + + """') !== -1) { + // Write "localhost" to the first argument + Memory.writeUtf8String(args[0], '""" + + to + + """'); + console.log('getaddrinfo("' + node + '", ...) => getaddrinfo("localhost", ...)'); + } else { + //console.log('getaddrinfo("' + node + '", ...)'); + } + } + }); + console.log('getaddrinfo() hook installed.'); + } + """ + ) + script.set_log_handler(lambda _, message: logging.debug(message)) + script.load() + + +def trust_all_hosts(session: frida.core.Session): + script = session.create_script( + """ + // Hook -[APSTCPStream isTrust:validWithPolicy:forPeer:] to always return true + var isTrust_handle = ObjC.classes.APSTCPStream['- isTrust:validWithPolicy:forPeer:']; + if (isTrust_handle) { + Interceptor.attach(isTrust_handle.implementation, { + onEnter: function(args) { + console.log('-[APSTCPStream isTrust:validWithPolicy:forPeer:]'); + //console.log(' - isTrust: ' + args[2]); + //console.log(' - validWithPolicy: ' + args[3]); + //console.log(' - forPeer: ' + args[4]); + //args[2] = true; + //console.log(' => isTrust: ' + args[2]); + }, + onLeave: function(retval) { + console.log(' <= ' + retval); + retval.replace(1); + } + }); + console.log('-[APSTCPStream isTrust:validWithPolicy:forPeer:] hook installed.'); + } + """ + ) + script.set_log_handler(lambda _, message: logging.debug(message)) + script.load() diff --git a/pypush/cli/proxy.py b/pypush/cli/proxy.py new file mode 100644 index 0000000..b801d43 --- /dev/null +++ b/pypush/cli/proxy.py @@ -0,0 +1,166 @@ +import datetime +import logging +import ssl +import tempfile +from typing import Optional + +import anyio +import anyio.abc +import anyio.to_thread +from anyio.streams.tls import TLSListener, TLSStream +from cryptography import x509 +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives.hashes import SHA256 +from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat + +# from pypush import apns +from pypush.apns import transport +from pypush.apns import protocol + +from . import _frida + + +async def forward_packets( + source: transport.PacketStream, + dest: transport.PacketStream, + name: str = "", +): + try: + async for packet in source: + try: + command = protocol.command_from_packet(packet) + if not isinstance(command, protocol.UnknownCommand): + logging.info(f"{name} -> {command}") + else: + logging.warning(f"{name} -> {command}") + except Exception as e: + logging.error(f"Error parsing packet: {e}") + logging.error(f"{name} => {packet}") + await dest.send(packet) + continue + await dest.send(command.to_packet()) + logging.info(f"{name} -> EOF") + except anyio.EndOfStream: + logging.info(f"{name} -> EOS") + except anyio.ClosedResourceError: + logging.info(f"{name} -> Closed") + except Exception as e: + logging.error(f"Error forwarding packets: {e}") + await dest.aclose() # close the other stream so that the other task exits cleanly + + +connection_cnt = 0 + + +async def handle(client: TLSStream): + global connection_cnt + connection_cnt += 1 + + sni = client._ssl_object.server_name # type: ignore + logging.debug(f"Got SNI: {sni}") + sandbox = "sandbox" in sni + + async with client: + client_pkt = transport.PacketStream(client) + logging.debug("Client connected") + + forward = ( + "1-courier.push.apple.com" + if not sandbox + else "1-courier.sandbox.push.apple.com" + ) + name = f"prod-{connection_cnt}" if not sandbox else f"sandbox-{connection_cnt}" + async with await transport.create_courier_connection(forward) as conn: + logging.debug("Connected to courier") + async with anyio.create_task_group() as tg: + tg.start_soon(forward_packets, client_pkt, conn, f"client-{name}") + tg.start_soon(forward_packets, conn, client_pkt, f"server-{name}") + logging.debug("Started forwarding") + + logging.debug("Courier disconnected") + + +def temp_certs(): + # Create a self-signed certificate for the server and write it to temporary files + key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + builder = x509.CertificateBuilder() + builder = builder.subject_name( + x509.Name([x509.NameAttribute(x509.NameOID.COMMON_NAME, "localhost")]) + ) + builder = builder.issuer_name( + x509.Name([x509.NameAttribute(x509.NameOID.COMMON_NAME, "localhost")]) + ) + builder = builder.not_valid_before(datetime.datetime.utcnow()) + builder = builder.not_valid_after( + datetime.datetime.utcnow() + datetime.timedelta(days=1) + ) + builder = builder.serial_number(x509.random_serial_number()) + builder = builder.public_key(key.public_key()) + builder = builder.add_extension( + x509.SubjectAlternativeName([x509.DNSName("localhost")]), critical=False + ) + certificate = builder.sign(key, SHA256()) + + cert_path, key_path = tempfile.mktemp(), tempfile.mktemp() + + with open(cert_path, "wb") as f: + f.write(certificate.public_bytes(Encoding.PEM)) + with open(key_path, "wb") as f: + f.write( + key.private_bytes( + Encoding.PEM, + serialization.PrivateFormat.TraditionalOpenSSL, + serialization.NoEncryption(), + ) + ) + + return cert_path, key_path + + +def sni_callback(conn, server_name, ssl_context): + # Set the server name in the conn so we can use it later + conn.server_name = server_name # type: ignore + + +async def courier_proxy(host): + # Start listening on localhost:COURIER_PORT + listener = await anyio.create_tcp_listener( + local_port=transport.COURIER_PORT, local_host=host + ) + # Create an SSL context + context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + context.set_alpn_protocols(["apns-security-v3"]) + context.load_cert_chain(*temp_certs()) + context.set_servername_callback(sni_callback) + listener = TLSListener(listener, ssl_context=context, standard_compatible=False) + logging.info(f"Listening on {host}:{transport.COURIER_PORT}") + + await listener.serve(handle) + + +async def ainput(prompt: str = "") -> str: + print(prompt, end="") + return await anyio.to_thread.run_sync(input) + + +async def start(attach): + async with anyio.create_task_group() as tg: + tg.start_soon(courier_proxy, "localhost") + if attach: + try: + apsd = _frida.attach_to_apsd() + _frida.redirect_courier(apsd, "courier.push.apple.com", "localhost") + _frida.redirect_courier( + apsd, "courier.sandbox.push.apple.com", "localhost" + ) + _frida.trust_all_hosts(apsd) + except Exception as e: + logging.error(f"Error attaching to apsd (did you run as root?): {e}") + logging.info("Press Enter to exit...") + await ainput() + tg.cancel_scope.cancel() + + +def main(attach): + anyio.run(start, attach) diff --git a/pypush/cli/pushclient.py b/pypush/cli/pushclient.py new file mode 100644 index 0000000..e69de29 diff --git a/pypush/cloudkit/.gitkeep b/pypush/cloudkit/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/pypush/grandslam/.gitkeep b/pypush/grandslam/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/pypush/ids/.gitkeep b/pypush/ids/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/pypush/imessage/.gitkeep b/pypush/imessage/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 3d7c2f4..0000000 --- a/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -requests -cryptography -wheel -tlslite-ng==0.8.0a43 -srp -pbkdf2 -unicorn -rich -prompt_toolkit \ No newline at end of file diff --git a/tests/test_apns.py b/tests/test_apns.py new file mode 100644 index 0000000..3b24508 --- /dev/null +++ b/tests/test_apns.py @@ -0,0 +1,42 @@ +import pytest +from pypush import apns +import asyncio + +# from aioapns import * +import uuid +import anyio + +# from pypush.apns import _util +# from pypush.apns import albert, lifecycle, protocol +from pypush import apns + +import logging +from rich.logging import RichHandler + +logging.basicConfig(level=logging.DEBUG, handlers=[RichHandler()], format="%(message)s") + + +@pytest.mark.asyncio +async def test_activate(): + global certificate, key + certificate, key = await apns.activate() + assert certificate is not None + assert key is not None + + +@pytest.mark.asyncio +async def test_lifecycle_2(): + async with apns.create_apns_connection( + certificate, key, courier="localhost" + ) as connection: + await connection.receive( + apns.protocol.ConnectAck + ) # Just wait until the initial connection is established. Don't do this in real code plz. + + +@pytest.mark.asyncio +async def test_shorthand(): + async with apns.create_apns_connection( + *await apns.activate(), courier="localhost" + ) as connection: + await connection.receive(apns.protocol.ConnectAck)