Ejemplo n.º 1
0
def getBackupKeyBag(backupfolder, passphrase):
    manifest = readPlist(backupfolder + "/Manifest.plist")

    kb = Keybag(manifest["BackupKeyBag"].data)

    if kb.unlockBackupKeybagWithPasscode(passphrase):
        print "BackupKeyBag unlock OK"
        return kb
    else:
        return None
Ejemplo n.º 2
0
def getBackupKeyBag(backupfolder, passphrase):
    manifest = BPlistReader.plistWithFile(backupfolder + "/Manifest.plist")

    kb = Keybag(manifest["BackupKeyBag"].data)

    if kb.unlockBackupKeybagWithPasscode(passphrase):
        print "BackupKeyBag unlock OK"
        return kb
    else:
        return None
Ejemplo n.º 3
0
class MobileBackupClient(object):
    def __init__(self, account_settings, dsPrsID, auth, output_folder):
        mobilebackup_url = account_settings["com.apple.mobileme"][
            "com.apple.Dataclass.Backup"]["url"]
        content_url = account_settings["com.apple.mobileme"][
            "com.apple.Dataclass.Content"]["url"]

        self.mobilebackup_host = host_from_url(mobilebackup_url)
        self.content_host = host_from_url(content_url)
        self.dsPrsID = dsPrsID
        self.headers = {
            'Authorization': auth,
            'X-MMe-Client-Info': CLIENT_INFO,
            'User-Agent': USER_AGENT_MOBILE_BACKUP,
            'X-Apple-MBS-Protocol-Version': "1.7"
        }
        self.headers2 = {
            'x-apple-mmcs-proto-version': "3.3",
            'x-apple-mmcs-dataclass': "com.apple.Dataclass.Backup",
            'x-apple-mme-dsid': str(self.dsPrsID),
            'User-Agent': USER_AGENT_BACKUPD,
            'Accept': "application/vnd.com.apple.me.ubchunk+protobuf",
            'Content-Type': "application/vnd.com.apple.me.ubchunk+protobuf",
            'x-mme-client-info': CLIENT_INFO_BACKUP
        }

        self.files = {}
        self.output_folder = output_folder

        self.chosen_snapshot_id = None
        self.combined = False
        self.itunes_style = False
        self.downloaded_files = []
        self.domain_filter = None
        self.threads = DEFAULT_THREADS

    def mobile_backup_request(self, method, url, msg=None, body=""):
        return probobuf_request(self.mobilebackup_host, method, url, body,
                                self.headers, msg)

    def get_account(self):
        return self.mobile_backup_request("GET", MBS[self.dsPrsID](),
                                          MBSAccount)

    def get_backup(self, backupUDID):
        return self.mobile_backup_request(
            "GET", MBS[self.dsPrsID][backupUDID.encode("hex")](), MBSBackup)

    def get_keys(self, backupUDID):
        return self.mobile_backup_request(
            "GET", MBS[self.dsPrsID][backupUDID.encode("hex")].getKeys(),
            MBSKeySet)

    def list_files(self, backupUDID, snapshotId):
        limit = 5000
        files = ""

        offset = 0
        new_files = self.mobile_backup_request(
            "GET", MBS[self.dsPrsID][backupUDID.encode("hex")][snapshotId]
            ['listFiles'](offset=offset, limit=limit))
        while new_files:
            files = files + new_files
            offset += limit

            new_files = self.mobile_backup_request(
                "GET", MBS[self.dsPrsID][backupUDID.encode("hex")]
                [snapshotId].listFiles(offset=offset, limit=limit))
            print "\tShifting offset: ", offset

        return decode_protobuf_array(files, MBSFile)

    def get_files(self, backupUDID, snapshotId, files):
        r = []
        h = {}
        for file in files:
            if file.Size == 0:
                continue

            ff = MBSFile()
            ff.FileID = file.FileID
            h[file.FileID] = file.Signature
            r.append(ff)
            self.files[file.Signature] = file

        body = encode_protobuf_array(r)
        z = self.mobile_backup_request(
            "POST",
            MBS[self.dsPrsID][backupUDID.encode("hex")][snapshotId].getFiles(),
            None, body)
        tokens = decode_protobuf_array(z, MBSFileAuthToken)
        z = MBSFileAuthTokens()

        for token in tokens:
            toto = z.tokens.add()
            toto.FileID = h[token.FileID]
            toto.AuthToken = token.AuthToken

        return z

    def authorize_get(self, tokens, snapshot):
        if len(tokens.tokens) == 0:
            return

        self.headers2["x-apple-mmcs-auth"] = "%s %s" % (
            tokens.tokens[0].FileID.encode("hex"), tokens.tokens[0].AuthToken)
        body = tokens.SerializeToString()

        file_groups = probobuf_request(self.content_host, "POST",
                                       URL[self.dsPrsID].authorizeGet(), body,
                                       self.headers2, FileGroups)
        file_chunks = {}
        pool = Pool(self.threads)

        containers = []
        for group in file_groups.file_groups:
            for container_index, container in enumerate(
                    group.storage_host_chunk_list):
                containers.append([group, container_index, container])

        for res in pool.imap_unordered(self.download_chunks, containers):
            args, data = res
            group, container_index, container = args

            for file_ref in group.file_checksum_chunk_references:
                if file_ref.file_checksum not in self.files:
                    continue

                decrypted_chunks = file_chunks.setdefault(
                    file_ref.file_checksum, {})

                for i, reference in enumerate(file_ref.chunk_references):
                    if reference.container_index == container_index:
                        decrypted_chunks[i] = data[reference.chunk_index]

                if len(decrypted_chunks) == len(file_ref.chunk_references):
                    file = self.files[file_ref.file_checksum]
                    try:
                        self.write_file(file, decrypted_chunks, snapshot)
                    except:
                        raise
                    else:
                        # With iTunes style we need to keep the file
                        if self.itunes_style:
                            self.downloaded_files.append(file)

                        del self.files[file_ref.file_checksum]

        pool.join()
        return file_groups

    def get_complete(self, mmcs_auth):
        self.headers2["x-apple-mmcs-auth"] = mmcs_auth
        body = ""
        probobuf_request(self.content_host, "POST",
                         URL[self.dsPrsID].getComplete(), body, self.headers2)

    def download_chunks(self, args):
        group, container_index, container = args

        headers = {}
        # XXX
        for header in container.host_info.headers:
            headers[header.name] = header.value

        d = probobuf_request(container.host_info.hostname,
                             container.host_info.method,
                             container.host_info.uri, "", headers)
        decrypted = []
        i = 0
        for chunk in container.chunk_info:
            dchunk = decrypt_chunk(d[i:i + chunk.chunk_length],
                                   chunk.chunk_encryption_key,
                                   chunk.chunk_checksum)
            if dchunk:
                decrypted.append(dchunk)
                i += chunk.chunk_length

        return args, decrypted

    def write_file(self, file, decrypted_chunks, snapshot):
        # If the filename should be left in the iTunes backup style
        if self.itunes_style:
            if self.combined:
                directory = self.output_folder
            else:
                directory = os.path.join(self.output_folder,
                                         "snapshot_" + str(snapshot))

            path_hash = hashlib.sha1(
                file.Domain.encode('utf-8') + "-" +
                file.RelativePath.encode('utf-8')).hexdigest()
            path = os.path.join(directory, path_hash)
        else:
            if self.combined:
                directory = os.path.join(
                    self.output_folder, re.sub(r'[:|*<>?"]', "_", file.Domain))
                path = os.path.join(directory, file.RelativePath)
            else:
                directory = os.path.join(
                    self.output_folder,
                    re.sub(r'[:|*<>?"]', "_",
                           "snapshot_" + str(snapshot) + "/" + file.Domain))
                path = os.path.join(directory, file.RelativePath)

        mkdir_p(os.path.dirname(path))

        print '\t', file.Domain, '\t', path
        with open(path, "wb") as ff:
            hash = hashlib.sha1()
            for key, chunk in decrypted_chunks.iteritems():
                hash.update(chunk)
                ff.write(chunk)

        # If file is encrypted
        if file.Attributes.EncryptionKey:
            key = file.Attributes.EncryptionKey
            ProtectionClass = struct.unpack(">L", key[0x18:0x1C])[0]
            if ProtectionClass == file.Attributes.ProtectionClass:
                wrapped_key = None
                filekey = None
                if file.Attributes.EncryptionKeyVersion and file.Attributes.EncryptionKeyVersion == 2:
                    if self.kb.uuid == key[:0x10]:
                        keyLength = struct.unpack(">L", key[0x20:0x24])[0]
                        if keyLength == 0x48:
                            wrapped_key = key[0x24:]
                else:
                    wrapped_key = key[0x1C:]

                if wrapped_key:
                    filekey = self.kb.unwrapCurve25519(ProtectionClass,
                                                       wrapped_key)

                if not filekey:
                    print "Failed to unwrap file key for file %s !!!" % file.RelativePath
                else:
                    print "\tfilekey", filekey.encode("hex")
                    self.decrypt_protected_file(path, filekey,
                                                file.Attributes.DecryptedSize)
            else:
                print "\tUnable to decrypt file, possible old backup format", file.RelativePath

    def decrypt_protected_file(self, path, filekey, decrypted_size=0):
        ivkey = hashlib.sha1(filekey).digest()[:16]
        hash = hashlib.sha1()
        sz = os.path.getsize(path)

        oldpath = path + ".encrypted"
        try:
            os.rename(path, oldpath)
        except:
            pass

        with open(oldpath, "rb") as old_file:
            with open(path, "wb") as new_file:
                n = sz / 0x1000
                if decrypted_size:
                    n += 1

                for block in xrange(n):
                    iv = AESencryptCBC(self.computeIV(block * 0x1000), ivkey)
                    old_data = old_file.read(0x1000)
                    hash.update(old_data)
                    new_file.write(AESdecryptCBC(old_data, filekey, iv))

                if decrypted_size == 0:  #old iOS 5 format
                    trailer = old_file.read(0x1C)
                    decrypted_size = struct.unpack(">Q", trailer[:8])[0]
                    assert hash.digest() == trailer[8:]

                new_file.truncate(decrypted_size)

                os.remove(oldpath)  # Delete the encrypted file

    def computeIV(self, lba):
        iv = ""
        lba &= 0xffffffff
        for _ in xrange(4):
            if (lba & 1):
                lba = 0x80000061 ^ (lba >> 1)
            else:
                lba = lba >> 1

            iv += struct.pack("<L", lba)

        return iv

    def download(self, backupUDID, item_types):
        mbsbackup = self.get_backup(backupUDID)
        self.output_folder = os.path.join(self.output_folder,
                                          backupUDID.encode("hex"))

        print "Downloading backup {} to {}".format(backupUDID.encode("hex"),
                                                   self.output_folder)

        try:
            mkdir_p(self.output_folder)
        except OSError:
            print "Directory \"{}\" already exists.".format(self.output_folder)
            return

        keys = self.get_keys(backupUDID)
        if not keys or not len(keys.Key):
            print "get_keys FAILED!"
            return

        print "Got OTA Keybag"

        self.kb = Keybag(keys.Key[-1].KeyData)
        if not self.kb.unlockBackupKeybagWithPasscode(keys.Key[0].KeyData):
            print "Unable to unlock OTA keybag !"
            return

        print "Available Snapshots: %d" % (mbsbackup.Snapshot.SnapshotID)
        if self.chosen_snapshot_id == None:
            snapshot_list = [
                1, mbsbackup.Snapshot.SnapshotID - 1,
                mbsbackup.Snapshot.SnapshotID
            ]
        elif self.chosen_snapshot_id < 0:
            snapshot_list = [
                mbsbackup.Snapshot.SnapshotID + self.chosen_snapshot_id + 1
            ]  # Remember chosen_snapshot_id is negative
        else:
            snapshot_list = [self.chosen_snapshot_id]

        for snapshot in snapshot_list:
            print "Listing snapshot %d..." % (snapshot)
            files = self.list_files(backupUDID, snapshot)
            print "Files in snapshot %d" % (len(files))

            def matches_allowed_domain(a_file):
                return self.domain_filter in a_file.Domain

            def matches_allowed_item_types(a_file):
                return any(ITEM_TYPES_TO_FILE_NAMES[item_type] in a_file.RelativePath.lower() \
                        for item_type in item_types)

            if self.domain_filter:
                files = filter(matches_allowed_domain, files)

            if len(item_types) > 0:
                files = filter(matches_allowed_item_types, files)

            print "Downloading %d files due to filter" % (len(files))

            if len(files):
                authTokens = self.get_files(backupUDID, snapshot, files)
                if len(authTokens.tokens) > 0:
                    self.authorize_get(authTokens, snapshot)

                    if self.itunes_style:
                        self.write_info_plist(mbsbackup, snapshot)
                        self.write_manifest_mbdb(snapshot)
                else:
                    print "Unable to download snapshot. This snapshot may not have finished uploading yet."

            # Clean up self.files
            if not self.combined:
                self.downloaded_files = []

    # Writes a plist file in the output_directory simular to that created by iTunes during backup
    def write_info_plist(self, mbsbackup, snapshot):
        if self.combined:
            directory = self.output_folder
        else:
            directory = os.path.join(self.output_folder,
                                     "snapshot_" + str(snapshot))

        info_plist = {
            "Device Name": mbsbackup.Attributes.DeviceClass,
            "Display Name": mbsbackup.Attributes.DeviceClass,
            "Product Type": mbsbackup.Attributes.ProductType,
            "Serial Number": mbsbackup.Attributes.SerialNumber,
            "Target Type": "Device",
            "iTunes Version": "11.1",
            "Product Version":
            "8.1.1",  # Must be higher than 4.0, current iTunes backup sets to 8.1.1
            "Target Identifier": mbsbackup.backupUDID.encode("hex"),
            "Unique Identifier": mbsbackup.backupUDID.encode("hex")
        }

        with open(directory + "/Info.plist", 'wb') as fp:
            plistlib.writePlist(info_plist, fp)

    def write_manifest_mbdb(self, snapshot):
        if self.combined:
            directory = self.output_folder
        else:
            directory = os.path.join(self.output_folder,
                                     "snapshot_" + str(snapshot))

        filename = os.path.join(directory, "Manifest.mbdb")

        # Generate the bare minimum MBDB file

        # Open file
        mbdb_file = open(filename, "wb")

        # Write file header
        mbdb_file.write("mbdb")
        mbdb_file.write("\x00\x00")

        # For each file
        for file in self.downloaded_files:
            # Write App Domain length
            mbdb_file.write(struct.pack('>h', len(file.Domain)))
            # Write App Domain
            mbdb_file.write(file.Domain)
            # Write iPhone Filename length
            mbdb_file.write(struct.pack('>h', len(file.RelativePath)))
            # Write iPhone Filename
            mbdb_file.write(file.RelativePath)
            # Write 0xFFFF for Link Target Length signifying that it is not present
            mbdb_file.write("\xFF\xFF")
            # Write 0xFFFF for SHAChecksum Length signifying the checksum is not present
            mbdb_file.write("\xFF\xFF")
            # Write 0xFFFF for the length of some unknown value, signifying it is not present
            mbdb_file.write("\xFF\xFF")
            # Write 0x27 bytes of 0x00, for the file properties as set by the iPhone during restore
            mbdb_file.write(
                "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
            )
            # Write 0x00 for extended property count
            mbdb_file.write("\x00")

        # Close file
        mbdb_file.close()
