class PatchELF::Saver

Internal use only.

For {Patcher} to do patching things and save to file. @private

Constants

IGNORE

To mark a not-using tag

Attributes

in_file[R]
out_file[R]

Public Class Methods

new(in_file, out_file, set) click to toggle source

Instantiate a {Saver} object. @param [String] in_file @param [String] out_file @param [{Symbol => String, Array}] set

# File lib/patchelf/saver.rb, line 24
def initialize(in_file, out_file, set)
  @in_file = in_file
  @out_file = out_file
  @set = set
  # [{Integer => String}]
  @inline_patch = {}
  @elf = ELFTools::ELFFile.new(File.open(in_file))
  @mm = PatchELF::MM.new(@elf)
  @strtab_extend_requests = []
  @append_dyn = []
end

Public Instance Methods

save!() click to toggle source

@return [void]

# File lib/patchelf/saver.rb, line 37
def save!
  # In this method we assume all attributes that should exist do exist.
  # e.g. DT_INTERP, DT_DYNAMIC. These should have been checked in the patcher.
  patch_interpreter
  patch_dynamic

  @mm.dispatch!

  FileUtils.cp(in_file, out_file) if out_file != in_file
  patch_out(@out_file)
  # Let output file have the same permission as input.
  FileUtils.chmod(File.stat(in_file).mode, out_file)
end

Private Instance Methods

dynamic() click to toggle source
# File lib/patchelf/saver.rb, line 278
def dynamic
  @dynamic ||= @elf.segment_by_type(:dynamic)
end
expand_dynamic!() click to toggle source
# File lib/patchelf/saver.rb, line 158
def expand_dynamic!
  return if @append_dyn.empty?

  dyn_sec = section_header('.dynamic')
  total = dynamic.tags.map(&:header)
  # the last must be a null-tag
  total = total[0..-2] + @append_dyn + [total.last]
  bytes = total.first.num_bytes * total.size
  @mm.malloc(bytes) do |off, vaddr|
    inline_patch(off, total.map(&:to_binary_s).join)
    dynamic.header.p_offset = off
    dynamic.header.p_vaddr = dynamic.header.p_paddr = vaddr
    dynamic.header.p_filesz = dynamic.header.p_memsz = bytes
    if dyn_sec
      dyn_sec.sh_offset = off
      dyn_sec.sh_addr = vaddr
      dyn_sec.sh_size = bytes
    end
  end
end
inline_patch(off, str) click to toggle source

This can only be used for patching interpreter's name or set strings in a malloc-ed area. i.e. NEVER intend to change the string defined in strtab

# File lib/patchelf/saver.rb, line 238
def inline_patch(off, str)
  @inline_patch[off] = str
end
lazy_dyn(sym) click to toggle source

Create a temp tag header. @return [ELFTools::Structs::ELF_Dyn]

# File lib/patchelf/saver.rb, line 150
def lazy_dyn(sym)
  ELFTools::Structs::ELF_Dyn.new(endian: @elf.endian).tap do |dyn|
    @append_dyn << dyn
    dyn.elf_class = @elf.elf_class
    dyn.d_tag = ELFTools::Util.to_constant(ELFTools::Constants::DT, sym)
  end
end
malloc_strtab!() click to toggle source
# File lib/patchelf/saver.rb, line 179
def malloc_strtab!
  return if @strtab_extend_requests.empty?

  strtab = dynamic.tag_by_type(:strtab)
  # Process registered requests
  need_size = strtab_string.size + @strtab_extend_requests.reduce(0) { |sum, (str, _)| sum + str.size + 1 }
  dynstr = section_header('.dynstr')
  @mm.malloc(need_size) do |off, vaddr|
    new_str = strtab_string + @strtab_extend_requests.map(&:first).join("\x00") + "\x00"
    inline_patch(off, new_str)
    cur = strtab_string.size
    @strtab_extend_requests.each do |str, block|
      block.call(cur)
      cur += str.size + 1
    end
    # Now patching strtab header
    strtab.header.d_val = vaddr
    # We also need to patch dynstr to let readelf have correct output.
    if dynstr
      dynstr.sh_size = new_str.size
      dynstr.sh_offset = off
      dynstr.sh_addr = vaddr
    end
  end
end
patch_dynamic() click to toggle source
# File lib/patchelf/saver.rb, line 88
def patch_dynamic
  # We never do inline patching on strtab's string.
  # 1. Search if there's useful string exists
  #   - only need header patching
  # 2. Append a new string to the strtab.
  #   - register strtab extension
  dynamic.tags # HACK, force @tags to be defined
  patch_soname if @set[:soname]
  patch_runpath if @set[:runpath]
  patch_runpath(:rpath) if @set[:rpath]
  patch_needed if @set[:needed]
  malloc_strtab!
  expand_dynamic!
