2

With the requirement set by Apple and MDM for force encryption on backups, is there a way to decrypt and encrypt iPhone backups?

We need access to the raw files that are saved within the backup to make slight changes to a few plist files inside, but the problem is that we need to do this with in code as doing this by hand will take forever with 1000+ phones. I have most of my code written in python and editing the plist seems to be working just fine until we put on the mdm profile resulting in the phone requiring encrypted backups from that point on.

Here is what I found so far: but unable to make this as 10> change the file from a mbdb to a db file. Reading from others, it also seems to that that might have change how the files are encrypted. Any help or update on this would be very helpful!

#!/usr/bin/env python2.7
# coding: UTF-8

# default to True to avoid leaking secrets
ANONYMIZE_OUTPUT = True

import PBKDF2 # http://iphone-dataprotection.googlecode.com/hg-history/tip/python_scripts/crypto/PBKDF2.py
import bplist # https://github.com/farcaller/bplist-python/raw/master/bplist.py
import Crypto.Cipher.AES # https://www.dlitz.net/software/pycrypto/

import hashlib
import os.path
import pprint
import sys

BACKUP_DIR = "data/encrypted"

def main():
    with open(os.path.join(BACKUP_DIR, 'Manifest.plist'), 'rb') as infile:
        manifest_plist = bplist.BPlistReader.plistWithString(infile.read())
    keybag = Keybag(manifest_plist['BackupKeyBag'])
    # the actual keys are unknown, but the wrapped keys are known
    keybag.printClassKeys()
    if not keybag.unlockWithPasscode('test'):
        raise Exception('Could not unlock keybag; bad password?')
    # now the keys are known too
    keybag.printClassKeys()

    for item in process_mbdb_file(
            os.path.join(BACKUP_DIR, 'Manifest.mbdb')).values():
        filename = item['filename']
        if not filename.endswith('calculator.plist'):
            continue
        encryption_key = item['unknown1'][4:]
        protection_class = item['flag']
        backup_filename = os.path.join(
            BACKUP_DIR,
            hashlib.sha1(item['domain'] + '-' + item['filename']).hexdigest())
        with open(backup_filename, 'rb') as infile:
            data = infile.read()
        print '== encrypted data:'
        print wrap(data)
        print

        key = keybag.unwrapKeyForClass(protection_class, encryption_key)
        # truncate to actual length, because encryption may introduce padding
        decrypted_data = AESdecryptCBC(data, key)[:item['filelen']]
        print '== decrypted data:'
        print wrap(decrypted_data)
        print

        print '== pretty-printed calculator preferences'
        pprint.pprint(bplist.BPlistReader.plistWithString(decrypted_data))

##
# this section is mostly copied from parts of iphone-dataprotection
# http://code.google.com/p/iphone-dataprotection/

import struct

CLASSKEY_TAGS = ["CLAS","WRAP","WPKY", "KTYP", "PBKY"]  #UUID
KEYBAG_TYPES = ["System", "Backup", "Escrow", "OTA (icloud)"]
KEY_TYPES = ["AES", "Curve25519"]
PROTECTION_CLASSES={
    1:"NSFileProtectionComplete",
    2:"NSFileProtectionCompleteUnlessOpen",
    3:"NSFileProtectionCompleteUntilFirstUserAuthentication",
    4:"NSFileProtectionNone",
    5:"NSFileProtectionRecovery?",

    6: "kSecAttrAccessibleWhenUnlocked",
    7: "kSecAttrAccessibleAfterFirstUnlock",
    8: "kSecAttrAccessibleAlways",
    9: "kSecAttrAccessibleWhenUnlockedThisDeviceOnly",
    10: "kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly",
    11: "kSecAttrAccessibleAlwaysThisDeviceOnly"
}
WRAP_DEVICE = 1
WRAP_PASSCODE = 2

