class MachO::MachOFile

Represents a Mach-O file, which contains a header and load commands as well as binary executable instructions. Mach-O binaries are architecture specific. @see en.wikipedia.org/wiki/Mach-O @see FatFile

Attributes

endianness[R]

@return [Symbol] the endianness of the file, :big or :little

filename[RW]

@return [String] the filename loaded from, or nil if loaded from a binary

string
header[R]

@return [Headers::MachHeader] if the Mach-O is 32-bit @return [Headers::MachHeader64] if the Mach-O is 64-bit

load_commands[R]

@return [Array<LoadCommands::LoadCommand>] an array of the file's load

commands

@note load commands are provided in order of ascending offset.

Public Class Methods

new(filename) click to toggle source

Creates a new FatFile from the given filename. @param filename [String] the Mach-O file to load from @raise [ArgumentError] if the given file does not exist

# File lib/macho/macho_file.rb, line 41
def initialize(filename)
  raise ArgumentError, "#{filename}: no such file" unless File.file?(filename)

  @filename = filename
  @raw_data = File.open(@filename, "rb", &:read)
  populate_fields
end
new_from_bin(bin) click to toggle source

Creates a new MachOFile instance from a binary string. @param bin [String] a binary string containing raw Mach-O data @return [MachOFile] a new MachOFile

# File lib/macho/macho_file.rb, line 31
def self.new_from_bin(bin)
  instance = allocate
  instance.initialize_from_bin(bin)

  instance
end

Public Instance Methods

[](name)
Alias for: command
add_command(lc, options = {}) click to toggle source

Appends a new load command to the Mach-O. @param lc [LoadCommands::LoadCommand] the load command being added @param options [Hash] @option options [Boolean] :repopulate (true) whether or not to repopulate

the instance fields