Ejemplo n.º 4
0
class MobileBackupClient(object):
    def __init__(self, account_settings, dsPrsID, auth, output_folder):
        mobilebackup_url = account_settings["com.apple.mobileme"]["com.apple.Dataclass.Backup"]["url"]
        content_url = account_settings["com.apple.mobileme"]["com.apple.Dataclass.Content"]["url"]

        self.mobilebackup_host = host_from_url(mobilebackup_url)
        self.content_host = host_from_url(content_url)
        self.dsPrsID = dsPrsID
        self.headers = {
            'Authorization': auth,
            'X-MMe-Client-Info': CLIENT_INFO,
            'User-Agent': USER_AGENT_MOBILE_BACKUP,
            'X-Apple-MBS-Protocol-Version': "1.7"
        }
        self.headers2 = {
            'x-apple-mmcs-proto-version': "3.3",
            'x-apple-mmcs-dataclass': "com.apple.Dataclass.Backup",
            'x-apple-mme-dsid': str(self.dsPrsID),
            'User-Agent': USER_AGENT_BACKUPD,
            'Accept': "application/vnd.com.apple.me.ubchunk+protobuf",
            'Content-Type': "application/vnd.com.apple.me.ubchunk+protobuf",
            'x-mme-client-info': CLIENT_INFO_BACKUP
        }

        self.files = {}
        self.output_folder = output_folder

        self.chosen_snapshot_id = None
        self.combined = False
        self.itunes_style = False
        self.downloaded_files = []
        self.domain_filter = None

    def mobile_backup_request(self, method, url, msg=None, body=""):
        return probobuf_request(self.mobilebackup_host, method, url, body, self.headers, msg)

    def get_account(self):
        return self.mobile_backup_request("GET", MBS[self.dsPrsID](), MBSAccount)

    def get_backup(self, backupUDID):
        return self.mobile_backup_request("GET", MBS[self.dsPrsID][backupUDID.encode("hex")](), MBSBackup)

    def get_keys(self, backupUDID):
        return self.mobile_backup_request("GET", MBS[self.dsPrsID][backupUDID.encode("hex")].getKeys(), MBSKeySet)

    def list_files(self, backupUDID, snapshotId):
        limit = 5000
        files = ""

        offset = 0
        new_files = self.mobile_backup_request("GET", MBS[self.dsPrsID][backupUDID.encode("hex")][snapshotId].listFiles(offset=offset, limit=limit))
        while new_files:
            files = files + new_files
            offset += limit

            new_files = self.mobile_backup_request("GET", MBS[self.dsPrsID][backupUDID.encode("hex")][snapshotId].listFiles(offset=offset, limit=limit))
            print "\tShifting offset: ", offset

        return decode_protobuf_array(files, MBSFile)

    def get_files(self, backupUDID, snapshotId, files):
        r = []
        h = {}
        for file in files:
            if file.Size == 0:
                continue

            ff = MBSFile()
            ff.FileID = file.FileID
            h[file.FileID] = file.Signature
            r.append(ff)
            self.files[file.Signature] = file

        body = encode_protobuf_array(r)
        z = self.mobile_backup_request("POST", MBS[self.dsPrsID][backupUDID.encode("hex")][snapshotId].getFiles(), None, body)
        tokens = decode_protobuf_array(z, MBSFileAuthToken)
        z = MBSFileAuthTokens()

        for token in tokens:
            toto = z.tokens.add()
            toto.FileID = h[token.FileID]
            toto.AuthToken = token.AuthToken

        return z

    def authorize_get(self, tokens, snapshot):
        if len(tokens.tokens) == 0:
            return

        self.headers2["x-apple-mmcs-auth"]= "%s %s" % (tokens.tokens[0].FileID.encode("hex"), tokens.tokens[0].AuthToken)
        body = tokens.SerializeToString()

        file_groups = probobuf_request(self.content_host, "POST", URL[self.dsPrsID].authorizeGet(), body, self.headers2, FileGroups)
        file_chunks = {}
        for group in file_groups.file_groups:
            for container_index, container in enumerate(group.storage_host_chunk_list):
                data = self.download_chunks(container)
                for file_ref in group.file_checksum_chunk_references:
                    if file_ref.file_checksum not in self.files:
                        continue

                    decrypted_chunks = file_chunks.setdefault(file_ref.file_checksum, {})

                    for i, reference in enumerate(file_ref.chunk_references):
                        if reference.container_index == container_index:
                            decrypted_chunks[i] = data[reference.chunk_index]

                    if len(decrypted_chunks) == len(file_ref.chunk_references):
                        file = self.files[file_ref.file_checksum]
                        try:
                            self.write_file(file, decrypted_chunks, snapshot)
                        except:
                            raise
                        else:
                            # With iTunes style we need to keep the file
                            if self.itunes_style :
                                self.downloaded_files.append(file)

                            del self.files[file_ref.file_checksum]

        return file_groups

    def get_complete(self, mmcs_auth):
        self.headers2["x-apple-mmcs-auth"] = mmcs_auth
        body = ""
        probobuf_request(self.content_host, "POST", URL[self.dsPrsID].getComplete(), body, self.headers2)

    def download_chunks(self, container):
        headers = {}
        # XXX
        for header in container.host_info.headers:
            headers[header.name] = header.value

        d = probobuf_request(container.host_info.hostname,
                         container.host_info.method,
                         container.host_info.uri, "", headers)
        decrypted = []
        i = 0
        for chunk in container.chunk_info:
            dchunk = decrypt_chunk(d[i:i+chunk.chunk_length], chunk.chunk_encryption_key, chunk.chunk_checksum)
            if dchunk:
                decrypted.append(dchunk)
                i += chunk.chunk_length

        return decrypted

    def write_file(self, file, decrypted_chunks, snapshot):
        # If the filename should be left in the iTunes backup style
        if self.itunes_style:
            if self.combined:
                directory = self.output_folder
            else:
                directory = os.path.join(self.output_folder, "snapshot_"+str(snapshot))

            path_hash = hashlib.sha1(file.Domain.encode('utf-8')+"-"+file.RelativePath.encode('utf-8')).hexdigest()
            path = os.path.join(directory, path_hash)
        else:
            if self.combined:
                directory = os.path.join(self.output_folder, re.sub(r'[:|*<>?"]', "_", file.Domain))
                path = os.path.join(directory, file.RelativePath)
            else:
                directory = os.path.join(self.output_folder, re.sub(r'[:|*<>?"]', "_", "snapshot_"+str(snapshot)+"/"+file.Domain))
                path = os.path.join(directory, file.RelativePath)

        mkdir_p(os.path.dirname(path))

        print '\t', file.Domain, '\t', path
        with open(path, "wb") as ff:
            hash = hashlib.sha1()
            for key, chunk in decrypted_chunks.iteritems():
                hash.update(chunk)
                ff.write(chunk)

        # If file is encrypted
        if file.Attributes.EncryptionKey:
            key = file.Attributes.EncryptionKey
            ProtectionClass = struct.unpack(">L", key[0x18:0x1C])[0]
            if ProtectionClass == file.Attributes.ProtectionClass:
                wrapped_key = None
                filekey = None
                if file.Attributes.EncryptionKeyVersion and file.Attributes.EncryptionKeyVersion == 2:
                    if self.kb.uuid == key[:0x10]:
                        keyLength = struct.unpack(">L", key[0x20:0x24])[0]
                        if keyLength == 0x48:
                            wrapped_key = key[0x24:]
                else:
                    wrapped_key = key[0x1C:]

                if wrapped_key:
                    filekey = self.kb.unwrapCurve25519(ProtectionClass, wrapped_key)

                if not filekey:
                    print "Failed to unwrap file key for file %s !!!" % file.RelativePath
                else:
                    print "\tfilekey", filekey.encode("hex")
                    self.decrypt_protected_file(path, filekey, file.Attributes.DecryptedSize)
            else:
                print "\tUnable to decrypt file, possible old backup format", file.RelativePath

    def decrypt_protected_file(self, path, filekey, decrypted_size=0):
        ivkey = hashlib.sha1(filekey).digest()[:16]
        hash = hashlib.sha1()
        sz = os.path.getsize(path)

        oldpath = path + ".encrypted"
        try:
            os.rename(path, oldpath)
        except:
            pass

        with open(oldpath, "rb") as old_file:
            with open(path, "wb") as new_file:
                n = sz / 0x1000
                if decrypted_size:
                    n += 1

                for block in xrange(n):
                    iv = AESencryptCBC(self.computeIV(block * 0x1000), ivkey)
                    old_data = old_file.read(0x1000)
                    hash.update(old_data)
                    new_file.write(AESdecryptCBC(old_data, filekey, iv))

                if decrypted_size == 0: #old iOS 5 format
                    trailer = old_file.read(0x1C)
                    decrypted_size = struct.unpack(">Q", trailer[:8])[0]
                    assert hash.digest() == trailer[8:]

                new_file.truncate(decrypted_size)

                os.remove(oldpath) # Delete the encrypted file

    def computeIV(self, lba):
        iv = ""
        lba &= 0xffffffff
        for _ in xrange(4):
            if (lba & 1):
                lba = 0x80000061 ^ (lba >> 1)
            else:
                lba = lba >> 1

            iv += struct.pack("<L", lba)

        return iv

    def download(self, backupUDID, item_types):
        mbsbackup = self.get_backup(backupUDID)
        self.output_folder = os.path.join(self.output_folder, backupUDID.encode("hex"))

        print "Downloading backup {} to {}".format(backupUDID.encode("hex"), self.output_folder)

        try:
            mkdir_p(self.output_folder)
        except OSError:
            print "Directory \"{}\" already exists.".format(self.output_folder)
            return

        keys = self.get_keys(backupUDID)
        if not keys or not len(keys.Key):
            print "get_keys FAILED!"
            return

        print "Got OTA Keybag"

        self.kb = Keybag(keys.Key[1].KeyData)
        if not self.kb.unlockBackupKeybagWithPasscode(keys.Key[0].KeyData):
            print "Unable to unlock OTA keybag !"
            return

        print "Available Snapshots: %d" % (mbsbackup.Snapshot.SnapshotID)
        if self.chosen_snapshot_id == None:
            snapshot_list = [1, mbsbackup.Snapshot.SnapshotID - 1, mbsbackup.Snapshot.SnapshotID]
        elif self.chosen_snapshot_id < 0:
            snapshot_list = [mbsbackup.Snapshot.SnapshotID + self.chosen_snapshot_id + 1] # Remember chosen_snapshot_id is negative
        else:
            snapshot_list = [self.chosen_snapshot_id]

        for snapshot in snapshot_list:
            print "Listing snapshot %d..." % (snapshot)
            files = self.list_files(backupUDID, snapshot)
            print "Files in snapshot %d" % (len(files))

            def matches_allowed_domain(a_file):
                return self.domain_filter in a_file.Domain

            def matches_allowed_item_types(a_file):
                return any(ITEM_TYPES_TO_FILE_NAMES[item_type] in a_file.RelativePath.lower() \
                        for item_type in item_types)

            if self.domain_filter:
                files = filter(matches_allowed_domain, files)

            if len(item_types) > 0:
                files = filter(matches_allowed_item_types, files)

            print "Downloading %d files due to filter" % (len(files))

            if len(files):
                authTokens = self.get_files(backupUDID, snapshot, files)
                if len(authTokens.tokens) > 0:
                    self.authorize_get(authTokens, snapshot)

                    if self.itunes_style:
                        self.write_info_plist(mbsbackup, snapshot)
                        self.write_manifest_mbdb(snapshot)
                else:
                  print "Unable to download snapshot. This snapshot may not have finished uploading yet."

            # Clean up self.files
            if not self.combined :
                self.downloaded_files = []


    # Writes a plist file in the output_directory simular to that created by iTunes during backup
    def write_info_plist(self, mbsbackup, snapshot):
        if self.combined:
            directory = self.output_folder
        else:
            directory = os.path.join(self.output_folder, "snapshot_"+str(snapshot))

        info_plist = {
            "Device Name" : mbsbackup.Attributes.DeviceClass,
            "Display Name" : mbsbackup.Attributes.DeviceClass,
            "Product Type" : mbsbackup.Attributes.ProductType,
            "Serial Number" : mbsbackup.Attributes.SerialNumber,
            "Target Type" : "Device",
            "iTunes Version" : "11.1",
            "Product Version" : "8.1.1",  # Must be higher than 4.0, current iTunes backup sets to 8.1.1
            "Target Identifier" : mbsbackup.backupUDID.encode("hex"),
            "Unique Identifier" : mbsbackup.backupUDID.encode("hex")
        }

        with open(directory+"/Info.plist", 'wb') as fp:
            plistlib.writePlist(info_plist, fp)

    def write_manifest_mbdb(self, snapshot):
        if self.combined:
            directory = self.output_folder
        else:
            directory = os.path.join(self.output_folder, "snapshot_"+str(snapshot))

        filename = os.path.join(directory, "Manifest.mbdb")

        # Generate the bare minimum MBDB file

        # Open file
        mbdb_file = open(filename, "wb")

        # Write file header
        mbdb_file.write("mbdb")
        mbdb_file.write("\x00\x00")

        # For each file
        for file in self.downloaded_files:
            # Write App Domain length
            mbdb_file.write( struct.pack('>h', len(file.Domain)) )
            # Write App Domain
            mbdb_file.write( file.Domain )
            # Write iPhone Filename length
            mbdb_file.write( struct.pack('>h', len(file.RelativePath)) )
            # Write iPhone Filename
            mbdb_file.write( file.RelativePath )
            # Write 0xFFFF for Link Target Length signifying that it is not present
            mbdb_file.write("\xFF\xFF")
            # Write 0xFFFF for SHAChecksum Length signifying the checksum is not present
            mbdb_file.write("\xFF\xFF")
            # Write 0xFFFF for the length of some unknown value, signifying it is not present
            mbdb_file.write("\xFF\xFF")
            # Write 0x27 bytes of 0x00, for the file properties as set by the iPhone during restore
            mbdb_file.write("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")
            # Write 0x00 for extended property count
            mbdb_file.write("\x00")

        # Close file
        mbdb_file.close()
