ruby-macho 2.5.1 → 4.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 +15 -2
- data/lib/macho/exceptions.rb +42 -10
- data/lib/macho/fat_file.rb +21 -4
- data/lib/macho/headers.rb +107 -103
- data/lib/macho/load_commands.rb +207 -627
- data/lib/macho/macho_file.rb +86 -21
- data/lib/macho/sections.rb +63 -54
- data/lib/macho/structure.rb +264 -22
- data/lib/macho/tools.rb +4 -0
- data/lib/macho/utils.rb +7 -0
- data/lib/macho/view.rb +10 -1
- data/lib/macho.rb +2 -2
- metadata +9 -8
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,18 @@ 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
|
-
|
375
|
-
# @note `_options` is currently unused and is provided for signature
|
376
|
-
# compatibility with {MachO::FatFile#change_rpath}
|
377
|
-
def change_rpath(old_path, new_path, _options = {})
|
384
|
+
def change_rpath(old_path, new_path, options = {})
|
378
385
|
old_lc = command(:LC_RPATH).find { |r| r.path.to_s == old_path }
|
379
386
|
raise RpathUnknownError, old_path if old_lc.nil?
|
380
|
-
raise RpathExistsError, new_path if rpaths.include?(new_path)
|
381
387
|
|
382
388
|
new_lc = LoadCommands::LoadCommand.create(:LC_RPATH, new_path)
|
383
389
|
|
384
|
-
delete_rpath(old_path)
|
390
|
+
delete_rpath(old_path, options)
|
385
391
|
insert_command(old_lc.view.offset, new_lc)
|
386
392
|
end
|
387
393
|
|
@@ -409,13 +415,27 @@ module MachO
|
|
409
415
|
# file.delete_rpath("/lib")
|
410
416
|
# file.rpaths # => []
|
411
417
|
# @param path [String] the runtime path to delete
|
412
|
-
# @param
|
418
|
+
# @param options [Hash]
|
419
|
+
# @option options [Boolean] :uniq (false) if true, also delete
|
420
|
+
# duplicates of the requested path. If false, delete the first
|
421
|
+
# instance (by offset) of the requested path, unless :last is true.
|
422
|
+
# Incompatible with :last.
|
423
|
+
# @option options [Boolean] :last (false) if true, delete the last
|
424
|
+
# instance (by offset) of the requested path. Incompatible with :uniq.
|
413
425
|
# @return void
|
414
426
|
# @raise [RpathUnknownError] if no such runtime path exists
|
415
|
-
# @
|
416
|
-
|
417
|
-
|
418
|
-
|
427
|
+
# @raise [ArgumentError] if both :uniq and :last are true
|
428
|
+
def delete_rpath(path, options = {})
|
429
|
+
uniq = options.fetch(:uniq, false)
|
430
|
+
last = options.fetch(:last, false)
|
431
|
+
raise ArgumentError, "Cannot set both :uniq and :last to true" if uniq && last
|
432
|
+
|
433
|
+
search_method = uniq || last ? :select : :find
|
434
|
+
rpath_cmds = command(:LC_RPATH).public_send(search_method) { |r| r.path.to_s == path }
|
435
|
+
rpath_cmds = rpath_cmds.last if last
|
436
|
+
|
437
|
+
# Cast rpath_cmds into an Array so we can handle the uniq and non-uniq cases the same way
|
438
|
+
rpath_cmds = Array(rpath_cmds)
|
419
439
|
raise RpathUnknownError, path if rpath_cmds.empty?
|
420
440
|
|
421
441
|
# delete the commands in reverse order, offset descending.
|
@@ -426,7 +446,7 @@ module MachO
|
|
426
446
|
# @param filename [String] the file to write to
|
427
447
|
# @return [void]
|
428
448
|
def write(filename)
|
429
|
-
File.
|
449
|
+
File.binwrite(filename, @raw_data)
|
430
450
|
end
|
431
451
|
|
432
452
|
# Write all Mach-O data to the file used to initialize the instance.
|
@@ -436,7 +456,7 @@ module MachO
|
|
436
456
|
def write!
|
437
457
|
raise MachOError, "no initial file to write to" if @filename.nil?
|
438
458
|
|
439
|
-
File.
|
459
|
+
File.binwrite(@filename, @raw_data)
|
440
460
|
end
|
441
461
|
|
442
462
|
# @return [Hash] a hash representation of this {MachOFile}
|
@@ -458,6 +478,9 @@ module MachO
|
|
458
478
|
# the smallest Mach-O header is 28 bytes
|
459
479
|
raise TruncatedFileError if @raw_data.size < 28
|
460
480
|
|
481
|
+
magic = @raw_data[0..3].unpack1("N")
|
482
|
+
populate_prelinked_kernel_header if Utils.compressed_magic?(magic)
|
483
|
+
|
461
484
|
magic = populate_and_check_magic
|
462
485
|
mh_klass = Utils.magic32?(magic) ? Headers::MachHeader : Headers::MachHeader64
|
463
486
|
mh = mh_klass.new_from_bin(endianness, @raw_data[0, mh_klass.bytesize])
|
@@ -469,6 +492,48 @@ module MachO
|
|
469
492
|
mh
|
470
493
|
end
|
471
494
|
|
495
|
+
# Read a compressed Mach-O header and check its validity, as well as whether we're able
|
496
|
+
# to parse it.
|
497
|
+
# @return [void]
|
498
|
+
# @raise [CompressedMachOError] if we weren't asked to perform decompression
|
499
|
+
# @raise [DecompressionError] if decompression is impossible or fails
|
500
|
+
# @api private
|
501
|
+
def populate_prelinked_kernel_header
|
502
|
+
raise CompressedMachOError unless options.fetch(:decompress, false)
|
503
|
+
|
504
|
+
@plh = Headers::PrelinkedKernelHeader.new_from_bin :big, @raw_data[0, Headers::PrelinkedKernelHeader.bytesize]
|
505
|
+
|
506
|
+
raise DecompressionError, "unsupported compression type: LZSS" if @plh.lzss?
|
507
|
+
raise DecompressionError, "unknown compression type: 0x#{plh.compress_type.to_s 16}" unless @plh.lzvn?
|
508
|
+
|
509
|
+
decompress_macho_lzvn
|
510
|
+
end
|
511
|
+
|
512
|
+
# Attempt to decompress a Mach-O file from the data specified in a prelinked kernel header.
|
513
|
+
# @return [void]
|
514
|
+
# @raise [DecompressionError] if decompression is impossible or fails
|
515
|
+
# @api private
|
516
|
+
# @note This method rewrites the internal state of {MachOFile} to pretend as if it was never
|
517
|
+
# compressed to begin with, allowing all other APIs to transparently act on compressed Mach-Os.
|
518
|
+
def decompress_macho_lzvn
|
519
|
+
begin
|
520
|
+
require "lzfse"
|
521
|
+
rescue LoadError
|
522
|
+
raise DecompressionError, "LZVN required but the optional 'lzfse' gem is not installed"
|
523
|
+
end
|
524
|
+
|
525
|
+
# From this point onwards, the internal buffer of this MachOFile refers to the decompressed
|
526
|
+
# contents specified by the prelinked kernel header.
|
527
|
+
begin
|
528
|
+
@raw_data = LZFSE.lzvn_decompress @raw_data.slice(Headers::PrelinkedKernelHeader.bytesize, @plh.compressed_size)
|
529
|
+
# Sanity checks.
|
530
|
+
raise DecompressionError if @raw_data.size != @plh.uncompressed_size
|
531
|
+
# TODO: check the adler32 CRC in @plh
|
532
|
+
rescue LZFSE::DecodeError
|
533
|
+
raise DecompressionError, "LZVN decompression failed"
|
534
|
+
end
|
535
|
+
end
|
536
|
+
|
472
537
|
# Read just the file's magic number and check its validity.
|
473
538
|
# @return [Integer] the magic
|
474
539
|
# @raise [MagicError] if the magic is not valid Mach-O magic
|
@@ -534,7 +599,7 @@ module MachO
|
|
534
599
|
LoadCommands::LoadCommand
|
535
600
|
end
|
536
601
|
|
537
|
-
view = MachOView.new(@raw_data, endianness, offset)
|
602
|
+
view = MachOView.new(self, @raw_data, endianness, offset)
|
538
603
|
command = klass.new_from_bin(view)
|
539
604
|
|
540
605
|
load_commands << command
|
@@ -553,8 +618,8 @@ module MachO
|
|
553
618
|
segments.each do |seg|
|
554
619
|
seg.sections.each do |sect|
|
555
620
|
next if sect.empty?
|
556
|
-
next if sect.
|
557
|
-
next if sect.
|
621
|
+
next if sect.type?(:S_ZEROFILL)
|
622
|
+
next if sect.type?(:S_THREAD_LOCAL_ZEROFILL)
|
558
623
|
next unless sect.offset < offset
|
559
624
|
|
560
625
|
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 = {
|
@@ -76,61 +89,38 @@ module MachO
|
|
76
89
|
# Represents a section of a segment for 32-bit architectures.
|
77
90
|
class Section < MachOStructure
|
78
91
|
# @return [String] the name of the section, including null pad bytes
|
79
|
-
|
92
|
+
field :sectname, :string, :padding => :null, :size => 16
|
80
93
|
|
81
94
|
# @return [String] the name of the segment's section, including null
|
82
95
|
# pad bytes
|
83
|
-
|
96
|
+
field :segname, :string, :padding => :null, :size => 16
|
84
97
|
|
85
98
|
# @return [Integer] the memory address of the section
|
86
|
-
|
99
|
+
field :addr, :uint32
|
87
100
|
|
88
101
|
# @return [Integer] the size, in bytes, of the section
|
89
|
-
|
102
|
+
field :size, :uint32
|
90
103
|
|
91
104
|
# @return [Integer] the file offset of the section
|
92
|
-
|
105
|
+
field :offset, :uint32
|
93
106
|
|
94
107
|
# @return [Integer] the section alignment (power of 2) of the section
|
95
|
-
|
108
|
+
field :align, :uint32
|
96
109
|
|
97
110
|
# @return [Integer] the file offset of the section's relocation entries
|
98
|
-
|
111
|
+
field :reloff, :uint32
|
99
112
|
|
100
113
|
# @return [Integer] the number of relocation entries
|
101
|
-
|
114
|
+
field :nreloc, :uint32
|
102
115
|
|
103
116
|
# @return [Integer] flags for type and attributes of the section
|
104
|
-
|
117
|
+
field :flags, :uint32
|
105
118
|
|
106
119
|
# @return [void] reserved (for offset or index)
|
107
|
-
|
120
|
+
field :reserved1, :uint32
|
108
121
|
|
109
122
|
# @return [void] reserved (for count or sizeof)
|
110
|
-
|
111
|
-
|
112
|
-
# @see MachOStructure::FORMAT
|
113
|
-
FORMAT = "Z16Z16L=9"
|
114
|
-
|
115
|
-
# @see MachOStructure::SIZEOF
|
116
|
-
SIZEOF = 68
|
117
|
-
|
118
|
-
# @api private
|
119
|
-
def initialize(sectname, segname, addr, size, offset, align, reloff,
|
120
|
-
nreloc, flags, reserved1, reserved2)
|
121
|
-
super()
|
122
|
-
@sectname = sectname
|
123
|
-
@segname = segname
|
124
|
-
@addr = addr
|
125
|
-
@size = size
|
126
|
-
@offset = offset
|
127
|
-
@align = align
|
128
|
-
@reloff = reloff
|
129
|
-
@nreloc = nreloc
|
130
|
-
@flags = flags
|
131
|
-
@reserved1 = reserved1
|
132
|
-
@reserved2 = reserved2
|
133
|
-
end
|
123
|
+
field :reserved2, :uint32
|
134
124
|
|
135
125
|
# @return [String] the section's name
|
136
126
|
def section_name
|
@@ -147,6 +137,33 @@ module MachO
|
|
147
137
|
size.zero?
|
148
138
|
end
|
149
139
|
|
140
|
+
# @return [Integer] the raw numeric type of this section
|
141
|
+
def type
|
142
|
+
flags & SECTION_TYPE_MASK
|
143
|
+
end
|
144
|
+
|
145
|
+
# @example
|
146
|
+
# puts "this section is regular" if sect.type?(:S_REGULAR)
|
147
|
+
# @param type_sym [Symbol] a section type symbol
|
148
|
+
# @return [Boolean] whether this section is of the given type
|
149
|
+
def type?(type_sym)
|
150
|
+
type == SECTION_TYPES[type_sym]
|
151
|
+
end
|
152
|
+
|
153
|
+
# @return [Integer] the raw numeric attributes of this section
|
154
|
+
def attributes
|
155
|
+
flags & SECTION_ATTRIBUTES_MASK
|
156
|
+
end
|
157
|
+
|
158
|
+
# @example
|
159
|
+
# puts "pure instructions" if sect.attribute?(:S_ATTR_PURE_INSTRUCTIONS)
|
160
|
+
# @param attr_sym [Symbol] a section attribute symbol
|
161
|
+
# @return [Boolean] whether this section is of the given type
|
162
|
+
def attribute?(attr_sym)
|
163
|
+
!!(attributes & SECTION_ATTRIBUTES[attr_sym])
|
164
|
+
end
|
165
|
+
|
166
|
+
# @deprecated Use {#type?} or {#attribute?} instead.
|
150
167
|
# @example
|
151
168
|
# puts "this section is regular" if sect.flag?(:S_REGULAR)
|
152
169
|
# @param flag [Symbol] a section flag symbol
|
@@ -179,22 +196,14 @@ module MachO
|
|
179
196
|
|
180
197
|
# Represents a section of a segment for 64-bit architectures.
|
181
198
|
class Section64 < Section
|
182
|
-
# @return [
|
183
|
-
|
184
|
-
|
185
|
-
# @see MachOStructure::FORMAT
|
186
|
-
FORMAT = "Z16Z16Q=2L=8"
|
199
|
+
# @return [Integer] the memory address of the section
|
200
|
+
field :addr, :uint64
|
187
201
|
|
188
|
-
# @
|
189
|
-
|
202
|
+
# @return [Integer] the size, in bytes, of the section
|
203
|
+
field :size, :uint64
|
190
204
|
|
191
|
-
# @
|
192
|
-
|
193
|
-
nreloc, flags, reserved1, reserved2, reserved3)
|
194
|
-
super(sectname, segname, addr, size, offset, align, reloff,
|
195
|
-
nreloc, flags, reserved1, reserved2)
|
196
|
-
@reserved3 = reserved3
|
197
|
-
end
|
205
|
+
# @return [void] reserved
|
206
|
+
field :reserved3, :uint32
|
198
207
|
|
199
208
|
# @return [Hash] a hash representation of this {Section64}
|
200
209
|
def to_h
|