ruby-macho 5.0.0 → 6.0.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 848be7615c0a26614ffc950ffd7a70b3efe594a4c2608aeb776d2f7a0fca472d
4
- data.tar.gz: e8e084691a090ddd084196567ee80848b2cb546f9bff70ec9d43b8a8a45b0cef
3
+ metadata.gz: b0a72ccdc56030f0b2d9ed6fae89837685eea3397361c2fcb2c06d33409b79a7
4
+ data.tar.gz: f7a9a49865c6bb11b0edb6579d4fe8291b70a8077999ef3225d13cfde9d1180f
5
5
  SHA512:
6
- metadata.gz: 2117ef6313e528a636b43b5c45c17729627cfae074957c6bfc7486040daf4c10b3f6ce74dc48a72a8375821f77de34d723c6c586eda27a5d0cc71d47a33cc42b
7
- data.tar.gz: cc3a7f948e6d9f6ae71b7f0f96eef0945d5269c8e59ab09239a61ce8f8564c9faa8b4484940907dc6bb299435f54d9a1c5b89714c692b15a048b0c0c49f6f955
6
+ metadata.gz: c82c7a5b06136795d956510b48cc039a684d1782ac354f3c3ef49f6c2c1d904bf31cf18a7c93dc12244a8d5abbb81a0b232a867578a5e9f82efb34fefbec8d80
7
+ data.tar.gz: 3bbb1dfe665e7a6f2071cd41bfa094458a964b63bfd68d6d9976af1a1ad155875ff250048ff2d6a70e01c0258c711777471820581d57d0feade5b0299e812777
data/README.md CHANGED
@@ -45,12 +45,25 @@ lc_vers = file[:LC_VERSION_MIN_MACOSX].first
45
45
  puts lc_vers.version_string # => "10.10.0"