Ejemplo n.º 5
0
class MobileBackupClient(object):
    def __init__(self, account_settings, dsPrsID, auth, outputFolder):
        mobilebackup_url = account_settings["com.apple.mobileme"]["com.apple.Dataclass.Backup"]["url"]
        content_url = account_settings["com.apple.mobileme"]["com.apple.Dataclass.Content"]["url"]
        
        self.mobilebackup_host = re.match("https://(.*):443", mobilebackup_url).group(1)
        self.content_host = re.match("https://(.*):443", content_url).group(1)
        self.dsPrsID = dsPrsID
        self.headers = {"Authorization": auth,
                        "X-MMe-Client-Info": Client_Info,
                        "User-Agent": USER_AGENT_MOBILE_BACKUP,
                        "X-Apple-MBS-Protocol-Version": "1.7" #error 400 without this
        }
        self.headers2 = {"x-apple-mmcs-proto-version": "3.3",
            "x-apple-mmcs-dataclass": "com.apple.Dataclass.Backup",
            "x-apple-mme-dsid": str(self.dsPrsID),
            "User-Agent":USER_AGENT_BACKUPD,
            "Accept": "application/vnd.com.apple.me.ubchunk+protobuf",
            "Content-Type": "application/vnd.com.apple.me.ubchunk+protobuf",
            "x-mme-client-info": Client_Info_backup
        }
        self.files = {}
        self.outputFolder = outputFolder
    
    def mobileBackupRequest(self, method, url, msg=None, body=""):
        return probobuf_request(self.mobilebackup_host, method, url, body, self.headers, msg)
    
    def getAccount(self):
        return self.mobileBackupRequest("GET", "/mbs/%d" % self.dsPrsID, MBSAccount)

    def getBackup(self, backupUDID):
        return self.mobileBackupRequest("GET", "/mbs/%d/%s" % (self.dsPrsID, backupUDID.encode("hex")), MBSBackup)
        
    def getKeys(self, backupUDID):
        return self.mobileBackupRequest("GET", "/mbs/%d/%s/getKeys" % (self.dsPrsID, backupUDID.encode("hex")), MBSKeySet)

    def listFiles(self, backupUDID, snapshotId):
        files = self.mobileBackupRequest("GET", "/mbs/%d/%s/%d/listFiles" % (self.dsPrsID, backupUDID.encode("hex"), snapshotId))
        return decode_protobuf_array(files, MBSFile)
    
    def getFiles(self, backupUDID, snapshotId, files):
        r = []
        h = {}
        for f in files:
            if f.Size == 0:
                continue
            ff = MBSFile()
            ff.FileID = f.FileID
            h[f.FileID] = f.Signature
            r.append(ff)
            self.files[f.Signature] = f
        body = encode_protobuf_array(r)
        z = self.mobileBackupRequest("POST", "/mbs/%d/%s/%d/getFiles" % (self.dsPrsID, backupUDID.encode("hex"), snapshotId), None, body)
        tokens = decode_protobuf_array(z, MBSFileAuthToken)
        z = MBSFileAuthTokens()
        for t in tokens:
            toto = z.tokens.add()
            toto.FileID = h[t.FileID]   #use signature
            toto.AuthToken = t.AuthToken
        return z
    
    def authorizeGet(self, tokens):
        self.headers2["x-apple-mmcs-auth"]= "%s %s" % (tokens.tokens[0].FileID.encode("hex"), tokens.tokens[0].AuthToken)
        body = tokens.SerializeToString()

        filegroups = probobuf_request(self.content_host, "POST", "/%d/authorizeGet" % self.dsPrsID, body, self.headers2, FileGroups)
        #print filegroups
        filechunks = {}
        for group in filegroups.file_groups:
            for container_index in xrange(len(group.storage_host_chunk_list)):
                data = self.downloadChunks(group.storage_host_chunk_list[container_index])
                for file_ref in group.file_checksum_chunk_references:
                    if not self.files.has_key(file_ref.file_checksum):
                        continue
                    decrypted_chunks = filechunks.setdefault(file_ref.file_checksum, {})
                    for i in xrange(len(file_ref.chunk_references)):
                        ref = file_ref.chunk_references[i]
                        if ref.container_index == container_index:
                            decrypted_chunks[i] = data[ref.chunk_index]
                    if len(decrypted_chunks) == len(file_ref.chunk_references):
                        f = self.files[file_ref.file_checksum]
                        self.writeFile(f, decrypted_chunks)
                        del self.files[file_ref.file_checksum]
                        
        pprint(self.files)
        return filegroups

    def getComplete(self, mmcs_auth):
        self.headers2["x-apple-mmcs-auth"] = mmcs_auth
        body = ""
        probobuf_request(self.content_host, "POST", "/%d/getComplete" % self.dsPrsID, body, self.headers2)
        
    def downloadChunks(self, storage_host):
        headers = {}
        for h in storage_host.host_info.headers:
            headers[h.name] = h.value
        d = probobuf_request(storage_host.host_info.hostname,
                         storage_host.host_info.method,
                         storage_host.host_info.uri, "", headers)
        decrypted = []
        i = 0
        for chunk in storage_host.chunk_info:
            decrypted.append(decrypt_chunk(d[i:i+chunk.chunk_length], chunk.chunk_encryption_key, chunk.chunk_checksum))
            i += chunk.chunk_length
        return decrypted

    def writeFile(self, f, decrypted_chunks):
        path = os.path.join(self.outputFolder, re.sub(r'[:|*<>?"]', "_", f.RelativePath))
        print path
        makedirs(os.path.dirname(path))
        ff = open(path, "wb")
        h = hashlib.sha1()
        for i in xrange(len(decrypted_chunks)):
            d = decrypted_chunks[i]
            h.update(d)
            ff.write(d)
        ff.close()

        if f.Attributes.EncryptionKey:
            EncryptionKey = f.Attributes.EncryptionKey
            #ProtectionClass = f.Attributes.ProtectionClass
            hexdump(EncryptionKey)
            ProtectionClass = struct.unpack(">L", EncryptionKey[0x18:0x1C])[0]
            assert ProtectionClass == f.Attributes.ProtectionClass
            #EncryptionKeyVersion=2 => starts with keybag uuid
            if f.Attributes.EncryptionKeyVersion and f.Attributes.EncryptionKeyVersion == 2:
                assert self.kb.uuid == EncryptionKey[:0x10]
                keyLength = struct.unpack(">L", EncryptionKey[0x20:0x24])[0]
                assert keyLength == 0x48
                wrapped_key = EncryptionKey[0x24:]
            else:#XXX old format ios 5 backup
                wrapped_key = EncryptionKey[0x1C:]
            print "ProtectionClass= %d" % ProtectionClass
            filekey = self.kb.unwrapCurve25519(ProtectionClass, wrapped_key)
            if not filekey:
                print "Failed to unwrap file key for file %s !!!" % f.RelativePath
            else:
                print "filekey",filekey.encode("hex")
                self.decryptProtectedFile(path, filekey, f.Attributes.DecryptedSize)

    def decryptProtectedFile(self, path, filekey, DecryptedSize=0):
        ivkey = hashlib.sha1(filekey).digest()[:16]
        h = hashlib.sha1()
        sz = os.path.getsize(path)
        #iOS 5 trailer = uint64 sz + sha1 of encrypted file
        #assert (sz % 0x1000) == 0x1C
        oldpath = path + ".encrypted"
        try:
            os.rename(path, oldpath)
        except:
            pass
        f1 = open(oldpath, "rb")
        f2 = open(path, "wb")
        n = (sz / 0x1000)
        if DecryptedSize:
            n += 1
        for block in xrange(n):
            iv = AESencryptCBC(self.computeIV(block * 0x1000), ivkey)
            data = f1.read(0x1000)
            h.update(data)
            f2.write(AESdecryptCBC(data, filekey, iv))
        if DecryptedSize == 0: #old iOS 5 format
            trailer = f1.read(0x1C)
            DecryptedSize = struct.unpack(">Q", trailer[:8])[0]
            assert h.digest() == trailer[8:]
        f1.close()
        f2.truncate(DecryptedSize)
        f2.close()

    def computeIV(self, lba):
        iv = ""
        lba &= 0xffffffff
        for _ in xrange(4):
            if (lba & 1):
                lba = 0x80000061 ^ (lba >> 1);
            else:
                lba = lba >> 1;
            iv += struct.pack("<L", lba)
        return iv

    def download(self, backupUDID):
        mbsbackup = self.getBackup(backupUDID)
        print "Downloading backup %s" % backupUDID.encode("hex")
        self.outputFolder = os.path.join(self.outputFolder, backupUDID.encode("hex"))
        makedirs(self.outputFolder)
        print backup_summary(mbsbackup)
        #print mbsbackup.Snapshot.Attributes.KeybagUUID.encode("hex")
        keys = self.getKeys(backupUDID)
        if not keys or not len(keys.Key):
            print "getKeys FAILED!"
            return
        
        print "Got OTA Keybag"
        self.kb = Keybag(keys.Key[1].KeyData)
        if not self.kb.unlockBackupKeybagWithPasscode(keys.Key[0].KeyData):
            print "Unable to unlock OTA keybag !"
            return

        for snapshot in xrange(1, mbsbackup.Snapshot.SnapshotID+1):
            files = self.listFiles(backupUDID, snapshot)
            print "%d files" % len(files)
            files2 = []
            for f in files:
                if f.Attributes.EncryptionKey:
                    files2.append(f)
                    print f
            if len(files2):
                authTokens = self.getFiles(backupUDID, snapshot, files)
                self.authorizeGet(authTokens)