class Keybag(object):
    def __init__(self, data):
        self.type = None
        self.uuid = None
        self.wrap = None
        self.deviceKey = None
        self.attrs = {}
        self.classKeys = {}
        self.KeyBagKeys = None #DATASIGN blob
        self.parseBinaryBlob(data)

    def parseBinaryBlob(self, data):
        currentClassKey = None

        for tag, data in loopTLVBlocks(data):
            if len(data) == 4:
                data = struct.unpack(">L", data)[0]
            if tag == "TYPE":
                self.type = data
                if self.type > 3:
                    print "FAIL: keybag type > 3 : %d" % self.type
            elif tag == "UUID" and self.uuid is None:
                self.uuid = data
            elif tag == "WRAP" and self.wrap is None:
                self.wrap = data
            elif tag == "UUID":
                if currentClassKey:
                    self.classKeys[currentClassKey["CLAS"]] = currentClassKey
                currentClassKey = {"UUID": data}
            elif tag in CLASSKEY_TAGS:
                currentClassKey[tag] = data
            else:
                self.attrs[tag] = data
        if currentClassKey:
            self.classKeys[currentClassKey["CLAS"]] = currentClassKey

    def unlockWithPasscode(self, passcode):
        passcodekey = PBKDF2.PBKDF2(passcode, self.attrs["SALT"],
                                    iterations=self.attrs["ITER"]).read(32)
        for classkey in self.classKeys.values():
            if not classkey.has_key("WPKY"):
                continue
            k = classkey["WPKY"]
            if classkey["WRAP"] & WRAP_PASSCODE:
                k = AESUnwrap(passcodekey, classkey["WPKY"])
                if not k:
                    return False
                classkey["KEY"] = k
        return True

    def unwrapKeyForClass(self, protection_class, persistent_key):
        ck = self.classKeys[protection_class]["KEY"]
        if len(persistent_key) != 0x28:
            raise Exception("Invalid key length")
        return AESUnwrap(ck, persistent_key)

    def printClassKeys(self):
        print "== Keybag"
        print "Keybag type: %s keybag (%d)" % (KEYBAG_TYPES[self.type], self.type)
        print "Keybag version: %d" % self.attrs["VERS"]
        print "Keybag iterations: %d, iv=%s" % (
            self.attrs["ITER"], anonymize(self.attrs["SALT"].encode('hex')))
        print "Keybag UUID: %s" % anonymize(self.uuid.encode("hex"))
        print "-"*209
        print "".join(["Class".ljust(53),
                      "WRAP".ljust(5),
                      "Type".ljust(11),
                      "Key".ljust(65),
                      "WPKY".ljust(65),
                      "Public key"])
        print "-"*208
        for k, ck in self.classKeys.items():
            if k == 6: print ""
            print "".join(
                [PROTECTION_CLASSES.get(k).ljust(53),
                 str(ck.get("WRAP","")).ljust(5),
                 KEY_TYPES[ck.get("KTYP",0)].ljust(11),
                 anonymize(ck.get("KEY", "").encode("hex")).ljust(65),
                 anonymize(ck.get("WPKY", "").encode("hex")).ljust(65),
                 ck.get("PBKY", "").encode("hex")])
        print

def loopTLVBlocks(blob):
    i = 0
    while i + 8 <= len(blob):
        tag = blob[i:i+4]
        length = struct.unpack(">L",blob[i+4:i+8])[0]
        data = blob[i+8:i+8+length]
        yield (tag,data)
        i += 8 + length

def unpack64bit(s):
    return struct.unpack(">Q",s)[0]
def pack64bit(s):
    return struct.pack(">Q",s)

def AESUnwrap(kek, wrapped):
    C = []
    for i in xrange(len(wrapped)/8):
        C.append(unpack64bit(wrapped[i*8:i*8+8]))
    n = len(C) - 1
    R = [0] * (n+1)
    A = C[0]

    for i in xrange(1,n+1):
        R[i] = C[i]

    for j in reversed(xrange(0,6)):
        for i in reversed(xrange(1,n+1)):
            todec = pack64bit(A ^ (n*j+i))
            todec += pack64bit(R[i])
            B = Crypto.Cipher.AES.new(kek).decrypt(todec)
            A = unpack64bit(B[:8])
            R[i] = unpack64bit(B[8:])

    if A != 0xa6a6a6a6a6a6a6a6:
        return None
    res = "".join(map(pack64bit, R[1:]))
    return res

