ruby-macho 2.5.1 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.open(@filename, "rb", &:read)
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
- # region **will leave the object in an inconsistent state**.
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 options [Boolean] :repopulate (true) whether or not to repopulate
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 _options [Hash]
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
- # @raise [RpathExistsError] if the new runtime path already exists
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 _options [Hash]
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
- # @note `_options` is currently unused and is provided for signature
416
- # compatibility with {MachO::FatFile#delete_rpath}
417
- def delete_rpath(path, _options = {})
418
- rpath_cmds = command(:LC_RPATH).select { |r| r.path.to_s == path }
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.open(filename, "wb") { |f| f.write(@raw_data) }
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.open(@filename, "wb") { |f| f.write(@raw_data) }
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.flag?(:S_ZEROFILL)
557
- next if sect.flag?(:S_THREAD_LOCAL_ZEROFILL)
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
@@ -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
- SECTION_TYPE = 0x000000ff
7
+ SECTION_TYPE_MASK = 0x000000ff
8
8
 
9
9
  # attributes mask
10
- SECTION_ATTRIBUTES = 0xffffff00
10
+ SECTION_ATTRIBUTES_MASK = 0xffffff00
11
11
 
12
12
  # user settable attributes mask
13
- SECTION_ATTRIBUTES_USR = 0xff000000
13
+ SECTION_ATTRIBUTES_USR_MASK = 0xff000000
14
14
 
15
15
  # system settable attributes mask
16
- SECTION_ATTRIBUTES_SYS = 0x00ffff00
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 flag symbols to values
22
+ # association of section type symbols to values
23
23
  # @api private
24
- SECTION_FLAGS = {
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
- attr_reader :sectname
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
- attr_reader :segname
96
+ field :segname, :string, :padding => :null, :size => 16
84
97
 
85
98
  # @return [Integer] the memory address of the section
86
- attr_reader :addr
99
+ field :addr, :uint32
87
100
 
88
101
  # @return [Integer] the size, in bytes, of the section
89
- attr_reader :size
102
+ field :size, :uint32
90
103
 
91
104
  # @return [Integer] the file offset of the section
92
- attr_reader :offset
105
+ field :offset, :uint32
93
106
 
94
107
  # @return [Integer] the section alignment (power of 2) of the section
95
- attr_reader :align
108
+ field :align, :uint32
96
109
 
97
110
  # @return [Integer] the file offset of the section's relocation entries
98
- attr_reader :reloff
111
+ field :reloff, :uint32
99
112
 
100
113
  # @return [Integer] the number of relocation entries
101
- attr_reader :nreloc
114
+ field :nreloc, :uint32
102
115
 
103
116
  # @return [Integer] flags for type and attributes of the section
104
- attr_reader :flags
117
+ field :flags, :uint32
105
118
 
106
119
  # @return [void] reserved (for offset or index)
107
- attr_reader :reserved1
120
+ field :reserved1, :uint32
108
121
 
109
122
  # @return [void] reserved (for count or sizeof)
110
- attr_reader :reserved2
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 [void] reserved
183
- attr_reader :reserved3
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
- # @see MachOStructure::SIZEOF
189
- SIZEOF = 80
202
+ # @return [Integer] the size, in bytes, of the section
203
+ field :size, :uint64
190
204
 
191
- # @api private
192
- def initialize(sectname, segname, addr, size, offset, align, reloff,
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