ruby-macho 4.1.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 +17 -2
- data/lib/macho/code_signing.rb +583 -0
- data/lib/macho/exceptions.rb +39 -31
- data/lib/macho/fat_file.rb +50 -4
- data/lib/macho/headers.rb +21 -9
- data/lib/macho/load_commands.rb +48 -3
- data/lib/macho/macho_file.rb +76 -25
- data/lib/macho/structure.rb +1 -1
- data/lib/macho.rb +12 -13
- metadata +6 -7
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
|
@@ -3,7 +3,7 @@ ruby-macho
|
|
|
3
3
|
|
|
4
4
|
[](http://badge.fury.io/rb/ruby-macho)
|
|
5
5
|
[](https://github.com/Homebrew/ruby-macho/actions/workflows/tests.yml)
|
|
6
|
-
[](https://codecov.io/gh/Homebrew/ruby-macho)
|
|
7
7
|
|
|
8
8
|
A Ruby library for examining and modifying Mach-O files.
|
|
9
9
|
|
|
@@ -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
|