ruby-macho 2.5.1 → 3.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 +1 -1
- data/lib/macho/exceptions.rb +15 -5
- data/lib/macho/fat_file.rb +9 -4
- data/lib/macho/headers.rb +109 -0
- data/lib/macho/load_commands.rb +48 -2
- data/lib/macho/macho_file.rb +76 -18
- data/lib/macho/sections.rb +46 -6
- data/lib/macho/tools.rb +4 -0
- data/lib/macho/utils.rb +7 -0
- data/lib/macho.rb +1 -1
- metadata +7 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ba2fc7b81345cb788c6e1de15bace8d35f7ac1f9a17b6b6a12289a4e51b978de
|
4
|
+
data.tar.gz: 395dd63235823f33f11463d395b790b1cee2003ab430bb54326c03ca3de402b8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c048978f3e0b1becaa3b050db8730f2f914cc8a8d662e1e7d24ca0a24c03c043747485e7e6033bad5789069795d8c04daad053d817f26c54dd809082975a01fe
|
7
|
+
data.tar.gz: 53a576e666842a464af2f961825becfa51da0b26f3dd9ec656ce9859a6c0e9c017fc6622ec330bb0fc04e63089053a6ecd3740f505eb0e5aefecfb5728672d72
|
data/README.md
CHANGED
@@ -59,7 +59,7 @@ puts lc_vers.version_string # => "10.10.0"
|
|
59
59
|
Attribution:
|
60
60
|
|
61
61
|
* Constants were taken from Apple, Inc's
|
62
|
-
[`loader.h` in `cctools/include/mach-o`](https://
|
62
|
+
[`loader.h` in `cctools/include/mach-o`](https://opensource.apple.com/source/cctools/cctools-973.0.1/include/mach-o/loader.h.auto.html).
|
63
63
|
(Apple Public Source License 2.0).
|
64
64
|
|
65
65
|
### License
|
data/lib/macho/exceptions.rb
CHANGED
@@ -84,7 +84,7 @@ module MachO
|
|
84
84
|
# @param cpusubtype [Integer] the CPU sub-type of the unknown pair
|
85
85
|
def initialize(cputype, cpusubtype)
|
86
86
|
super "Unrecognized CPU sub-type: 0x%08<cpusubtype>x" \
|
87
|
-
|
87
|
+
" (for CPU type: 0x%08<cputype>x" % { :cputype => cputype, :cpusubtype => cpusubtype }
|
88
88
|
end
|
89
89
|
end
|
90
90
|
|
@@ -120,7 +120,7 @@ module MachO
|
|
120
120
|
# @param actual_arity [Integer] the number of arguments received
|
121
121
|
def initialize(cmd_sym, expected_arity, actual_arity)
|
122
122
|
super "Expected #{expected_arity} arguments for #{cmd_sym} creation," \
|
123
|
-
|
123
|
+
" got #{actual_arity}"
|
124
124
|
end
|
125
125
|
end
|
126
126
|
|
@@ -137,7 +137,7 @@ module MachO
|
|
137
137
|
# @param lc [MachO::LoadCommand] the load command containing the string
|
138
138
|
def initialize(lc)
|
139
139
|
super "Load command #{lc.type} at offset #{lc.view.offset} contains a" \
|
140
|
-
|
140
|
+
" malformed string"
|
141
141
|
end
|
142
142
|
end
|
143
143
|
|
@@ -154,8 +154,8 @@ module MachO
|
|
154
154
|
# @param filename [String] the filename
|
155
155
|
def initialize(filename)
|
156
156
|
super "Updated load commands do not fit in the header of " \
|
157
|
-
|
158
|
-
|
157
|
+
"#{filename}. #{filename} needs to be relinked, possibly with " \
|
158
|
+
"-headerpad or -headerpad_max_install_names"
|
159
159
|
end
|
160
160
|
end
|
161
161
|
|
@@ -207,4 +207,14 @@ module MachO
|
|
207
207
|
" Consider merging with `fat64: true`"
|
208
208
|
end
|
209
209
|
end
|
210
|
+
|
211
|
+
# Raised when attempting to parse a compressed Mach-O without explicitly
|
212
|
+
# requesting decompression.
|
213
|
+
class CompressedMachOError < MachOError
|
214
|
+
end
|
215
|
+
|
216
|
+
# Raised when attempting to decompress a compressed Mach-O without adequate
|
217
|
+
# dependencies, or on other decompression errors.
|
218
|
+
class DecompressionError < MachOError
|
219
|
+
end
|
210
220
|
end
|
data/lib/macho/fat_file.rb
CHANGED
@@ -55,7 +55,7 @@ module MachO
|
|
55
55
|
machos.each do |macho|
|
56
56
|
macho_offset = Utils.round(offset, 2**macho.segment_alignment)
|
57
57
|
|
58
|
-
raise FatArchOffsetOverflowError, macho_offset if !fat64 && macho_offset > (2**32 - 1)
|
58
|
+
raise FatArchOffsetOverflowError, macho_offset if !fat64 && macho_offset > ((2**32) - 1)
|
59
59
|
|
60
60
|
macho_pads[macho] = Utils.padding_for(offset, 2**macho.segment_alignment)
|
61
61
|
|
@@ -96,7 +96,7 @@ module MachO
|
|
96
96
|
|
97
97
|
@filename = filename
|
98
98
|
@options = opts
|
99
|
-
@raw_data = File.
|
99
|
+
@raw_data = File.binread(@filename)
|
100
100
|
populate_fields
|
101
101
|
end
|
102
102
|
|
@@ -238,6 +238,8 @@ module MachO
|
|
238
238
|
# @param options [Hash]
|
239
239
|
# @option options [Boolean] :strict (true) if true, fail if one slice fails.
|
240
240
|
# if false, fail only if all slices fail.
|
241
|
+
# @option options [Boolean] :uniq (false) for each slice: if true, change
|
242
|
+
# each rpath simultaneously.
|
241
243
|
# @return [void]
|
242
244
|
# @see MachOFile#change_rpath
|
243
245
|
def change_rpath(old_path, new_path, options = {})
|
@@ -268,6 +270,9 @@ module MachO
|
|
268
270
|
# @param options [Hash]
|
269
271
|
# @option options [Boolean] :strict (true) if true, fail if one slice fails.
|
270
272
|
# if false, fail only if all slices fail.
|
273
|
+
# @option options [Boolean] :uniq (false) for each slice: if true, delete
|
274
|
+
# only the first runtime path that matches. if false, delete all duplicate
|
275
|
+
# paths that match.
|
271
276
|
# @return void
|
272
277
|
# @see MachOFile#delete_rpath
|
273
278
|
def delete_rpath(path, options = {})
|
@@ -291,7 +296,7 @@ module MachO
|
|
291
296
|
# @param filename [String] the file to write to
|
292
297
|
# @return [void]
|
293
298
|
def write(filename)
|
294
|
-
File.
|
299
|
+
File.binwrite(filename, @raw_data)
|
295
300
|
end
|
296
301
|
|
297
302
|
# Write all (fat) data to the file used to initialize the instance.
|
@@ -301,7 +306,7 @@ module MachO
|
|
301
306
|
def write!
|
302
307
|
raise MachOError, "no initial file to write to" if filename.nil?
|
303
308
|
|
304
|
-
File.
|
309
|
+
File.binwrite(@filename, @raw_data)
|
305
310
|
end
|
306
311
|
|
307
312
|
# @return [Hash] a hash representation of this {FatFile}
|
data/lib/macho/headers.rb
CHANGED
@@ -37,6 +37,18 @@ module MachO
|
|
37
37
|
# @api private
|
38
38
|
MH_CIGAM_64 = 0xcffaedfe
|
39
39
|
|
40
|
+
# compressed mach-o magic
|
41
|
+
# @api private
|
42
|
+
COMPRESSED_MAGIC = 0x636f6d70 # "comp"
|
43
|
+
|
44
|
+
# a compressed mach-o slice, using LZSS for compression
|
45
|
+
# @api private
|
46
|
+
COMP_TYPE_LZSS = 0x6c7a7373 # "lzss"
|
47
|
+
|
48
|
+
# a compressed mach-o slice, using LZVN ("FastLib") for compression
|
49
|
+
# @api private
|
50
|
+
COMP_TYPE_FASTLIB = 0x6c7a766e # "lzvn"
|
51
|
+
|
40
52
|
# association of magic numbers to string representations
|
41
53
|
# @api private
|
42
54
|
MH_MAGICS = {
|
@@ -433,6 +445,11 @@ module MachO
|
|
433
445
|
# @api private
|
434
446
|
MH_KEXT_BUNDLE = 0xb
|
435
447
|
|
448
|
+
# a set of Mach-Os, running in the same userspace, sharing a linkedit. The kext collection files are an example
|
449
|
+
# of this object type
|
450
|
+
# @api private
|
451
|
+
MH_FILESET = 0xc
|
452
|
+
|
436
453
|
# association of filetypes to Symbol representations
|
437
454
|
# @api private
|
438
455
|
MH_FILETYPES = {
|
@@ -447,6 +464,7 @@ module MachO
|
|
447
464
|
MH_DYLIB_STUB => :dylib_stub,
|
448
465
|
MH_DSYM => :dsym,
|
449
466
|
MH_KEXT_BUNDLE => :kext_bundle,
|
467
|
+
MH_FILESET => :fileset,
|
450
468
|
}.freeze
|
451
469
|
|
452
470
|
# association of mach header flag symbols to values
|
@@ -478,6 +496,9 @@ module MachO
|
|
478
496
|
:MH_HAS_TLV_DESCRIPTORS => 0x800000,
|
479
497
|
:MH_NO_HEAP_EXECUTION => 0x1000000,
|
480
498
|
:MH_APP_EXTENSION_SAFE => 0x02000000,
|
499
|
+
:MH_NLIST_OUTOFSYNC_WITH_DYLDINFO => 0x04000000,
|
500
|
+
:MH_SIM_SUPPORT => 0x08000000,
|
501
|
+
:MH_DYLIB_IN_CACHE => 0x80000000,
|
481
502
|
}.freeze
|
482
503
|
|
483
504
|
# Fat binary header structure
|
@@ -724,6 +745,11 @@ module MachO
|
|
724
745
|
filetype == Headers::MH_KEXT_BUNDLE
|
725
746
|
end
|
726
747
|
|
748
|
+
# @return [Boolean] whether or not the file is of type `MH_FILESET`
|
749
|
+
def fileset?
|
750
|
+
filetype == Headers::MH_FILESET
|
751
|
+
end
|
752
|
+
|
727
753
|
# @return [Boolean] true if the Mach-O has 32-bit magic, false otherwise
|
728
754
|
def magic32?
|
729
755
|
Utils.magic32?(magic)
|
@@ -785,5 +811,88 @@ module MachO
|
|
785
811
|
}.merge super
|
786
812
|
end
|
787
813
|
end
|
814
|
+
|
815
|
+
# Prelinked kernel/"kernelcache" header structure
|
816
|
+
class PrelinkedKernelHeader < MachOStructure
|
817
|
+
# @return [Integer] the magic number for a compressed header ({COMPRESSED_MAGIC})
|
818
|
+
attr_reader :signature
|
819
|
+
|
820
|
+
# @return [Integer] the type of compression used
|
821
|
+
attr_reader :compress_type
|
822
|
+
|
823
|
+
# @return [Integer] a checksum for the uncompressed data
|
824
|
+
attr_reader :adler32
|
825
|
+
|
826
|
+
# @return [Integer] the size of the uncompressed data, in bytes
|
827
|
+
attr_reader :uncompressed_size
|
828
|
+
|
829
|
+
# @return [Integer] the size of the compressed data, in bytes
|
830
|
+
attr_reader :compressed_size
|
831
|
+
|
832
|
+
# @return [Integer] the version of the prelink format
|
833
|
+
attr_reader :prelink_version
|
834
|
+
|
835
|
+
# @return [void]
|
836
|
+
attr_reader :reserved
|
837
|
+
|
838
|
+
# @return [void]
|
839
|
+
attr_reader :platform_name
|
840
|
+
|
841
|
+
# @return [void]
|
842
|
+
attr_reader :root_path
|
843
|
+
|
844
|
+
# @see MachOStructure::FORMAT
|
845
|
+
# @api private
|
846
|
+
FORMAT = "L>6a40a64a256"
|
847
|
+
|
848
|
+
# @see MachOStructure::SIZEOF
|
849
|
+
# @api private
|
850
|
+
SIZEOF = 384
|
851
|
+
|
852
|
+
# @api private
|
853
|
+
def initialize(signature, compress_type, adler32, uncompressed_size, compressed_size, prelink_version, reserved, platform_name, root_path)
|
854
|
+
super()
|
855
|
+
|
856
|
+
@signature = signature
|
857
|
+
@compress_type = compress_type
|
858
|
+
@adler32 = adler32
|
859
|
+
@uncompressed_size = uncompressed_size
|
860
|
+
@compressed_size = compressed_size
|
861
|
+
@prelink_version = prelink_version
|
862
|
+
@reserved = reserved.unpack("L>10")
|
863
|
+
@platform_name = platform_name
|
864
|
+
@root_path = root_path
|
865
|
+
end
|
866
|
+
|
867
|
+
# @return [Boolean] whether this prelinked kernel supports KASLR
|
868
|
+
def kaslr?
|
869
|
+
prelink_version >= 1
|
870
|
+
end
|
871
|
+
|
872
|
+
# @return [Boolean] whether this prelinked kernel is compressed with LZSS
|
873
|
+
def lzss?
|
874
|
+
compress_type == COMP_TYPE_LZSS
|
875
|
+
end
|
876
|
+
|
877
|
+
# @return [Boolean] whether this prelinked kernel is compressed with LZVN
|
878
|
+
def lzvn?
|
879
|
+
compress_type == COMP_TYPE_FASTLIB
|
880
|
+
end
|
881
|
+
|
882
|
+
# @return [Hash] a hash representation of this {PrelinkedKernelHeader}
|
883
|
+
def to_h
|
884
|
+
{
|
885
|
+
"signature" => signature,
|
886
|
+
"compress_type" => compress_type,
|
887
|
+
"adler32" => adler32,
|
888
|
+
"uncompressed_size" => uncompressed_size,
|
889
|
+
"compressed_size" => compressed_size,
|
890
|
+
"prelink_version" => prelink_version,
|
891
|
+
"reserved" => reserved,
|
892
|
+
"platform_name" => platform_name,
|
893
|
+
"root_path" => root_path,
|
894
|
+
}.merge super
|
895
|
+
end
|
896
|
+
end
|
788
897
|
end
|
789
898
|
end
|
data/lib/macho/load_commands.rb
CHANGED
@@ -63,7 +63,8 @@ module MachO
|
|
63
63
|
0x31 => :LC_NOTE,
|
64
64
|
0x32 => :LC_BUILD_VERSION,
|
65
65
|
(0x33 | LC_REQ_DYLD) => :LC_DYLD_EXPORTS_TRIE,
|
66
|
-
(0x34 | LC_REQ_DYLD) => :
|
66
|
+
(0x34 | LC_REQ_DYLD) => :LC_DYLD_CHAINED_FIXUPS,
|
67
|
+
(0x35 | LC_REQ_DYLD) => :LC_FILESET_ENTRY,
|
67
68
|
}.freeze
|
68
69
|
|
69
70
|
# association of symbol representations to load command constants
|
@@ -150,7 +151,8 @@ module MachO
|
|
150
151
|
:LC_NOTE => "NoteCommand",
|
151
152
|
:LC_BUILD_VERSION => "BuildVersionCommand",
|
152
153
|
:LC_DYLD_EXPORTS_TRIE => "LinkeditDataCommand",
|
153
|
-
:
|
154
|
+
:LC_DYLD_CHAINED_FIXUPS => "LinkeditDataCommand",
|
155
|
+
:LC_FILESET_ENTRY => "FilesetEntryCommand",
|
154
156
|
}.freeze
|
155
157
|
|
156
158
|
# association of segment name symbols to names
|
@@ -173,6 +175,7 @@ module MachO
|
|
173
175
|
:SG_FVMLIB => 0x2,
|
174
176
|
:SG_NORELOC => 0x4,
|
175
177
|
:SG_PROTECTED_VERSION_1 => 0x8,
|
178
|
+
:SG_READ_ONLY => 0x10,
|
176
179
|
}.freeze
|
177
180
|
|
178
181
|
# The top-level Mach-O load command structure.
|
@@ -1794,5 +1797,48 @@ module MachO
|
|
1794
1797
|
}.merge super
|
1795
1798
|
end
|
1796
1799
|
end
|
1800
|
+
|
1801
|
+
# A load command containing a description of a Mach-O that is a constituent of a fileset.
|
1802
|
+
# Each entry is further described by its own Mach header.
|
1803
|
+
# Corresponds to LC_FILESET_ENTRY.
|
1804
|
+
class FilesetEntryCommand < LoadCommand
|
1805
|
+
# @return [Integer] the virtual memory address of the entry
|
1806
|
+
attr_reader :vmaddr
|
1807
|
+
|
1808
|
+
# @return [Integer] the file offset of the entry
|
1809
|
+
attr_reader :fileoff
|
1810
|
+
|
1811
|
+
# @return [LCStr] the entry's ID
|
1812
|
+
attr_reader :entry_id
|
1813
|
+
|
1814
|
+
# @return [void]
|
1815
|
+
attr_reader :reserved
|
1816
|
+
|
1817
|
+
# @see MachOStructure::FORMAT
|
1818
|
+
# @api private
|
1819
|
+
FORMAT = "L=2Q=2L=2"
|
1820
|
+
|
1821
|
+
# @see MachOStructure::SIZEOF
|
1822
|
+
# @api private
|
1823
|
+
SIZEOF = 28
|
1824
|
+
|
1825
|
+
def initialize(view, cmd, cmdsize, vmaddr, fileoff, entry_id, reserved)
|
1826
|
+
super(view, cmd, cmdsize)
|
1827
|
+
@vmaddr = vmaddr
|
1828
|
+
@fileoff = fileoff
|
1829
|
+
@entry_id = LCStr.new(self, entry_id)
|
1830
|
+
@reserved = reserved
|
1831
|
+
end
|
1832
|
+
|
1833
|
+
# @return [Hash] a hash representation of this {FilesetEntryCommand}
|
1834
|
+
def to_h
|
1835
|
+
{
|
1836
|
+
"vmaddr" => vmaddr,
|
1837
|
+
"fileoff" => fileoff,
|
1838
|
+
"entry_id" => entry_id,
|
1839
|
+
"reserved" => reserved,
|
1840
|
+
}.merge super
|
1841
|
+
end
|
1842
|
+
end
|
1797
1843
|
end
|
1798
1844
|
end
|
data/lib/macho/macho_file.rb
CHANGED
@@ -34,7 +34,11 @@ module MachO
|
|
34
34
|
# @param bin [String] a binary string containing raw Mach-O data
|
35
35
|
# @param opts [Hash] options to control the parser with
|
36
36
|
# @option opts [Boolean] :permissive whether to ignore unknown load commands
|
37
|
+
# @option opts [Boolean] :decompress whether to decompress, if capable
|
37
38
|
# @return [MachOFile] a new MachOFile
|
39
|
+
# @note The `:decompress` option relies on non-default dependencies. Compression
|
40
|
+
# is only used in niche Mach-Os, so leaving this disabled is a reasonable default for
|
41
|
+
# virtually all normal uses.
|
38
42
|
def self.new_from_bin(bin, **opts)
|
39
43
|
instance = allocate
|
40
44
|
instance.initialize_from_bin(bin, opts)
|
@@ -46,13 +50,17 @@ module MachO
|
|
46
50
|
# @param filename [String] the Mach-O file to load from
|
47
51
|
# @param opts [Hash] options to control the parser with
|
48
52
|
# @option opts [Boolean] :permissive whether to ignore unknown load commands
|
53
|
+
# @option opts [Boolean] :decompress whether to decompress, if capable
|
49
54
|
# @raise [ArgumentError] if the given file does not exist
|
55
|
+
# @note The `:decompress` option relies on non-default dependencies. Compression
|
56
|
+
# is only used in niche Mach-Os, so leaving this disabled is a reasonable default for
|
57
|
+
# virtually all normal uses.
|
50
58
|
def initialize(filename, **opts)
|
51
59
|
raise ArgumentError, "#{filename}: no such file" unless File.file?(filename)
|
52
60
|
|
53
61
|
@filename = filename
|
54
62
|
@options = opts
|
55
|
-
@raw_data = File.
|
63
|
+
@raw_data = File.binread(@filename)
|
56
64
|
populate_fields
|
57
65
|
end
|
58
66
|
|
@@ -152,8 +160,8 @@ module MachO
|
|
152
160
|
# the instance fields
|
153
161
|
# @raise [OffsetInsertionError] if the offset is not in the load command region
|
154
162
|
# @raise [HeaderPadError] if the new command exceeds the header pad buffer
|
155
|
-
# @note Calling this method with an arbitrary offset in the load command
|
156
|
-
#
|
163
|
+
# @note Calling this method with an arbitrary offset in the load command region
|
164
|
+
# **will leave the object in an inconsistent state**.
|
157
165
|
def insert_command(offset, lc, options = {})
|
158
166
|
context = LoadCommands::LoadCommand::SerializationContext.context_for(self)
|
159
167
|
cmd_raw = lc.serialize(context)
|
@@ -196,7 +204,7 @@ module MachO
|
|
196
204
|
# Appends a new load command to the Mach-O.
|
197
205
|
# @param lc [LoadCommands::LoadCommand] the load command being added
|
198
206
|
# @param options [Hash]
|
199
|
-
# @option
|
207
|
+
# @option f [Boolean] :repopulate (true) whether or not to repopulate
|
200
208
|
# the instance fields
|
201
209
|
# @return [void]
|
202
210
|
# @see #insert_command
|
@@ -368,20 +376,20 @@ module MachO
|
|
368
376
|
# file.change_rpath("/usr/lib", "/usr/local/lib")
|
369
377
|
# @param old_path [String] the old runtime path
|
370
378
|
# @param new_path [String] the new runtime path
|
371
|
-
# @param
|
379
|
+
# @param options [Hash]
|
380
|
+
# @option options [Boolean] :uniq (false) if true, change duplicate
|
381
|
+
# rpaths simultaneously.
|
372
382
|
# @return [void]
|
373
383
|
# @raise [RpathUnknownError] if no such old runtime path exists
|
374
384
|
# @raise [RpathExistsError] if the new runtime path already exists
|
375
|
-
|
376
|
-
# compatibility with {MachO::FatFile#change_rpath}
|
377
|
-
def change_rpath(old_path, new_path, _options = {})
|
385
|
+
def change_rpath(old_path, new_path, options = {})
|
378
386
|
old_lc = command(:LC_RPATH).find { |r| r.path.to_s == old_path }
|
379
387
|
raise RpathUnknownError, old_path if old_lc.nil?
|
380
388
|
raise RpathExistsError, new_path if rpaths.include?(new_path)
|
381
389
|
|
382
390
|
new_lc = LoadCommands::LoadCommand.create(:LC_RPATH, new_path)
|
383
391
|
|
384
|
-
delete_rpath(old_path)
|
392
|
+
delete_rpath(old_path, options)
|
385
393
|
insert_command(old_lc.view.offset, new_lc)
|
386
394
|
end
|
387
395
|
|
@@ -409,13 +417,18 @@ module MachO
|
|
409
417
|
# file.delete_rpath("/lib")
|
410
418
|
# file.rpaths # => []
|
411
419
|
# @param path [String] the runtime path to delete
|
412
|
-
# @param
|
420
|
+
# @param options [Hash]
|
421
|
+
# @option options [Boolean] :uniq (false) if true, also delete
|
422
|
+
# duplicates of the requested path. If false, delete the first
|
423
|
+
# instance (by offset) of the requested path.
|
413
424
|
# @return void
|
414
425
|
# @raise [RpathUnknownError] if no such runtime path exists
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
426
|
+
def delete_rpath(path, options = {})
|
427
|
+
uniq = options.fetch(:uniq, false)
|
428
|
+
search_method = uniq ? :select : :find
|
429
|
+
|
430
|
+
# Cast rpath_cmds into an Array so we can handle the uniq and non-uniq cases the same way
|
431
|
+
rpath_cmds = Array(command(:LC_RPATH).method(search_method).call { |r| r.path.to_s == path })
|
419
432
|
raise RpathUnknownError, path if rpath_cmds.empty?
|
420
433
|
|
421
434
|
# delete the commands in reverse order, offset descending.
|
@@ -426,7 +439,7 @@ module MachO
|
|
426
439
|
# @param filename [String] the file to write to
|
427
440
|
# @return [void]
|
428
441
|
def write(filename)
|
429
|
-
File.
|
442
|
+
File.binwrite(filename, @raw_data)
|
430
443
|
end
|
431
444
|
|
432
445
|
# Write all Mach-O data to the file used to initialize the instance.
|
@@ -436,7 +449,7 @@ module MachO
|
|
436
449
|
def write!
|
437
450
|
raise MachOError, "no initial file to write to" if @filename.nil?
|
438
451
|
|
439
|
-
File.
|
452
|
+
File.binwrite(@filename, @raw_data)
|
440
453
|
end
|
441
454
|
|
442
455
|
# @return [Hash] a hash representation of this {MachOFile}
|
@@ -458,6 +471,9 @@ module MachO
|
|
458
471
|
# the smallest Mach-O header is 28 bytes
|
459
472
|
raise TruncatedFileError if @raw_data.size < 28
|
460
473
|
|
474
|
+
magic = @raw_data[0..3].unpack1("N")
|
475
|
+
populate_prelinked_kernel_header if Utils.compressed_magic?(magic)
|
476
|
+
|
461
477
|
magic = populate_and_check_magic
|
462
478
|
mh_klass = Utils.magic32?(magic) ? Headers::MachHeader : Headers::MachHeader64
|
463
479
|
mh = mh_klass.new_from_bin(endianness, @raw_data[0, mh_klass.bytesize])
|
@@ -469,6 +485,48 @@ module MachO
|
|
469
485
|
mh
|
470
486
|
end
|
471
487
|
|
488
|
+
# Read a compressed Mach-O header and check its validity, as well as whether we're able
|
489
|
+
# to parse it.
|
490
|
+
# @return [void]
|
491
|
+
# @raise [CompressedMachOError] if we weren't asked to perform decompression
|
492
|
+
# @raise [DecompressionError] if decompression is impossible or fails
|
493
|
+
# @api private
|
494
|
+
def populate_prelinked_kernel_header
|
495
|
+
raise CompressedMachOError unless options.fetch(:decompress, false)
|
496
|
+
|
497
|
+
@plh = Headers::PrelinkedKernelHeader.new_from_bin :big, @raw_data[0, Headers::PrelinkedKernelHeader.bytesize]
|
498
|
+
|
499
|
+
raise DecompressionError, "unsupported compression type: LZSS" if @plh.lzss?
|
500
|
+
raise DecompressionError, "unknown compression type: 0x#{plh.compress_type.to_s 16}" unless @plh.lzvn?
|
501
|
+
|
502
|
+
decompress_macho_lzvn
|
503
|
+
end
|
504
|
+
|
505
|
+
# Attempt to decompress a Mach-O file from the data specified in a prelinked kernel header.
|
506
|
+
# @return [void]
|
507
|
+
# @raise [DecompressionError] if decompression is impossible or fails
|
508
|
+
# @api private
|
509
|
+
# @note This method rewrites the internal state of {MachOFile} to pretend as if it was never
|
510
|
+
# compressed to begin with, allowing all other APIs to transparently act on compressed Mach-Os.
|
511
|
+
def decompress_macho_lzvn
|
512
|
+
begin
|
513
|
+
require "lzfse"
|
514
|
+
rescue LoadError
|
515
|
+
raise DecompressionError, "LZVN required but the optional 'lzfse' gem is not installed"
|
516
|
+
end
|
517
|
+
|
518
|
+
# From this point onwards, the internal buffer of this MachOFile refers to the decompressed
|
519
|
+
# contents specified by the prelinked kernel header.
|
520
|
+
begin
|
521
|
+
@raw_data = LZFSE.lzvn_decompress @raw_data.slice(Headers::PrelinkedKernelHeader.bytesize, @plh.compressed_size)
|
522
|
+
# Sanity checks.
|
523
|
+
raise DecompressionError if @raw_data.size != @plh.uncompressed_size
|
524
|
+
# TODO: check the adler32 CRC in @plh
|
525
|
+
rescue LZFSE::DecodeError
|
526
|
+
raise DecompressionError, "LZVN decompression failed"
|
527
|
+
end
|
528
|
+
end
|
529
|
+
|
472
530
|
# Read just the file's magic number and check its validity.
|
473
531
|
# @return [Integer] the magic
|
474
532
|
# @raise [MagicError] if the magic is not valid Mach-O magic
|
@@ -553,8 +611,8 @@ module MachO
|
|
553
611
|
segments.each do |seg|
|
554
612
|
seg.sections.each do |sect|
|
555
613
|
next if sect.empty?
|
556
|
-
next if sect.
|
557
|
-
next if sect.
|
614
|
+
next if sect.type?(:S_ZEROFILL)
|
615
|
+
next if sect.type?(:S_THREAD_LOCAL_ZEROFILL)
|
558
616
|
next unless sect.offset < offset
|
559
617
|
|
560
618
|
offset = sect.offset
|
data/lib/macho/sections.rb
CHANGED
@@ -4,24 +4,24 @@ module MachO
|
|
4
4
|
# Classes and constants for parsing sections in Mach-O binaries.
|
5
5
|
module Sections
|
6
6
|
# type mask
|
7
|
-
|
7
|
+
SECTION_TYPE_MASK = 0x000000ff
|
8
8
|
|
9
9
|
# attributes mask
|
10
|
-
|
10
|
+
SECTION_ATTRIBUTES_MASK = 0xffffff00
|
11
11
|
|
12
12
|
# user settable attributes mask
|
13
|
-
|
13
|
+
SECTION_ATTRIBUTES_USR_MASK = 0xff000000
|
14
14
|
|
15
15
|
# system settable attributes mask
|
16
|
-
|
16
|
+
SECTION_ATTRIBUTES_SYS_MASK = 0x00ffff00
|
17
17
|
|
18
18
|
# maximum specifiable section alignment, as a power of 2
|
19
19
|
# @note see `MAXSECTALIGN` macro in `cctools/misc/lipo.c`
|
20
20
|
MAX_SECT_ALIGN = 15
|
21
21
|
|
22
|
-
# association of section
|
22
|
+
# association of section type symbols to values
|
23
23
|
# @api private
|
24
|
-
|
24
|
+
SECTION_TYPES = {
|
25
25
|
:S_REGULAR => 0x0,
|
26
26
|
:S_ZEROFILL => 0x1,
|
27
27
|
:S_CSTRING_LITERALS => 0x2,
|
@@ -44,6 +44,12 @@ module MachO
|
|
44
44
|
:S_THREAD_LOCAL_VARIABLES => 0x13,
|
45
45
|
:S_THREAD_LOCAL_VARIABLE_POINTERS => 0x14,
|
46
46
|
:S_THREAD_LOCAL_INIT_FUNCTION_POINTERS => 0x15,
|
47
|
+
:S_INIT_FUNC_OFFSETS => 0x16,
|
48
|
+
}.freeze
|
49
|
+
|
50
|
+
# association of section attribute symbols to values
|
51
|
+
# @api private
|
52
|
+
SECTION_ATTRIBUTES = {
|
47
53
|
:S_ATTR_PURE_INSTRUCTIONS => 0x80000000,
|
48
54
|
:S_ATTR_NO_TOC => 0x40000000,
|
49
55
|
:S_ATTR_STRIP_STATIC_SYMS => 0x20000000,
|
@@ -56,6 +62,13 @@ module MachO
|
|
56
62
|
:S_ATTR_LOC_RELOC => 0x00000100,
|
57
63
|
}.freeze
|
58
64
|
|
65
|
+
# association of section flag symbols to values
|
66
|
+
# @api private
|
67
|
+
SECTION_FLAGS = {
|
68
|
+
**SECTION_TYPES,
|
69
|
+
**SECTION_ATTRIBUTES,
|
70
|
+
}.freeze
|
71
|
+
|
59
72
|
# association of section name symbols to names
|
60
73
|
# @api private
|
61
74
|
SECTION_NAMES = {
|
@@ -147,6 +160,33 @@ module MachO
|
|
147
160
|
size.zero?
|
148
161
|
end
|
149
162
|
|
163
|
+
# @return [Integer] the raw numeric type of this section
|
164
|
+
def type
|
165
|
+
flags & SECTION_TYPE_MASK
|
166
|
+
end
|
167
|
+
|
168
|
+
# @example
|
169
|
+
# puts "this section is regular" if sect.type?(:S_REGULAR)
|
170
|
+
# @param type_sym [Symbol] a section type symbol
|
171
|
+
# @return [Boolean] whether this section is of the given type
|
172
|
+
def type?(type_sym)
|
173
|
+
type == SECTION_TYPES[type_sym]
|
174
|
+
end
|
175
|
+
|
176
|
+
# @return [Integer] the raw numeric attributes of this section
|
177
|
+
def attributes
|
178
|
+
flags & SECTION_ATTRIBUTES_MASK
|
179
|
+
end
|
180
|
+
|
181
|
+
# @example
|
182
|
+
# puts "pure instructions" if sect.attribute?(:S_ATTR_PURE_INSTRUCTIONS)
|
183
|
+
# @param attr_sym [Symbol] a section attribute symbol
|
184
|
+
# @return [Boolean] whether this section is of the given type
|
185
|
+
def attribute?(attr_sym)
|
186
|
+
!!(attributes & SECTION_ATTRIBUTES[attr_sym])
|
187
|
+
end
|
188
|
+
|
189
|
+
# @deprecated Use {#type?} or {#attribute?} instead.
|
150
190
|
# @example
|
151
191
|
# puts "this section is regular" if sect.flag?(:S_REGULAR)
|
152
192
|
# @param flag [Symbol] a section flag symbol
|
data/lib/macho/tools.rb
CHANGED
@@ -51,6 +51,8 @@ module MachO
|
|
51
51
|
# @param options [Hash]
|
52
52
|
# @option options [Boolean] :strict (true) whether or not to fail loudly
|
53
53
|
# with an exception if the change cannot be performed
|
54
|
+
# @option options [Boolean] :uniq (false) whether or not to change duplicate
|
55
|
+
# rpaths simultaneously
|
54
56
|
# @return [void]
|
55
57
|
def self.change_rpath(filename, old_path, new_path, options = {})
|
56
58
|
file = MachO.open(filename)
|
@@ -80,6 +82,8 @@ module MachO
|
|
80
82
|
# @param options [Hash]
|
81
83
|
# @option options [Boolean] :strict (true) whether or not to fail loudly
|
82
84
|
# with an exception if the change cannot be performed
|
85
|
+
# @option options [Boolean] :uniq (false) whether or not to delete duplicate
|
86
|
+
# rpaths simultaneously
|
83
87
|
# @return [void]
|
84
88
|
def self.delete_rpath(filename, old_path, options = {})
|
85
89
|
file = MachO.open(filename)
|
data/lib/macho/utils.rb
CHANGED
@@ -121,5 +121,12 @@ module MachO
|
|
121
121
|
def self.big_magic?(num)
|
122
122
|
[Headers::MH_MAGIC, Headers::MH_MAGIC_64].include? num
|
123
123
|
end
|
124
|
+
|
125
|
+
# Compares the given number to the known magic number for a compressed Mach-O slice.
|
126
|
+
# @param num [Integer] the number being checked
|
127
|
+
# @return [Boolean] whether `num` is a valid compressed header magic number
|
128
|
+
def self.compressed_magic?(num)
|
129
|
+
num == Headers::COMPRESSED_MAGIC
|
130
|
+
end
|
124
131
|
end
|
125
132
|
end
|
data/lib/macho.rb
CHANGED
@@ -16,7 +16,7 @@ require_relative "macho/tools"
|
|
16
16
|
# The primary namespace for ruby-macho.
|
17
17
|
module MachO
|
18
18
|
# release version
|
19
|
-
VERSION = "
|
19
|
+
VERSION = "3.0.0"
|
20
20
|
|
21
21
|
# Opens the given filename as a MachOFile or FatFile, depending on its magic.
|
22
22
|
# @param filename [String] the file being opened
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby-macho
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- William Woodruff
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-01-11 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: A library for viewing and manipulating Mach-O files in Ruby.
|
14
14
|
email: william@yossarian.net
|
@@ -34,7 +34,7 @@ homepage: https://github.com/Homebrew/ruby-macho
|
|
34
34
|
licenses:
|
35
35
|
- MIT
|
36
36
|
metadata: {}
|
37
|
-
post_install_message:
|
37
|
+
post_install_message:
|
38
38
|
rdoc_options: []
|
39
39
|
require_paths:
|
40
40
|
- lib
|
@@ -42,15 +42,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
42
42
|
requirements:
|
43
43
|
- - ">="
|
44
44
|
- !ruby/object:Gem::Version
|
45
|
-
version: '2.
|
45
|
+
version: '2.6'
|
46
46
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
47
47
|
requirements:
|
48
48
|
- - ">="
|
49
49
|
- !ruby/object:Gem::Version
|
50
50
|
version: '0'
|
51
51
|
requirements: []
|
52
|
-
rubygems_version: 3.
|
53
|
-
signing_key:
|
52
|
+
rubygems_version: 3.2.32
|
53
|
+
signing_key:
|
54
54
|
specification_version: 4
|
55
55
|
summary: ruby-macho - Mach-O file analyzer.
|
56
56
|
test_files: []
|