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 +4 -4
- data/README.md +16 -1
- data/lib/macho/code_signing.rb +583 -0
- data/lib/macho/exceptions.rb +8 -0
- data/lib/macho/fat_file.rb +50 -4
- data/lib/macho/load_commands.rb +25 -1
- data/lib/macho/macho_file.rb +76 -25
- data/lib/macho.rb +12 -13
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b0a72ccdc56030f0b2d9ed6fae89837685eea3397361c2fcb2c06d33409b79a7
|
|
4
|
+
data.tar.gz: f7a9a49865c6bb11b0edb6579d4fe8291b70a8077999ef3225d13cfde9d1180f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/macho/exceptions.rb
CHANGED
|
@@ -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
|
data/lib/macho/fat_file.rb
CHANGED
|
@@ -96,7 +96,12 @@ module MachO
|
|
|
96
96
|
|
|
97
97
|
@filename = filename
|
|
98
98
|
@options = opts
|
|
99
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
data/lib/macho/load_commands.rb
CHANGED
|
@@ -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
|
|
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
|
{
|
data/lib/macho/macho_file.rb
CHANGED
|
@@ -60,7 +60,12 @@ module MachO
|
|
|
60
60
|
|
|
61
61
|
@filename = filename
|
|
62
62
|
@options = opts
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 = "
|
|
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
|
|
46
|
-
# Necessary after
|
|
47
|
-
#
|
|
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 [
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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:
|
|
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.
|
|
53
|
+
rubygems_version: 4.0.10
|
|
53
54
|
specification_version: 4
|
|
54
55
|
summary: ruby-macho - Mach-O file analyzer.
|
|
55
56
|
test_files: []
|