Ejemplo n.º 6
0
class MobileBackupClient(object):
    def __init__(self, account_settings, dsPrsID, auth, output_folder):
        mobilebackup_url = account_settings["com.apple.mobileme"]["com.apple.Dataclass.Backup"]["url"]
        content_url = account_settings["com.apple.mobileme"]["com.apple.Dataclass.Content"]["url"]

        self.mobilebackup_host = host_from_url(mobilebackup_url)
        self.content_host = host_from_url(content_url)
        self.dsPrsID = dsPrsID
        self.headers = {
            'Authorization': auth,
            'X-MMe-Client-Info': CLIENT_INFO,
            'User-Agent': USER_AGENT_MOBILE_BACKUP,
            'X-Apple-MBS-Protocol-Version': "1.7"
        }
        self.headers2 = {
            'x-apple-mmcs-proto-version': "3.3",
            'x-apple-mmcs-dataclass': "com.apple.Dataclass.Backup",
            'x-apple-mme-dsid': str(self.dsPrsID),
            'User-Agent': USER_AGENT_BACKUPD,
            'Accept': "application/vnd.com.apple.me.ubchunk+protobuf",
            'Content-Type': "application/vnd.com.apple.me.ubchunk+protobuf",
            'x-mme-client-info': CLIENT_INFO_BACKUP
        }

        self.files = {}
        self.output_folder = output_folder

    def mobile_backup_request(self, method, url, msg=None, body=""):
        return probobuf_request(self.mobilebackup_host, method, url, body, self.headers, msg)

    def get_account(self):
        return self.mobile_backup_request("GET", MBS[self.dsPrsID](), MBSAccount)

    def get_backup(self, backupUDID):
        return self.mobile_backup_request("GET", MBS[self.dsPrsID][backupUDID.encode("hex")](), MBSBackup)

    def get_keys(self, backupUDID):
        return self.mobile_backup_request("GET", MBS[self.dsPrsID][backupUDID.encode("hex")].getKeys(), MBSKeySet)

    def list_files(self, backupUDID, snapshotId):
        limit = 5000
        files = ""

        offset = 0
        new_files = self.mobile_backup_request("GET", MBS[self.dsPrsID][backupUDID.encode("hex")][snapshotId]['listFiles'](offset=offset, limit=limit))
        while new_files:
            files = files + new_files
            offset += limit;

            new_files = self.mobile_backup_request("GET", MBS[self.dsPrsID][backupUDID.encode("hex")][snapshotId](offset=offset, limit=limit))
            print "\tShifting offset: ", offset

        return decode_protobuf_array(files, MBSFile)

    def get_files(self, backupUDID, snapshotId, files):
        r = []
        h = {}
        for file in files:
            if file.Size == 0:
                continue

            ff = MBSFile()
            ff.FileID = file.FileID
            h[file.FileID] = file.Signature
            r.append(ff)
            self.files[file.Signature] = file

        body = encode_protobuf_array(r)
        z = self.mobile_backup_request("POST", MBS[self.dsPrsID][backupUDID.encode("hex")][snapshotId].getFiles(), None, body)
        tokens = decode_protobuf_array(z, MBSFileAuthToken)
        z = MBSFileAuthTokens()

        for token in tokens:
            toto = z.tokens.add()
            toto.FileID = h[token.FileID]
            toto.AuthToken = token.AuthToken

        return z

    def authorize_get(self, tokens, snapshot):
        self.headers2["x-apple-mmcs-auth"]= "%s %s" % (tokens.tokens[0].FileID.encode("hex"), tokens.tokens[0].AuthToken)
        body = tokens.SerializeToString()

        file_groups = probobuf_request(self.content_host, "POST", URL[self.dsPrsID].authorizeGet(), body, self.headers2, FileGroups)
        file_chunks = {}
        for group in file_groups.file_groups:
            for container_index, container in enumerate(group.storage_host_chunk_list):
                data = self.download_chunks(container)
                for file_ref in group.file_checksum_chunk_references:
                    if file_ref.file_checksum not in self.files:
                        continue

                    decrypted_chunks = file_chunks.setdefault(file_ref.file_checksum, {})

                    for i, reference in enumerate(file_ref.chunk_references):
                        if reference.container_index == container_index:
                            decrypted_chunks[i] = data[reference.chunk_index]

                    if len(decrypted_chunks) == len(file_ref.chunk_references):
                        file = self.files[file_ref.file_checksum]
                        try:
                            self.write_file(file, decrypted_chunks, snapshot)
                        except:
                            raise
                        else:
                            del self.files[file_ref.file_checksum]

        return file_groups

    def get_complete(self, mmcs_auth):
        self.headers2["x-apple-mmcs-auth"] = mmcs_auth
        body = ""
        probobuf_request(self.content_host, "POST", URL[self.dsPrsID].getComplete(), body, self.headers2)

    def download_chunks(self, container):
        headers = {}
        # XXX
        for header in container.host_info.headers:
            headers[header.name] = header.value

        d = probobuf_request(container.host_info.hostname,
                         container.host_info.method,
                         container.host_info.uri, "", headers)
        decrypted = []
        i = 0
        for chunk in container.chunk_info:
            dchunk = decrypt_chunk(d[i:i+chunk.chunk_length], chunk.chunk_encryption_key, chunk.chunk_checksum)
            if dchunk:
                decrypted.append(dchunk)
                i += chunk.chunk_length

        return decrypted

    def write_file(self, file, decrypted_chunks, snapshot):
        # If the filename should be left in the iTunes backup style
        if self.itunes_style:
            if self.combined:
                directory = self.output_folder
            else:
                directory = os.path.join(self.output_folder, "snapshot_"+str(snapshot))

            path_hash = hashlib.sha1(file.Domain+"-"+file.RelativePath).hexdigest()
            path = os.path.join(directory, path_hash)
        else:
            if self.combined:
                directory = os.path.join(self.output_folder, re.sub(r'[:|*<>?"]', "_",file.Domain))
                path = os.path.join(directory, file.RelativePath)
            else:
                directory = os.path.join(self.output_folder, re.sub(r'[:|*<>?"]', "_", "snapshot_"+str(snapshot)+"/"+file.Domain))
                path = os.path.join(directory, file.RelativePath)

        mkdir_p(os.path.dirname(path))

        print '\t', file.Domain, '\t', path
        with open(path, "wb") as ff:
            hash = hashlib.sha1()
            for key, chunk in decrypted_chunks.iteritems():
                hash.update(chunk)
                ff.write(chunk)

        # If file is encrypted
        if file.Attributes.EncryptionKey:
            key = file.Attributes.EncryptionKey
            ProtectionClass = struct.unpack(">L", key[0x18:0x1C])[0]
            if ProtectionClass == file.Attributes.ProtectionClass:
                if file.Attributes.EncryptionKeyVersion and file.Attributes.EncryptionKeyVersion == 2:
                    assert self.kb.uuid == key[:0x10]
                    keyLength = struct.unpack(">L", key[0x20:0x24])[0]
                    assert keyLength == 0x48
                    wrapped_key = key[0x24:]
                else:
                    wrapped_key = key[0x1C:]

                filekey = self.kb.unwrapCurve25519(ProtectionClass, wrapped_key)

                if not filekey:
                    print "Failed to unwrap file key for file %s !!!" % file.RelativePath
                else:
                    print "\tfilekey", filekey.encode("hex")
                    self.decrypt_protected_file(path, filekey, file.Attributes.DecryptedSize)
            else:
                print "\tUnable to decrypt file, possible old backup format", file.RelativePath

    def decrypt_protected_file(self, path, filekey, decrypted_size=0):
        ivkey = hashlib.sha1(filekey).digest()[:16]
        hash = hashlib.sha1()
        sz = os.path.getsize(path)

        oldpath = path + ".encrypted"
        try:
            os.rename(path, oldpath)
        except:
            pass

        with open(oldpath, "rb") as old_file:
            with open(path, "wb") as new_file:
                n = sz / 0x1000
                if decrypted_size:
                    n += 1

                for block in xrange(n):
                    iv = AESencryptCBC(self.computeIV(block * 0x1000), ivkey)
                    old_data = old_file.read(0x1000)
                    hash.update(old_data)
                    new_file.write(AESdecryptCBC(old_data, filekey, iv))

                if decrypted_size == 0: #old iOS 5 format
                    trailer = old_file.read(0x1C)
                    decrypted_size = struct.unpack(">Q", trailer[:8])[0]
                    assert hash.digest() == trailer[8:]

                new_file.truncate(decrypted_size)

    def computeIV(self, lba):
        iv = ""
        lba &= 0xffffffff
        for _ in xrange(4):
            if (lba & 1):
                lba = 0x80000061 ^ (lba >> 1);
            else:
                lba = lba >> 1;

            iv += struct.pack("<L", lba)

        return iv

    def download(self, backupUDID, item_types):
        mbsbackup = self.get_backup(backupUDID)
        self.output_folder = os.path.join(self.output_folder, backupUDID.encode("hex"))

        print "Downloading backup {} to {}".format(backupUDID.encode("hex"), self.output_folder)

        try:
            mkdir_p(self.output_folder)
        except OSError:
            print "Directory \"{}\" already exists.".format(self.output_folder)
            return

        keys = self.get_keys(backupUDID)
        if not keys or not len(keys.Key):
            print "get_keys FAILED!"
            return

        print "Got OTA Keybag"

        self.kb = Keybag(keys.Key[1].KeyData)
        if not self.kb.unlockBackupKeybagWithPasscode(keys.Key[0].KeyData):
            print "Unable to unlock OTA keybag !"
            return

        print "Available Snapshots: ", mbsbackup.Snapshot.SnapshotID
        #for snapshot in xrange(1, mbsbackup.Snapshot.SnapshotID+1):
        for snapshot in [1, mbsbackup.Snapshot.SnapshotID - 1, mbsbackup.Snapshot.SnapshotID]:
            print "Listing snapshot..."
            files = self.list_files(backupUDID, snapshot)
            print "Files in snapshot %s : %s" % (snapshot, len(files))

            def matches_allowed_item_types(file):
                return any(ITEM_TYPES_TO_FILE_NAMES[item_type] in file.RelativePath \
                        for item_type in item_types)

            if len(item_types) > 0:
                files = filter(matches_allowed_item_types, files)

            if len(files):
                authTokens = self.get_files(backupUDID, snapshot, files)
                self.authorize_get(authTokens, snapshot)

            if self.itunes_style:
                self.write_info_plist(mbsbackup, snapshot)

    # Writes a plist file in the output_directory simular to that created by iTunes during backup
    def write_info_plist(self, mbsbackup, snapshot):
        if self.combined:
            directory = self.output_folder
        else:
            directory = os.path.join(self.output_folder, "snapshot_"+str(snapshot))

        plist_file = open(directory+"/Info.plist", "w")

        # TODO: Use plistlib to generate the XML
        plist_file.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
        plist_file.write("<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n")
        plist_file.write("<plist version=\"1.0\">\n")
        plist_file.write("<dict>\n")
        plist_file.write("    <key>Build Version</key>\n")
        plist_file.write("    <string>10B329</string>\n")
        plist_file.write("    <key>Device Name</key>\n")
        plist_file.write("    <string>{}</string>\n".format(mbsbackup.Attributes.DeviceClass))
        plist_file.write("    <key>Display Name</key>\n")
        plist_file.write("    <string>{}</string>\n".format(mbsbackup.Attributes.DeviceClass))
        plist_file.write("    <key>GUID</key>\n")
        plist_file.write("    <string></string>\n")
        plist_file.write("    <key>IMEI</key>\n")
        plist_file.write("    <string></string>\n")
        plist_file.write("    <key>Product Type</key>\n")
        plist_file.write("    <string>{}</string>\n".format(mbsbackup.Attributes.HardwareModel))
        plist_file.write("    <key>Product Version</key>\n")
        plist_file.write("    <string>6.1.3</string>\n")
        plist_file.write("    <key>Serial Number</key>\n")
        plist_file.write("    <string></string>\n")
        plist_file.write("    <key>Target Identifier</key>\n")
        plist_file.write("    <string></string>\n")
        plist_file.write("    <key>Target Type</key>\n")
        plist_file.write("    <string>Device</string>\n")
        plist_file.write("    <key>Unique Identifier</key>\n")
        plist_file.write("    <string></string>\n")
        plist_file.write("    <key>iTunes Settings</key>\n")
        plist_file.write("    <dict/>\n")
        plist_file.write("    <key>iTunes Version</key>\n")
        plist_file.write("    <string>11.1</string>\n")
        plist_file.write("</dict>\n")
        plist_file.write("</plist>\n")

        plist_file.close()
