From 3a0c7ea79aea73c1a4679429bad63f33493131d3 Mon Sep 17 00:00:00 2001 From: malicious <38064672+malicious@users.noreply.github.com> Date: Thu, 10 Nov 2022 15:28:14 +0000 Subject: [PATCH 1/4] Handle cases where Manifest.db's reported size doesn't match - AES padding is up to 16 bytes, so this is the "normal" case - if the original file more than 16 bytes greater, leave it alone (and remove some extra padding that gets added during encryption) - if the original file is _smaller_ than the recorded size, avoid `truncate`, which extends it automatically --- iOSbackup/__init__.py | 59 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/iOSbackup/__init__.py b/iOSbackup/__init__.py index 05273a0..db17837 100644 --- a/iOSbackup/__init__.py +++ b/iOSbackup/__init__.py @@ -976,6 +976,9 @@ def getFileDecryptedCopy(self, relativePath=None, manifestEntry=None, targetName decryptor = AES.new(key, AES.MODE_CBC, b'\x00'*16) + chunkIndex=0 + bytesWritten=0 + finalByteWritten=None # {BACKUP_ROOT}/{UDID}/ae/ae2c3d4e5f6... with open(os.path.join(self.backupRoot, self.udid, fileNameHash[:2], fileNameHash), 'rb') as inFile: @@ -985,18 +988,64 @@ def getFileDecryptedCopy(self, relativePath=None, manifestEntry=None, targetName mappedInFile = mmap.mmap(inFile.fileno(), length=0, prot=mmap.PROT_READ) with open(targetFileName, 'wb') as outFile: - - chunkIndex=0 while True: chunk = mappedInFile[chunkIndex*chunkSize:(chunkIndex+1)*chunkSize] - if len(chunk) == 0: break - outFile.write(decryptor.decrypt(chunk)) + decrypted_chunk = decryptor.decrypt(chunk) + outFile.write(decrypted_chunk) + chunkIndex+=1 + bytesWritten+=len(decrypted_chunk) + finalByteWritten=decrypted_chunk[-1] + + # Compare file sizes across 1) Manifest.db record, 2) original file, and 3) decrypted output + # (decrypted output sometimes adds RFC 1423 padding, aligning data on a 16-byte boundary) + originalSize=os.path.getsize(os.path.join(self.backupRoot, self.udid, fileNameHash[:2], fileNameHash)) + if bytesWritten - info['size'] > 16: + # For more than 16 bytes appended, freak out and do a bunch of logging + print(f"[WARN] Decrypted file size exceeds reported by {bytesWritten - info['size']} bytes, {targetFileName}:\n" + f" - encrypted: {originalSize}\n" + f" - decrypted: {bytesWritten}\n" + f" - reported: {info['size']}") + + # Check if we have a final encryption pass of 16 bytes appended, for some reason + if finalByteWritten == 16: + assert decrypted_chunk[-1] == 16 + assert decrypted_chunk[-2] == 16 + assert decrypted_chunk[-15] == 16 + assert decrypted_chunk[-16] == 16 + print(f"[INFO] File seems to have had 16 bytes of padding appended, removing") + print() + outFile.truncate(bytesWritten - finalByteWritten) + else: + print(f"[DEBUG] Found a wrongly-sized file with weird padding, you should flip out (final byte: {finalByteWritten})") + + if bytesWritten - info['size'] == 16: + # TODO: Merge this cleanly into the above case + assert finalByteWritten == 16 + #print(f"[INFO] Decrypted file size exceeds reported by exactly 16 bytes, {targetFileName}:") + outFile.truncate(bytesWritten - 16) + + if bytesWritten - info['size'] < 16 and bytesWritten - info['size'] > 0: + # This is the "normal" case, where we added a few bytes of extra padding + #print(f"[WARN] Final byte says padding is {finalByteWritten} bytes, file size % 16 = {bytesWritten % 16}, removing that many bytes from end of file") + outFile.truncate(bytesWritten - finalByteWritten) + + if bytesWritten - info['size'] < 0: + # For an over-reported size, do nothing because we can't conjure data from nowhere + print(f"[WARN] Recorded file size in Manifest.db is greater than actual bytes written ({bytesWritten} > {info['size']}): {targetFileName}") + # Still, check if the last 16 bytes looks like padding + if finalByteWritten <= 16: + print(f" Final recorded byte is {finalByteWritten}, assuming this is encryption-related padding and removing those bytes") + if finalByteWritten == 16: + assert decrypted_chunk[-1] == 16 + assert decrypted_chunk[-16] == 16 + outFile.truncate(bytesWritten - finalByteWritten) + else: + print(f"[DEBUG] Found a wrongly-sized file with weird padding, you should flip out (final byte: {finalByteWritten})") - outFile.truncate(info['size']) elif info['isFolder']: # Plain folder Path(targetFileName).mkdir(parents=True, exist_ok=True) From 2ba2a3b5c0cc9ab00b835ea273078bec21df35cb Mon Sep 17 00:00:00 2001 From: malicious <38064672+malicious@users.noreply.github.com> Date: Thu, 10 Nov 2022 15:34:04 +0000 Subject: [PATCH 2/4] Remove debug prints in file truncation code --- iOSbackup/__init__.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/iOSbackup/__init__.py b/iOSbackup/__init__.py index db17837..4705cf8 100644 --- a/iOSbackup/__init__.py +++ b/iOSbackup/__init__.py @@ -1004,47 +1004,29 @@ def getFileDecryptedCopy(self, relativePath=None, manifestEntry=None, targetName # (decrypted output sometimes adds RFC 1423 padding, aligning data on a 16-byte boundary) originalSize=os.path.getsize(os.path.join(self.backupRoot, self.udid, fileNameHash[:2], fileNameHash)) if bytesWritten - info['size'] > 16: - # For more than 16 bytes appended, freak out and do a bunch of logging - print(f"[WARN] Decrypted file size exceeds reported by {bytesWritten - info['size']} bytes, {targetFileName}:\n" - f" - encrypted: {originalSize}\n" - f" - decrypted: {bytesWritten}\n" - f" - reported: {info['size']}") - # Check if we have a final encryption pass of 16 bytes appended, for some reason if finalByteWritten == 16: assert decrypted_chunk[-1] == 16 assert decrypted_chunk[-2] == 16 assert decrypted_chunk[-15] == 16 assert decrypted_chunk[-16] == 16 - print(f"[INFO] File seems to have had 16 bytes of padding appended, removing") - print() outFile.truncate(bytesWritten - finalByteWritten) - else: - print(f"[DEBUG] Found a wrongly-sized file with weird padding, you should flip out (final byte: {finalByteWritten})") if bytesWritten - info['size'] == 16: - # TODO: Merge this cleanly into the above case assert finalByteWritten == 16 - #print(f"[INFO] Decrypted file size exceeds reported by exactly 16 bytes, {targetFileName}:") outFile.truncate(bytesWritten - 16) if bytesWritten - info['size'] < 16 and bytesWritten - info['size'] > 0: # This is the "normal" case, where we added a few bytes of extra padding - #print(f"[WARN] Final byte says padding is {finalByteWritten} bytes, file size % 16 = {bytesWritten % 16}, removing that many bytes from end of file") outFile.truncate(bytesWritten - finalByteWritten) if bytesWritten - info['size'] < 0: # For an over-reported size, do nothing because we can't conjure data from nowhere - print(f"[WARN] Recorded file size in Manifest.db is greater than actual bytes written ({bytesWritten} > {info['size']}): {targetFileName}") # Still, check if the last 16 bytes looks like padding - if finalByteWritten <= 16: - print(f" Final recorded byte is {finalByteWritten}, assuming this is encryption-related padding and removing those bytes") if finalByteWritten == 16: assert decrypted_chunk[-1] == 16 assert decrypted_chunk[-16] == 16 outFile.truncate(bytesWritten - finalByteWritten) - else: - print(f"[DEBUG] Found a wrongly-sized file with weird padding, you should flip out (final byte: {finalByteWritten})") elif info['isFolder']: # Plain folder From 6f5fb24fa3ba01672ac2bebe5fe26b0c79405f71 Mon Sep 17 00:00:00 2001 From: malicious <38064672+malicious@users.noreply.github.com> Date: Thu, 10 Nov 2022 15:44:53 +0000 Subject: [PATCH 3/4] Standardize final-byte checking --- iOSbackup/__init__.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/iOSbackup/__init__.py b/iOSbackup/__init__.py index 4705cf8..059a076 100644 --- a/iOSbackup/__init__.py +++ b/iOSbackup/__init__.py @@ -1000,32 +1000,36 @@ def getFileDecryptedCopy(self, relativePath=None, manifestEntry=None, targetName bytesWritten+=len(decrypted_chunk) finalByteWritten=decrypted_chunk[-1] + def has_aes_padding(expectedPaddingSize): + """Checks the content of the last N bytes, which should be filled with 'N' repeating""" + if finalByteWritten != expectedPaddingSize: + return False + + # Checks each byte by counting occurrences + potential_padding = decrypted_chunk[-finalByteWritten:] + return potential_padding.count(finalByteWritten) == finalByteWritten + # Compare file sizes across 1) Manifest.db record, 2) original file, and 3) decrypted output # (decrypted output sometimes adds RFC 1423 padding, aligning data on a 16-byte boundary) originalSize=os.path.getsize(os.path.join(self.backupRoot, self.udid, fileNameHash[:2], fileNameHash)) if bytesWritten - info['size'] > 16: # Check if we have a final encryption pass of 16 bytes appended, for some reason - if finalByteWritten == 16: - assert decrypted_chunk[-1] == 16 - assert decrypted_chunk[-2] == 16 - assert decrypted_chunk[-15] == 16 - assert decrypted_chunk[-16] == 16 + if has_aes_padding(16): outFile.truncate(bytesWritten - finalByteWritten) if bytesWritten - info['size'] == 16: - assert finalByteWritten == 16 - outFile.truncate(bytesWritten - 16) + if has_aes_padding(16): + outFile.truncate(bytesWritten - 16) if bytesWritten - info['size'] < 16 and bytesWritten - info['size'] > 0: # This is the "normal" case, where we added a few bytes of extra padding - outFile.truncate(bytesWritten - finalByteWritten) + if has_aes_padding(bytesWritten - info['size']): + outFile.truncate(bytesWritten - finalByteWritten) if bytesWritten - info['size'] < 0: # For an over-reported size, do nothing because we can't conjure data from nowhere # Still, check if the last 16 bytes looks like padding - if finalByteWritten == 16: - assert decrypted_chunk[-1] == 16 - assert decrypted_chunk[-16] == 16 + if has_aes_padding(16): outFile.truncate(bytesWritten - finalByteWritten) elif info['isFolder']: From e513764f642169d5cb7243d4d257b2a378f85080 Mon Sep 17 00:00:00 2001 From: malicious <38064672+malicious@users.noreply.github.com> Date: Thu, 10 Nov 2022 15:48:01 +0000 Subject: [PATCH 4/4] Convert variables to camelCase for consistency in the file --- iOSbackup/__init__.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/iOSbackup/__init__.py b/iOSbackup/__init__.py index 059a076..9e2a5ef 100644 --- a/iOSbackup/__init__.py +++ b/iOSbackup/__init__.py @@ -978,6 +978,7 @@ def getFileDecryptedCopy(self, relativePath=None, manifestEntry=None, targetName chunkIndex=0 bytesWritten=0 + decryptedChunk=None finalByteWritten=None # {BACKUP_ROOT}/{UDID}/ae/ae2c3d4e5f6... @@ -993,43 +994,43 @@ def getFileDecryptedCopy(self, relativePath=None, manifestEntry=None, targetName if len(chunk) == 0: break - decrypted_chunk = decryptor.decrypt(chunk) - outFile.write(decrypted_chunk) + decryptedChunk = decryptor.decrypt(chunk) + outFile.write(decryptedChunk) chunkIndex+=1 - bytesWritten+=len(decrypted_chunk) - finalByteWritten=decrypted_chunk[-1] + bytesWritten+=len(decryptedChunk) + finalByteWritten=decryptedChunk[-1] - def has_aes_padding(expectedPaddingSize): + def hasAesPadding(expectedPaddingSize): """Checks the content of the last N bytes, which should be filled with 'N' repeating""" if finalByteWritten != expectedPaddingSize: return False # Checks each byte by counting occurrences - potential_padding = decrypted_chunk[-finalByteWritten:] - return potential_padding.count(finalByteWritten) == finalByteWritten + potentialPadding = decryptedChunk[-finalByteWritten:] + return potentialPadding.count(finalByteWritten) == finalByteWritten # Compare file sizes across 1) Manifest.db record, 2) original file, and 3) decrypted output # (decrypted output sometimes adds RFC 1423 padding, aligning data on a 16-byte boundary) originalSize=os.path.getsize(os.path.join(self.backupRoot, self.udid, fileNameHash[:2], fileNameHash)) if bytesWritten - info['size'] > 16: # Check if we have a final encryption pass of 16 bytes appended, for some reason - if has_aes_padding(16): + if hasAesPadding(16): outFile.truncate(bytesWritten - finalByteWritten) if bytesWritten - info['size'] == 16: - if has_aes_padding(16): + if hasAesPadding(16): outFile.truncate(bytesWritten - 16) if bytesWritten - info['size'] < 16 and bytesWritten - info['size'] > 0: # This is the "normal" case, where we added a few bytes of extra padding - if has_aes_padding(bytesWritten - info['size']): + if hasAesPadding(bytesWritten - info['size']): outFile.truncate(bytesWritten - finalByteWritten) if bytesWritten - info['size'] < 0: # For an over-reported size, do nothing because we can't conjure data from nowhere # Still, check if the last 16 bytes looks like padding - if has_aes_padding(16): + if hasAesPadding(16): outFile.truncate(bytesWritten - finalByteWritten) elif info['isFolder']: