ipasign 1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ipasign/__init__.py +40 -0
- ipasign/_apple_ca.py +284 -0
- ipasign/_plist.py +28 -0
- ipasign/archive.py +173 -0
- ipasign/blobs.py +362 -0
- ipasign/bundle.py +282 -0
- ipasign/cms.py +164 -0
- ipasign/credentials.py +259 -0
- ipasign/errors.py +56 -0
- ipasign/key.py +251 -0
- ipasign/macho.py +504 -0
- ipasign/signer.py +275 -0
- ipasign-1.0.dist-info/METADATA +96 -0
- ipasign-1.0.dist-info/RECORD +17 -0
- ipasign-1.0.dist-info/WHEEL +5 -0
- ipasign-1.0.dist-info/licenses/LICENSE +21 -0
- ipasign-1.0.dist-info/top_level.txt +1 -0
ipasign/macho.py
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
"""Mach-O container parsing and rewriting.
|
|
2
|
+
|
|
3
|
+
A Mach-O file is either thin (one slice) or fat (several slices behind a fat
|
|
4
|
+
header). Everything here works on an in-memory ``bytes`` buffer and hands out
|
|
5
|
+
views into it, so a caller can read a binary once and sign it without a second
|
|
6
|
+
pass over the file.
|
|
7
|
+
|
|
8
|
+
Only what signing needs is modelled: the header, the load commands worth
|
|
9
|
+
looking at, the segments that carry a signature and the embedded info plist.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import struct
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
|
|
17
|
+
from .errors import MachOError, NotEnoughSpaceError
|
|
18
|
+
|
|
19
|
+
# Header magics, as seen when the first four bytes are read little-endian.
|
|
20
|
+
MH_MAGIC = 0xFEEDFACE
|
|
21
|
+
MH_CIGAM = 0xCEFAEDFE
|
|
22
|
+
MH_MAGIC_64 = 0xFEEDFACF
|
|
23
|
+
MH_CIGAM_64 = 0xCFFAEDFE
|
|
24
|
+
|
|
25
|
+
# Fat headers are detected on their raw bytes. The canonical order
|
|
26
|
+
# ``ca fe ba be`` stores its fields big-endian; the byte-swapped form stores
|
|
27
|
+
# them little-endian. Getting this backwards rejects real Apple frameworks.
|
|
28
|
+
FAT_MAGIC_BYTES = b"\xca\xfe\xba\xbe"
|
|
29
|
+
FAT_CIGAM_BYTES = b"\xbe\xba\xfe\xca"
|
|
30
|
+
|
|
31
|
+
THIN_MAGICS = (MH_MAGIC, MH_CIGAM, MH_MAGIC_64, MH_CIGAM_64)
|
|
32
|
+
|
|
33
|
+
MH_EXECUTE = 0x2
|
|
34
|
+
|
|
35
|
+
LC_SEGMENT = 0x1
|
|
36
|
+
LC_SEGMENT_64 = 0x19
|
|
37
|
+
LC_CODE_SIGNATURE = 0x1D
|
|
38
|
+
LC_ENCRYPTION_INFO = 0x21
|
|
39
|
+
LC_ENCRYPTION_INFO_64 = 0x2C
|
|
40
|
+
|
|
41
|
+
FAT_ALIGN_SHIFT = 14 # 16384-byte slice alignment
|
|
42
|
+
SIGNATURE_ALIGN = 4096
|
|
43
|
+
|
|
44
|
+
_HEADER_SIZE = {False: 28, True: 32}
|
|
45
|
+
_SEGMENT_SIZE = {False: 56, True: 72}
|
|
46
|
+
_SECTION_SIZE = {False: 68, True: 80}
|
|
47
|
+
|
|
48
|
+
# name, vmaddr, vmsize, fileoff, filesize, maxprot, initprot, nsects, flags
|
|
49
|
+
_SEGMENT = {False: "16s8I", True: "16s4Q4I"}
|
|
50
|
+
# sectname, segname, addr, size, offset, align, reloff, nreloc, flags, reserved
|
|
51
|
+
_SECTION = {False: "16s16s9I", True: "16s16s2Q8I"}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _cstr(raw: bytes) -> str:
|
|
55
|
+
"""Decode a fixed-size NUL-padded name field."""
|
|
56
|
+
return raw.split(b"\0", 1)[0].decode("utf-8", "replace")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(slots=True)
|
|
60
|
+
class Section:
|
|
61
|
+
"""One entry of a segment's section table."""
|
|
62
|
+
|
|
63
|
+
name: str
|
|
64
|
+
segname: str
|
|
65
|
+
addr: int
|
|
66
|
+
size: int
|
|
67
|
+
offset: int
|
|
68
|
+
align: int
|
|
69
|
+
reloff: int
|
|
70
|
+
nreloc: int
|
|
71
|
+
flags: int
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(slots=True)
|
|
75
|
+
class Segment:
|
|
76
|
+
"""A load command describing one segment."""
|
|
77
|
+
|
|
78
|
+
name: str
|
|
79
|
+
vmaddr: int
|
|
80
|
+
vmsize: int
|
|
81
|
+
fileoff: int
|
|
82
|
+
filesize: int
|
|
83
|
+
maxprot: int
|
|
84
|
+
initprot: int
|
|
85
|
+
flags: int
|
|
86
|
+
sections: list[Section]
|
|
87
|
+
command_offset: int
|
|
88
|
+
command_size: int
|
|
89
|
+
is_64: bool
|
|
90
|
+
|
|
91
|
+
def section(self, name: str) -> Section | None:
|
|
92
|
+
for sect in self.sections:
|
|
93
|
+
if sect.name == name:
|
|
94
|
+
return sect
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(slots=True)
|
|
99
|
+
class LoadCommand:
|
|
100
|
+
"""A load command this library has an opinion about."""
|
|
101
|
+
|
|
102
|
+
cmd: int
|
|
103
|
+
size: int
|
|
104
|
+
offset: int
|
|
105
|
+
data: tuple[int, ...] = ()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(slots=True)
|
|
109
|
+
class Slice:
|
|
110
|
+
"""One architecture of a Mach-O file, pointing back at the source buffer."""
|
|
111
|
+
|
|
112
|
+
data: bytes
|
|
113
|
+
base: int
|
|
114
|
+
size: int
|
|
115
|
+
big_endian: bool
|
|
116
|
+
is_64: bool
|
|
117
|
+
cpu_type: int
|
|
118
|
+
cpu_subtype: int
|
|
119
|
+
file_type: int
|
|
120
|
+
ncmds: int
|
|
121
|
+
sizeofcmds: int
|
|
122
|
+
flags: int
|
|
123
|
+
segments: list[Segment] = field(default_factory=list)
|
|
124
|
+
commands: list[LoadCommand] = field(default_factory=list)
|
|
125
|
+
code_signature: LoadCommand | None = None
|
|
126
|
+
info_plist: bytes = b""
|
|
127
|
+
encrypted: bool = False
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def endian(self) -> str:
|
|
131
|
+
return ">" if self.big_endian else "<"
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def header_size(self) -> int:
|
|
135
|
+
return _HEADER_SIZE[self.is_64]
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def view(self) -> memoryview:
|
|
139
|
+
"""The slice's bytes as a view, without copying."""
|
|
140
|
+
return memoryview(self.data)[self.base : self.base + self.size]
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def is_execute(self) -> bool:
|
|
144
|
+
return self.file_type == MH_EXECUTE
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def code_length(self) -> int:
|
|
148
|
+
"""Where the signature region starts.
|
|
149
|
+
|
|
150
|
+
For a signed file that is ``LC_CODE_SIGNATURE.dataoff``. For an unsigned
|
|
151
|
+
one it is the file size rounded up to a 16-byte boundary, which is where
|
|
152
|
+
a signature would be appended.
|
|
153
|
+
"""
|
|
154
|
+
if self.code_signature is not None:
|
|
155
|
+
return self.code_signature.data[0]
|
|
156
|
+
return round_up(self.size, 16)
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def text_segment(self) -> Segment | None:
|
|
160
|
+
for seg in self.segments:
|
|
161
|
+
if seg.name == "__TEXT":
|
|
162
|
+
return seg
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def linkedit_segment(self) -> Segment | None:
|
|
167
|
+
for seg in self.segments:
|
|
168
|
+
if seg.name == "__LINKEDIT":
|
|
169
|
+
return seg
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def load_commands_free_space(self) -> int:
|
|
174
|
+
"""Bytes between the end of the load commands and the first section data."""
|
|
175
|
+
text = self.text_segment
|
|
176
|
+
if text is None:
|
|
177
|
+
return 0
|
|
178
|
+
text_sect = text.section("__text")
|
|
179
|
+
if text_sect is None or text_sect.offset <= self.header_size + self.sizeofcmds:
|
|
180
|
+
return 0
|
|
181
|
+
return text_sect.offset - self.header_size - self.sizeofcmds
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def exec_seg_limit(self) -> int:
|
|
185
|
+
"""The ``__TEXT`` segment vmsize, sealed into the CodeDirectory."""
|
|
186
|
+
text = self.text_segment
|
|
187
|
+
return text.vmsize if text is not None else 0
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _parse_slice(data: bytes, base: int, size: int) -> Slice:
|
|
191
|
+
if base + size > len(data):
|
|
192
|
+
raise MachOError("slice runs past the end of the file")
|
|
193
|
+
magic = struct.unpack_from("<I", data, base)[0]
|
|
194
|
+
if magic not in THIN_MAGICS:
|
|
195
|
+
raise MachOError(f"not a Mach-O magic at offset {base}: 0x{magic:08x}")
|
|
196
|
+
|
|
197
|
+
big_endian = magic in (MH_CIGAM, MH_CIGAM_64)
|
|
198
|
+
is_64 = magic in (MH_MAGIC_64, MH_CIGAM_64)
|
|
199
|
+
endian = ">" if big_endian else "<"
|
|
200
|
+
|
|
201
|
+
fields = struct.unpack_from(endian + ("8I" if is_64 else "7I"), data, base)
|
|
202
|
+
slc = Slice(
|
|
203
|
+
data=data,
|
|
204
|
+
base=base,
|
|
205
|
+
size=size,
|
|
206
|
+
big_endian=big_endian,
|
|
207
|
+
is_64=is_64,
|
|
208
|
+
cpu_type=fields[1],
|
|
209
|
+
cpu_subtype=fields[2],
|
|
210
|
+
file_type=fields[3],
|
|
211
|
+
ncmds=fields[4],
|
|
212
|
+
sizeofcmds=fields[5],
|
|
213
|
+
flags=fields[6],
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Parsing walks absolute file offsets; everything stored on the dataclasses is
|
|
217
|
+
# relative to the slice base, because the writers rebuild a slice-only buffer.
|
|
218
|
+
cursor = base + _HEADER_SIZE[is_64]
|
|
219
|
+
for _ in range(slc.ncmds):
|
|
220
|
+
if cursor + 8 > base + size:
|
|
221
|
+
raise MachOError("load command runs past the end of the slice")
|
|
222
|
+
cmd, cmdsize = struct.unpack_from(endian + "II", data, cursor)
|
|
223
|
+
if cmdsize < 8 or cursor + cmdsize > base + size:
|
|
224
|
+
raise MachOError(f"bad load command size {cmdsize} for cmd 0x{cmd:x}")
|
|
225
|
+
|
|
226
|
+
offset = cursor - base
|
|
227
|
+
if cmd in (LC_SEGMENT, LC_SEGMENT_64):
|
|
228
|
+
slc.segments.append(_parse_segment(data, cursor, offset, cmd == LC_SEGMENT_64, endian))
|
|
229
|
+
elif cmd == LC_CODE_SIGNATURE:
|
|
230
|
+
dataoff, datasize = struct.unpack_from(endian + "II", data, cursor + 8)
|
|
231
|
+
slc.code_signature = LoadCommand(cmd, cmdsize, offset, (dataoff, datasize))
|
|
232
|
+
elif cmd in (LC_ENCRYPTION_INFO, LC_ENCRYPTION_INFO_64):
|
|
233
|
+
cryptid = struct.unpack_from(endian + "I", data, cursor + 16)[0]
|
|
234
|
+
slc.encrypted = cryptid >= 1
|
|
235
|
+
slc.commands.append(LoadCommand(cmd, cmdsize, offset))
|
|
236
|
+
else:
|
|
237
|
+
slc.commands.append(LoadCommand(cmd, cmdsize, offset))
|
|
238
|
+
|
|
239
|
+
cursor += cmdsize
|
|
240
|
+
|
|
241
|
+
text = slc.text_segment
|
|
242
|
+
if text is not None:
|
|
243
|
+
info = text.section("__info_plist")
|
|
244
|
+
if info is not None and info.offset and info.size:
|
|
245
|
+
slc.info_plist = bytes(data[base + info.offset : base + info.offset + info.size])
|
|
246
|
+
|
|
247
|
+
return slc
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _parse_segment(data: bytes, cursor: int, offset: int, is_64: bool, endian: str) -> Segment:
|
|
251
|
+
fields = struct.unpack_from(endian + _SEGMENT[is_64], data, cursor + 8)
|
|
252
|
+
name = _cstr(fields[0])
|
|
253
|
+
vmaddr, vmsize, fileoff, filesize = fields[1:5]
|
|
254
|
+
maxprot, initprot, nsects, flags = fields[5:9]
|
|
255
|
+
command_size = struct.unpack_from(endian + "I", data, cursor + 4)[0]
|
|
256
|
+
|
|
257
|
+
sections: list[Section] = []
|
|
258
|
+
sect_off = cursor + _SEGMENT_SIZE[is_64]
|
|
259
|
+
for _ in range(nsects):
|
|
260
|
+
raw = struct.unpack_from(endian + _SECTION[is_64], data, sect_off)
|
|
261
|
+
sections.append(
|
|
262
|
+
Section(
|
|
263
|
+
name=_cstr(raw[0]),
|
|
264
|
+
segname=_cstr(raw[1]),
|
|
265
|
+
addr=raw[2],
|
|
266
|
+
size=raw[3],
|
|
267
|
+
offset=raw[4],
|
|
268
|
+
align=raw[5],
|
|
269
|
+
reloff=raw[6],
|
|
270
|
+
nreloc=raw[7],
|
|
271
|
+
flags=raw[8],
|
|
272
|
+
)
|
|
273
|
+
)
|
|
274
|
+
sect_off += _SECTION_SIZE[is_64]
|
|
275
|
+
|
|
276
|
+
return Segment(
|
|
277
|
+
name=name,
|
|
278
|
+
vmaddr=vmaddr,
|
|
279
|
+
vmsize=vmsize,
|
|
280
|
+
fileoff=fileoff,
|
|
281
|
+
filesize=filesize,
|
|
282
|
+
maxprot=maxprot,
|
|
283
|
+
initprot=initprot,
|
|
284
|
+
flags=flags,
|
|
285
|
+
sections=sections,
|
|
286
|
+
command_offset=offset,
|
|
287
|
+
command_size=command_size,
|
|
288
|
+
is_64=is_64,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@dataclass(slots=True)
|
|
293
|
+
class FatArch:
|
|
294
|
+
"""One entry of a fat header."""
|
|
295
|
+
|
|
296
|
+
cpu_type: int
|
|
297
|
+
cpu_subtype: int
|
|
298
|
+
offset: int
|
|
299
|
+
size: int
|
|
300
|
+
align: int
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@dataclass(slots=True)
|
|
304
|
+
class MachOFile:
|
|
305
|
+
"""A parsed Mach-O file, thin or fat.
|
|
306
|
+
|
|
307
|
+
``data`` is the original buffer and stays untouched, so a failed sign cannot
|
|
308
|
+
leave a half-written binary behind.
|
|
309
|
+
"""
|
|
310
|
+
|
|
311
|
+
data: bytes
|
|
312
|
+
is_fat: bool
|
|
313
|
+
fat_big_endian: bool
|
|
314
|
+
slices: list[Slice]
|
|
315
|
+
archs: list[FatArch] = field(default_factory=list)
|
|
316
|
+
|
|
317
|
+
@classmethod
|
|
318
|
+
def parse(cls, data: bytes) -> "MachOFile":
|
|
319
|
+
try:
|
|
320
|
+
return cls._parse(data)
|
|
321
|
+
except struct.error as exc:
|
|
322
|
+
# struct.error is not part of the public API: a truncated or
|
|
323
|
+
# malformed binary becomes a MachOError at the boundary.
|
|
324
|
+
raise MachOError(f"malformed Mach-O: {exc}") from exc
|
|
325
|
+
|
|
326
|
+
@classmethod
|
|
327
|
+
def _parse(cls, data: bytes) -> "MachOFile":
|
|
328
|
+
if len(data) < 8:
|
|
329
|
+
raise MachOError("file is too short to be a Mach-O")
|
|
330
|
+
head = bytes(data[:4])
|
|
331
|
+
if head in (FAT_MAGIC_BYTES, FAT_CIGAM_BYTES):
|
|
332
|
+
return cls._parse_fat(data, head == FAT_MAGIC_BYTES)
|
|
333
|
+
magic = struct.unpack_from("<I", data, 0)[0]
|
|
334
|
+
if magic in THIN_MAGICS:
|
|
335
|
+
return cls(
|
|
336
|
+
data=data,
|
|
337
|
+
is_fat=False,
|
|
338
|
+
fat_big_endian=False,
|
|
339
|
+
slices=[_parse_slice(data, 0, len(data))],
|
|
340
|
+
)
|
|
341
|
+
raise MachOError(f"unrecognised Mach-O magic 0x{magic:08x}")
|
|
342
|
+
|
|
343
|
+
@classmethod
|
|
344
|
+
def _parse_fat(cls, data: bytes, big_endian: bool) -> "MachOFile":
|
|
345
|
+
endian = ">" if big_endian else "<"
|
|
346
|
+
if len(data) < 8:
|
|
347
|
+
raise MachOError("fat header is truncated")
|
|
348
|
+
nfat = struct.unpack_from(endian + "I", data, 4)[0]
|
|
349
|
+
if len(data) < 8 + nfat * 20:
|
|
350
|
+
raise MachOError(f"fat header claims {nfat} architectures but is truncated")
|
|
351
|
+
archs: list[FatArch] = []
|
|
352
|
+
slices: list[Slice] = []
|
|
353
|
+
for i in range(nfat):
|
|
354
|
+
cputype, cpusubtype, offset, size, align = struct.unpack_from(
|
|
355
|
+
endian + "5I", data, 8 + i * 20
|
|
356
|
+
)
|
|
357
|
+
if offset + size > len(data):
|
|
358
|
+
raise MachOError(f"fat arch {i} runs past the end of the file")
|
|
359
|
+
archs.append(FatArch(cputype, cpusubtype, offset, size, align))
|
|
360
|
+
slices.append(_parse_slice(data, offset, size))
|
|
361
|
+
return cls(data=data, is_fat=True, fat_big_endian=big_endian, slices=slices, archs=archs)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def align_up(value: int, align: int) -> int:
|
|
365
|
+
"""Round ``value`` up to a multiple of ``align``, never a no-op."""
|
|
366
|
+
return value + (align - value % align)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def round_up(value: int, align: int) -> int:
|
|
370
|
+
"""Round ``value`` up to a multiple of ``align``, a no-op when already aligned."""
|
|
371
|
+
return value if value % align == 0 else value + align - value % align
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def signature_region_size(code_length: int) -> int:
|
|
375
|
+
"""How much room to reserve for a signature starting at ``code_length``.
|
|
376
|
+
|
|
377
|
+
One slot per page for both SHA-1 and SHA-256, plus a fixed tail for the
|
|
378
|
+
SuperBlob header, Requirements, Entitlements and the CMS blob.
|
|
379
|
+
"""
|
|
380
|
+
return align_up(((code_length // SIGNATURE_ALIGN) + 1) * (20 + 32), SIGNATURE_ALIGN) + 32768
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def grow_slice(slc: Slice, new_length: int) -> bytes:
|
|
384
|
+
"""Return the slice resized to ``new_length`` with room for a signature.
|
|
385
|
+
|
|
386
|
+
The signature region itself is left as zero padding; call
|
|
387
|
+
:func:`place_signature` once the blob is built. ``__LINKEDIT`` and
|
|
388
|
+
``LC_CODE_SIGNATURE`` are brought up to date here.
|
|
389
|
+
"""
|
|
390
|
+
if new_length < slc.size:
|
|
391
|
+
raise MachOError("cannot shrink a slice to make room for a signature")
|
|
392
|
+
|
|
393
|
+
out = bytearray(slc.view)
|
|
394
|
+
out.extend(b"\0" * (new_length - len(out)))
|
|
395
|
+
|
|
396
|
+
code_length = slc.code_length
|
|
397
|
+
_set_signature_command(slc, out, code_length, new_length - code_length)
|
|
398
|
+
|
|
399
|
+
if new_length > slc.size:
|
|
400
|
+
_grow_linkedit(slc, out, new_length)
|
|
401
|
+
|
|
402
|
+
return bytes(out)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def place_signature(slc: Slice, data: bytes, code_length: int, signature: bytes) -> bytes:
|
|
406
|
+
"""Write ``signature`` into a slice previously sized by :func:`grow_slice`."""
|
|
407
|
+
out = bytearray(data)
|
|
408
|
+
end = code_length + len(signature)
|
|
409
|
+
if end > len(out) or code_length < 0:
|
|
410
|
+
raise MachOError("signature does not fit in the region it was sized for")
|
|
411
|
+
out[code_length:end] = signature
|
|
412
|
+
_set_signature_command(slc, out, code_length, len(out) - code_length)
|
|
413
|
+
return bytes(out)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _set_signature_command(slc: Slice, out: bytearray, code_length: int, region: int) -> None:
|
|
417
|
+
"""Create or update the ``LC_CODE_SIGNATURE`` load command."""
|
|
418
|
+
endian = slc.endian
|
|
419
|
+
if slc.code_signature is None:
|
|
420
|
+
if slc.load_commands_free_space < 16:
|
|
421
|
+
raise NotEnoughSpaceError(
|
|
422
|
+
"no room in the load commands for LC_CODE_SIGNATURE; "
|
|
423
|
+
f"{slc.load_commands_free_space} bytes free, 16 needed"
|
|
424
|
+
)
|
|
425
|
+
cmd_off = slc.header_size + slc.sizeofcmds
|
|
426
|
+
struct.pack_into(endian + "IIII", out, cmd_off, LC_CODE_SIGNATURE, 16, code_length, region)
|
|
427
|
+
struct.pack_into(endian + "I", out, 16, slc.ncmds + 1)
|
|
428
|
+
struct.pack_into(endian + "I", out, 20, slc.sizeofcmds + 16)
|
|
429
|
+
else:
|
|
430
|
+
struct.pack_into(endian + "II", out, slc.code_signature.offset + 8, code_length, region)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _grow_linkedit(slc: Slice, out: bytearray, new_length: int) -> None:
|
|
434
|
+
"""Extend ``__LINKEDIT`` to cover the grown file."""
|
|
435
|
+
linkedit = slc.linkedit_segment
|
|
436
|
+
if linkedit is None:
|
|
437
|
+
raise MachOError("slice has no __LINKEDIT segment")
|
|
438
|
+
endian = slc.endian
|
|
439
|
+
grow = new_length - slc.size
|
|
440
|
+
if linkedit.is_64:
|
|
441
|
+
vmsize = align_up(linkedit.vmsize + grow, SIGNATURE_ALIGN)
|
|
442
|
+
struct.pack_into(endian + "Q", out, linkedit.command_offset + 32, vmsize)
|
|
443
|
+
struct.pack_into(endian + "Q", out, linkedit.command_offset + 48, new_length - linkedit.fileoff)
|
|
444
|
+
else:
|
|
445
|
+
vmsize = align_up((linkedit.vmsize + grow) & 0xFFFFFFFF, SIGNATURE_ALIGN)
|
|
446
|
+
struct.pack_into(endian + "I", out, linkedit.command_offset + 28, vmsize)
|
|
447
|
+
struct.pack_into(endian + "I", out, linkedit.command_offset + 36, new_length - linkedit.fileoff)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def build_fat(
|
|
451
|
+
slices: list[bytes],
|
|
452
|
+
archs: list[FatArch],
|
|
453
|
+
big_endian: bool,
|
|
454
|
+
align: int = 1 << FAT_ALIGN_SHIFT,
|
|
455
|
+
) -> bytes:
|
|
456
|
+
"""Lay slices out behind a fat header at ``align`` and return the container."""
|
|
457
|
+
if len(slices) != len(archs):
|
|
458
|
+
raise MachOError("slice count does not match fat arch count")
|
|
459
|
+
|
|
460
|
+
endian = ">" if big_endian else "<"
|
|
461
|
+
magic = FAT_MAGIC_BYTES if big_endian else FAT_CIGAM_BYTES
|
|
462
|
+
offset = align_up(8 + 20 * len(archs), align)
|
|
463
|
+
|
|
464
|
+
out = bytearray(b"\0" * offset)
|
|
465
|
+
out[0:4] = magic
|
|
466
|
+
struct.pack_into(endian + "I", out, 4, len(archs))
|
|
467
|
+
|
|
468
|
+
for i, (blob, arch) in enumerate(zip(slices, archs)):
|
|
469
|
+
if len(out) < offset:
|
|
470
|
+
out.extend(b"\0" * (offset - len(out)))
|
|
471
|
+
out[offset : offset + len(blob)] = blob
|
|
472
|
+
struct.pack_into(
|
|
473
|
+
endian + "5I",
|
|
474
|
+
out,
|
|
475
|
+
8 + i * 20,
|
|
476
|
+
arch.cpu_type,
|
|
477
|
+
arch.cpu_subtype,
|
|
478
|
+
offset,
|
|
479
|
+
len(blob),
|
|
480
|
+
FAT_ALIGN_SHIFT,
|
|
481
|
+
)
|
|
482
|
+
offset = align_up(offset + len(blob), align)
|
|
483
|
+
|
|
484
|
+
if len(out) < offset:
|
|
485
|
+
out.extend(b"\0" * (offset - len(out)))
|
|
486
|
+
return bytes(out)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
__all__ = [
|
|
490
|
+
"MachOFile",
|
|
491
|
+
"Slice",
|
|
492
|
+
"Segment",
|
|
493
|
+
"Section",
|
|
494
|
+
"FatArch",
|
|
495
|
+
"LoadCommand",
|
|
496
|
+
"align_up",
|
|
497
|
+
"round_up",
|
|
498
|
+
"signature_region_size",
|
|
499
|
+
"grow_slice",
|
|
500
|
+
"place_signature",
|
|
501
|
+
"build_fat",
|
|
502
|
+
"MH_EXECUTE",
|
|
503
|
+
"LC_CODE_SIGNATURE",
|
|
504
|
+
]
|