Ejemplo n.º 7
0
Archivo: iloot.py Proyecto: yike8/iloot
class MobileBackupClient(object):
    def __init__(self, account_settings, dsPrsID, auth, outputFolder):
        mobilebackup_url = account_settings["com.apple.mobileme"][
            "com.apple.Dataclass.Backup"]["url"]
        content_url = account_settings["com.apple.mobileme"][
            "com.apple.Dataclass.Content"]["url"]

        self.mobilebackup_host = re.match("https://(.*):443",
                                          mobilebackup_url).group(1)
        self.content_host = re.match("https://(.*):443", content_url).group(1)
        self.dsPrsID = dsPrsID
        self.headers = {
            "Authorization": auth,
            "X-MMe-Client-Info": Client_Info,
            "User-Agent": USER_AGENT_MOBILE_BACKUP,
            "X-Apple-MBS-Protocol-Version": "1.7"
        }
        self.headers2 = {
            "x-apple-mmcs-proto-version": "3.3",
            "x-apple-mmcs-dataclass": "com.apple.Dataclass.Backup",
            "x-apple-mme-dsid": str(self.dsPrsID),
            "User-Agent": USER_AGENT_BACKUPD,
            "Accept": "application/vnd.com.apple.me.ubchunk+protobuf",
            "Content-Type": "application/vnd.com.apple.me.ubchunk+protobuf",
            "x-mme-client-info": Client_Info_backup
        }
        self.files = {}
        self.outputFolder = outputFolder

    def mobileBackupRequest(self, method, url, msg=None, body=""):
        return probobuf_request(self.mobilebackup_host, method, url, body,
                                self.headers, msg)

    def getAccount(self):
        return self.mobileBackupRequest("GET", "/mbs/%d" % self.dsPrsID,
                                        MBSAccount)

    def getBackup(self, backupUDID):
        return self.mobileBackupRequest(
            "GET", "/mbs/%d/%s" % (self.dsPrsID, backupUDID.encode("hex")),
            MBSBackup)

    def getKeys(self, backupUDID):
        return self.mobileBackupRequest(
            "GET",
            "/mbs/%d/%s/getKeys" % (self.dsPrsID, backupUDID.encode("hex")),
            MBSKeySet)

    def listFiles(self, backupUDID, snapshotId):
        files = self.mobileBackupRequest(
            "GET", "/mbs/%d/%s/%d/listFiles?offset=0&limit=100" %
            (self.dsPrsID, backupUDID.encode("hex"), snapshotId))
        i = 100
        files2 = 1
        while files2:

            files2 = self.mobileBackupRequest(
                "GET", "/mbs/%d/%s/%d/listFiles?offset=%s&limit=100" %
                (self.dsPrsID, backupUDID.encode("hex"), snapshotId, str(i)))
            if files2:
                i = i + 100
                files = files + files2
                print "\tShifting offset: ", i

        #print files

        return decode_protobuf_array(files, MBSFile)

    def getFiles(self, backupUDID, snapshotId, files):
        r = []
        h = {}
        for f in files:
            if f.Size == 0:
                continue
            ff = MBSFile()
            ff.FileID = f.FileID
            h[f.FileID] = f.Signature
            r.append(ff)
            self.files[f.Signature] = f
        body = encode_protobuf_array(r)
        z = self.mobileBackupRequest(
            "POST", "/mbs/%d/%s/%d/getFiles" %
            (self.dsPrsID, backupUDID.encode("hex"), snapshotId), None, body)
        tokens = decode_protobuf_array(z, MBSFileAuthToken)
        z = MBSFileAuthTokens()
        for t in tokens:
            toto = z.tokens.add()
            toto.FileID = h[t.FileID]
            toto.AuthToken = t.AuthToken
        return z

    def authorizeGet(self, tokens, snapshot):
        self.headers2["x-apple-mmcs-auth"] = "%s %s" % (
            tokens.tokens[0].FileID.encode("hex"), tokens.tokens[0].AuthToken)
        body = tokens.SerializeToString()

        filegroups = probobuf_request(self.content_host, "POST",
                                      "/%d/authorizeGet" % self.dsPrsID, body,
                                      self.headers2, FileGroups)
        #print filegroups
        filechunks = {}
        for group in filegroups.file_groups:
            for container_index in xrange(len(group.storage_host_chunk_list)):
                data = self.downloadChunks(
                    group.storage_host_chunk_list[container_index])
                for file_ref in group.file_checksum_chunk_references:
                    if not self.files.has_key(file_ref.file_checksum):
                        continue
                    decrypted_chunks = filechunks.setdefault(
                        file_ref.file_checksum, {})
                    for i in xrange(len(file_ref.chunk_references)):
                        ref = file_ref.chunk_references[i]
                        if ref.container_index == container_index:
                            decrypted_chunks[i] = data[ref.chunk_index]
                    if len(decrypted_chunks) == len(file_ref.chunk_references):
                        f = self.files[file_ref.file_checksum]
                        self.writeFile(f, decrypted_chunks, snapshot)
                        del self.files[file_ref.file_checksum]

        return filegroups

    def getComplete(self, mmcs_auth):
        self.headers2["x-apple-mmcs-auth"] = mmcs_auth
        body = ""
        probobuf_request(self.content_host, "POST",
                         "/%d/getComplete" % self.dsPrsID, body, self.headers2)

    def downloadChunks(self, storage_host):
        headers = {}
        for h in storage_host.host_info.headers:
            headers[h.name] = h.value
        d = probobuf_request(storage_host.host_info.hostname,
                             storage_host.host_info.method,
                             storage_host.host_info.uri, "", headers)
        decrypted = []
        i = 0
        for chunk in storage_host.chunk_info:
            dchunk = decrypt_chunk(d[i:i + chunk.chunk_length],
                                   chunk.chunk_encryption_key,
                                   chunk.chunk_checksum)
            if dchunk:
                decrypted.append(dchunk)
                i += chunk.chunk_length
        return decrypted

    def writeFile(self, f, decrypted_chunks, snapshot):
        if not os.path.exists(
                os.path.join(
                    self.outputFolder,
                    re.sub(r'[:|*<>?"]', "_",
                           "snapshot_" + str(snapshot) + "/" + f.Domain))):
            os.makedirs(
                os.path.join(
                    self.outputFolder,
                    re.sub(r'[:|*<>?"]', "_",
                           "snapshot_" + str(snapshot) + "/" + f.Domain)))

        path = os.path.join(
            self.outputFolder,
            re.sub(
                r'[:|*<>?"]', "_", "snapshot_" + str(snapshot) + "/" +
                f.Domain + "/" + f.RelativePath))
        #print '\t',f
        print '\t', f.Domain, '\t', path
        makedirs(os.path.dirname(path))
        ff = open(path, "wb")
        h = hashlib.sha1()
        for i in xrange(len(decrypted_chunks)):
            d = decrypted_chunks[i]
            h.update(d)
            ff.write(d)
        ff.close()

        #If file is encrypted
        if f.Attributes.EncryptionKey:
            EncryptionKey = f.Attributes.EncryptionKey
            ProtectionClass = struct.unpack(">L", EncryptionKey[0x18:0x1C])[0]
            if ProtectionClass == f.Attributes.ProtectionClass:
                if f.Attributes.EncryptionKeyVersion and f.Attributes.EncryptionKeyVersion == 2:
                    assert self.kb.uuid == EncryptionKey[:0x10]
                    keyLength = struct.unpack(">L",
                                              EncryptionKey[0x20:0x24])[0]
                    assert keyLength == 0x48
                    wrapped_key = EncryptionKey[0x24:]
                else:
                    wrapped_key = EncryptionKey[0x1C:]
                filekey = self.kb.unwrapCurve25519(ProtectionClass,
                                                   wrapped_key)
                if not filekey:
                    print "Failed to unwrap file key for file %s !!!" % f.RelativePath
                else:
                    print "\tfilekey", filekey.encode("hex")
                    self.decryptProtectedFile(path, filekey,
                                              f.Attributes.DecryptedSize)
            else:
                print "\tUnable to decrypt file, possible old backup format", f.RelativePath
                #hexdump(EncryptionKey)

    def decryptProtectedFile(self, path, filekey, DecryptedSize=0):
        ivkey = hashlib.sha1(filekey).digest()[:16]
        h = hashlib.sha1()
        sz = os.path.getsize(path)
        #iOS 5 trailer = uint64 sz + sha1 of encrypted file
        #assert (sz % 0x1000) == 0x1C
        oldpath = path + ".encrypted"
        try:
            os.rename(path, oldpath)
        except:
            pass
        f1 = open(oldpath, "rb")
        f2 = open(path, "wb")
        n = (sz / 0x1000)
        if DecryptedSize:
            n += 1
        for block in xrange(n):
            iv = AESencryptCBC(self.computeIV(block * 0x1000), ivkey)
            data = f1.read(0x1000)
            h.update(data)
            f2.write(AESdecryptCBC(data, filekey, iv))
        if DecryptedSize == 0:  #old iOS 5 format
            trailer = f1.read(0x1C)
            DecryptedSize = struct.unpack(">Q", trailer[:8])[0]
            assert h.digest() == trailer[8:]
        f1.close()
        f2.truncate(DecryptedSize)
        f2.close()

    def computeIV(self, lba):
        iv = ""
        lba &= 0xffffffff
        for _ in xrange(4):
            if (lba & 1):
                lba = 0x80000061 ^ (lba >> 1)
            else:
                lba = lba >> 1
            iv += struct.pack("<L", lba)
        return iv

    def download(self, backupUDID, fast):
        mbsbackup = self.getBackup(backupUDID)
        print "Downloading backup %s" % backupUDID.encode("hex")
        self.outputFolder = os.path.join(self.outputFolder,
                                         backupUDID.encode("hex"))
        makedirs(self.outputFolder)
        #print backup_summary(mbsbackup)
        #print mbsbackup.Snapshot.Attributes.KeybagUUID.encode("hex")
        keys = self.getKeys(backupUDID)
        if not keys or not len(keys.Key):
            print "getKeys FAILED!"
            return
        print "Got OTA Keybag"

        self.kb = Keybag(keys.Key[1].KeyData)
        if not self.kb.unlockBackupKeybagWithPasscode(keys.Key[0].KeyData):
            print "Unable to unlock OTA keybag !"
            return

        print "Available Snapshots: ", mbsbackup.Snapshot.SnapshotID
        for snapshot in xrange(1, mbsbackup.Snapshot.SnapshotID + 1):
            print "Listing snapshot..."
            files = self.listFiles(backupUDID, snapshot)
            print "Files in snapshot %s : %s" % (snapshot, len(files))
            files2 = []
            if fast == 'y':
                for f in files:
                    if 'AddressBook.sqlitedb' in f.RelativePath or 'Calendar.sqlitedb' in f.RelativePath or 'sms.db' in f.RelativePath or 'call_history.db' in f.RelativePath or '.JPG' in f.RelativePath:
                        files2.append(f)
                files = files2
                if len(files):
                    authTokens = self.getFiles(backupUDID, snapshot, files)
                    #print authTokens
                    self.authorizeGet(authTokens, snapshot)

            if fast == 'n':
                if len(files):
                    authTokens = self.getFiles(backupUDID, snapshot, files)
                    #print authTokens
                    self.authorizeGet(authTokens, snapshot)