-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathmain.py
163 lines (135 loc) · 5.44 KB
/
main.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
import argparse
import os
import pathlib
import sys
from compressed_payload import decompress_payload_chunks
from fota_payload import FotaPayload
from rofs import find_rofs
from super_binary import SuperBinary
parser = argparse.ArgumentParser(
description="Extracts a FOTA within a SuperBinary container."
)
parser.add_argument(
"source",
help='Path to the SuperBinary. Typically called "FirmwareUpdate.uarp".',
type=argparse.FileType("rb"),
)
parser.add_argument(
"output_dir",
help="The directory to save payloads to.",
type=pathlib.Path,
)
parser.add_argument(
"--extract-payloads",
help="Whether to extract all payloads of this SuperBinary.",
action=argparse.BooleanOptionalAction,
default=True,
)
parser.add_argument(
"--use-tag-name",
help="Whether to extract payloads via their tag name instead of full path.",
action=argparse.BooleanOptionalAction,
)
parser.add_argument(
"--decompress-fota",
help="Whether to decompress the FOTA.",
action=argparse.BooleanOptionalAction,
)
parser.add_argument(
"--extract-rofs",
help="Whether to extract the ROFS partition to the output directory.",
action=argparse.BooleanOptionalAction,
)
parser.add_argument(
"--decompress-payload-contents",
help="Whether to decompress payload contents in particular types of SuperBinaries.",
action=argparse.BooleanOptionalAction,
default=True,
)
args = parser.parse_args()
if args.extract_rofs and not args.decompress_fota:
print("Please ensure that --decompress-fota is specified.")
exit(1)
super_binary = SuperBinary(args.source)
# Ensure our payload directory can be written to.
payload_dir = args.output_dir
payload_dir.mkdir(parents=True, exist_ok=True)
def write_payload(file_name: str, file_contents: bytes):
file_path: pathlib.Path = payload_dir / file_name
# In the case of fullpaths, we may need to make a parent directory first.
file_path.parent.mkdir(parents=True, exist_ok=True)
with open(file_path, "wb") as f:
f.write(file_contents)
# Write out payloads if desired.
if args.extract_payloads:
# Used to avoid conflicts in both tag names and fullpaths.
seen_filenames: dict[str, int] = {}
for payload in super_binary.payloads:
tag_name = payload.get_tag()
payload_name = payload.plist_metadata.long_name or "no long name"
payload_filename: str
if args.use_tag_name:
payload_filename = f"{tag_name}.bin"
else:
# We want to leverage the payload's given filepath.
# Ensure its parent directories exist.
payload_filename = payload.plist_metadata.filepath
# Sometimes, tags have multiple payloads, and filepaths conflict.
# Let's append a number for every occurrence.
seen_count = seen_filenames.get(payload_filename)
print(seen_count)
if seen_count is not None:
# We have a tag! Increment its seen count.
seen_filenames[payload_filename] += 1
# Append the count at the end of the file.
payload_filename = f"{payload_filename}.{seen_count}"
else:
seen_filenames[payload_filename] = 1
print(f"Saving {tag_name} ({payload_name}) to {payload_filename}...")
# Sometimes, this may be an absolute path.
# For example, some filepaths start with `/Library` or `/tmp`.
# Tags should (hopefully) never run in to this.
#
# Let's append `./` to the start to ensure relative resolution.
payload_filename = f"./{payload_filename}"
write_payload(payload_filename, payload.contents)
# Lastly, write the SuperBinary plist.
write_payload("SuperBinary.plist", super_binary.raw_plist_data)
if args.decompress_fota:
# Ensure we have a payload of this type.
fota_payload = super_binary.get_tag(b"FOTA")
if not fota_payload:
print("Missing FOTA payload!")
exit(1)
fota = FotaPayload(fota_payload.contents)
write_payload("FOTA.bin.lzma", fota.compressed)
# Decompress payload.
write_payload("FOTA", fota.decompressed)
# Separate segments within.
os.makedirs(payload_dir / "segments", exist_ok=True)
for i, segment_contents in enumerate(fota.segments):
# Each segment offset is 0x1000 ahead
# as the decompressed portions likely
# overwrites the compressed portion in memory.
write_payload(f"segments/{i}.bin", segment_contents)
print("Extracted FOTA payload!")
if args.extract_rofs:
rofs_partition = find_rofs(fota.segments)
os.makedirs(payload_dir / "files", exist_ok=True)
for file in rofs_partition.files:
write_payload(f"files/{file.file_name}", file.contents)
if args.decompress_payload_contents:
# TODO(spotlightishere): This should function on platforms beyond macOS.
assert (
sys.platform == "darwin"
), "Decompression is not yet supported on this platform."
for payload in super_binary.payloads:
# The metadata plist present at the end of the SuperBinary
# defines what segments are compressed.
# For our purpose, any compressed segment has a `compressed_chunk_size` that is not None.
chunk_size = payload.plist_metadata.compressed_chunk_size
if not chunk_size:
continue
print(f"Decompressing {payload.get_tag()}...")
contents = decompress_payload_chunks(payload)
write_payload(f"{payload.get_tag()}.decompressed.bin", contents)