lib/elftools/elf_file.rb
# frozen_string_literal: true
require 'elftools/constants'
require 'elftools/exceptions'
require 'elftools/lazy_array'
require 'elftools/sections/sections'
require 'elftools/segments/segments'
require 'elftools/structs'
module ELFTools
# The main class for using elftools.
class ELFFile
attr_reader :stream # @return [#pos=, #read] The +File+ object.
attr_reader :elf_class # @return [Integer] 32 or 64.
attr_reader :endian # @return [Symbol] +:little+ or +:big+.
# Instantiate an {ELFFile} object.
#
# @param [#pos=, #read] stream
# The +File+ object to be fetch information from.
# @example
# ELFFile.new(File.open('/bin/cat'))
# #=> #<ELFTools::ELFFile:0x00564b106c32a0 @elf_class=64, @endian=:little, @stream=#<File:/bin/cat>>
def initialize(stream)
@stream = stream
# always set binmode if stream is an IO object.
@stream.binmode if @stream.respond_to?(:binmode)
identify # fetch the most basic information
end
# Return the file header.
#
# Lazy loading.
# @return [ELFTools::Structs::ELF_Ehdr] The header.
def header
return @header if defined?(@header)
stream.pos = 0
@header = Structs::ELF_Ehdr.new(endian:, offset: stream.pos)
@header.elf_class = elf_class
@header.read(stream)
end
# Return the BuildID of ELF.
# @return [String, nil]
# BuildID in hex form will be returned.
# +nil+ is returned if the .note.gnu.build-id section
# is not found.
# @example
# elf.build_id
# #=> '73ab62cb7bc9959ce053c2b711322158708cdc07'
def build_id
section = section_by_name('.note.gnu.build-id')
return nil if section.nil?
note = section.notes.first
return nil if note.nil?
note.desc.unpack1('H*')
end
# Get machine architecture.
#
# Mappings of architecture can be found
# in {ELFTools::Constants::EM.mapping}.
# @return [String]
# Name of architecture.
# @example
# elf.machine
# #=> 'Advanced Micro Devices X86-64'
def machine
ELFTools::Constants::EM.mapping(header.e_machine)
end
# Return the ELF type according to +e_type+.
# @return [String] Type in string format.
# @example
# ELFFile.new(File.open('spec/files/libc.so.6')).elf_type
# #=> 'DYN'
# ELFFile.new(File.open('spec/files/amd64.elf')).elf_type
# #=> 'EXEC'
def elf_type
ELFTools::Constants::ET.mapping(header.e_type)
end
#========= method about sections
# Number of sections in this file.
# @return [Integer] The desired number.
# @example
# elf.num_sections
# #=> 29
def num_sections
header.e_shnum
end
# Acquire the section named as +name+.
# @param [String] name The desired section name.
# @return [ELFTools::Sections::Section, nil] The target section.
# @example
# elf.section_by_name('.note.gnu.build-id')
# #=> #<ELFTools::Sections::Section:0x005647b1282428>
# elf.section_by_name('')
# #=> #<ELFTools::Sections::NullSection:0x005647b11da110>
# elf.section_by_name('no such section')
# #=> nil
def section_by_name(name)
each_sections.find { |sec| sec.name == name }
end
# Iterate all sections.
#
# All sections are lazy loading, the section
# only be created whenever accessing it.
# This method is useful for {#section_by_name}
# since not all sections need to be created.
# @yieldparam [ELFTools::Sections::Section] section A section.
# @yieldreturn [void]
# @return [Enumerator<ELFTools::Sections::Section>, Array<ELFTools::Sections::Section>]
# As +Array#each+, if block is not given, a enumerator will be returned,
# otherwise, the whole sections will be returned.
def each_sections(&block)
return enum_for(:each_sections) unless block_given?
Array.new(num_sections) do |i|
section_at(i).tap(&block)
end
end
# Simply use {#sections} to get all sections.
# @return [Array<ELFTools::Sections::Section>]
# Whole sections.
def sections
each_sections.to_a
end
# Acquire the +n+-th section, 0-based.
#
# Sections are lazy loaded.
# @param [Integer] n The index.
# @return [ELFTools::Sections::Section, nil]
# The target section.
# If +n+ is out of bound, +nil+ is returned.
def section_at(n)
@sections ||= LazyArray.new(num_sections, &method(:create_section))
@sections[n]
end
# Fetch all sections with specific type.
#
# The available types are listed in {ELFTools::Constants::PT}.
# This method accept giving block.
# @param [Integer, Symbol, String] type
# The type needed, similar format as {#segment_by_type}.
# @yieldparam [ELFTools::Sections::Section] section A section in specific type.
# @yieldreturn [void]
# @return [Array<ELFTools::Sections::section>] The target sections.
# @example
# elf = ELFTools::ELFFile.new(File.open('spec/files/amd64.elf'))
# elf.sections_by_type(:rela)
# #=> [#<ELFTools::Sections::RelocationSection:0x00563cd3219970>,
# # #<ELFTools::Sections::RelocationSection:0x00563cd3b89d70>]
def sections_by_type(type, &block)
type = Util.to_constant(Constants::SHT, type)
Util.select_by_type(each_sections, type, &block)
end
# Get the string table section.
#
# This section is acquired by using the +e_shstrndx+
# in ELF header.
# @return [ELFTools::Sections::StrTabSection] The desired section.
def strtab_section
section_at(header.e_shstrndx)
end
#========= method about segments
# Number of segments in this file.
# @return [Integer] The desited number.
def num_segments
header.e_phnum
end
# Iterate all segments.
#
# All segments are lazy loading, the segment
# only be created whenever accessing it.
# This method is useful for {#segment_by_type}
# since not all segments need to be created.
# @yieldparam [ELFTools::Segments::Segment] segment A segment.
# @yieldreturn [void]
# @return [Array<ELFTools::Segments::Segment>]
# Whole segments will be returned.
def each_segments(&block)
return enum_for(:each_segments) unless block_given?
Array.new(num_segments) do |i|
segment_at(i).tap(&block)
end
end
# Simply use {#segments} to get all segments.
# @return [Array<ELFTools::Segments::Segment>]
# Whole segments.
def segments
each_segments.to_a
end
# Get the first segment with +p_type=type+.
# The available types are listed in {ELFTools::Constants::PT}.
#
# @note
# This method will return the first segment found,
# to found all segments with specific type you can use {#segments_by_type}.
# @param [Integer, Symbol, String] type
# See examples for clear usage.
# @return [ELFTools::Segments::Segment] The target segment.
# @example
# # type as an integer
# elf.segment_by_type(ELFTools::Constants::PT_NOTE)
# #=> #<ELFTools::Segments::NoteSegment:0x005629dda1e4f8>
#
# elf.segment_by_type(4) # PT_NOTE
# #=> #<ELFTools::Segments::NoteSegment:0x005629dda1e4f8>
#
# # type as a symbol
# elf.segment_by_type(:PT_NOTE)
# #=> #<ELFTools::Segments::NoteSegment:0x005629dda1e4f8>
#
# # you can do this
# elf.segment_by_type(:note) # will be transformed into `PT_NOTE`
# #=> #<ELFTools::Segments::NoteSegment:0x005629dda1e4f8>
#
# # type as a string
# elf.segment_by_type('PT_NOTE')
# #=> #<ELFTools::Segments::NoteSegment:0x005629dda1e4f8>
#
# # this is ok
# elf.segment_by_type('note') # will be transformed into `PT_NOTE`
# #=> #<ELFTools::Segments::NoteSegment:0x005629dda1e4f8>
# @example
# elf.segment_by_type(1337)
# # ArgumentError: No constants in Constants::PT is 1337
#
# elf.segment_by_type('oao')
# # ArgumentError: No constants in Constants::PT named "PT_OAO"
# @example
# elf.segment_by_type(0)
# #=> nil # no such segment exists
def segment_by_type(type)
type = Util.to_constant(Constants::PT, type)
each_segments.find { |seg| seg.header.p_type == type }
end
# Fetch all segments with specific type.
#
# If you want to find only one segment,
# use {#segment_by_type} instead.
# This method accept giving block.
# @param [Integer, Symbol, String] type
# The type needed, same format as {#segment_by_type}.
# @yieldparam [ELFTools::Segments::Segment] segment A segment in specific type.
# @yieldreturn [void]
# @return [Array<ELFTools::Segments::Segment>] The target segments.
def segments_by_type(type, &block)
type = Util.to_constant(Constants::PT, type)
Util.select_by_type(each_segments, type, &block)
end
# Acquire the +n+-th segment, 0-based.
#
# Segments are lazy loaded.
# @param [Integer] n The index.
# @return [ELFTools::Segments::Segment, nil]
# The target segment.
# If +n+ is out of bound, +nil+ is returned.
def segment_at(n)
@segments ||= LazyArray.new(num_segments, &method(:create_segment))
@segments[n]
end
# Get the offset related to file, given virtual memory address.
#
# This method should work no matter ELF is a PIE or not.
# This method refers from (actually equals to) binutils/readelf.c#offset_from_vma.
# @param [Integer] vma The virtual address to be queried.
# @return [Integer] Related file offset.
# @example
# elf = ELFTools::ELFFile.new(File.open('/bin/cat'))
# elf.offset_from_vma(0x401337)
# #=> 4919 # 0x1337
def offset_from_vma(vma, size = 1)
segments_by_type(:load) do |seg|
return seg.vma_to_offset(vma) if seg.vma_in?(vma, size)
end
end
# The patch status.
# @return [Hash{Integer => String}]
def patches
patch = {}
loaded_headers.each do |header|
header.patches.each do |key, val|
patch[key + header.offset] = val
end
end
patch
end
# Apply patches and save as +filename+.
#
# @param [String] filename
# @return [void]
def save(filename)
stream.pos = 0
all = stream.read.force_encoding('ascii-8bit')
patches.each do |pos, val|
all[pos, val.size] = val
end
File.binwrite(filename, all)
end
private
# bad idea..
def loaded_headers
explore = lambda do |obj|
return obj if obj.is_a?(::ELFTools::Structs::ELFStruct)
return obj.map(&explore) if obj.is_a?(Array)
obj.instance_variables.map do |s|
explore.call(obj.instance_variable_get(s))
end
end
explore.call(self).flatten
end
def identify
stream.pos = 0
magic = stream.read(4)
raise ELFMagicError, "Invalid magic number #{magic.inspect}" unless magic == Constants::ELFMAG
ei_class = stream.read(1).ord
@elf_class = {
1 => 32,
2 => 64
}[ei_class]
raise ELFClassError, format('Invalid EI_CLASS "\x%02x"', ei_class) if elf_class.nil?
ei_data = stream.read(1).ord
@endian = {
1 => :little,
2 => :big
}[ei_data]
raise ELFDataError, format('Invalid EI_DATA "\x%02x"', ei_data) if endian.nil?
end
def create_section(n)
stream.pos = header.e_shoff + n * header.e_shentsize
shdr = Structs::ELF_Shdr.new(endian:, offset: stream.pos)
shdr.elf_class = elf_class
shdr.read(stream)
Sections::Section.create(shdr, stream,
offset_from_vma: method(:offset_from_vma),
strtab: method(:strtab_section),
section_at: method(:section_at))
end
def create_segment(n)
stream.pos = header.e_phoff + n * header.e_phentsize
phdr = Structs::ELF_Phdr[elf_class].new(endian:, offset: stream.pos)
phdr.elf_class = elf_class
Segments::Segment.create(phdr.read(stream), stream, offset_from_vma: method(:offset_from_vma))
end
end
end