end
patch_interpreter() click to toggle source
# File lib/patchelf/saver.rb, line 53
def patch_interpreter
  return if @set[:interpreter].nil?

  new_interp = @set[:interpreter] + "\x00"
  old_interp = @elf.segment_by_type(:interp).interp_name + "\x00"
  return if old_interp == new_interp

  # These headers must be found here but not in the proc.
  seg_header = @elf.segment_by_type(:interp).header
  sec_header = section_header('.interp')

  patch = proc do |off, vaddr|
    # Register an inline patching
    inline_patch(off, new_interp)

    # The patching feature of ELFTools
    seg_header.p_offset = off
    seg_header.p_vaddr = seg_header.p_paddr = vaddr
    seg_header.p_filesz = seg_header.p_memsz = new_interp.size

    if sec_header
      sec_header.sh_offset = off
      sec_header.sh_size = new_interp.size
    end
  end

  if new_interp.size <= old_interp.size
    # easy case
    patch.call(seg_header.p_offset.to_i, seg_header.p_vaddr.to_i)
  else
    # hard case, we have to request a new LOAD area
    @mm.malloc(new_interp.size, &patch)
  end
end
patch_needed() click to toggle source
# File lib/patchelf/saver.rb, line 121
def patch_needed
  original_needs = dynamic.tags_by_type(:needed)
  @set[:needed].uniq!

  original = original_needs.map(&:name)
  replace = @set[:needed]

  # 3 sets:
  # 1. in original and in needs - remain unchanged
  # 2. in original but not in needs - remove
  # 3. not in original and in needs - append
  append = replace - original
  remove = original - replace

  ignored_dyns = remove.each_with_object([]) do |name, ignored|
    dyn = original_needs.find { |n| n.name == name }.header
    dyn.d_tag = IGNORE
    ignored << dyn
  end

  append.zip(ignored_dyns) do |name, ignored_dyn|
    dyn = ignored_dyn || lazy_dyn(:needed)
    dyn.d_tag = ELFTools::Constants::DT_NEEDED
    reg_str_table(name) { |idx| dyn.d_val = idx }
  end
end
patch_out(out_file) click to toggle source

Modify the out_file according to registered patches.

# File lib/patchelf/saver.rb, line 243
def patch_out(out_file)
  File.open(out_file, 'r+') do |f|
    if @mm.extended?
      original_head = @mm.threshold
      extra = {}
      # Copy all data after the second load
      @elf.stream.pos = original_head
      extra[original_head + @mm.extend_size] = @elf.stream.read # read to end
      # zero out the 'gap' we created
      extra[original_head] = "\x00" * @mm.extend_size
      extra.each do |pos, str|
        f.pos = pos
        f.write(str)
      end
    end
    @elf.patches.each do |pos, str|
      f.pos = @mm.extended_offset(pos)
      f.write(str)
    end

    @inline_patch.each do |pos, str|
      f.pos = pos
      f.write(str)
    end
  end
end
patch_runpath(sym = :runpath) click to toggle source
# File lib/patchelf/saver.rb, line 111
def patch_runpath(sym = :runpath)
  tag = dynamic.tag_by_type(sym)
  tag = tag.nil? ? lazy_dyn(sym) : tag.header
  reg_str_table(@set[sym]) do |idx|
    tag.d_val = idx
  end
end
patch_soname() click to toggle source
# File lib/patchelf/saver.rb, line 103
def patch_soname
  # The tag must exist.
  so_tag = dynamic.tag_by_type(:soname)
  reg_str_table(@set[:soname]) do |idx|
    so_tag.header.d_val = idx
  end
end
reg_str_table(str) { |idx| ... } click to toggle source

@param [String] str @yieldparam [Integer] idx @yieldreturn [void]

# File lib/patchelf/saver.rb, line 208
def reg_str_table(str, &block)
  idx = strtab_string.index(str + "\x00")
  # Request string is already exist
  return yield idx if idx

  # Record the request
  @strtab_extend_requests << [str, block]
end
section_header(name) click to toggle source

@return [ELFTools::Sections::Section?]

# File lib/patchelf/saver.rb, line 271
def section_header(name)
  sec = @elf.section_by_name(name)
  return if sec.nil?

  sec.header
end
strtab_string() click to toggle source
# File lib/patchelf/saver.rb, line 217
def strtab_string
  return @strtab_string if defined?(@strtab_string)

  # TODO: handle no strtab exists..
  offset = @elf.offset_from_vma(dynamic.tag_by_type(:strtab).value)
  # This is a little tricky since no length information is stored in the tag.
  # We first get the file offset of the string then 'guess' where the end is.
  @elf.stream.pos = offset
  @strtab_string = +''
  loop do
    c = @elf.stream.read(1)
    break unless c =~ /\x00|[[:print:]]/

    @strtab_string << c
  end
  @strtab_string
end