-
Notifications
You must be signed in to change notification settings - Fork 25
/
iChainbreaker.py
271 lines (210 loc) · 8.77 KB
/
iChainbreaker.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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
import os
import string
import uuid
import argparse
from getpass import getpass
from binascii import hexlify
import sys
from itemv7 import ItemV7
from keybag import Keybag
from blobparser import BlobParser
import sqlite3 as lite
from hexdump import hexdump
from exportDB import ExporySQLiteDB
from crypto.aeswrap import AESUnwrap
from Crypto.Cipher import AES
#from crypto.gcm import gcm_decrypt
from crypto.aes import AESdecryptCBC
from ctypes import *
class _EncryptedBlobHeader(LittleEndianStructure):
_fields_ = [
('version', c_uint32),
('clas', c_uint32),
('length', c_uint32)
]
def _memcpy(buf, fmt):
return cast(c_char_p(buf), POINTER(fmt)).contents
def GetTableFullName(table):
if table == 'genp':
return 'Generic Password'
elif table == 'inet':
return 'Internet Password'
elif table == 'cert':
return 'Certification'
elif table == 'keys':
return 'Keys'
else:
return 'Unknown'
def main():
parser = argparse.ArgumentParser(description='Tool for Local Items (iCloud) Keychain Analysis by @n0fate')
parser.add_argument('-p', '--path', nargs=1, help='iCloud Keychain Path(~/Library/Keychains/[UUID]/)', required=True)
parser.add_argument('-k', '--key', help='User Password (optional, will ask via stdin if not provided)', required=False)
parser.add_argument('-x', '--exportfile', nargs=1, help='Write a decrypted contents to SQLite file (optional)', required=False)
parser.add_argument('-v', '--version', nargs=1, help='macOS version(ex. 10.13)', required=True)
args = parser.parse_args()
Pathoficloudkeychain = args.path[0]
if os.path.isdir(Pathoficloudkeychain) is False:
print '[!] Path is not directory'
parser.print_help()
sys.exit()
if os.path.exists(Pathoficloudkeychain) is False:
print '[!] Path is not exists'
parser.print_help()
sys.exit()
# version check
import re
gcmIV = ''
re1='(10)' # Integer Number 1
re2='(\\.)' # Any Single Character 1
re3='(\\d+)' # Integer Number 2
rg = re.compile(re1+re2+re3,re.IGNORECASE|re.DOTALL)
m = rg.match(args.version[0])
if m:
minorver = m.group(3)
if minorver >= 12:
# Security-57740.51.3/OSX/sec/securityd/SecDbKeychainItem.c:97
#
# // echo "keychainblobstaticiv" | openssl dgst -sha256 | cut -c1-24 | xargs -I {} echo "0x{}" | xxd -r | xxd -p -i
# static const uint8_t gcmIV[kIVSizeAESGCM] = {
# 0x1e, 0xa0, 0x5c, 0xa9, 0x98, 0x2e, 0x87, 0xdc, 0xf1, 0x45, 0xe8, 0x24
# };
gcmIV = '\x1e\xa0\x5c\xa9\x98\x2e\x87\xdc\xf1\x45\xe8\x24'
else:
gcmIV = ''
else:
print '[!] Invalid version'
parser.print_help()
sys.exit()
export = 0
if args.exportfile is not None:
if os.path.exists(args.exportfile[0]):
print '[*] Export DB File is exists.'
sys.exit()
export = 1
# Start to analysis
print 'Tool for iCloud Keychain Analysis by @n0fate'
MachineUUID = os.path.basename(os.path.normpath(Pathoficloudkeychain))
PathofKeybag = os.path.join(Pathoficloudkeychain, 'user.kb')
PathofKeychain = os.path.join(Pathoficloudkeychain, 'keychain-2.db')
print '[*] macOS version is %s'%args.version[0]
print '[*] UUID : %s'%MachineUUID
print '[*] Keybag : %s'%PathofKeybag
print '[*] iCloud Keychain File : %s'%PathofKeychain
if os.path.exists(PathofKeybag) is False or os.path.exists(PathofKeychain) is False:
print '[!] Can not found KeyBag or iCloud Keychain File'
sys.exit()
keybag = Keybag(PathofKeybag)
keybag.load_keybag_header()
keybag.debug_print_header()
devicekey = keybag.device_key_init(uuid.UUID(MachineUUID).bytes)
print '[*] The Device key : %s'%hexlify(devicekey)
bresult = keybag.device_key_validation()
if bresult == False:
print '[!] Device Key validation : Failed. Maybe Invalid PlatformUUID'
return
else:
print '[*] Device Key validation : Pass'
passcodekey = keybag.generatepasscodekey(args.key if args.key else getpass())
print '[*] The passcode key : %s'%hexlify(passcodekey)
keybag.Decryption()
con = lite.connect(PathofKeychain)
con.text_factory = str
cur = con.cursor()
tablelist = ['genp', 'inet', 'cert', 'keys']
if export:
# Create DB
exportDB = ExporySQLiteDB()
exportDB.createDB(args.exportfile[0])
print '[*] Export DB Name : %s'%args.exportfile[0]
else:
exportDB = None
for tablename in tablelist:
if export is not 1:
print '[+] Table Name : %s'%GetTableFullName(tablename)
try:
cur.execute("SELECT data FROM %s"%tablename)
except lite.OperationalError:
continue
if export:
# Get Table Schema
sql = con.execute("pragma table_info('%s')"%tablename).fetchall()
# Create a table
exportDB.createTable(tablename, sql)
for data, in cur:
encblobheader = _memcpy(data[:sizeof(_EncryptedBlobHeader)], _EncryptedBlobHeader)
if encblobheader.version == 7:
item = ItemV7(data)
key = keybag.GetKeybyClass(item.keyclass)
decrypted = item.decrypt_secret_data(key)
metadatakey_row = con.execute('select data from metadatakeys where keyclass=?', (item.keyclass,)).fetchone()
metadatakey = metadatakey_row[0]
unwrapped_metadata_key = AESUnwrap(key, metadatakey)
metadata = item.decrypt_metadata(unwrapped_metadata_key)
if export is 0:
print '[+] DECRYPTED METADATA'
handle_decrypted(metadata, export, tablename)
print '[+] DECRYPTED SECRET INFO'
handle_decrypted(decrypted, export, tablename)
elif export is 1:
blobparse = BlobParser()
record = blobparse.ParseIt(metadata, tablename, export)
record.update(blobparse.ParseIt(decrypted, tablename, export))
else:
encblobheader.clas &= 0x0F
wrappedkey = data[sizeof(_EncryptedBlobHeader):sizeof(_EncryptedBlobHeader)+encblobheader.length]
if encblobheader.clas == 11:
encrypted_data = data[sizeof(_EncryptedBlobHeader)+encblobheader.length:]
auth_tag = data[-20:-4]
else:
encrypted_data = data[sizeof(_EncryptedBlobHeader)+encblobheader.length:-16]
auth_tag = data[-16:]
key = keybag.GetKeybyClass(encblobheader.clas)
if key == '':
print '[!] Could not found any key at %d'%encblobheader.clas
continue
unwrappedkey = AESUnwrap(key, wrappedkey)
if unwrappedkey is None:
continue
gcm = AES.new(unwrappedkey, AES.MODE_GCM, gcmIV)
decrypted = gcm.decrypt_and_verify(encrypted_data, auth_tag)
#decrypted = gcm_decrypt(unwrappedkey, gcmIV, encrypted_data, data[:sizeof(_EncryptedBlobHeader)], auth_tag)
if len(decrypted) is 0:
#print(" [-] Decryption Process Failed. Invalid Key or Data is corrupted.")
continue
if export is 0:
print '[+] DECRYPTED INFO'
handle_decrypted(decrypted)
blobparse = BlobParser()
record = blobparse.ParseIt(decrypted, tablename, export)
if export is 1:
export_Database(record, export, exportDB, tablename)
if export:
exportDB.commit()
exportDB.close()
cur.close()
con.close()
def handle_decrypted(decrypted, export, tablename):
blobparse = BlobParser()
record = blobparse.ParseIt(decrypted, tablename, export)
for k, v in record.items():
if k == 'Data':
print u' [-]', k
hexdump(v)
elif k == 'Type' and GetTableFullName(tablename) == 'Keys':
print u' [-]', k, ':', blobparse.GetKeyType(int(v))
else:
printable = str(v)
if not all(c in string.printable for c in printable):
printable = repr(v)
print u' [-]', k, ':', printable
print ''
def export_Database(record, export, exportDB, tablename):
record_lst = []
for k, v in record.items():
if k != 'SecAccessControl':
if k != 'TamperCheck':
record_lst.append([k, v])
if len(record_lst) != 0:
exportDB.insertData(tablename, record_lst)
if __name__ == "__main__":
main()