forked from FloatingOctothorpe/dump_android_backup
-
Notifications
You must be signed in to change notification settings - Fork 0
/
dump_android_backup.py
199 lines (159 loc) · 8.06 KB
/
dump_android_backup.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
#!/usr/bin/env python
"""Extract Android backup files
Read and extract a tar file from an Android backup file. If the file is
encrypted, a password will be required to decrypt the file.
"""
import argparse
import getpass
import hashlib
import io
import logging
import sys
import zlib
import pyaes
__version__ = '0.1.0'
__author__ = 'Floating Octothorpe'
PBKDF2_KEY_SIZE = 32
class AndroidBackupParseError(Exception):
"""Exception raised file parsing an android backup file"""
pass
def to_utf8_bytes(input_bytes):
"""Emulate bytes being converted into a "UTF8 byte array"
For more info see the Bouncy Castle Crypto package Strings.toUTF8ByteArray
method:
https://github.com/bcgit/bc-java/blob/master/core/src/main/java/org/bouncycastle/util/Strings.java#L142
"""
output = []
for byte in input_bytes:
if byte < ord(b'\x80'):
output.append(byte)
else:
output.append(ord('\xef') | (byte >> 12))
output.append(ord('\xbc') | ((byte >> 6) & ord('\x3f')))
output.append(ord('\x80') | (byte & ord('\x3f')))
return bytes(output)
def decrypt_master_key_blob(key, aes_iv, cipher_text):
"""Decrypt the master key blob with AES"""
aes = pyaes.AESModeOfOperationCBC(key, aes_iv)
plain_text = b''
while len(plain_text) < len(cipher_text):
offset = len(plain_text)
plain_text += aes.decrypt(cipher_text[offset:(offset + 16)])
blob = io.BytesIO(plain_text)
master_iv_length = ord(blob.read(1))
master_iv = blob.read(master_iv_length)
master_key_length = ord(blob.read(1))
master_key = blob.read(master_key_length)
master_key_checksum_length = ord(blob.read(1))
master_key_checksum = blob.read(master_key_checksum_length)
return master_iv, master_key, master_key_checksum
def check_header(backup_file, password=None):
"""Extract and validate the backup header"""
header = {}
with open(backup_file, 'rb') as backup:
if backup.readline() != b'ANDROID BACKUP\n':
raise AndroidBackupParseError('Unrecognised file format!')
header['format_version'] = int(backup.readline())
header['compression_version'] = int(backup.readline())
header['encryption'] = backup.readline().decode('utf-8').strip()
header['payload_offset'] = backup.tell()
if header['format_version'] > 4:
raise AndroidBackupParseError('Unsupported format version, \
only version 1-4 is supported')
if header['compression_version'] != 1:
raise AndroidBackupParseError('Unsupported compression version, \
only version 1 is supported')
if not header['encryption'] in ['none', 'AES-256']:
raise AndroidBackupParseError('Unsupported encryption scheme: %s' %
header['encryption'])
logging.debug('Format version: %d', header['format_version'])
logging.debug('Compression version: %d', header['compression_version'])
logging.debug('Encryption algorithm: %s', header['encryption'])
if header['encryption'] == 'AES-256':
if not password:
password = getpass.getpass()
header['user_salt'] = bytes.fromhex(backup.readline().decode('utf-8').strip())
header['checksum_salt'] = bytes.fromhex(backup.readline().decode('utf-8').strip())
header['pbkdf2_rounds'] = int(backup.readline())
header['user_iv'] = bytes.fromhex(backup.readline().decode('utf-8').strip())
header['master_key_blob'] = bytes.fromhex(backup.readline().decode('utf-8').strip())
header['payload_offset'] = backup.tell()
logging.debug('User password salt: %s', header['user_salt'].hex().upper())
logging.debug('Master key checksum salt: %s', header['checksum_salt'].hex().upper())
logging.debug('PBKDF2 rounds: %d', header['pbkdf2_rounds'])
logging.debug('IV of the user key: %s', header['user_iv'].hex().upper())
logging.debug('Master key blob: %s', header['master_key_blob'].hex().upper())
key = hashlib.pbkdf2_hmac('sha1', password.encode('utf-8'),
header['user_salt'],
header['pbkdf2_rounds'], PBKDF2_KEY_SIZE)
logging.debug('User key bytes: %s', key.hex().upper())
try:
header['master_iv'], header['master_key'], header['master_key_checksum'] = \
decrypt_master_key_blob(key, header['user_iv'], header['master_key_blob'])
except TypeError:
raise AndroidBackupParseError('Invalid decryption password')
# v2 plus needs utf-8 byte array
if header['format_version'] > 1:
hmac_mk = to_utf8_bytes(header['master_key'])
else:
hmac_mk = header['master_key']
calculated_checksum = hashlib.pbkdf2_hmac('sha1', hmac_mk,
header['checksum_salt'],
header['pbkdf2_rounds'],
PBKDF2_KEY_SIZE)
if not header['master_key_checksum'] == calculated_checksum:
raise AndroidBackupParseError('Invalid decryption password')
logging.debug('Master key IV: %s', header['master_iv'].hex().upper())
logging.debug('Master key: %s', header['master_key'].hex().upper())
logging.debug('Master key checksum: %s', header['master_key_checksum'].hex().upper())
return header
def extract_backup(backup_file, output, password):
"""Extract a tar file from an Android backup file."""
try:
header = check_header(backup_file, password)
except (FileNotFoundError, AndroidBackupParseError) as error:
logging.error(error)
raise error
with open(backup_file, 'rb') as backup:
logging.debug('moving to payload offset (%d bytes)', header['payload_offset'])
backup.seek(header['payload_offset'])
with open(output, 'wb') as output_file:
if header['encryption'] == 'AES-256':
decrypter = pyaes.Decrypter(pyaes.AESModeOfOperationCBC(header['master_key'],
header['master_iv']))
data = b''
chunk = backup.read(1000000)
while chunk:
data += decrypter.feed(chunk)
chunk = backup.read(1000000)
data = data + decrypter.feed()
else:
data = backup.read()
output_file.write(zlib.decompress(data))
logging.info('Successfully written data to "%s"', output)
def main():
"""Parse arguments and try to extract an Android backup"""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('backup_file', metavar='BACKUP_FILE',
help='Android backup file to extract')
parser.add_argument('output_file', metavar='OUTPUT_FILE',
help='File to write the extracted tar file to')
parser.add_argument('-p', '--password', dest='password', metavar='PASSWORD',
help='Password to decrypt Android backup')
parser.add_argument('-d', '--debug', dest='debug', action='store_true',
help='Enable debug messages')
options = parser.parse_args()
logging.basicConfig(stream=sys.stderr, level=logging.INFO,
format='[%(levelname)s]: %(message)s')
if options.debug:
logging.getLogger().setLevel(logging.DEBUG)
logging.debug('called with arguments: %s', vars(options))
if options.backup_file == options.output_file:
logging.error('The input and output file cannot be the same!')
sys.exit(1)
try:
extract_backup(options.backup_file, options.output_file, options.password)
except (FileNotFoundError, AndroidBackupParseError):
sys.exit(1)
if __name__ == '__main__':
main()