mirror of
https://github.com/Sneed-Group/pypush-plus-plus
synced 2024-12-23 11:22:42 -06:00
Squashed commit of lots of dev stuff that may have sensitive info
This commit is contained in:
parent
22899ee98e
commit
d9feada49c
14 changed files with 659 additions and 114 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -2,6 +2,8 @@
|
||||||
ids.key
|
ids.key
|
||||||
ids.crt
|
ids.crt
|
||||||
|
|
||||||
|
keychain-extract
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
|
25
apns.py
25
apns.py
|
@ -1,12 +1,33 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import random
|
import random
|
||||||
|
import socket
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
|
|
||||||
|
import tlslite
|
||||||
|
|
||||||
import albert
|
import albert
|
||||||
import courier
|
|
||||||
|
COURIER_HOST = "windows.courier.push.apple.com" # TODO: Get this from config
|
||||||
|
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)
|
||||||
|
|
||||||
|
return sock
|
||||||
|
|
||||||
|
|
||||||
class APNSConnection:
|
class APNSConnection:
|
||||||
|
@ -47,7 +68,7 @@ class APNSConnection:
|
||||||
else:
|
else:
|
||||||
self.private_key, self.cert = private_key, cert
|
self.private_key, self.cert = private_key, cert
|
||||||
|
|
||||||
self.sock = courier.connect(self.private_key, self.cert)
|
self.sock = _connect(self.private_key, self.cert)
|
||||||
|
|
||||||
# Start the queue filler thread
|
# Start the queue filler thread
|
||||||
self.queue_filler_thread = threading.Thread(
|
self.queue_filler_thread = threading.Thread(
|
||||||
|
|
29
courier.py
29
courier.py
|
@ -1,29 +0,0 @@
|
||||||
import socket
|
|
||||||
|
|
||||||
import tlslite
|
|
||||||
|
|
||||||
COURIER_HOST = "windows.courier.push.apple.com" # TODO: Get this from config
|
|
||||||
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)
|
|
||||||
|
|
||||||
return sock
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sock = connect()
|
|
||||||
sock.write(b"Hello World!")
|
|
||||||
print(sock.read())
|
|
||||||
sock.close()
|
|
|
@ -12,4 +12,6 @@ conn1.connect()
|
||||||
|
|
||||||
conn1.filter(["com.apple.madrid"])
|
conn1.filter(["com.apple.madrid"])
|
||||||
|
|
||||||
print(ids.lookup(conn1, ["mailto:jjtech@jjtech.dev"]))
|
#print(ids.lookup(conn1, ["mailto:jjtech@jjtech.dev"]))
|
||||||
|
|
||||||
|
print(ids.register(conn1, "user_test2@icloud.com", "wowSecure1"))
|
82
development/test.py
Normal file
82
development/test.py
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import ids
|
||||||
|
import hashlib
|
||||||
|
from base64 import b64decode
|
||||||
|
import zlib
|
||||||
|
import plistlib
|
||||||
|
|
||||||
|
with open('body.txt', 'r') as f:
|
||||||
|
BODY = f.read()
|
||||||
|
|
||||||
|
CERT = "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"
|
||||||
|
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 = "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"
|
||||||
|
|
||||||
|
print(hex(extract_hash(b64decode(SIG)[2:], CERT)))
|
||||||
|
|
|
@ -1,79 +0,0 @@
|
||||||
var SecTrustEvaluate_handle =
|
|
||||||
Module.findExportByName('Security', 'SecTrustEvaluate');
|
|
||||||
var SecTrustEvaluateWithError_handle =
|
|
||||||
Module.findExportByName('Security', 'SecTrustEvaluateWithError');
|
|
||||||
var SSL_CTX_set_custom_verify_handle =
|
|
||||||
Module.findExportByName('libboringssl.dylib', 'SSL_CTX_set_custom_verify');
|
|
||||||
var SSL_get_psk_identity_handle =
|
|
||||||
Module.findExportByName('libboringssl.dylib', 'SSL_get_psk_identity');
|
|
||||||
var boringssl_context_set_verify_mode_handle = Module.findExportByName(
|
|
||||||
'libboringssl.dylib', 'boringssl_context_set_verify_mode');
|
|
||||||
|
|
||||||
if (SecTrustEvaluateWithError_handle) {
|
|
||||||
var SecTrustEvaluateWithError = new NativeFunction(
|
|
||||||
SecTrustEvaluateWithError_handle, 'int', ['pointer', 'pointer']);
|
|
||||||
|
|
||||||
Interceptor.replace(
|
|
||||||
SecTrustEvaluateWithError_handle,
|
|
||||||
new NativeCallback(function(trust, error) {
|
|
||||||
console.log('[*] Called SecTrustEvaluateWithError()');
|
|
||||||
SecTrustEvaluateWithError(trust, NULL);
|
|
||||||
Memory.writeU8(error, 0);
|
|
||||||
return 1;
|
|
||||||
}, 'int', ['pointer', 'pointer']));
|
|
||||||
console.log('[+] SecTrustEvaluateWithError() hook installed.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SecTrustEvaluate_handle) {
|
|
||||||
var SecTrustEvaluate = new NativeFunction(
|
|
||||||
SecTrustEvaluate_handle, 'int', ['pointer', 'pointer']);
|
|
||||||
|
|
||||||
Interceptor.replace(
|
|
||||||
SecTrustEvaluate_handle, new NativeCallback(function(trust, result) {
|
|
||||||
console.log('[*] Called SecTrustEvaluate()');
|
|
||||||
SecTrustEvaluate(trust, result);
|
|
||||||
Memory.writeU8(result, 1);
|
|
||||||
return 0;
|
|
||||||
}, 'int', ['pointer', 'pointer']));
|
|
||||||
console.log('[+] SecTrustEvaluate() hook installed.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SSL_CTX_set_custom_verify_handle) {
|
|
||||||
var SSL_CTX_set_custom_verify = new NativeFunction(
|
|
||||||
SSL_CTX_set_custom_verify_handle, 'void', ['pointer', 'int', 'pointer']);
|
|
||||||
|
|
||||||
var replaced_callback = new NativeCallback(function(ssl, out) {
|
|
||||||
console.log('[*] Called custom SSL verifier')
|
|
||||||
return 0;
|
|
||||||
}, 'int', ['pointer', 'pointer']);
|
|
||||||
|
|
||||||
Interceptor.replace(
|
|
||||||
SSL_CTX_set_custom_verify_handle,
|
|
||||||
new NativeCallback(function(ctx, mode, callback) {
|
|
||||||
console.log('[*] Called SSL_CTX_set_custom_verify()');
|
|
||||||
SSL_CTX_set_custom_verify(ctx, 0, replaced_callback);
|
|
||||||
}, 'int', ['pointer', 'int', 'pointer']));
|
|
||||||
console.log('[+] SSL_CTX_set_custom_verify() hook installed.')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SSL_get_psk_identity_handle) {
|
|
||||||
Interceptor.replace(
|
|
||||||
SSL_get_psk_identity_handle, new NativeCallback(function(ssl) {
|
|
||||||
console.log('[*] Called SSL_get_psk_identity_handle()');
|
|
||||||
return 'notarealPSKidentity';
|
|
||||||
}, 'pointer', ['pointer']));
|
|
||||||
console.log('[+] SSL_get_psk_identity() hook installed.')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (boringssl_context_set_verify_mode_handle) {
|
|
||||||
var boringssl_context_set_verify_mode = new NativeFunction(
|
|
||||||
boringssl_context_set_verify_mode_handle, 'int', ['pointer', 'pointer']);
|
|
||||||
|
|
||||||
Interceptor.replace(
|
|
||||||
boringssl_context_set_verify_mode_handle,
|
|
||||||
new NativeCallback(function(a, b) {
|
|
||||||
console.log('[*] Called boringssl_context_set_verify_mode()');
|
|
||||||
return 0;
|
|
||||||
}, 'int', ['pointer', 'pointer']));
|
|
||||||
console.log('[+] boringssl_context_set_verify_mode() hook installed.')
|
|
||||||
}
|
|
511
gsa.py
Normal file
511
gsa.py
Normal file
|
@ -0,0 +1,511 @@
|
||||||
|
from base64 import b64encode, b64decode
|
||||||
|
from datetime import datetime
|
||||||
|
from random import randbytes
|
||||||
|
import uuid
|
||||||
|
import locale
|
||||||
|
import plistlib as plist
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import requests
|
||||||
|
import srp._pysrp as srp
|
||||||
|
import pbkdf2
|
||||||
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||||
|
from cryptography.hazmat.primitives import padding
|
||||||
|
import getpass
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
DEBUG = False # Allows using a proxy for debugging (disables SSL verification)
|
||||||
|
# Server to use for anisette generation
|
||||||
|
#ANISETTE = "https://sign.rheaa.xyz/"
|
||||||
|
ANISETTE = 'http://45.132.246.138:6969/'
|
||||||
|
# ANISETTE = 'https://sideloadly.io/anisette/irGb3Quww8zrhgqnzmrx'
|
||||||
|
# ANISETTE = "http://jkcoxson.com:2052/"
|
||||||
|
|
||||||
|
# Configure SRP library for compatibility with Apple's implementation
|
||||||
|
srp.rfc5054_enable()
|
||||||
|
srp.no_username_in_x()
|
||||||
|
|
||||||
|
# Disable SSL Warning
|
||||||
|
import urllib3
|
||||||
|
|
||||||
|
urllib3.disable_warnings()
|
||||||
|
|
||||||
|
|
||||||
|
def generate_anisette() -> dict:
|
||||||
|
r = requests.get(ANISETTE, verify=False if DEBUG else True, timeout=5)
|
||||||
|
r = json.loads(r.text)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
class Anisette:
|
||||||
|
@staticmethod
|
||||||
|
def _fetch(url: str) -> dict:
|
||||||
|
"""Fetches anisette data that we cannot calculate from a remote server"""
|
||||||
|
r = requests.get(url, verify=False if DEBUG else True, timeout=5)
|
||||||
|
r = json.loads(r.text)
|
||||||
|
return r
|
||||||
|
|
||||||
|
def __init__(self, url: str = ANISETTE, name: str = "") -> None:
|
||||||
|
self._name = name
|
||||||
|
self._url = url
|
||||||
|
self._anisette = self._fetch(self._url)
|
||||||
|
|
||||||
|
# Generate a "user id": just a random UUID
|
||||||
|
# TODO: Figure out how to tie it to the user's account on the device
|
||||||
|
self._user_id = str(uuid.uuid4()).upper()
|
||||||
|
self._device_id = str(uuid.uuid4()).upper()
|
||||||
|
|
||||||
|
# override string printing
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self._name} ({self.backend})"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self) -> str:
|
||||||
|
return self._url
|
||||||
|
|
||||||
|
@property
|
||||||
|
def backend(self) -> str:
|
||||||
|
if (
|
||||||
|
self._anisette["X-MMe-Client-Info"]
|
||||||
|
== "<MacBookPro15,1> <Mac OS X;10.15.2;19C57> <com.apple.AuthKit/1 (com.apple.dt.Xcode/3594.4.19)>"
|
||||||
|
):
|
||||||
|
return "AltServer"
|
||||||
|
elif (
|
||||||
|
self._anisette["X-MMe-Client-Info"]
|
||||||
|
== "<iMac11,3> <Mac OS X;10.15.6;19G2021> <com.apple.AuthKit/1 (com.apple.dt.Xcode/3594.4.19)>"
|
||||||
|
):
|
||||||
|
return "Provision"
|
||||||
|
else:
|
||||||
|
return f"Unknown ({self._anisette['X-MMe-Client-Info']})"
|
||||||
|
|
||||||
|
# Getters
|
||||||
|
@property
|
||||||
|
def timestamp(self) -> str:
|
||||||
|
"""'Timestamp'
|
||||||
|
Current timestamp in ISO 8601 format
|
||||||
|
"""
|
||||||
|
|
||||||
|
# We only want sencond precision, so we set the microseconds to 0
|
||||||
|
# We also add 'Z' to the end to indicate UTC
|
||||||
|
# An alternate way to write this is strftime("%FT%T%zZ")
|
||||||
|
return datetime.utcnow().replace(microsecond=0).isoformat() + "Z"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def timezone(self) -> str:
|
||||||
|
"""'Time Zone'
|
||||||
|
Abbreviation of the timezone of the device (e.g. EST)"""
|
||||||
|
|
||||||
|
return str(datetime.utcnow().astimezone().tzinfo)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def locale(self) -> str:
|
||||||
|
"""'Locale'
|
||||||
|
Locale of the device (e.g. en_US)
|
||||||
|
"""
|
||||||
|
|
||||||
|
return locale.getdefaultlocale()[0] or "en_US"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def otp(self) -> str:
|
||||||
|
"""'One Time Password'
|
||||||
|
A seemingly random base64 string containing 28 bytes
|
||||||
|
TODO: Figure out how to generate this
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self._anisette["X-Apple-I-MD"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def local_user(self) -> str:
|
||||||
|
"""'Local User ID'
|
||||||
|
There are 2 possible implementations of this value
|
||||||
|
1. Uppercase hex of the SHA256 hash of some unknown value (used by Windows based servers)
|
||||||
|
2. Base64 encoding of an uppercase UUID (used by android based servers)
|
||||||
|
I picked the second one because it's more fully understood.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return b64encode(self._user_id.encode()).decode()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def machine(self) -> str:
|
||||||
|
"""'Machine ID'
|
||||||
|
This is a base64 encoded string of 60 'random' bytes
|
||||||
|
We're not sure how this is generated, we have to rely on the server
|
||||||
|
TODO: Figure out how to generate this
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self._anisette["X-Apple-I-MD-M"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def router(self) -> str:
|
||||||
|
"""'Routing Info'
|
||||||
|
This is a number, either 17106176 or 50660608
|
||||||
|
It doesn't seem to matter which one we use,
|
||||||
|
17106176 is used by Sideloadly and Provision (android) based servers
|
||||||
|
50660608 is used by Windows iCloud based servers
|
||||||
|
"""
|
||||||
|
|
||||||
|
return "17106176"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def serial(self) -> str:
|
||||||
|
"""'Device Serial Number'
|
||||||
|
This is the serial number of the device
|
||||||
|
You can use a legitimate serial number, but Apple accepts '0' as well (for andriod devices)
|
||||||
|
See https://github.com/acidanthera/OpenCorePkg/blob/master/Utilities/macserial/macserial.c for how to generate a legit serial
|
||||||
|
"""
|
||||||
|
|
||||||
|
return "0"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device(self) -> str:
|
||||||
|
"""'Device Unique Identifier'
|
||||||
|
This is just an uppercase UUID"""
|
||||||
|
|
||||||
|
return self._device_id
|
||||||
|
|
||||||
|
def _build_client(self, emulated_device: str, emulated_app: str) -> str:
|
||||||
|
# TODO: Update OS version and app versions
|
||||||
|
|
||||||
|
model = emulated_device
|
||||||
|
if emulated_device == "PC":
|
||||||
|
# We're emulating a PC, so we run Windows (Vista?)
|
||||||
|
os = "Windows"
|
||||||
|
os_version = "6.2(0,0);9200"
|
||||||
|
else:
|
||||||
|
# We're emulating a Mac, so we run macOS (What is 15.6?)
|
||||||
|
os = "Mac OS X"
|
||||||
|
os_version = "10.15.6;19G2021"
|
||||||
|
|
||||||
|
if emulated_app == "Xcode":
|
||||||
|
app_bundle = "com.apple.dt.Xcode"
|
||||||
|
app_version = "3594.4.19"
|
||||||
|
else:
|
||||||
|
app_bundle = "com.apple.iCloud"
|
||||||
|
app_version = "7.21"
|
||||||
|
|
||||||
|
if os == "Windows":
|
||||||
|
authkit_bundle = "com.apple.AuthKitWin"
|
||||||
|
else:
|
||||||
|
authkit_bundle = "com.apple.AuthKit"
|
||||||
|
authkit_version = "1"
|
||||||
|
|
||||||
|
return f"<{model}> <{os};{os_version}> <{authkit_bundle}/{authkit_version} ({app_bundle}/{app_version})>"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client(self) -> str:
|
||||||
|
"""'Client Information'
|
||||||
|
String in the following format:
|
||||||
|
<%MODEL%> <%OS%;%MAJOR%.%MINOR%(%SPMAJOR%,%SPMINOR%);%BUILD%> <%AUTHKIT_BUNDLE_ID%/%AUTHKIT_VERSION% (%APP_BUNDLE_ID%/%APP_VERSION%)>
|
||||||
|
Where:
|
||||||
|
MODEL: The model of the device (e.g. MacBookPro15,1 or 'PC'
|
||||||
|
OS: The OS of the device (e.g. Mac OS X or Windows)
|
||||||
|
MAJOR: The major version of the OS (e.g. 10)
|
||||||
|
MINOR: The minor version of the OS (e.g. 15)
|
||||||
|
SPMAJOR: The major version of the service pack (e.g. 0) (Windows only)
|
||||||
|
SPMINOR: The minor version of the service pack (e.g. 0) (Windows only)
|
||||||
|
BUILD: The build number of the OS (e.g. 19C57)
|
||||||
|
AUTHKIT_BUNDLE_ID: The bundle ID of the AuthKit framework (e.g. com.apple.AuthKit)
|
||||||
|
AUTHKIT_VERSION: The version of the AuthKit framework (e.g. 1)
|
||||||
|
APP_BUNDLE_ID: The bundle ID of the app (e.g. com.apple.dt.Xcode)
|
||||||
|
APP_VERSION: The version of the app (e.g. 3594.4.19)
|
||||||
|
"""
|
||||||
|
return self._build_client("iMac11,3", "Xcode")
|
||||||
|
|
||||||
|
def generate_headers(self, client_info: bool = False) -> dict:
|
||||||
|
h = {
|
||||||
|
# Current Time
|
||||||
|
"X-Apple-I-Client-Time": self.timestamp,
|
||||||
|
"X-Apple-I-TimeZone": self.timezone,
|
||||||
|
# Locale
|
||||||
|
# Some implementations only use this for locale
|
||||||
|
"loc": self.locale,
|
||||||
|
"X-Apple-Locale": self.locale,
|
||||||
|
# Anisette
|
||||||
|
"X-Apple-I-MD": self.otp, # 'One Time Password'
|
||||||
|
# 'Local User ID'
|
||||||
|
"X-Apple-I-MD-LU": self.local_user,
|
||||||
|
"X-Apple-I-MD-M": self.machine, # 'Machine ID'
|
||||||
|
# 'Routing Info', some implementations convert this to an integer
|
||||||
|
"X-Apple-I-MD-RINFO": self.router,
|
||||||
|
# Device information
|
||||||
|
# 'Device Unique Identifier'
|
||||||
|
"X-Mme-Device-Id": self.device,
|
||||||
|
# 'Device Serial Number'
|
||||||
|
"X-Apple-I-SRL-NO": self.serial,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Additional client information only used in some requests
|
||||||
|
if client_info:
|
||||||
|
h["X-Mme-Client-Info"] = self.client
|
||||||
|
h["X-Apple-App-Info"] = "com.apple.gs.xcode.auth"
|
||||||
|
h["X-Xcode-Version"] = "11.2 (11B41)"
|
||||||
|
|
||||||
|
return h
|
||||||
|
|
||||||
|
def generate_cpd(self) -> dict:
|
||||||
|
cpd = {
|
||||||
|
# Many of these values are not strictly necessary, but may be tracked by Apple
|
||||||
|
# I've chosen to match the AltServer implementation
|
||||||
|
# Not sure what these are for, needs some investigation
|
||||||
|
"bootstrap": True, # All implementations set this to true
|
||||||
|
"icscrec": True, # Only AltServer sets this to true
|
||||||
|
"pbe": False, # All implementations explicitly set this to false
|
||||||
|
"prkgen": True, # I've also seen ckgen
|
||||||
|
"svct": "iCloud", # In certian circumstances, this can be 'iTunes' or 'iCloud'
|
||||||
|
# Not included, but I've also seen:
|
||||||
|
# 'capp': 'AppStore',
|
||||||
|
# 'dc': '#d4c5b3',
|
||||||
|
# 'dec': '#e1e4e3',
|
||||||
|
# 'prtn': 'ME349',
|
||||||
|
}
|
||||||
|
|
||||||
|
cpd.update(self.generate_headers())
|
||||||
|
return cpd
|
||||||
|
|
||||||
|
|
||||||
|
def authenticated_request(parameters, anisette: Anisette) -> dict:
|
||||||
|
body = {
|
||||||
|
"Header": {
|
||||||
|
"Version": "1.0.1",
|
||||||
|
},
|
||||||
|
"Request": {
|
||||||
|
"cpd": anisette.generate_cpd(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
body["Request"].update(parameters)
|
||||||
|
# print(plist.dumps(body).decode('utf-8'))
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "text/x-xml-plist",
|
||||||
|
"Accept": "*/*",
|
||||||
|
"User-Agent": "akd/1.0 CFNetwork/978.0.7 Darwin/18.7.0",
|
||||||
|
"X-MMe-Client-Info": anisette.client,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = requests.post(
|
||||||
|
#"https://17.32.194.2/grandslam/GsService2",
|
||||||
|
"https://gsa.apple.com/grandslam/GsService2",
|
||||||
|
headers=headers,
|
||||||
|
data=plist.dumps(body),
|
||||||
|
verify=False, # TODO: Verify Apple's self-signed cert
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
|
||||||
|
return plist.loads(resp.content)["Response"]
|
||||||
|
|
||||||
|
|
||||||
|
def check_error(r):
|
||||||
|
# Check for an error code
|
||||||
|
if "Status" in r:
|
||||||
|
status = r["Status"]
|
||||||
|
else:
|
||||||
|
status = r
|
||||||
|
|
||||||
|
if status["ec"] != 0:
|
||||||
|
print(f"Error {status['ec']}: {status['em']}")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_password(password: str, salt: bytes, iterations: int) -> bytes:
|
||||||
|
p = hashlib.sha256(password.encode("utf-8")).digest()
|
||||||
|
return pbkdf2.PBKDF2(p, salt, iterations, hashlib.sha256).read(32)
|
||||||
|
|
||||||
|
|
||||||
|
def create_session_key(usr: srp.User, name: str) -> bytes:
|
||||||
|
k = usr.get_session_key()
|
||||||
|
if k is None:
|
||||||
|
raise Exception("No session key")
|
||||||
|
return hmac.new(k, name.encode(), hashlib.sha256).digest()
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_cbc(usr: srp.User, data: bytes) -> bytes:
|
||||||
|
extra_data_key = create_session_key(usr, "extra data key:")
|
||||||
|
extra_data_iv = create_session_key(usr, "extra data iv:")
|
||||||
|
# Get only the first 16 bytes of the iv
|
||||||
|
extra_data_iv = extra_data_iv[:16]
|
||||||
|
|
||||||
|
# Decrypt with AES CBC
|
||||||
|
cipher = Cipher(algorithms.AES(extra_data_key), modes.CBC(extra_data_iv))
|
||||||
|
decryptor = cipher.decryptor()
|
||||||
|
data = decryptor.update(data) + decryptor.finalize()
|
||||||
|
# Remove PKCS#7 padding
|
||||||
|
padder = padding.PKCS7(128).unpadder()
|
||||||
|
return padder.update(data) + padder.finalize()
|
||||||
|
|
||||||
|
|
||||||
|
def trusted_second_factor(dsid, idms_token, anisette: Anisette):
|
||||||
|
identity_token = b64encode((dsid + ":" + idms_token).encode()).decode()
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "text/x-xml-plist",
|
||||||
|
"User-Agent": "Xcode",
|
||||||
|
"Accept": "text/x-xml-plist",
|
||||||
|
"Accept-Language": "en-us",
|
||||||
|
"X-Apple-Identity-Token": identity_token,
|
||||||
|
}
|
||||||
|
|
||||||
|
headers.update(anisette.generate_headers(client_info=True))
|
||||||
|
|
||||||
|
# This will trigger the 2FA prompt on trusted devices
|
||||||
|
# We don't care about the response, it's just some HTML with a form for entering the code
|
||||||
|
# Easier to just use a text prompt
|
||||||
|
requests.get(
|
||||||
|
"https://gsa.apple.com/auth/verify/trusteddevice",
|
||||||
|
headers=headers,
|
||||||
|
verify=False,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Prompt for the 2FA code. It's just a string like '123456', no dashes or spaces
|
||||||
|
code = getpass.getpass("Enter 2FA code: ")
|
||||||
|
# code = input("Enter 2FA code: ")
|
||||||
|
headers["security-code"] = code
|
||||||
|
|
||||||
|
# Send the 2FA code to Apple
|
||||||
|
resp = requests.get(
|
||||||
|
"https://gsa.apple.com/grandslam/GsService2/validate",
|
||||||
|
headers=headers,
|
||||||
|
verify=False,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
r = plist.loads(resp.content)
|
||||||
|
if check_error(r):
|
||||||
|
return
|
||||||
|
|
||||||
|
print("2FA successful")
|
||||||
|
|
||||||
|
|
||||||
|
def sms_second_factor(dsid, idms_token, anisette: Anisette):
|
||||||
|
# TODO: Figure out how to make SMS 2FA work correctly
|
||||||
|
raise NotImplementedError("SMS 2FA is not yet implemented")
|
||||||
|
identity_token = b64encode((dsid + ":" + idms_token).encode()).decode()
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "text/x-xml-plist",
|
||||||
|
"User-Agent": "Xcode",
|
||||||
|
# "Accept": "text/x-xml-plist",
|
||||||
|
"Accept": "application/x-buddyml",
|
||||||
|
"Accept-Language": "en-us",
|
||||||
|
"X-Apple-Identity-Token": identity_token,
|
||||||
|
}
|
||||||
|
|
||||||
|
headers.update(anisette.generate_headers(client_info=True))
|
||||||
|
|
||||||
|
body = {"serverInfo": {"phoneNumber.id": "1"}}
|
||||||
|
|
||||||
|
# This will send the 2FA code to the user's phone over SMS
|
||||||
|
# We don't care about the response, it's just some HTML with a form for entering the code
|
||||||
|
# Easier to just use a text prompt
|
||||||
|
requests.post(
|
||||||
|
"https://gsa.apple.com/auth/verify/phone/put?mode=sms",
|
||||||
|
data=plist.dumps(body),
|
||||||
|
headers=headers,
|
||||||
|
verify=False,
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Prompt for the 2FA code. It's just a string like '123456', no dashes or spaces
|
||||||
|
code = input("Enter 2FA code: ")
|
||||||
|
|
||||||
|
body = {
|
||||||
|
"securityCode.code": code,
|
||||||
|
"serverInfo": {"mode": "sms", "phoneNumber.id": "1"},
|
||||||
|
}
|
||||||
|
# headers["security-code"] = code
|
||||||
|
|
||||||
|
# Send the 2FA code to Apple
|
||||||
|
resp = requests.post(
|
||||||
|
"https://gsa.apple.com/auth/verify/phone/securitycode?referrer=/auth/verify/phone/put",
|
||||||
|
headers=headers,
|
||||||
|
data=plist.dumps(body),
|
||||||
|
verify=False,
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
print(resp.content.decode())
|
||||||
|
# r = plist.loads(resp.content)
|
||||||
|
# if check_error(r):
|
||||||
|
# return
|
||||||
|
|
||||||
|
# print("2FA successful")
|
||||||
|
|
||||||
|
|
||||||
|
def authenticate(username, password, anisette: Anisette):
|
||||||
|
# Password is None as we'll provide it later
|
||||||
|
usr = srp.User(username, bytes(), hash_alg=srp.SHA256, ng_type=srp.NG_2048)
|
||||||
|
_, A = usr.start_authentication()
|
||||||
|
|
||||||
|
r = authenticated_request(
|
||||||
|
{
|
||||||
|
"A2k": A,
|
||||||
|
"ps": ["s2k", "s2k_fo"],
|
||||||
|
#"ps": ["s2k"],
|
||||||
|
"u": username,
|
||||||
|
"o": "init",
|
||||||
|
},
|
||||||
|
anisette,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for an error code
|
||||||
|
if check_error(r):
|
||||||
|
return
|
||||||
|
|
||||||
|
if r["sp"] != "s2k":
|
||||||
|
print(f"This implementation only supports s2k. Server returned {r['sp']}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Change the password out from under the SRP library, as we couldn't calculate it without the salt.
|
||||||
|
usr.p = encrypt_password(password, r["s"], r["i"]) # type: ignore
|
||||||
|
|
||||||
|
M = usr.process_challenge(r["s"], r["B"])
|
||||||
|
|
||||||
|
# Make sure we processed the challenge correctly
|
||||||
|
if M is None:
|
||||||
|
print("Failed to process challenge")
|
||||||
|
return
|
||||||
|
|
||||||
|
r = authenticated_request(
|
||||||
|
{
|
||||||
|
"c": r["c"],
|
||||||
|
"M1": M,
|
||||||
|
"u": username,
|
||||||
|
"o": "complete",
|
||||||
|
},
|
||||||
|
anisette,
|
||||||
|
)
|
||||||
|
|
||||||
|
if check_error(r):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Make sure that the server's session key matches our session key (and thus that they are not an imposter)
|
||||||
|
usr.verify_session(r["M2"])
|
||||||
|
if not usr.authenticated():
|
||||||
|
print("Failed to verify session")
|
||||||
|
return
|
||||||
|
|
||||||
|
spd = decrypt_cbc(usr, r["spd"])
|
||||||
|
# For some reason plistlib doesn't accept it without the header...
|
||||||
|
PLISTHEADER = b"""\
|
||||||
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
|
<!DOCTYPE plist PUBLIC '-//Apple//DTD PLIST 1.0//EN' 'http://www.apple.com/DTDs/PropertyList-1.0.dtd'>
|
||||||
|
"""
|
||||||
|
spd = plist.loads(PLISTHEADER + spd)
|
||||||
|
|
||||||
|
if "au" in r["Status"] and r["Status"]["au"] == "trustedDeviceSecondaryAuth":
|
||||||
|
print("Trusted device authentication required")
|
||||||
|
# Replace bytes with strings
|
||||||
|
for k, v in spd.items():
|
||||||
|
if isinstance(v, bytes):
|
||||||
|
spd[k] = b64encode(v).decode()
|
||||||
|
trusted_second_factor(spd["adsid"], spd["GsIdmsToken"], anisette)
|
||||||
|
return authenticate(username, password, anisette)
|
||||||
|
elif "au" in r["Status"] and r["Status"]["au"] == "secondaryAuth":
|
||||||
|
print("SMS authentication required")
|
||||||
|
sms_second_factor(spd["adsid"], spd["GsIdmsToken"], anisette)
|
||||||
|
elif "au" in r["Status"]:
|
||||||
|
print(f"Unknown auth value {r['Status']['au']}")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
print("Assuming 2FA is not required")
|
||||||
|
return spd
|
41
ids.py
41
ids.py
|
@ -42,10 +42,15 @@ def load_keys() -> tuple[str, str]:
|
||||||
|
|
||||||
|
|
||||||
def _create_payload(
|
def _create_payload(
|
||||||
bag_key: str, query_string: str, push_token: str, payload: bytes
|
bag_key: str,
|
||||||
|
query_string: str,
|
||||||
|
push_token: str,
|
||||||
|
payload: bytes,
|
||||||
|
nonce: bytes = None,
|
||||||
) -> tuple[str, bytes]:
|
) -> tuple[str, bytes]:
|
||||||
# Generate the nonce
|
# Generate the nonce
|
||||||
nonce = generate_nonce()
|
if nonce is None:
|
||||||
|
nonce = generate_nonce()
|
||||||
push_token = b64decode(push_token)
|
push_token = b64decode(push_token)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -79,7 +84,7 @@ def sign_payload(
|
||||||
return sig, nonce
|
return sig, nonce
|
||||||
|
|
||||||
|
|
||||||
global_key, global_cert = load_keys()
|
#global_key, global_cert = load_keys()
|
||||||
|
|
||||||
|
|
||||||
def _send_request(conn: apns.APNSConnection, bag_key: str, body: bytes) -> bytes:
|
def _send_request(conn: apns.APNSConnection, bag_key: str, body: bytes) -> bytes:
|
||||||
|
@ -129,3 +134,33 @@ def lookup(conn: apns.APNSConnection, query: list[str]) -> any:
|
||||||
resp = zlib.decompress(resp["b"], 16 + zlib.MAX_WBITS)
|
resp = zlib.decompress(resp["b"], 16 + zlib.MAX_WBITS)
|
||||||
resp = plistlib.loads(resp)
|
resp = plistlib.loads(resp)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
def get_auth_token(username: str, password: str) -> str:
|
||||||
|
# Get a PET from GSA
|
||||||
|
import gsa
|
||||||
|
g = gsa.authenticate(username, password, gsa.Anisette())
|
||||||
|
#print(g['t']['com.apple.gs.idms.pet'])
|
||||||
|
pet = g['t']['com.apple.gs.idms.pet']['token']
|
||||||
|
|
||||||
|
# Turn the PET into an auth token
|
||||||
|
import requests
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'apple-id': username,
|
||||||
|
'client-id': str(uuid.uuid4()),
|
||||||
|
'delegates': {
|
||||||
|
'com.apple.private.ids': {
|
||||||
|
'protocol-version': '4'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'password': pet,
|
||||||
|
}
|
||||||
|
data = plistlib.dumps(data)
|
||||||
|
headers = {'Content-Type': 'text/plist'}.update(gsa.Anisette().generate_headers())
|
||||||
|
r = requests.post("https://setup.icloud.com/setup/prefpane/loginDelegates", headers=headers, auth=(username, pet), data=data, verify=False)
|
||||||
|
r = plistlib.loads(r.content)
|
||||||
|
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}")
|
Loading…
Reference in a new issue