-
Notifications
You must be signed in to change notification settings - Fork 1
/
iCamasu.py
688 lines (570 loc) · 23.1 KB
/
iCamasu.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
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
#!/usr/bin/env python
# -*- coding: utf-8 -*-
__author__ = "Raul Siles"
__email__ = "[email protected]"
__copyright__ = "Copyright (c) 2014 DinoSec SL (www.dinosec.com)"
__license__ = "GPL"
__version__ = "0.42"
__date__ = "2014-06-14"
import plistlib
import argparse
import os
import sys
import hashlib
import binascii
from collections import defaultdict
#
# Version history:
#
# - Version v0.41 (original version): 2014-05-29
# Released at Area41 in June, 2014
#
# - Version v0.42: 2014-06-14
# New '-q' option (--quiet)
# New '-x' option (--xml-schema-count)
# New '-X' option (--xml-schema)
#
# -- iCamasu --
# Latest iOS version whose PLIST file has been tested:
ios_version_tested = "7.1.1"
asciiart = '''
_ ____
(_)/ ___|__ _ _ __ ___ __ _ ___ _ _
| | | / _` | '_ ` _ \ / _` / __| | | |
| | |__| (_| | | | | | | (_| \__ \ |_| |
|_|\____\__,_|_| |_| |_|\__,_|___/\__,_|
'''
# ----
# Variables:
verbose = False
quiet = False
full_details = False
# File variables
input_file = "com_apple_MobileAsset_SoftwareUpdate.xml"
filesize = 0
filesha1 = ""
# URL variables
url = "http://mesu.apple.com/assets/com_apple_MobileAsset_SoftwareUpdate/com_apple_MobileAsset_SoftwareUpdate.xml"
urldoc = "http://mesu.apple.com/assets/com_apple_MobileAsset_SoftwareUpdateDocumentation/" \
"com_apple_MobileAsset_SoftwareUpdateDocumentation.xml"
# PLIST file entries or assets (dictionaries)
assets = defaultdict(list)
assets_by_ios_version = defaultdict(list)
# PLIST file XML schema
schema = defaultdict(int)
# Total number of assets or entries
num_assets = 0
# Total number of devices
num_devices = 0
# Total number of iOS versions
num_versions = 0
# Minimum & maximum iOS versions
min_iOS_version = ""
max_iOS_version = ""
# Are there any iOS beta versions?
has_beta_versions = False
beta_versions = []
# Selectors
device = ""
ios_version = ""
min_version = False
max_version = False
both_versions = False
summary = False
file_summary = False
summary_by_device = False
summary_by_ios_version = False
xml_schema = False
xml_schema_count = False
# Default response if an element/key is not found in a dictionary
default_response = "None"
# ----
# Print error message and exit
def error(msg):
print("\n[!] ERROR - {0}\n".format(msg))
sys.exit(1)
# Print warning message and continue
def warning(msg):
print("[/] WARNING - {0}".format(msg))
# Return file size
def fileSize(filename):
try:
return os.path.getsize(filename)
except os.error as e:
error("File does not exist: {0} ({1})".format(filename, e))
# Return file SHA-1 hash
def fileSHA1(filename):
with open(filename, 'rb') as f:
return hashlib.sha1(f.read()).hexdigest()
# Check if version is less than the minimum iOS version already found
def isMiniOSVersion(version, current_min_ios_version):
if current_min_ios_version == "":
return True
elif version < current_min_ios_version:
return True
else:
return False
# Check if version is greater than the maximum iOS version already found
def isMaxiOSVersion(version, current_max_ios_version):
if current_max_ios_version == "":
return True
elif version > current_max_ios_version:
return True
else:
return False
# Parse PLIST file
def parse(infile):
global min_iOS_version
global max_iOS_version
global has_beta_versions
count = 0
plist = plistlib.readPlist(infile)
#list_of_assets = plist["Assets"]
list_of_assets = plist.get("Assets", default_response)
#print list_of_assets
if list_of_assets == default_response:
error("The 'Assets' key is not available in the PLIST file: {0}".format(infile))
for entry in list_of_assets:
# Apple device(s) - list
devices = entry.get("SupportedDevices", default_response)
if devices == default_response:
warning("There is no 'SupportedDevices' key for entry {0}.".format(count+1))
product = entry.get("SUProductSystemName", default_response)
if product == default_response:
warning("There is no 'SUProductSystemName' key for entry {0}.".format(count+1))
elif product != "iOS":
error("Product name different from 'iOS': {0}".format(product))
publisher = entry.get("SUPublisher", default_response)
if publisher == default_response:
warning("There is no 'SUPublisher' key for entry {0}.".format(count+1))
elif publisher != "Apple Inc.":
error("Publisher different from Apple: {0}".format(publisher))
# Documentation ID (string)
documentationID = entry.get("SUDocumentationID", default_response)
# iOS version (string) & Prerequisite iOS version (string)
version = entry.get("OSVersion", default_response)
# Add iOS beta version details, if available:
release_type = entry.get("ReleaseType", default_response)
if version == default_response:
warning("There is no 'OSVersion' key for entry {0}.".format(count+1))
else:
if release_type != default_response:
if release_type != "Beta":
warning("Release type key different from 'Beta': {0}".format(release_type))
else:
has_beta_versions = True
if documentationID != default_response:
version = version + "(" + documentationID + ")"
if version not in beta_versions:
beta_versions.append(version)
# Min & max iOS versions
if isMiniOSVersion(version, min_iOS_version):
min_iOS_version = version
if isMaxiOSVersion(version, max_iOS_version):
max_iOS_version = version
# "PrerequisiteOSVersion" might not exist = None
fromVersion = entry.get("PrerequisiteOSVersion", default_response)
# Build (string) & Prerequisite Build (string)
# ("PrerequisiteBuild" (and other entries) might not exist = "None")
build = entry.get("Build", default_response)
preBuild = entry.get("PrerequisiteBuild", default_response)
# Installation size (string)
installSize = entry.get("InstallationSize", default_response)
# Download size (integer)
downloadSize = entry.get("_DownloadSize", default_response)
# Unarchived size (integer)
unarchivedSize = entry.get("_UnarchivedSize", default_response)
# Download format (string)
fileFormat = entry.get("_CompressionAlgorithm", default_response)
if fileFormat != "zip":
warning("Download file format different from ZIP: {0}".format(fileFormat))
# Base URL (string)
baseURL = entry.get("__BaseURL", default_response)
# Path (string)
path = entry.get("__RelativePath", default_response)
if baseURL == default_response or path == default_response:
warning("There is no '__BaseURL' or '__RelativePath' key for entry {0}.".format(count+1))
url_entry = baseURL+path
# Hash format (string)
hashFormat = entry.get("_MeasurementAlgorithm", default_response)
# Encoded hash (data - in Base64)
value = entry.get("_Measurement")
hash_value = str("None" if value is None else binascii.b2a_hex(value.data))
# In iOS 7 PLIST files there can be more than one Apple device for a single iOS entry
#
#if len(devices) > 1:
# error("There is more than one Apple device for an entry: {0}".format(devices))
#else:
# dev = devices[0]
#
for dev in devices:
# Asset id (from 1 to N)
count += 1
# Add details from this entry to assets
new_entry = {version: {}}
#new_entry[version]['version'] = version
new_entry[version]['fromVersion'] = fromVersion
new_entry[version]['build'] = build
new_entry[version]['preBuild'] = preBuild
new_entry[version]['installSize'] = installSize
new_entry[version]['downloadSize'] = downloadSize
new_entry[version]['unarchivedSize'] = unarchivedSize
new_entry[version]['fileFormat'] = fileFormat
#new_entry[version]['baseURL'] = baseURL
#new_entry[version]['path'] = path
new_entry[version]['url'] = url_entry
new_entry[version]['hashFormat'] = hashFormat
new_entry[version]['hash'] = hash_value
new_entry[version]['beta'] = True if release_type != default_response else False
# Add a new entry to the list of assets for this device
assets[dev].append(new_entry)
# Return total number of assets or entries
return count
# Parse PLIST file XML schema
def parseXMLSchema(infile):
count = 0
plist = plistlib.readPlist(infile)
list_of_assets = plist.get("Assets", default_response)
#print list_of_assets
if list_of_assets == default_response:
error("The 'Assets' key is not available in the PLIST file: {0}".format(infile))
for entry in list_of_assets: # dict
# Increase entry id
count += 1
for element in entry:
schema[element] +=1
# Return total number of entries
return count
# Get assets classified by iOS version
def getAssetsByiOSVersion():
global assets_by_ios_version
for dev in assets:
for entry in assets[dev]:
for ver in entry.keys():
assets_by_ios_version[ver].append(dev)
# Get (sorted & unique) list of iOS versions for a specific device
def iOSVersionsFor(this_device):
if assets.get(this_device, default_response) == default_response:
return default_response
else:
versions = []
for entry in assets[this_device]:
for ver in entry.keys():
versions.append(ver)
return sorted(set(versions))
# Get (sorted & unique) list of devices for a specific iOS version
def devicesFor(this_ios_version):
return assets_by_ios_version.get(this_ios_version, default_response)
# PRINT & SUMMARY FUNCTIONS:
# ----------------------------
# Print minimum version
def miniOSVersion():
print min_iOS_version
# Print maximum version
def maxiOSVersion():
print max_iOS_version
# Print one-line summary of an asset
def onelineSummaryOfAsset(count, dev, version, entry):
beta = " (beta)" if entry[version]["beta"] else ""
print "[%d] %s: %s (%s) [from version %s (%s)]%s %s" % (count, dev, version,
entry[version]['build'], entry[version]['fromVersion'], entry[version]['preBuild'],
beta, entry[version]["hash"])
# Print full summary of an asset
def assetSummary(count, dev, version, entry):
beta = " (beta)" if entry[version]["beta"] else ""
print "[%d]" % count
print "%s: %s (%s) [from version %s (%s)]%s" \
% (dev, version, entry[version]['build'],
entry[version]['fromVersion'], entry[version]['preBuild'], beta)
print "Size: %s (%s) --> %s (Install: %s)" \
% (entry[version]['downloadSize'], entry[version]['fileFormat'],
entry[version]['unarchivedSize'], entry[version]['installSize'])
print "URL: %s" % (entry[version]["url"])
print "%s: %s" % (entry[version]["hashFormat"], entry[version]["hash"])
# Print details for assets for a specific device
def printAssetsForDevice(this_device):
if not quiet:
print "- Assets Details for Device %s: " % this_device
print ""
count = 0
# Print sorted list of assets and all their associated details for a specific device
for entry in sorted(assets[this_device]):
for version in entry.keys():
count += 1
if not full_details:
onelineSummaryOfAsset(count, device, version, entry)
else:
assetSummary(count, device, version, entry)
print
# Print details for assets for a specific iOS version
def printAssetsForiOSVersion(this_ios_version):
print "- Assets Details for iOS Version %s: " % this_ios_version
print ""
count = 0
# Print sorted list of assets and all their associated details for a specific iOS version
for dev in sorted(assets):
for entry in sorted(assets[dev]):
for version in entry.keys():
if version == this_ios_version:
count += 1
if not full_details:
onelineSummaryOfAsset(count, dev, version, entry)
else:
assetSummary(count, dev, version, entry)
print
# Print details for all assets in PLIST file
def printAssets():
#print assets
if not quiet:
print "- PLIST File Details: (%d assets)" % num_assets
print ""
count = 0
# Print sorted list of assets and all their associated details
for dev in sorted(assets):
for entry in sorted(assets[dev]):
for version in entry.keys():
count += 1
if not full_details:
onelineSummaryOfAsset(count, dev, version, entry)
else:
assetSummary(count, dev, version, entry)
print
# Print one-line summary of PLIST file
def summaryOneLine():
beta = " (beta)" if has_beta_versions else ""
print "%s (SHA-1: %s) = %s bytes, %s assets, %s devices, %s versions%s, min: %s, max: %s" % \
(input_file, filesha1, filesize, num_assets, num_devices, num_versions, beta, min_iOS_version, max_iOS_version)
# Print summary of PLIST file
def summaryFile():
beta = " (beta)" if has_beta_versions else ""
if not quiet:
print "- File Summary: "
print ""
print "Filename: %s" % input_file
print "SHA1: %s" % filesha1
print "Size: %d" % filesize
print "# Assets: %d" % num_assets
print "# Devices: %d" % num_devices
print "# iOS versions: %d%s" % (num_versions, beta)
print "Min. iOS: %s" % min_iOS_version
print "Max. iOS: %s" % max_iOS_version
if has_beta_versions:
print "# Beta versions: %d" % len(beta_versions)
print "Beta versions: %s" % " ".join(sorted(set(beta_versions)))
# Print summary of PLIST file by model
def summaryByDevice():
if not quiet:
print "- Summary By Device: (%d devices)" % num_devices
print ""
# Print (sorted & unique) list of devices and (sorted & unique) associated iOS versions
for dev in sorted(assets):
print "%s: %s" % (dev, " ".join(iOSVersionsFor(dev)))
# Print summary of PLIST file by iOS version
def summaryByiOSVersion():
if not quiet:
print "- Summary By iOS Version: (%d iOS versions)" % num_versions
print ""
# Print (sorted & unique) list of iOS versions and associated devices
for ver, assets_list in sorted(assets_by_ios_version.items()):
print "%s: %s" % (ver, " ".join(sorted(set(assets_list))))
# Print (sorted & unique) list of iOS versions for a specific device
def summaryiOSVersionsFor(this_device):
versions = iOSVersionsFor(this_device)
if versions == default_response:
print "%s" % default_response
else:
print "%s" % (" ".join(sorted(set(versions))))
# Print (sorted & unique) list of devices for a specific iOS version
def summaryDevicesFor(this_ios_version):
devices = devicesFor(this_ios_version)
if devices == default_response:
print "%s" % default_response
else:
print "%s" % (" ".join(sorted(set(devices))))
# Print XML schema of PLIST file
def printXMLSchema(infile):
# Parse XML schema
num_entries = parseXMLSchema(infile)
#print schema
if not quiet:
print "- XML schema: (%d assets)" % num_entries
print ""
# Print XML schema entries with count numbers
for entry in sorted(schema):
print "%s: %d" % (entry, schema[entry])
# Print number of entries in XML schema of PLIST file
def printXMLSchemaCount(infile):
# Parse XML schema
num_entries = parseXMLSchema(infile)
print num_entries
# MAIN:
# -------
if __name__ == "__main__":
header_info = "\n\tiCamasu: iOS com_apple_MobileAsset_SoftwareUpdate\n\t (v" + __version__ + \
" - " + __date__ + ")\n\n\t" + __copyright__ + " - " + __author__ + "\n"
header_description = "\tTool that parses and extracts details from Apple iOS software\n" + \
"\tupdate PLIST files: com_apple_MobileAsset_SoftwareUpdate.xml.\n"
header = asciiart + header_info
header_tested = "\t(v" + __version__ + " - PLIST file tested up to iOS version " + ios_version_tested + ")\n\n"
# Parse arguments...
parser = argparse.ArgumentParser(description=asciiart + "\n" + header_info + "\n" +
header_description + header_tested,
formatter_class=argparse.RawTextHelpFormatter,
epilog='\t-- Check the new details about the latest iOS updates! --')
# General flags:
parser.add_argument("-v", "--verbose", action="store_true",
help="Increase output verbosity (default = off).")
parser.add_argument("-q", "--quiet", action="store_true",
help="Do not show headers and other details (default = off).")
parser.add_argument("-f", "--file", help="iOS software update PLIST file:\n" +
"(e.g. com_apple_MobileAsset_SoftwareUpdate.xml)")
parser.add_argument("-V", "--version", action='version', version=__version__,
help="Show version information and exit.")
parser.add_argument("-F", "--full-details", action="store_true",
help="Show full details for assets.")
# Output selectors
group_selectors = parser.add_mutually_exclusive_group()
group_selectors.add_argument("-s", "--summary", action="store_true",
help="Show one-line PLIST file summary (default = on).")
group_selectors.add_argument("-S", "--file-summary", action="store_true",
help="Show PLIST file summary.\n(optional: use with '-v' or '-vF')")
group_selectors.add_argument("-d", "--device",
help="Show iOS version for this device.\n(optional: use with '-v' or '-vF')")
group_selectors.add_argument("-i", "--ios-version",
help="Show devices for this iOS version.\n(optional: use with '-v' or '-vF')")
group_selectors.add_argument("-D", "--summary-by-device", action="store_true",
help="Show PLIST file summary by device.")
group_selectors.add_argument("-I", "--summary-by-ios-version", action="store_true",
help="Show PLIST file summary by iOS version.")
group_selectors.add_argument("-m", "--min-version", action="store_true",
help="Show minimum iOS version.")
group_selectors.add_argument("-M", "--max-version", action="store_true",
help="Show maximum iOS version.")
group_selectors.add_argument("-b", "--both-versions", action="store_true",
help="Show both minimum & maximum iOS version.")
group_selectors.add_argument("-X", "--xml-schema", action="store_true",
help="Show the PLIST file XML schema.")
group_selectors.add_argument("-x", "--xml-schema-count", action="store_true",
help="Show the number of entries in the PLIST file XML schema.")
args = parser.parse_args()
if args.file is not None:
input_file = args.file
else:
input_file = "com_apple_MobileAsset_SoftwareUpdate.xml"
#parser.print_help()
#error("It is mandatory to specify the input file name.")
if args.verbose:
verbose = args.verbose
if args.quiet:
quiet = args.quiet
if args.full_details:
full_details = args.full_details
if args.device is not None:
device = args.device
elif args.ios_version is not None:
ios_version = args.ios_version
elif args.min_version:
min_version = args.min_version
elif args.max_version:
max_version = args.max_version
elif args.both_versions:
both_versions = args.both_versions
elif args.summary:
summary = args.summary
elif args.file_summary:
file_summary = args.file_summary
elif args.summary_by_device:
summary_by_device = args.summary_by_device
elif args.summary_by_ios_version:
summary_by_ios_version = args.summary_by_ios_version
elif args.xml_schema:
xml_schema = args.xml_schema
elif args.xml_schema_count:
xml_schema_count = args.xml_schema_count
else:
# Show a one-line summary of the PLIST file (default output)
summary = True
# Process PLIST file
filesize = fileSize(input_file)
filesha1 = fileSHA1(input_file)
# Parse PLIST file (and get total number of assets or entries)
num_assets = parse(input_file)
# Classify assets by iOS version
getAssetsByiOSVersion()
# Total number of devices and iOS versions
num_devices = len(assets)
num_versions = len(assets_by_ios_version)
# Sort iOS beta versions list
beta_versions.sort()
if device: # If is not an empty string
if not verbose:
summaryiOSVersionsFor(device)
else:
if not quiet:
print
print(header)
printAssetsForDevice(device)
#print
elif ios_version: # If is not an empty string
if not verbose:
summaryDevicesFor(ios_version)
else:
if not quiet:
print
print(header)
printAssetsForiOSVersion(ios_version)
#print
elif min_version:
miniOSVersion()
elif max_version:
maxiOSVersion()
elif both_versions:
miniOSVersion()
maxiOSVersion()
elif summary:
# Print one-line PLIST summary
summaryOneLine()
elif file_summary:
if not verbose:
# Print PLIST file summary
if not quiet:
print
print(header)
summaryFile()
#print
else:
# Print full details from PLIST file
if not quiet:
print
print(header)
summaryFile()
print
printAssets()
#print
elif summary_by_device:
# Print PLIST summary by device
if not quiet:
print
print(header)
summaryByDevice()
#print
elif summary_by_ios_version:
# Print PLIST summary by iOS version
if not quiet:
print
print(header)
summaryByiOSVersion()
#print
elif xml_schema:
# Print PLIST file XML schema
if not quiet:
print
print(header)
printXMLSchema(input_file)
elif xml_schema_count:
# Print number of entries in PLIST file XML schema
printXMLSchemaCount(input_file)
else:
# Default:
# Print one-line PLIST summary
summaryOneLine()