46
46
  ```
47
47
 
48
+ ### Ad-hoc code signing
49
+
50
+ Changing a Mach-O load command invalidates any existing code signature. This is
51
+ especially important when Homebrew pours bottles on Apple Silicon, where native
52
+ code must remain signed after its paths are rewritten. `MachO.codesign!` creates
53
+ the required ad-hoc signature in Ruby instead of invoking `/usr/bin/codesign`:
54
+
55
+ ```ruby
56
+ MachO.codesign!("/path/to/my/binary")
57
+ ```
58
+
59
+
48
60
  ### What works?
49
61
 
50
- * Reading data from x86/x86_64/PPC Mach-O files
62
+ * Reading data from x86/x86_64/arm64/PPC Mach-O files (other architectures are unsupported, but may work)
51
63
  * Changing the IDs of Mach-O and Fat dylibs
52
64
  * Changing install names in Mach-O and Fat files
53
65
  * Adding, deleting, and modifying rpaths.
66
+ * Parsing embedded code signatures and applying ad-hoc signatures in pure Ruby.
54
67
 
55
68
  ### What needs to be done?
56
69
 
@@ -73,6 +86,8 @@ overcommit --install
73
86
  * Constants were taken from Apple, Inc's
74
87
  [`loader.h` in `cctools/include/mach-o`](https://opensource.apple.com/source/cctools/cctools-973.0.1/include/mach-o/loader.h.auto.html).
75
88
  (Apple Public Source License 2.0).
89
+ * Code-signing constants and structures follow Apple, Inc's
90
+ [`cs_blobs.h` in XNU](https://github.com/apple-oss-distributions/xnu/blob/main/osfmk/kern/cs_blobs.h).
76
91
  * Binary files used for testing were taken from The LLVM Project. ([Apache License v2.0 with LLVM Exceptions](test/bin/llvm/LICENSE.txt)).
77
92
 
78
93
  ### License
@@ -0,0 +1,583 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/sha1"
4
+ require "digest/sha2"
5
+
6
+ module MachO
7
+ # Structures and helpers for embedded Mach-O code signatures.
8
+ module CodeSigning
9
+ CSMAGIC_REQUIREMENT = 0xfade0c00
10
+ CSMAGIC_REQUIREMENTS = 0xfade0c01
11
+ CSMAGIC_CODEDIRECTORY = 0xfade0c02
12
+ CSMAGIC_BLOBWRAPPER = 0xfade0b01
13
+ CSMAGIC_EMBEDDED_SIGNATURE = 0xfade0cc0
14
+ CSMAGIC_DETACHED_SIGNATURE = 0xfade0cc1
15
+ CSMAGIC_EMBEDDED_ENTITLEMENTS = 0xfade7171
16
+ CSMAGIC_EMBEDDED_DER_ENTITLEMENTS = 0xfade7172
17
+ CSMAGIC_EMBEDDED_LAUNCH_CONSTRAINT = 0xfade8181
18
+ CSMAGIC_ENTITLEMENT = CSMAGIC_EMBEDDED_ENTITLEMENTS
19
+ CSMAGIC_ENTITLEMENTDER = CSMAGIC_EMBEDDED_DER_ENTITLEMENTS
20
+
21
+ CS_MAGICS = {
22
+ CSMAGIC_REQUIREMENT => :CSMAGIC_REQUIREMENT,
23
+ CSMAGIC_REQUIREMENTS => :CSMAGIC_REQUIREMENTS,
24
+ CSMAGIC_CODEDIRECTORY => :CSMAGIC_CODEDIRECTORY,
25
+ CSMAGIC_BLOBWRAPPER => :CSMAGIC_BLOBWRAPPER,
26
+ CSMAGIC_EMBEDDED_SIGNATURE => :CSMAGIC_EMBEDDED_SIGNATURE,
27
+ CSMAGIC_DETACHED_SIGNATURE => :CSMAGIC_DETACHED_SIGNATURE,
28
+ CSMAGIC_EMBEDDED_ENTITLEMENTS => :CSMAGIC_EMBEDDED_ENTITLEMENTS,
29
+ CSMAGIC_EMBEDDED_DER_ENTITLEMENTS => :CSMAGIC_EMBEDDED_DER_ENTITLEMENTS,
30
+ CSMAGIC_EMBEDDED_LAUNCH_CONSTRAINT => :CSMAGIC_EMBEDDED_LAUNCH_CONSTRAINT,
31
+ }.freeze
32
+
33
+ CSSLOT_CODEDIRECTORY = 0
34
+ CSSLOT_INFOSLOT = 1
35
+ CSSLOT_REQUIREMENTS = 2
36
+ CSSLOT_ENTITLEMENTS = 5
37
+ CSSLOT_DER_ENTITLEMENTS = 7
38
+ CSSLOT_ALTERNATE_CODEDIRECTORIES = 0x1000
39
+ CSSLOT_SIGNATURESLOT = 0x10000
40
+
41
+ CS_HASHTYPE_SHA1 = 1
42
+ CS_HASHTYPE_SHA256 = 2
43
+ CS_HASHTYPE_SHA256_TRUNCATED = 3
44
+ CS_HASHTYPE_SHA384 = 4
45
+
46
+ CS_HASHTYPES = {
47
+ CS_HASHTYPE_SHA1 => :CS_HASHTYPE_SHA1,
48
+ CS_HASHTYPE_SHA256 => :CS_HASHTYPE_SHA256,
49
+ CS_HASHTYPE_SHA256_TRUNCATED => :CS_HASHTYPE_SHA256_TRUNCATED,
50
+ CS_HASHTYPE_SHA384 => :CS_HASHTYPE_SHA384,
51
+ }.freeze
52
+
53
+ CS_ADHOC = 0x2
54
+ CS_HARD = 0x100
55
+ CS_RUNTIME = 0x10000
56
+ CS_LINKER_SIGNED = 0x20000
57
+ CS_EXECSEG_MAIN_BINARY = 0x1
58
+ CS_EXECSEG_JIT = 0x40
59
+
60
+ CS_SUPPORTSCODELIMIT64 = 0x20300
61
+ CS_SUPPORTSEXECSEG = 0x20400
62
+ CS_SUPPORTSRUNTIME = 0x20500
63
+ CS_SUPPORTSLINKAGE = 0x20600
64
+
65
+ PAGE_SIZE = 4096
66
+ PRESERVED_COMPONENT_SLOTS = [
67
+ CSSLOT_REQUIREMENTS,
68
+ CSSLOT_ENTITLEMENTS,
69
+ CSSLOT_DER_ENTITLEMENTS,
70
+ ].freeze
71
+
72
+ HASHES = {
73
+ CS_HASHTYPE_SHA1 => [Digest::SHA1, 20],
74
+ CS_HASHTYPE_SHA256 => [Digest::SHA256, 32],
75
+ }.freeze
76
+
77
+ # An entry in a code-signing SuperBlob index.
78
+ BlobIndex = Struct.new(:type, :offset)
79
+
80
+ # A generic code-signing blob.
81
+ class Blob
82
+ # @return [Integer] the blob magic
83
+ attr_reader :magic
84
+
85
+ # @return [Integer] the complete blob length
86
+ attr_reader :length
87
+
88
+ # Parses the concrete blob type represented by `data`.
89
+ # @param data [String] raw blob data
90
+ # @return [Blob]
91
+ def self.parse(data)
92
+ raise CodeSigningError, "code-signing blob is truncated" if data.nil? || data.bytesize < 8
93
+
94
+ case data.unpack1("N")
95
+ when CSMAGIC_CODEDIRECTORY
96
+ CodeDirectory.new(data)
97
+ when CSMAGIC_EMBEDDED_SIGNATURE
98
+ SuperBlob.new(data)
99
+ else
100
+ new(data)
101
+ end
102
+ end
103
+
104
+ # @param data [String] raw blob data
105
+ def initialize(data)
106
+ raise CodeSigningError, "code-signing blob is truncated" if data.nil? || data.bytesize < 8
107
+
108
+ @magic, @length = data.unpack("N2")
109
+ raise CodeSigningError, "invalid code-signing blob length: #{length}" if length < 8 || length > data.bytesize
110
+
111
+ @raw_data = data.byteslice(0, length).freeze
112
+ end
113
+
114
+ # @return [String] raw blob data
115
+ def serialize
116
+ @raw_data
117
+ end
118
+
119
+ # @return [Symbol, nil] the symbolic blob magic
120
+ def magic_sym
121
+ CS_MAGICS[magic]
122
+ end
123
+
124
+ # @return [Hash] a hash representation of this blob
125
+ def to_h
126
+ {
127
+ "magic" => magic,
128
+ "magic_sym" => magic_sym,
129
+ "length" => length,
130
+ }
131
+ end
132
+ end
133
+
134
+ # An embedded-signature SuperBlob.
135
+ class SuperBlob < Blob
136
+ # @return [Integer] the number of indexed blobs
137
+ attr_reader :count
138
+
139
+ # @return [Array<BlobIndex>] the blob index
140
+ attr_reader :indices
141
+
142
+ # @return [Array<Blob>] the indexed blobs
143
+ attr_reader :blobs
144
+
145
+ # Builds a SuperBlob containing the given slot/blob pairs.
146
+ # @param entries [Hash<Integer, String>] blobs keyed by slot
147
+ # @return [String] serialised SuperBlob data
148
+ def self.build(entries)
149
+ entries = entries.sort_by(&:first)
150
+ offset = 12 + (entries.size * 8)
151
+ index = +"".b
152
+ payload = +"".b
153
+ entries.each do |type, blob|
154
+ index << [type, offset].pack("N2")
155
+ payload << blob
156
+ offset += blob.bytesize
157
+ end
158
+
159
+ [CSMAGIC_EMBEDDED_SIGNATURE, offset, entries.size].pack("N3") + index + payload
160
+ end
161
+
162
+ # @param data [String] raw SuperBlob data
163
+ def initialize(data)
164
+ super
165
+ raise CodeSigningError, "invalid embedded-signature magic: 0x#{magic.to_s(16)}" unless magic == CSMAGIC_EMBEDDED_SIGNATURE
166
+ raise CodeSigningError, "code-signing SuperBlob is truncated" if length < 12
167
+
168
+ @count = serialize.unpack1("N", :offset => 8)
169
+ raise CodeSigningError, "code-signing SuperBlob index is truncated" if 12 + (count * 8) > length
170
+
171
+ @indices = count.times.map do |index|
172
+ BlobIndex.new(*serialize.unpack("N2", :offset => 12 + (index * 8)))
173
+ end.freeze
174
+ @blobs = indices.map do |entry|
175
+ raise CodeSigningError, "code-signing blob offset is invalid: #{entry.offset}" if entry.offset < 12 + (count * 8) || entry.offset + 8 > length
176
+
177
+ Blob.parse(serialize.byteslice(entry.offset, length - entry.offset))
178
+ end.freeze
179
+ end
180
+
181
+ # Returns the blob stored in `type`, if present.
182
+ # @param type [Integer] the slot type
183
+ # @return [Blob, nil]
184
+ def blob(type)
185
+ index = indices.index { |entry| entry.type == type }
186
+ blobs[index] if index
187
+ end
188
+
189
+ # Yields every index entry.
190
+ # @yieldparam index [BlobIndex]
191
+ # @return [Enumerator, void]
192
+ def each_blob_index(&block)
193
+ return indices.each unless block
194
+
195
+ indices.each(&block)
196
+ end
197
+
198
+ # Yields every indexed blob.
199
+ # @yieldparam blob [Blob]
200
+ # @return [Enumerator, void]
201
+ def each_blob(&block)
202
+ return blobs.each unless block
203
+
204
+ blobs.each(&block)
205
+ end
206
+
207
+ # @return [Hash] a hash representation of this SuperBlob
208
+ def to_h
209
+ {
210
+ "count" => count,
211
+ "indices" => indices.map { |entry| { "type" => entry.type, "offset" => entry.offset } },
212
+ "blobs" => blobs.map(&:to_h),
213
+ }.merge super
214
+ end
215
+ end
216
+
217
+ # A CodeDirectory describing signed code pages and special components.
218
+ class CodeDirectory < Blob
219
+ attr_reader :version, :flags, :hash_offset, :ident_offset,
220
+ :n_special_slots, :n_code_slots, :hash_size, :hash_type,
221
+ :platform, :page_size, :scatter_offset, :team_offset,
222
+ :code_limit64, :exec_seg_base, :exec_seg_limit,
223
+ :exec_seg_flags, :runtime, :pre_encrypt_offset,
224
+ :linkage_hash_type, :linkage_application_type,
225
+ :linkage_application_subtype, :linkage_offset, :linkage_size
226
+
227
+ # Builds a CodeDirectory for `source`.
228
+ # @param source [String] bytes before the embedded signature
229
+ # @param identifier [String] the signing identifier
230
+ # @param hash_type [Integer] the hash algorithm
231
+ # @param flags [Integer] CodeDirectory flags
232
+ # @param special_slots [Hash<Integer, String>] special-slot contents
233
+ # @param exec_seg_base [Integer] executable segment offset
234
+ # @param exec_seg_limit [Integer] executable segment size
235
+ # @param exec_seg_flags [Integer] executable segment flags
236
+ # @param runtime [Integer] hardened runtime version
237
+ # @param hashes [Boolean] whether to calculate hashes
238
+ # @return [String] serialised CodeDirectory data
239
+ def self.build(source, identifier:, hash_type:, flags:, special_slots:,
240
+ exec_seg_base:, exec_seg_limit:, exec_seg_flags:, runtime:, hashes: true)
241
+ digest, hash_size = HASHES.fetch(hash_type)
242
+ version = runtime.zero? ? CS_SUPPORTSEXECSEG : CS_SUPPORTSRUNTIME
243
+ fixed_size = runtime.zero? ? 88 : 96
244
+ identifier = "#{identifier.b.delete("\x00")}\x00".b
245
+ n_special_slots = special_slots.keys.max || 0
246
+ n_code_slots = (source.bytesize + PAGE_SIZE - 1) / PAGE_SIZE
247
+ hash_offset = fixed_size + identifier.bytesize + (n_special_slots * hash_size)
248
+ length = hash_offset + (n_code_slots * hash_size)
249
+ code_limit = [source.bytesize, 0xffffffff].min
250
+ code_limit64 = source.bytesize > 0xffffffff ? source.bytesize : 0
251
+
252
+ data = [CSMAGIC_CODEDIRECTORY, length, version, flags, hash_offset,
253
+ fixed_size, n_special_slots, n_code_slots, code_limit].pack("N9")
254
+ data << [hash_size, hash_type, 0, 12].pack("C4")
255
+ data << [0, 0, 0, 0].pack("N4")
256
+ data << [code_limit64, exec_seg_base, exec_seg_limit, exec_seg_flags].pack("Q>4")
257
+ data << [runtime, 0].pack("N2") unless runtime.zero?
258
+ data << identifier
259
+ if hashes
260
+ n_special_slots.downto(1) do |slot|
261
+ data << if special_slots.key?(slot)
262
+ digest.digest(special_slots.fetch(slot))
263
+ else
264
+ "\x00" * hash_size
265
+ end
266
+ end
267
+ 0.step(source.bytesize - 1, PAGE_SIZE) do |offset|
268
+ data << digest.digest(source.byteslice(offset, PAGE_SIZE))
269
+ end
270
+ else
271
+ data << Utils.nullpad((n_special_slots + n_code_slots) * hash_size)
272
+ end
273
+ data
274
+ end
275
+
276
+ # @param data [String] raw CodeDirectory data
277
+ def initialize(data)
278
+ super
279
+ raise CodeSigningError, "invalid CodeDirectory magic: 0x#{magic.to_s(16)}" unless magic == CSMAGIC_CODEDIRECTORY
280
+ raise CodeSigningError, "CodeDirectory is truncated" if length < 44
281
+
282
+ _, _, @version, @flags, @hash_offset, @ident_offset,
283
+ @n_special_slots, @n_code_slots, @code_limit = serialize.unpack("N9")
284
+ @hash_size, @hash_type, @platform, @page_size = serialize.unpack("C4", :offset => 36)
285
+ fixed_size = 44
286
+ @scatter_offset = unpack_uint32(44) if version >= 0x20100
287
+ fixed_size = 48 if version >= 0x20100
288
+ @team_offset = unpack_uint32(48) if version >= 0x20200
289
+ fixed_size = 52 if version >= 0x20200
290
+ if version >= CS_SUPPORTSCODELIMIT64
291
+ raise CodeSigningError, "CodeDirectory is truncated" if length < 64
292
+
293
+ @code_limit64 = serialize.unpack1("Q>", :offset => 56)
294
+ fixed_size = 64
295
+ end
296
+ if version >= CS_SUPPORTSEXECSEG
297
+ raise CodeSigningError, "CodeDirectory is truncated" if length < 88
298
+
299
+ @exec_seg_base, @exec_seg_limit, @exec_seg_flags = serialize.unpack("Q>3", :offset => 64)
300
+ fixed_size = 88
301
+ end
302
+ if version >= CS_SUPPORTSRUNTIME
303
+ raise CodeSigningError, "CodeDirectory is truncated" if length < 96
304
+
305
+ @runtime, @pre_encrypt_offset = serialize.unpack("N2", :offset => 88)
306
+ fixed_size = 96
307
+ end
308
+ if version >= CS_SUPPORTSLINKAGE
309
+ raise CodeSigningError, "CodeDirectory is truncated" if length < 108
310
+
311
+ @linkage_hash_type, @linkage_application_type,
312
+ @linkage_application_subtype, @linkage_offset,
313
+ @linkage_size = serialize.unpack("CCnN2", :offset => 96)
314
+ fixed_size = 108
315
+ end
316
+ raise CodeSigningError, "CodeDirectory identifier offset is invalid" if ident_offset < fixed_size || ident_offset >= length
317
+
318
+ terminator = serialize.index("\x00", ident_offset)
319
+ raise CodeSigningError, "CodeDirectory identifier is unterminated" unless terminator && terminator < length
320
+ if hash_size.zero? ||
321
+ hash_offset < fixed_size ||
322
+ hash_offset - (n_special_slots * hash_size) < terminator + 1 ||
323
+ hash_offset + (n_code_slots * hash_size) > length
324
+ raise CodeSigningError, "CodeDirectory hash range is invalid"
325
+ end
326
+
327
+ @identifier = serialize.byteslice(ident_offset, terminator - ident_offset)
328
+ end
329
+
330
+ # @return [String] the signing identifier
331
+ attr_reader :identifier
332
+
333
+ # @return [Integer] the number of signed bytes
334
+ def code_limit
335
+ version >= CS_SUPPORTSCODELIMIT64 && code_limit64&.positive? ? code_limit64 : @code_limit
336
+ end
337
+
338
+ # Returns a code page hash.
339
+ # @param slot [Integer] a zero-based page slot
340
+ # @return [String, nil]
341
+ def code_hash(slot)
342
+ return if slot.negative? || slot >= n_code_slots
343
+
344
+ serialize.byteslice(hash_offset + (slot * hash_size), hash_size)
345
+ end
346
+
347
+ # @return [Symbol, nil] the symbolic hash type
348
+ def hash_type_sym
349
+ CS_HASHTYPES[hash_type]
350
+ end
351
+
352
+ # Returns a special-slot hash.
353
+ # @param slot [Integer] a positive special-slot number
354
+ # @return [String, nil]
355
+ def special_hash(slot)
356
+ return unless slot.positive? && slot <= n_special_slots
357
+
358
+ serialize.byteslice(hash_offset - (slot * hash_size), hash_size)
359
+ end
360
+
361
+ # @return [Hash] a hash representation of this CodeDirectory
362
+ def to_h
363
+ {
364
+ "version" => version,
365
+ "flags" => flags,
366
+ "identifier" => identifier,
367
+ "hash_offset" => hash_offset,
368
+ "n_special_slots" => n_special_slots,
369
+ "n_code_slots" => n_code_slots,
370
+ "code_limit" => code_limit,
371
+ "hash_size" => hash_size,
372
+ "hash_type" => hash_type,
373
+ "hash_type_sym" => hash_type_sym,
374
+ "page_size" => page_size,
375
+ }.merge super
376
+ end
377
+
378
+ private
379
+
380
+ def unpack_uint32(offset)
381
+ raise CodeSigningError, "CodeDirectory is truncated" if length < offset + 4
382
+
383
+ serialize.unpack1("N", :offset => offset)
384
+ end
385
+ end
386
+
387
+ # Generates and embeds an ad-hoc signature in one Mach-O slice.
388
+ class AdhocSigner
389
+ # @param macho [MachOFile] the slice to sign
390
+ # @param identifier [String] the signing identifier
391
+ def initialize(macho, identifier)
392
+ @macho = macho
393
+ @identifier = identifier
394
+ end
395
+
396
+ # Embeds a new signature.
397
+ # @return [void]
398
+ def sign!
399
+ signature_commands = @macho[:LC_CODE_SIGNATURE]
400
+ raise CodeSigningError, "Mach-O contains multiple LC_CODE_SIGNATURE commands" if signature_commands.size > 1
401
+
402
+ signature_command = signature_commands.first
403
+ metadata = metadata_from(signature_command)
404
+ remove_signature(signature_command) if signature_command&.datasize&.positive?
405
+ add_signature_command unless signature_command
406
+
407
+ signature_command = @macho[:LC_CODE_SIGNATURE].first
408
+ linkedit_segments = @macho.segments.select { |segment| segment.segname == "__LINKEDIT" }
409
+ raise CodeSigningError, "Mach-O must contain exactly one __LINKEDIT segment" unless linkedit_segments.one?
410
+
411
+ linkedit = linkedit_segments.first
412
+
413
+ @macho.serialize << Utils.nullpad(Utils.padding_for(@macho.serialize.bytesize, 16))
414
+ dataoff = @macho.serialize.bytesize
415
+ raise CodeSigningError, "code signature offset exceeds LC_CODE_SIGNATURE" if dataoff > 0xffffffff
416
+
417
+ entries = signature_entries(metadata, :hashes => false)
418
+ datasize = Utils.round(SuperBlob.build(entries).bytesize, 16)
419
+
420
+ update_linkedit_data_command(signature_command, dataoff, datasize)
421
+ update_linkedit_segment(linkedit, dataoff + datasize)
422
+
423
+ entries = signature_entries(metadata)
424
+ signature = SuperBlob.build(entries)
425
+ @macho.serialize << signature << Utils.nullpad(datasize - signature.bytesize)
426
+ @macho.populate_fields
427
+ nil
428
+ end
429
+
430
+ private
431
+
432
+ def metadata_from(signature_command)
433
+ return default_metadata unless signature_command&.datasize&.positive?
434
+
435
+ superblob = signature_command.superblob
436
+ code_directory = superblob.blobs.grep(CodeDirectory).find { |blob| blob.hash_type == CS_HASHTYPE_SHA256 } ||
437
+ superblob.blobs.grep(CodeDirectory).first
438
+ return default_metadata unless code_directory
439
+ return default_metadata if code_directory.flags.anybits?(CS_LINKER_SIGNED)
440
+
441
+ components = PRESERVED_COMPONENT_SLOTS.to_h do |slot|
442
+ [slot, superblob.blob(slot)&.serialize]
443
+ end.compact
444
+ {
445
+ :components => components,
446
+ :exec_seg_flags => code_directory.exec_seg_flags.to_i,
447
+ :flags => (code_directory.flags & ~CS_LINKER_SIGNED) | CS_ADHOC,
448
+ :runtime => code_directory.runtime.to_i,
449
+ }
450
+ end
451
+
452
+ def default_metadata
453
+ {
454
+ :components => {},
455
+ :exec_seg_flags => 0,
456
+ :flags => CS_ADHOC,
457
+ :runtime => 0,
458
+ }
459
+ end
460
+
461
+ def remove_signature(signature_command)
462
+ end_offset = signature_command.dataoff + signature_command.datasize
463
+ trailing_data = @macho.serialize.byteslice(end_offset..).to_s
464
+ unless signature_command.dataoff <= @macho.serialize.bytesize &&
465
+ end_offset <= @macho.serialize.bytesize &&
466
+ trailing_data.bytesize <= 15 && trailing_data.bytes.all?(&:zero?)
467
+ raise CodeSigningError, "LC_CODE_SIGNATURE does not point to the end of the Mach-O"
468
+ end
469
+
470
+ @macho.serialize.slice!(signature_command.dataoff..)
471
+ @macho.populate_fields
472
+ end
473
+
474
+ def add_signature_command
475
+ @macho.add_command(LoadCommands::LoadCommand.create(:LC_CODE_SIGNATURE, 0, 0))
476
+ rescue ModificationError => e
477
+ raise CodeSigningError, e.message
478
+ end
479
+
480
+ def signature_entries(metadata, hashes: true)
481
+ components = metadata.fetch(:components).dup
482
+ components[CSSLOT_REQUIREMENTS] ||= [CSMAGIC_REQUIREMENTS, 12, 0].pack("N3")
483
+ special_slots = components.dup
484
+ if (info_plist = CodeSigning.info_plist(@macho))
485
+ special_slots[CSSLOT_INFOSLOT] = info_plist
486
+ end
487
+
488
+ text = @macho.segments.find { |segment| segment.segname == "__TEXT" }
489
+ exec_seg_flags = metadata.fetch(:exec_seg_flags)
490
+ if @macho.executable?
491
+ exec_seg_flags |= CS_EXECSEG_MAIN_BINARY
492
+ else
493
+ exec_seg_flags &= ~CS_EXECSEG_MAIN_BINARY
494
+ end
495
+ source = @macho.serialize
496
+ entries = {}
497
+ hash_types.each_with_index do |hash_type, index|
498
+ entries[index.zero? ? CSSLOT_CODEDIRECTORY : CSSLOT_ALTERNATE_CODEDIRECTORIES + index - 1] =
499
+ CodeDirectory.build(source,
500
+ :identifier => @identifier,
501
+ :hash_type => hash_type,
502
+ :flags => metadata.fetch(:flags),
503
+ :special_slots => special_slots,
504
+ :exec_seg_base => text&.fileoff.to_i,
505
+ :exec_seg_limit => text&.filesize.to_i,
506
+ :exec_seg_flags => exec_seg_flags,
507
+ :runtime => metadata.fetch(:runtime),
508
+ :hashes => hashes)
509
+ end
510
+ components.each { |slot, blob| entries[slot] = blob }
511
+ entries[CSSLOT_SIGNATURESLOT] = [CSMAGIC_BLOBWRAPPER, 8].pack("N2")
512
+ entries
513
+ end
514
+
515
+ def hash_types
516
+ version = @macho[:LC_VERSION_MIN_MACOSX].first&.version
517
+ build_version = @macho[:LC_BUILD_VERSION].first
518
+ version ||= build_version.minos if build_version&.platform == 1
519
+ version && version < 0x000a0b04 ? [CS_HASHTYPE_SHA1, CS_HASHTYPE_SHA256] : [CS_HASHTYPE_SHA256]
520
+ end
521
+
522
+ def update_linkedit_data_command(command, dataoff, datasize)
523
+ format = Utils.specialize_format("L=", @macho.endianness)
524
+ @macho.serialize[command.view.offset + 8, 8] = [dataoff, datasize].pack(format * 2)
525
+ end
526
+
527
+ def update_linkedit_segment(segment, final_size)
528
+ filesize = final_size - segment.fileoff
529
+ raise CodeSigningError, "__LINKEDIT extends past the end of the Mach-O" if filesize.negative?
530
+ raise CodeSigningError, "__LINKEDIT exceeds its 32-bit filesize" if @macho.magic32? && filesize > 0xffffffff
531
+
532
+ if @macho.magic64?
533
+ format = Utils.specialize_format("Q=", @macho.endianness)
534
+ @macho.serialize[segment.view.offset + 48, 8] = [filesize].pack(format)
535
+ @macho.serialize[segment.view.offset + 32, 8] = [Utils.round(filesize, 2**@macho.segment_alignment)].pack(format) if filesize > segment.vmsize
536
+ else
537
+ format = Utils.specialize_format("L=", @macho.endianness)
538
+ @macho.serialize[segment.view.offset + 36, 4] = [filesize].pack(format)
539
+ @macho.serialize[segment.view.offset + 28, 4] = [Utils.round(filesize, 2**@macho.segment_alignment)].pack(format) if filesize > segment.vmsize
540
+ end
541
+ end
542
+ end
543
+
544
+ # Derives the identifier used by Apple's ad-hoc signer for a bare Mach-O.
545
+ # @param macho [MachOFile] a Mach-O slice
546
+ # @param filename [String, nil] its filename
547
+ # @return [String]
548
+ def self.identifier(macho, filename)
549
+ if (match = info_plist(macho)&.match(%r{<key>\s*CFBundleIdentifier\s*</key>\s*<string>\s*([^<]+?)\s*</string>}m))
550
+ return match[1]
551
+ end
552
+
553
+ filename = File.basename(filename || "adhoc")
554
+ filename = File.basename(filename, File.extname(filename))
555
+ return filename if filename.include?(".")
556
+
557
+ # Apple hex-encodes either "UUID" plus LC_UUID or, for legacy inputs,
558
+ # SHA-1 of the base Mach header and load-command region.
559
+ # https://github.com/apple-oss-distributions/Security/blob/db15acbe6a7f257a859ad9a3bb86097bfe0679d9/OSX/libsecurity_codesigning/lib/machorep.cpp#L232-L264
560
+ # https://github.com/apple-oss-distributions/Security/blob/db15acbe6a7f257a859ad9a3bb86097bfe0679d9/OSX/libsecurity_codesigning/lib/signer.cpp#L1025-L1047
561
+ uuid = macho[:LC_UUID].first
562
+ identity = if uuid
563
+ ("UUID".b + uuid.uuid.pack("C*")).unpack1("H*")
564
+ else
565
+ digest = Digest::SHA1.new
566
+ digest << macho.serialize.byteslice(0, Headers::MachHeader.bytesize)
567
+ digest << macho.serialize.byteslice(macho.header.class.bytesize, macho.sizeofcmds)
568
+ digest.hexdigest
569
+ end
570
+ "#{filename}-#{identity}"
571
+ end
572
+
573
+ # Returns an embedded Info.plist, if present.
574
+ # @param macho [MachOFile] a Mach-O slice
575
+ # @return [String, nil]
576
+ def self.info_plist(macho)
577
+ section = macho.segments.flat_map(&:sections).find do |candidate|
578
+ candidate.segname == "__TEXT" && candidate.sectname == "__info_plist"
579
+ end
580
+ macho.serialize.byteslice(section.offset, section.size) if section
581
+ end
582
+ end
583
+ end
@@ -126,6 +126,14 @@ module MachO
126
126
  end
127
127
  end
128
128
 
129
+ # Raised when a load command has an invalid size.
130
+ class LoadCommandSizeError < NotAMachOError
131
+ # @param size [Integer] the invalid size
132
+ def initialize(size)
133
+ super("Invalid Mach-O load command size: #{size}")
134
+ end
135
+ end
136
+
129
137
  # Raised when a load command can't be created manually.
130
138
  class LoadCommandNotCreatableError < MachOError
131
139
  # @param cmd_sym [Symbol] the uncreatable load command's symbol
@@ -96,7 +96,12 @@ module MachO
96
96
 
97
97
  @filename = filename
98
98
  @options = opts
99
- @raw_data = File.binread(@filename)
99
+ File.open(@filename, "rb") do |file|
100
+ @raw_data = file.read(Headers::FatHeader.bytesize)
101
+ @raw_data ||= ""
102
+ populate_fat_header
103
+ @raw_data << file.read.to_s
104
+ end
100
105
  populate_fields
101
106
  end
102
107
 
@@ -165,7 +170,7 @@ module MachO
165
170
  # All load commands responsible for loading dylibs in the file's Mach-O's.
166
171
  # @return [Array<LoadCommands::DylibCommand>] an array of DylibCommands
167
172
  def dylib_load_commands
168
- machos.map(&:dylib_load_commands).flatten
173
+ machos.flat_map(&:dylib_load_commands)
169
174
  end
170
175
 
171
176
  # Changes the file's dylib ID to `new_id`. If the file is not a dylib,
@@ -199,7 +204,7 @@ module MachO
199
204
  # Individual architectures in a fat binary can link to different subsets
200
205
  # of libraries, but at this point we want to have the full picture, i.e.
201
206
  # the union of all libraries used by all architectures.
202
- machos.map(&:linked_dylibs).flatten.uniq
207
+ machos.flat_map(&:linked_dylibs).uniq
203
208
  end
204
209
 
205
210
  # Changes all dependent shared library install names from `old_name` to
@@ -229,7 +234,7 @@ module MachO
229
234
  # @see MachOFile#rpaths
230
235
  def rpaths
231
236
  # Can individual architectures have different runtime paths?
232
- machos.map(&:rpaths).flatten.uniq
237
+ machos.flat_map(&:rpaths).uniq
233
238
  end
234
239
 
235
240
  # Change the runtime path `old_path` to `new_path` in the file's Mach-Os.
@@ -283,6 +288,16 @@ module MachO
283
288
  repopulate_raw_machos
284
289
  end
285
290
 
291
+ # Replaces every embedded signature with a pure-Ruby ad-hoc signature.
292
+ # @param identifier [String, nil] the signing identifier
293
+ # @return [void]
294
+ def codesign!(identifier: nil)
295
+ identifier ||= CodeSigning.identifier(canonical_macho, filename)
296
+ machos.each { |macho| macho.codesign!(:identifier => identifier) }
297
+ repopulate_resized_raw_machos
298
+ nil
299
+ end
300
+
286
301
  # Extract a Mach-O with the given CPU type from the file.
287
302
  # @example
288
303
  # file.extract(:i386) # => MachO::MachOFile
@@ -402,6 +417,37 @@ module MachO
402
417
  end
403
418
  end
404
419
 
420
+ # Rebuild the fat header and slice layout after internal Mach-Os change size.
421
+ # {#repopulate_raw_machos} assumes slice lengths are unchanged and writes at
422
+ # their recorded ranges. Signing appends data, so every architecture's size
423
+ # and each following aligned offset must be recalculated.
424
+ # @return [void]
425
+ # @api private
426
+ def repopulate_resized_raw_machos
427
+ fat_arch_class = Utils.fat_magic32?(header.magic) ? Headers::FatArch : Headers::FatArch64
428
+ header_size = Headers::FatHeader.bytesize + (fat_archs.size * fat_arch_class.bytesize)
429
+ header_data = @raw_data.byteslice(0, header_size)
430
+ slices = +"".b
431
+ offset = header_size
432
+
433
+ fat_archs.zip(machos).each_with_index do |(arch, macho), index|
434
+ macho_offset = Utils.round(offset, 2**arch.align)
435
+ slices << Utils.nullpad(macho_offset - offset) << macho.serialize
436
+ arch_offset = Headers::FatHeader.bytesize + (index * fat_arch_class.bytesize)
437
+ if fat_arch_class == Headers::FatArch
438
+ raise FatArchOffsetOverflowError, macho_offset if macho_offset > 0xffffffff
439
+
440
+ header_data[arch_offset + 8, 8] = [macho_offset, macho.serialize.bytesize].pack("N2")
441
+ else
442
+ header_data[arch_offset + 8, 16] = [macho_offset, macho.serialize.bytesize].pack("Q>2")
443
+ end
444
+ offset = macho_offset + macho.serialize.bytesize
445
+ end
446
+
447
+ @raw_data = header_data + slices
448
+ populate_fields
449
+ end
450
+
405
451
  # Yield each Mach-O object in the file, rescuing and accumulating errors.
406
452
  # @param options [Hash]
407
453
  # @option options [Boolean] :strict (true) whether or not to fail loudly
@@ -69,6 +69,7 @@ module MachO
69
69
  0x37 => :LC_FUNCTION_VARIANTS,
70
70
  0x38 => :LC_FUNCTION_VARIANT_FIXUPS,
71
71
  0x39 => :LC_TARGET_TRIPLE,
72
+ 0x3a => :LC_LAZY_LOAD_DYLIB_INFO,
72
73
  }.freeze
73
74
 
74
75
  # association of symbol representations to load command constants
@@ -91,6 +92,7 @@ module MachO
91
92
  LC_ID_DYLIB
92
93
  LC_RPATH
93
94
  LC_LOAD_DYLINKER
95
+ LC_CODE_SIGNATURE
94
96
  ].freeze
95
97
 
96
98
  # association of load command symbols to string representations of classes
@@ -161,6 +163,7 @@ module MachO
161
163
  :LC_FUNCTION_VARIANTS => "LinkeditDataCommand",
162
164
  :LC_FUNCTION_VARIANT_FIXUPS => "LinkeditDataCommand",
163
165
  :LC_TARGET_TRIPLE => "TargetTripleCommand",
166
+ :LC_LAZY_LOAD_DYLIB_INFO => "LinkeditDataCommand",
164
167
  }.freeze
165
168
 
166
169
  # association of segment name symbols to names
@@ -330,6 +333,8 @@ module MachO
330
333
  view = lc.view
331
334
 
332
335
  if view
336
+ raise LCStrMalformedError, lc if lc_str < lc.class.bytesize || lc_str >= lc.cmdsize
337
+
333
338
  lc_str_abs = view.offset + lc_str
334
339
  lc_end = view.offset + lc.cmdsize - 1
335
340
  raw_string = view.raw_data.slice(lc_str_abs..lc_end)
@@ -1078,7 +1083,7 @@ module MachO
1078
1083
  # LC_SEGMENT_SPLIT_INFO, LC_FUNCTION_STARTS, LC_DATA_IN_CODE,
1079
1084
  # LC_DYLIB_CODE_SIGN_DRS, LC_LINKER_OPTIMIZATION_HINT, LC_DYLD_EXPORTS_TRIE,
1080
1085
  # LC_DYLD_CHAINED_FIXUPS, LC_ATOM_INFO, LC_FUNCTION_VARIANTS,
1081
- # or LC_FUNCTION_VARIANT_FIXUPS.
1086
+ # LC_FUNCTION_VARIANT_FIXUPS, or LC_LAZY_LOAD_DYLIB_INFO.
1082
1087
  class LinkeditDataCommand < LoadCommand
1083
1088
  # @return [Integer] offset to the data in the __LINKEDIT segment
1084
1089
  field :dataoff, :uint32
@@ -1086,6 +1091,25 @@ module MachO
1086
1091
  # @return [Integer] size of the data in the __LINKEDIT segment
1087
1092
  field :datasize, :uint32
1088
1093
 
1094
+ # @param context [SerializationContext] the context
1095
+ # @return [String] the serialized fields of the load command
1096
+ # @api private
1097
+ def serialize(context)
1098
+ raise LoadCommandNotSerializableError, type unless serializable?
1099
+
1100
+ format = Utils.specialize_format(self.class.format, context.endianness)
1101
+ [cmd, self.class.bytesize, dataoff, datasize].pack(format)
1102
+ end
1103
+
1104
+ # The embedded signature referenced by this command.
1105
+ # @return [CodeSigning::SuperBlob]
1106
+ # @raise [CodeSigningError] if this is not an LC_CODE_SIGNATURE command
1107
+ def superblob
1108
+ raise CodeSigningError, "#{type} does not contain a code signature" unless type == :LC_CODE_SIGNATURE
1109
+
1110
+ CodeSigning::SuperBlob.new(view.raw_data.byteslice(dataoff, datasize))
1111
+ end
1112
+
1089
1113
  # @return [Hash] a hash representation of this {LinkeditDataCommand}
1090
1114
  def to_h
1091
1115
  {
@@ -60,7 +60,12 @@ module MachO
60
60
 
61
61
  @filename = filename
62
62
  @options = opts
63
- @raw_data = File.binread(@filename)
63
+ File.open(@filename, "rb") do |file|
64
+ @raw_data = file.read(Headers::MachHeader.bytesize)
65
+ @raw_data ||= ""
66
+ populate_mach_header if !opts.fetch(:decompress, false) || !Utils.compressed_magic?(@raw_data.unpack1("N"))
67
+ @raw_data << file.read.to_s
68
+ end
64
69
  populate_fields
65
70
  end
66
71
 
@@ -147,7 +152,7 @@ module MachO
147
152
  # @return [Array<LoadCommands::LoadCommand>] an array of load commands
148
153
  # corresponding to `name`
149
154
  def command(name)
150
- load_commands.select { |lc| lc.type == name.to_sym }
155
+ @load_commands_by_type.fetch(name.to_sym, []).dup
151
156
  end
152
157
 
153
158
  alias [] command
@@ -245,6 +250,7 @@ module MachO
245
250
  # The exception to this rule is when methods like {#add_command} and
246
251
  # {#delete_command} have been called with `repopulate = false`.
247
252
  def populate_fields
253
+ clear_memoization_cache
248
254
  @header = populate_mach_header
249
255
  @load_commands = populate_load_commands
250
256
  end
@@ -252,7 +258,8 @@ module MachO
252
258
  # All load commands responsible for loading dylibs.
253
259
  # @return [Array<LoadCommands::DylibCommand>] an array of DylibCommands
254
260
  def dylib_load_commands
255
- load_commands.select { |lc| LoadCommands::DYLIB_LOAD_COMMANDS.include?(lc.type) }
261
+ @dylib_load_commands ||= load_commands.select { |lc| LoadCommands::DYLIB_LOAD_COMMANDS.include?(lc.type) }
262
+ @dylib_load_commands.dup
256
263
  end
257
264
 
258
265
  # All segment load commands in the Mach-O.
@@ -271,26 +278,7 @@ module MachO
271
278
  # @note This is **not** the same as {#alignment}!
272
279
  # @note See `get_align` and `get_align_64` in `cctools/misc/lipo.c`
273
280
  def segment_alignment
274
- # special cases: 12 for x86/64/PPC/PP64, 14 for ARM/ARM64
275
- return 12 if %i[i386 x86_64 ppc ppc64].include?(cputype)
276
- return 14 if %i[arm arm64].include?(cputype)
277
-
278
- cur_align = Sections::MAX_SECT_ALIGN
279
-
280
- segments.each do |segment|
281
- if filetype == :object
282
- # start with the smallest alignment, and work our way up
283
- align = magic32? ? 2 : 3
284
- segment.sections.each do |section|
285
- align = section.align unless section.align <= align
286
- end
287
- else
288
- align = segment.guess_align
289
- end
290
- cur_align = align if align < cur_align
291
- end
292
-
293
- cur_align
281
+ @segment_alignment ||= calculate_segment_alignment
294
282
  end
295
283
 
296
284
  # The Mach-O's dylib ID, or `nil` if not a dylib.
@@ -338,7 +326,8 @@ module MachO
338
326
  # library, but at this point we're really only interested in a list of
339
327
  # unique libraries this Mach-O file links to, thus: `uniq`. (This is also
340
328
  # for consistency with `FatFile` that merges this list across all archs.)
341
- dylib_load_commands.map(&:name).map(&:to_s).uniq
329
+ @linked_dylibs ||= dylib_load_commands.map { |lc| lc.name.to_s }.uniq
330
+ @linked_dylibs.dup
342
331
  end
343
332
 
344
333
  # Changes the shared library `old_name` to `new_name`
@@ -368,7 +357,8 @@ module MachO
368
357
  # All runtime paths searched by the dynamic linker for the Mach-O.
369
358
  # @return [Array<String>] an array of all runtime paths
370
359
  def rpaths
371
- command(:LC_RPATH).map(&:path).map(&:to_s)
360
+ @rpaths ||= command(:LC_RPATH).map { |lc| lc.path.to_s }
361
+ @rpaths.dup
372
362
  end
373
363
 
374
364
  # Changes the runtime path `old_path` to `new_path`
@@ -448,6 +438,13 @@ module MachO
448
438
  rpath_cmds.reverse_each { |cmd| delete_command(cmd) }
449
439
  end
450
440
 
441
+ # Replaces the embedded signature with a pure-Ruby ad-hoc signature.
442
+ # @param identifier [String, nil] the signing identifier
443
+ # @return [void]
444
+ def codesign!(identifier: nil)
445
+ CodeSigning::AdhocSigner.new(self, identifier || CodeSigning.identifier(self, filename)).sign!
446
+ end
447
+
451
448
  # Write all Mach-O data to the given filename.
452
449
  # @param filename [String] the file to write to
453
450
  # @return [void]
@@ -475,6 +472,17 @@ module MachO
475
472
 
476
473
  private
477
474
 
475
+ # Clears all memoized values. Called when the file is repopulated.
476
+ # @return [void]
477
+ # @api private
478
+ def clear_memoization_cache
479
+ @linked_dylibs = nil
480
+ @rpaths = nil
481
+ @dylib_load_commands = nil
482
+ @load_commands_by_type = nil
483
+ @segment_alignment = nil
484
+ end
485
+
478
486
  # The file's Mach-O header structure.
479
487
  # @return [Headers::MachHeader] if the Mach-O is 32-bit
480
488
  # @return [Headers::MachHeader64] if the Mach-O is 64-bit
@@ -583,16 +591,27 @@ module MachO
583
591
 
584
592
  # All load commands in the file.
585
593
  # @return [Array<LoadCommands::LoadCommand>] an array of load commands
594
+ # @raise [TruncatedFileError] if the declared load command data is incomplete
586
595
  # @raise [LoadCommandError] if an unknown load command is encountered
596
+ # @raise [LoadCommandSizeError] if a load command's size is invalid
587
597
  # @api private
588
598
  def populate_load_commands
589
599
  permissive = options.fetch(:permissive, false)
590
600
  offset = header.class.bytesize
601
+ load_commands_end = offset + sizeofcmds
602
+ raise TruncatedFileError if load_commands_end > @raw_data.bytesize
603
+
591
604
  load_commands = []
605
+ @load_commands_by_type = Hash.new { |h, k| h[k] = [] }
592
606
 
593
607
  header.ncmds.times do
608
+ raise TruncatedFileError if offset + LoadCommands::LoadCommand.bytesize > load_commands_end
609
+
594
610
  fmt = Utils.specialize_format("L=", endianness)
595
611
  cmd = @raw_data.slice(offset, 4).unpack1(fmt)
612
+ cmdsize = @raw_data.slice(offset + 4, 4).unpack1(fmt)
613
+ raise LoadCommandSizeError, cmdsize if cmdsize % 4 != 0 || offset + cmdsize > load_commands_end
614
+
596
615
  cmd_sym = LoadCommands::LOAD_COMMANDS[cmd]
597
616
 
598
617
  raise LoadCommandError, cmd unless cmd_sym || permissive
@@ -605,16 +624,45 @@ module MachO
605
624
  LoadCommands::LoadCommand
606
625
  end
607
626
 
627
+ raise LoadCommandSizeError, cmdsize if cmdsize < klass.bytesize
628
+
608
629
  view = MachOView.new(self, @raw_data, endianness, offset)
609
630
  command = klass.new_from_bin(view)
610
631
 
611
632
  load_commands << command
633
+ @load_commands_by_type[command.type] << command
612
634
  offset += command.cmdsize
613
635
  end
614
636
 
615
637
  load_commands
616
638
  end
617
639
 
640
+ # Calculate the segment alignment for the Mach-O. Guesses conservatively.
641
+ # @return [Integer] the alignment, as a power of 2
642
+ # @api private
643
+ def calculate_segment_alignment
644
+ # special cases: 12 for x86/64/PPC/PP64, 14 for ARM/ARM64
645
+ return 12 if %i[i386 x86_64 ppc ppc64].include?(cputype)
646
+ return 14 if %i[arm arm64].include?(cputype)
647
+
648
+ cur_align = Sections::MAX_SECT_ALIGN
649
+
650
+ segments.each do |segment|
651
+ if filetype == :object
652
+ # start with the smallest alignment, and work our way up
653
+ align = magic32? ? 2 : 3
654
+ segment.sections.each do |section|
655
+ align = section.align unless section.align <= align
656
+ end
657
+ else
658
+ align = segment.guess_align
659
+ end
660
+ cur_align = align if align < cur_align
661
+ end
662
+
663
+ cur_align
664
+ end
665
+
618
666
  # The low file offset (offset to first section data).
619
667
  # @return [Integer] the offset
620
668
  # @api private
@@ -622,6 +670,9 @@ module MachO
622
670
  offset = @raw_data.size
623
671
 
624
672
  segments.each do |seg|
673
+ offset = seg.fileoff if seg.nsects.zero? && seg.fileoff.positive? &&
674
+ seg.filesize.positive? && seg.fileoff < offset
675
+
625
676
  seg.sections.each do |sect|
626
677
  next if sect.empty?
627
678
  next if sect.type?(:S_ZEROFILL)
data/lib/macho.rb CHANGED
@@ -1,11 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "open3"
4
-
5
3
  require_relative "macho/utils"
6
4
  require_relative "macho/structure"
7
5
  require_relative "macho/view"
8
6
  require_relative "macho/headers"
7
+ require_relative "macho/code_signing"
9
8
  require_relative "macho/load_commands"
10
9
  require_relative "macho/sections"
11
10
  require_relative "macho/macho_file"
@@ -16,7 +15,7 @@ require_relative "macho/tools"
16
15
  # The primary namespace for ruby-macho.
17
16
  module MachO
18
17
  # release version
19
- VERSION = "5.0.0"
18
+ VERSION = "6.0.0"
20
19
 
21
20
  # Opens the given filename as a MachOFile or FatFile, depending on its magic.
22
21
  # @param filename [String] the file being opened
@@ -42,20 +41,20 @@ module MachO
42
41
  file
43
42
  end
44
43
 
45
- # Signs the dylib using an ad-hoc identity.
46
- # Necessary after making any changes to a dylib, since otherwise
47
- # changing a signed file invalidates its signature.
44
+ # Signs a thin or fat Mach-O using an ad-hoc identity.
45
+ # Necessary after changing signed Mach-O data because the signature covers
46
+ # the header, load commands and all bytes preceding the signature.
48
47
  # @param filename [String] the file being opened
49
48
  # @return [void]
50
- # @raise [ModificationError] if the operation fails
49
+ # @raise [CodeSigningError] if the operation fails
51
50
  def self.codesign!(filename)
52
- raise ArgumentError, "codesign binary is not available on Linux" if RUBY_PLATFORM !~ /darwin/
53
51
  raise ArgumentError, "#{filename}: no such file" unless File.file?(filename)
54
52
 
55
- _, _, status = Open3.capture3("codesign", "--sign", "-", "--force",
56
- "--preserve-metadata=entitlements,requirements,flags,runtime",
57
- filename)
58
-
59
- raise CodeSigningError, "#{filename}: signing failed!" unless status.success?
53
+ file = MachO.open(filename)
54
+ file.codesign!
55
+ file.write!
56
+ nil
57
+ rescue MachOError => e
58
+ raise CodeSigningError, "#{filename}: signing failed: #{e.message}"
60
59
  end
61
60
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-macho
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.0
4
+ version: 6.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - William Woodruff
@@ -19,6 +19,7 @@ files:
19
19
  - LICENSE
20
20
  - README.md
21
21
  - lib/macho.rb
22
+ - lib/macho/code_signing.rb
22
23
  - lib/macho/exceptions.rb
23
24
  - lib/macho/fat_file.rb
24
25
  - lib/macho/headers.rb
@@ -49,7 +50,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
49
50
  - !ruby/object:Gem::Version
50
51
  version: '0'
51
52
  requirements: []
52
- rubygems_version: 4.0.3
53
+ rubygems_version: 4.0.10
53
54
  specification_version: 4
54
55
  summary: ruby-macho - Mach-O file analyzer.
55
56
  test_files: []