ZEROIV = "\x00"*16
def AESdecryptCBC(data, key, iv=ZEROIV, padding=False):
    if len(data) % 16:
        print "AESdecryptCBC: data length not /16, truncating"
        data = data[0:(len(data)/16) * 16]
    data = Crypto.Cipher.AES.new(key, Crypto.Cipher.AES.MODE_CBC, iv).decrypt(data)
    if padding:
        return removePadding(16, data)
    return data

##
# this .mbdb-parsing code is from http://stackoverflow.com/q/3085153/14558:

def getint(data, offset, intsize):
    """Retrieve an integer (big-endian) and new offset from the current offset"""
    value = 0
    while intsize > 0:
        value = (value<<8) + ord(data[offset])
        offset = offset + 1
        intsize = intsize - 1
    return value, offset

def getstring(data, offset):
    """Retrieve a string and new offset from the current offset into the data"""
    if data[offset] == chr(0xFF) and data[offset+1] == chr(0xFF):
        return '', offset+2 # Blank string
    length, offset = getint(data, offset, 2) # 2-byte length
    value = data[offset:offset+length]
    return value, (offset + length)

def process_mbdb_file(filename):
    mbdb = {} # Map offset of info in this file => file info
    data = open(filename).read()
    if data[0:4] != "mbdb": raise Exception("This does not look like an MBDB file")
    offset = 4
    offset = offset + 2 # value x05 x00, not sure what this is
    while offset < len(data):
        fileinfo = {}
        fileinfo['start_offset'] = offset
        fileinfo['domain'], offset = getstring(data, offset)
        fileinfo['filename'], offset = getstring(data, offset)
        fileinfo['linktarget'], offset = getstring(data, offset)
        fileinfo['datahash'], offset = getstring(data, offset)
        fileinfo['unknown1'], offset = getstring(data, offset)
        fileinfo['mode'], offset = getint(data, offset, 2)
        fileinfo['unknown2'], offset = getint(data, offset, 4)
        fileinfo['unknown3'], offset = getint(data, offset, 4)
        fileinfo['userid'], offset = getint(data, offset, 4)
        fileinfo['groupid'], offset = getint(data, offset, 4)
        fileinfo['mtime'], offset = getint(data, offset, 4)
        fileinfo['atime'], offset = getint(data, offset, 4)
        fileinfo['ctime'], offset = getint(data, offset, 4)
        fileinfo['filelen'], offset = getint(data, offset, 8)
        fileinfo['flag'], offset = getint(data, offset, 1)
        fileinfo['numprops'], offset = getint(data, offset, 1)
        fileinfo['properties'] = {}
        for ii in range(fileinfo['numprops']):
            propname, offset = getstring(data, offset)
            propval, offset = getstring(data, offset)
            fileinfo['properties'][propname] = propval
        mbdb[fileinfo['start_offset']] = fileinfo
    return mbdb

##
# and here are some utility functions, one making sure I don’t leak my
# secret keys when posting the output on Stack Exchange

if ANONYMIZE_OUTPUT:
    memo = {}
    def anonymize(s):
        global memo
        if s in memo:
            return memo[s]
        import random
        import string
        r = random.Random(0)
        possible_alphabets = [
            string.digits,
            string.digits + 'abcdef',
            "".join(chr(x) for x in range(0, 256)),
        ]
        for a in possible_alphabets:
            if all(c in a for c in s):
                alphabet = a
                break
        ret = "".join([r.choice(alphabet) for i in range(len(s))])
        memo[s] = ret
        return ret
else:
    def anonymize(s): return s

def wrap(s, width=78):
    "Return a width-wrapped repr(s)-like string without breaking on \’s"
    s = repr(s)
    quote = s[0]
    s = s[1:-1]
    ret = []
    while len(s):
        i = s.rfind('\\', 0, width)
        if i <= width - 4: # "\x??" is four characters
            i = width
        ret.append(s[:i])
        s = s[i:]
    return '\n'.join("%s%s%s" % (quote, line ,quote) for line in ret)

if __name__ == '__main__':
    main()
Travis
  • 41
  • 6

0 Answers0