@return [void] @see insert_command @note This is public, but methods like {#add_rpath} should be preferred.

Setting `repopulate` to false **will leave the instance in an
inconsistent state** unless {#populate_fields} is called **immediately**
afterwards.
# File lib/macho/macho_file.rb, line 200
def add_command(lc, options = {})
  insert_command(header.class.bytesize + sizeofcmds, lc, options)
end
add_rpath(path, _options = {}) click to toggle source

Add the given runtime path to the Mach-O. @example

file.rpaths # => ["/lib"]
file.add_rpath("/usr/lib")
file.rpaths # => ["/lib", "/usr/lib"]

@param path [String] the new runtime path @param _options [Hash] @return [void] @raise [RpathExistsError] if the runtime path already exists @note `_options` is currently unused and is provided for signature

compatibility with {MachO::FatFile#add_rpath}
# File lib/macho/macho_file.rb, line 366
def add_rpath(path, _options = {})
  raise RpathExistsError, path if rpaths.include?(path)

  rpath_cmd = LoadCommands::LoadCommand.create(:LC_RPATH, path)
  add_command(rpath_cmd)
end
change_dylib(old_name, new_name, _options = {})
Alias for: change_install_name
change_dylib_id(new_id, _options = {}) click to toggle source

Changes the Mach-O's dylib ID to `new_id`. Does nothing if not a dylib. @example

file.change_dylib_id("libFoo.dylib")

@param new_id [String] the dylib's new ID @param _options [Hash] @return [void] @raise [ArgumentError] if `new_id` is not a String @note `_options` is currently unused and is provided for signature

compatibility with {MachO::FatFile#change_dylib_id}
# File lib/macho/macho_file.rb, line 276
def change_dylib_id(new_id, _options = {})
  raise ArgumentError, "new ID must be a String" unless new_id.is_a?(String)
  return unless dylib?

  old_lc = command(:LC_ID_DYLIB).first
  raise DylibIdMissingError unless old_lc

  new_lc = LoadCommands::LoadCommand.create(:LC_ID_DYLIB, new_id,
                                            old_lc.timestamp,
                                            old_lc.current_version,
                                            old_lc.compatibility_version)

  replace_command(old_lc, new_lc)
end
Also aliased as: dylib_id=
change_install_name(old_name, new_name, _options = {}) click to toggle source

Changes the shared library `old_name` to `new_name` @example

file.change_install_name("abc.dylib", "def.dylib")

@param old_name [String] the shared library's old name @param new_name [String] the shared library's new name @param _options [Hash] @return [void] @raise [DylibUnknownError] if no shared library has the old name @note `_options` is currently unused and is provided for signature

compatibility with {MachO::FatFile#change_install_name}
# File lib/macho/macho_file.rb, line 313
def change_install_name(old_name, new_name, _options = {})
  old_lc = dylib_load_commands.find { |d| d.name.to_s == old_name }
  raise DylibUnknownError, old_name if old_lc.nil?

  new_lc = LoadCommands::LoadCommand.create(old_lc.type, new_name,
                                            old_lc.timestamp,
                                            old_lc.current_version,
                                            old_lc.compatibility_version)

  replace_command(old_lc, new_lc)
end
Also aliased as: change_dylib
change_rpath(old_path, new_path, _options = {}) click to toggle source

Changes the runtime path `old_path` to `new_path` @example

file.change_rpath("/usr/lib", "/usr/local/lib")

@param old_path [String] the old runtime path @param new_path [String] the new runtime path @param _options [Hash] @return [void] @raise [RpathUnknownError] if no such old runtime path exists @raise [RpathExistsError] if the new runtime path already exists @note `_options` is currently unused and is provided for signature

compatibility with {MachO::FatFile#change_rpath}
# File lib/macho/macho_file.rb, line 344
def change_rpath(old_path, new_path, _options = {})
  old_lc = command(:LC_RPATH).find { |r| r.path.to_s == old_path }
  raise RpathUnknownError, old_path if old_lc.nil?
  raise RpathExistsError, new_path if rpaths.include?(new_path)

  new_lc = LoadCommands::LoadCommand.create(:LC_RPATH, new_path)

  delete_rpath(old_path)
  insert_command(old_lc.view.offset, new_lc)
end
command(name) click to toggle source

All load commands of a given name. @example

file.command("LC_LOAD_DYLIB")
file[:LC_LOAD_DYLIB]

@param [String, Symbol] name the load command ID @return [Array<LoadCommands::LoadCommand>] an array of load commands

corresponding to `name`
# File lib/macho/macho_file.rb, line 130
def command(name)
  load_commands.select { |lc| lc.type == name.to_sym }
end
Also aliased as: []
cpusubtype() click to toggle source

@return [Symbol] a symbol representation of the Mach-O's CPU subtype

# File lib/macho/macho_file.rb, line 119
def cpusubtype
  Headers::CPU_SUBTYPES[header.cputype][header.cpusubtype]
end
cputype() click to toggle source

@return [Symbol] a symbol representation of the Mach-O's CPU type

# File lib/macho/macho_file.rb, line 114
def cputype
  Headers::CPU_TYPES[header.cputype]
end
delete_command(lc, options = {}) click to toggle source

Delete a load command from the Mach-O. @param lc [LoadCommands::LoadCommand] the load command being deleted @param options [Hash] @option options [Boolean] :repopulate (true) whether or not to repopulate

the instance fields

@return [void] @note This is public, but methods like {#delete_rpath} should be preferred.

Setting `repopulate` to false **will leave the instance in an
inconsistent state** unless {#populate_fields} is called **immediately**
afterwards.
# File lib/macho/macho_file.rb, line 214
def delete_command(lc, options = {})
  @raw_data.slice!(lc.view.offset, lc.cmdsize)

  # update Mach-O header fields to account for deleted load command
  update_ncmds(ncmds - 1)
  update_sizeofcmds(sizeofcmds - lc.cmdsize)

  # pad the space after the load commands to preserve offsets
  null_pad = "\x00" * lc.cmdsize
  @raw_data.insert(header.class.bytesize + sizeofcmds - lc.cmdsize, null_pad)

  populate_fields if options.fetch(:repopulate, true)
end
delete_rpath(path, _options = {}) click to toggle source

Delete the given runtime path from the Mach-O. @example

file.rpaths # => ["/lib"]
file.delete_rpath("/lib")
file.rpaths # => []

@param path [String] the runtime path to delete @param _options [Hash] @return void @raise [RpathUnknownError] if no such runtime path exists @note `_options` is currently unused and is provided for signature

compatibility with {MachO::FatFile#delete_rpath}
# File lib/macho/macho_file.rb, line 384
def delete_rpath(path, _options = {})
  rpath_cmds = command(:LC_RPATH).select { |r| r.path.to_s == path }
  raise RpathUnknownError, path if rpath_cmds.empty?

  # delete the commands in reverse order, offset descending. this
  # allows us to defer (expensive) field population until the very end
  rpath_cmds.reverse_each { |cmd| delete_command(cmd, :repopulate => false) }

  populate_fields
end
dylib_id() click to toggle source

The Mach-O's dylib ID, or `nil` if not a dylib. @example

file.dylib_id # => 'libBar.dylib'

@return [String, nil] the Mach-O's dylib ID

# File lib/macho/macho_file.rb, line 259
def dylib_id
  return unless dylib?

  dylib_id_cmd = command(:LC_ID_DYLIB).first

  dylib_id_cmd.name.to_s
end
dylib_id=(new_id, _options = {})
Alias for: change_dylib_id
dylib_load_commands() click to toggle source

All load commands responsible for loading dylibs. @return [Array<LoadCommands::DylibCommand>] an array of DylibCommands

# File lib/macho/macho_file.rb, line 240
def dylib_load_commands
  load_commands.select { |lc| LoadCommands::DYLIB_LOAD_COMMANDS.include?(lc.type) }
end
filetype() click to toggle source

@return [Symbol] a string representation of the Mach-O's filetype

# File lib/macho/macho_file.rb, line 109
def filetype
  Headers::MH_FILETYPES[header.filetype]
end
initialize_from_bin(bin) click to toggle source

Initializes a new MachOFile instance from a binary string. @see MachO::MachOFile.new_from_bin @api private

# File lib/macho/macho_file.rb, line 52
def initialize_from_bin(bin)
  @filename = nil
  @raw_data = bin
  populate_fields
end
insert_command(offset, lc, options = {}) click to toggle source

Inserts a load command at the given offset. @param offset [Fixnum] the offset to insert at @param lc [LoadCommands::LoadCommand] the load command to insert @param options [Hash] @option options [Boolean] :repopulate (true) whether or not to repopulate

the instance fields

@raise [OffsetInsertionError] if the offset is not in the load command region @raise [HeaderPadError] if the new command exceeds the header pad buffer @note Calling this method with an arbitrary offset in the load command

region **will leave the object in an inconsistent state**.
# File lib/macho/macho_file.rb, line 146
def insert_command(offset, lc, options = {})
  context = LoadCommands::LoadCommand::SerializationContext.context_for(self)
  cmd_raw = lc.serialize(context)

  if offset < header.class.bytesize || offset + cmd_raw.bytesize > low_fileoff
    raise OffsetInsertionError, offset
  end

  new_sizeofcmds = sizeofcmds + cmd_raw.bytesize

  if header.class.bytesize + new_sizeofcmds > low_fileoff
    raise HeaderPadError, @filename
  end

  # update Mach-O header fields to account for inserted load command
  update_ncmds(ncmds + 1)
  update_sizeofcmds(new_sizeofcmds)

  @raw_data.insert(offset, cmd_raw)
  @raw_data.slice!(header.class.bytesize + new_sizeofcmds, cmd_raw.bytesize)

  populate_fields if options.fetch(:repopulate, true)
end
linked_dylibs() click to toggle source

All shared libraries linked to the Mach-O. @return [Array<String>] an array of all shared libraries

# File lib/macho/macho_file.rb, line 295
def linked_dylibs
  # Some linkers produce multiple `LC_LOAD_DYLIB` load commands for the same
  # library, but at this point we're really only interested in a list of
  # unique libraries this Mach-O file links to, thus: `uniq`. (This is also
  # for consistency with `FatFile` that merges this list across all archs.)
  dylib_load_commands.map(&:name).map(&:to_s).uniq
end
magic_string() click to toggle source

@return [String] a string representation of the file's magic number

# File lib/macho/macho_file.rb, line 104
def magic_string
  Headers::MH_MAGICS[magic]
end
populate_fields() click to toggle source

Populate the instance's fields with the raw Mach-O data. @return [void] @note This method is public, but should (almost) never need to be called.

The exception to this rule is when methods like {#add_command} and
{#delete_command} have been called with `repopulate = false`.
# File lib/macho/macho_file.rb, line 233
def populate_fields
  @header = populate_mach_header
  @load_commands = populate_load_commands
end
replace_command(old_lc, new_lc) click to toggle source

Replace a load command with another command in the Mach-O, preserving location. @param old_lc [LoadCommands::LoadCommand] the load command being replaced @param new_lc [LoadCommands::LoadCommand] the load command being added @return [void] @raise [HeaderPadError] if the new command exceeds the header pad buffer @see insert_command @note This is public, but methods like {#dylib_id=} should be preferred.

# File lib/macho/macho_file.rb, line 177
def replace_command(old_lc, new_lc)
  context = LoadCommands::LoadCommand::SerializationContext.context_for(self)
  cmd_raw = new_lc.serialize(context)
  new_sizeofcmds = sizeofcmds + cmd_raw.bytesize - old_lc.cmdsize
  if header.class.bytesize + new_sizeofcmds > low_fileoff
    raise HeaderPadError, @filename
  end

  delete_command(old_lc)
  insert_command(old_lc.view.offset, new_lc)
end
rpaths() click to toggle source

All runtime paths searched by the dynamic linker for the Mach-O. @return [Array<String>] an array of all runtime paths

# File lib/macho/macho_file.rb, line 329
def rpaths
  command(:LC_RPATH).map(&:path).map(&:to_s)
end
segments() click to toggle source

All segment load commands in the Mach-O. @return [Array<LoadCommands::SegmentCommand>] if the Mach-O is 32-bit @return [Array<LoadCommands::SegmentCommand64>] if the Mach-O is 64-bit

# File lib/macho/macho_file.rb, line 247
def segments
  if magic32?
    command(:LC_SEGMENT)
  else
    command(:LC_SEGMENT_64)
  end
end
serialize() click to toggle source

The file's raw Mach-O data. @return [String] the raw Mach-O data

# File lib/macho/macho_file.rb, line 60
def serialize
  @raw_data
end
write(filename) click to toggle source

Write all Mach-O data to the given filename. @param filename [String] the file to write to @return [void]

# File lib/macho/macho_file.rb, line 398
def write(filename)
  File.open(filename, "wb") { |f| f.write(@raw_data) }
end
write!() click to toggle source

Write all Mach-O data to the file used to initialize the instance. @return [void] @raise [MachOError] if the instance was initialized without a file @note Overwrites all data in the file!

# File lib/macho/macho_file.rb, line 406
def write!
  if @filename.nil?
    raise MachOError, "cannot write to a default file when initialized from a binary string"
  else
    File.open(@filename, "wb") { |f| f.write(@raw_data) }
  end
end

Private Instance Methods

check_cpusubtype(cputype, cpusubtype) click to toggle source

Check the file's CPU type/subtype pair. @param cpusubtype [Fixnum] the CPU subtype @raise [CPUSubtypeError] if the CPU sub-type is unknown @api private

# File lib/macho/macho_file.rb, line 464
def check_cpusubtype(cputype, cpusubtype)
  # Only check sub-type w/o capability bits (see `populate_mach_header`).
  raise CPUSubtypeError.new(cputype, cpusubtype) unless Headers::CPU_SUBTYPES[cputype].key?(cpusubtype)
end
check_cputype(cputype) click to toggle source

Check the file's CPU type. @param cputype [Fixnum] the CPU type @raise [CPUTypeError] if the CPU type is unknown @api private

# File lib/macho/macho_file.rb, line 456
def check_cputype(cputype)
  raise CPUTypeError, cputype unless Headers::CPU_TYPES.key?(cputype)
end
check_filetype(filetype) click to toggle source

Check the file's type. @param filetype [Fixnum] the file type @raise [FiletypeError] if the file type is unknown @api private

# File lib/macho/macho_file.rb, line 473
def check_filetype(filetype)
  raise FiletypeError, filetype unless Headers::MH_FILETYPES.key?(filetype)
end
low_fileoff() click to toggle source

The low file offset (offset to first section data). @return [Fixnum] the offset @api private

# File lib/macho/macho_file.rb, line 508
def low_fileoff
  offset = @raw_data.size

  segments.each do |seg|
    seg.sections.each do |sect|
      next if sect.empty?
      next if sect.flag?(:S_ZEROFILL)
      next if sect.flag?(:S_THREAD_LOCAL_ZEROFILL)
      next unless sect.offset < offset

      offset = sect.offset
    end
  end

  offset
end
populate_and_check_magic() click to toggle source

Read just the file's magic number and check its validity. @return [Fixnum] the magic @raise [MagicError] if the magic is not valid Mach-O magic @raise [FatBinaryError] if the magic is for a Fat file @api private

# File lib/macho/macho_file.rb, line 441
def populate_and_check_magic
  magic = @raw_data[0..3].unpack("N").first

  raise MagicError, magic unless Utils.magic?(magic)
  raise FatBinaryError if Utils.fat_magic?(magic)

  @endianness = Utils.little_magic?(magic) ? :little : :big

  magic
end
populate_load_commands() click to toggle source

All load commands in the file. @return [Array<LoadCommands::LoadCommand>] an array of load commands @raise [LoadCommandError] if an unknown load command is encountered @api private

# File lib/macho/macho_file.rb, line 481
def populate_load_commands
  offset = header.class.bytesize
  load_commands = []

  header.ncmds.times do
    fmt = Utils.specialize_format("L=", endianness)
    cmd = @raw_data.slice(offset, 4).unpack(fmt).first
    cmd_sym = LoadCommands::LOAD_COMMANDS[cmd]

    raise LoadCommandError, cmd if cmd_sym.nil?

    # why do I do this? i don't like declaring constants below
    # classes, and i need them to resolve...
    klass = LoadCommands.const_get LoadCommands::LC_STRUCTURES[cmd_sym]
    view = MachOView.new(@raw_data, endianness, offset)
    command = klass.new_from_bin(view)

    load_commands << command
    offset += command.cmdsize
  end

  load_commands
end
populate_mach_header() click to toggle source

The file's Mach-O header structure. @return [Headers::MachHeader] if the Mach-O is 32-bit @return [Headers::MachHeader64] if the Mach-O is 64-bit @raise [TruncatedFileError] if the file is too small to have a valid header @api private

# File lib/macho/macho_file.rb, line 421
def populate_mach_header
  # the smallest Mach-O header is 28 bytes
  raise TruncatedFileError if @raw_data.size < 28

  magic = populate_and_check_magic
  mh_klass = Utils.magic32?(magic) ? Headers::MachHeader : Headers::MachHeader64
  mh = mh_klass.new_from_bin(endianness, @raw_data[0, mh_klass.bytesize])

  check_cputype(mh.cputype)
  check_cpusubtype(mh.cputype, mh.cpusubtype)
  check_filetype(mh.filetype)

  mh
end
update_ncmds(ncmds) click to toggle source

Updates the number of load commands in the raw data. @param ncmds [Fixnum] the new number of commands @return [void] @api private

# File lib/macho/macho_file.rb, line 529
def update_ncmds(ncmds)
  fmt = Utils.specialize_format("L=", endianness)
  ncmds_raw = [ncmds].pack(fmt)
  @raw_data[16..19] = ncmds_raw
end
update_sizeofcmds(size) click to toggle source

Updates the size of all load commands in the raw data. @param size [Fixnum] the new size, in bytes @return [void] @api private

# File lib/macho/macho_file.rb, line 539
def update_sizeofcmds(size)
  fmt = Utils.specialize_format("L=", endianness)
  size_raw = [size].pack(fmt)
  @raw_data[20..23] = size_raw
end