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.
- 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
|