class PEdump::Loader

This class is kinda Virtual Machine that mimics executable loading as real OS does. Can be used for unpacking, emulating, reversing, …

Constants

AddressOfEntryPoint
DEFAULT_FIND_LIMIT
FileAlignment
Magic
PointerToRawData
SectionAlignment
SizeOfRawData

Attributes

dos_stub[RW]
find_limit[RW]
image_base[RW]
mz_hdr[RW]
pe[RW]
pe_hdr[RW]
pedump[RW]
sections[RW]

Public Class Methods

load_minidump(io) click to toggle source
# File lib/pedump/loader.rb, line 96
def self.load_minidump io
  new.tap{ |ldr| ldr.load_minidump io }
end
new(io = nil, params = {}) click to toggle source

constructors

# File lib/pedump/loader.rb, line 33
def initialize io = nil, params = {}
  # @delta is for EFI TE loading, based on https://github.com/gdbinit/TELoader/blob/master/teloader.cpp
  @delta = 0
  @pedump = PEdump.new(io, params)
  if io
    @mz_hdr     = @pedump.mz
    @dos_stub   = @pedump.dos_stub
    @pe_hdr     = @pedump.pe
    @te_hdr     = @pedump.te

    @image_base = params[:image_base]
    if @pe_hdr
      @image_base ||= @pe_hdr.try(:ioh).try(:ImageBase)
    elsif @te_hdr
      @image_base ||= @te_hdr.ImageBase
      @delta = @pedump.te_shift
    end
    @image_base ||= 0

    load_sections @pedump.sections, io
  end
  @find_limit = params[:find_limit] || DEFAULT_FIND_LIMIT
end

Public Instance Methods

[](va, size, params = {}) click to toggle source

read arbitrary string

# File lib/pedump/loader.rb, line 130
def [] va, size, params = {}
  section = va2section(va)
  raise "no section for va=0x#{va.to_s 16}" unless section
  offset = va - section.va
  raise "negative offset #{offset}" if offset < 0
  r = section.data[offset,size]
  return nil if r.nil?
  if r.size < size && params.fetch(:zerofill, true)
    # append some empty data
    r << ("\x00".force_encoding('binary')) * (size - r.size)
  end
  r
end
[]=(va, size, data) click to toggle source

write arbitrary string

# File lib/pedump/loader.rb, line 145
def []= va, size, data
  raise "data.size != size" if data.size != size
  section = va2section(va)
  raise "no section for va=0x#{va.to_s 16}" unless section
  offset = va - section.va
  raise "negative offset #{offset}" if offset < 0
  if section.data.size < offset
    # append some empty data
    section.data << ("\x00".force_encoding('binary') * (offset-section.data.size))
  end
  section.data[offset, data.size] = data
end
_merge_ranges(max_diff = nil) click to toggle source

increasing max_diff speed ups the :valid_va? method, but may cause false positives

# File lib/pedump/loader.rb, line 187
def _merge_ranges max_diff = nil
  max_diff ||=
    if sections.size > 100
      1024*1024
    else
      0
    end

  ranges0 = sections.map(&:range).sort_by(&:begin)
  #puts "[.] #{ranges0.size} ranges"
  ranges1 = []
  range = ranges0.shift
  while ranges0.any?
    while (ranges0.first.begin-range.end).abs <= max_diff
      range = range.begin...ranges0.shift.end
      break if ranges0.empty?
    end
    #puts "[.] diff #{ranges0.first.begin-range.end}"
    ranges1 << range
    range = ranges0.shift
  end
  ranges1 << range
  #puts "[=] #{ranges1.size} ranges"
  ranges1.uniq.compact
end
_parse_exports() click to toggle source
# File lib/pedump/loader.rb, line 295
def _parse_exports
  return {} unless @pedump.exports
  @pedump.exports.functions.each do |func|
    @names[@image_base + func.va] = func.name || "##{func.ordinal}"
  end
end
_parse_imports() click to toggle source
# File lib/pedump/loader.rb, line 284
def _parse_imports
  @pedump.imports.each do |iid| # Image Import Descriptor
    va = iid.FirstThunk + @image_base
    (Array(iid.original_first_thunk) + Array(iid.first_thunk)).uniq.each do |func|
      name = "__imp_" + (func.name || "#{func.ordinal}")
      @names[va] = name
      va += 4
    end
  end
end
dump(io)
Alias for: export
dw(va, n=nil) click to toggle source

read single DWord (4 bytes) if no ā€˜n’ specified delegate to dwords otherwise

# File lib/pedump/loader.rb, line 170
def dw va, n=nil
  n ? dwords(va,n) : self[va,4].unpack('L')[0]
end
Also aliased as: dword
dword(va, n=nil)
Alias for: dw
dwords(va, n) click to toggle source

read N DWords, returns array

# File lib/pedump/loader.rb, line 176
def dwords va, n
  self[va,4*n].unpack('L*')
end
ep() click to toggle source
# File lib/pedump/loader.rb, line 19
def ep
  if @pe_hdr
    @pe_hdr.try(:ioh).try(:AddressOfEntryPoint)
  elsif @te_hdr
    @te_hdr.AddressOfEntryPoint - @delta
  end
end
ep=(v;) click to toggle source
# File lib/pedump/loader.rb, line 27
def ep= v; @pe_hdr.ioh.AddressOfEntryPoint=v; end
export(io) click to toggle source

save a new PE file to specified IO

# File lib/pedump/loader.rb, line 319
def export io
  @mz_hdr     ||= PEdump::MZ.new("MZ", *[0]*22)
  @dos_stub   ||= ''
  @pe_hdr     ||= PEdump::PE.new("PE\x00\x00")
  @pe_hdr.ioh ||=
    PEdump::IMAGE_OPTIONAL_HEADER32.read( StringIO.new("\x00" * 224) ).tap do |ioh|
      ioh.Magic               = 0x10b # 32-bit executable
      #ioh.NumberOfRvaAndSizes = 0x10
    end
  @pe_hdr.ifh ||= PEdump::IMAGE_FILE_HEADER.new(
    :Machine              => 0x14c,          # x86
    :NumberOfSections     => @sections.size,
    :TimeDateStamp        => 0,
    :PointerToSymbolTable => 0,
    :NumberOfSymbols      => 0,
    :SizeOfOptionalHeader => @pe_hdr.ioh.pack.size,
    :Characteristics      => 0x102           # EXECUTABLE_IMAGE | 32BIT_MACHINE
  )

  if @pe_hdr.ioh.FileAlignment.to_i == 0
    # default file align = 512 bytes
    @pe_hdr.ioh.FileAlignment = 0x200
  end
  if @pe_hdr.ioh.SectionAlignment.to_i == 0
    # default section align = 4k
    @pe_hdr.ioh.SectionAlignment = 0x1000
  end

  mz_size = @mz_hdr.pack.size
  raise "odd mz_size #{mz_size}" if mz_size % 0x10 != 0
  @mz_hdr.header_paragraphs = mz_size / 0x10              # offset of dos_stub
  @mz_hdr.lfanew = mz_size + @dos_stub.size               # offset of PE hdr
  io.write @mz_hdr.pack
  io.write @dos_stub
  io.write @pe_hdr.pack
  io.write @pe_hdr.ioh.DataDirectory.map(&:pack).join

  section_tbl_offset = io.tell # store offset for 2nd write of section table
  io.write section_table

  align = @pe_hdr.ioh.FileAlignment
  @sections.each do |section|
    io.seek(align - (io.tell % align), IO::SEEK_CUR) if io.tell % align != 0
    section.hdr.PointerToRawData = io.tell  # fix raw_ptr
    io.write(section.data)
  end

  eof = io.tell

  # 2nd write of section table with correct raw_ptr's
  io.seek section_tbl_offset
  io.write section_table

  io.seek eof
end
Also aliased as: dump
find(needle, options = {}) click to toggle source

find first occurence of string returns VA

# File lib/pedump/loader.rb, line 215
def find needle, options = {}
  options[:align] ||= 1
  options[:limit] ||= @find_limit

  if needle.is_a?(Fixnum)
    # silently convert to DWORD
    needle = [needle].pack('L')
  end

  if options[:align] == 1
    # fastest find?
    processed_bytes = 0
    sections.each do |section|
      next unless section.data # skip empty sections
      pos = section.data.index(needle)
      return section.va+pos if pos
      processed_bytes += section.vsize
      return nil if processed_bytes >= options[:limit]
    end
  end
  nil
end
find_all(needle, options = {}) click to toggle source

find all occurences of string returns array of VAs or empty array

# File lib/pedump/loader.rb, line 240
def find_all needle, options = {}
  options[:align] ||= 1
  options[:limit] ||= @find_limit

  if needle.is_a?(Fixnum)
    # silently convert to DWORD
    needle = [needle].pack('L')
  end

  r = []
  if options[:align] == 1
    # fastest find?
    processed_bytes = 0
    sections.each do |section|
      next unless section.data # skip empty sections
      section.data.scan(needle) do
        r << $~.begin(0) + section.va
      end
      processed_bytes += section.vsize
      return r if processed_bytes >= options[:limit]
    end
  end
  r
end
io(va) click to toggle source

returns StringIO with section data, pre-seeked to specified VA TODO: make io cross sections

# File lib/pedump/loader.rb, line 160
def io va
  section = va2section(va)
  raise "no section for va=0x#{va.to_s 16}" unless section
  offset = va - section.va
  raise "negative offset #{offset}" if offset < 0
  StringIO.new(section.data).tap{ |io| io.seek offset }
end
load_minidump(io, options = {}) click to toggle source

load MS Minidump (*.dmp) file, that can be created in Task Manager via right click on process -> save memory dump

# File lib/pedump/loader.rb, line 81
def load_minidump io, options = {}
  @sections ||= []
  md = Minidump.new io
  options[:merge] = true unless options.key?(:merge)
  md.memory_ranges(options).each do |mr|
    hdr = PEdump::IMAGE_SECTION_HEADER.new(
      :VirtualAddress   => mr.va,
      :PointerToRawData => mr.file_offset,
      :SizeOfRawData    => mr.size,
      :VirtualSize      => mr.size            # XXX may be larger than SizeOfRawData
    )
    @sections << Section.new( hdr, :deferred_load_io => io )
  end
end
load_sections(section_hdrs, f = nil) click to toggle source
# File lib/pedump/loader.rb, line 57
  def load_sections section_hdrs, f = nil
    if section_hdrs.is_a?(Array)
      @sections = section_hdrs.map do |x|
        raise "unknown section hdr: #{x.inspect}" unless x.is_a?(PEdump::IMAGE_SECTION_HEADER)
        Section.new(x, :deferred_load_io => f, :image_base => @image_base, :delta => @delta )
      end
      if f.respond_to?(:seek) && f.respond_to?(:read)
        #
        # converted to deferred loading
        #
#        section_hdrs.each_with_index do |sect_hdr, idx|
#          f.seek sect_hdr.PointerToRawData
#          @sections[idx].data = f.read(sect_hdr.SizeOfRawData)
#        end
      elsif f
        raise "invalid 2nd arg: #{f.inspect}"
      end
    else
      raise "invalid arg: #{section_hdrs.inspect}"
    end
  end
names() click to toggle source

parsing names

# File lib/pedump/loader.rb, line 269
def names
  return @names if @names
  @names = {}

  oep = ep()
  if oep
    @names[oep + @image_base] = 'start'
  end

  _parse_imports
  _parse_exports
  #TODO: debug info
  @names
end
rva2section(rva) click to toggle source

RVA (Relative VA) to section

# File lib/pedump/loader.rb, line 110
def rva2section rva
  va2section( rva + @image_base )
end
rva2stream(rva) click to toggle source
# File lib/pedump/loader.rb, line 121
def rva2stream rva
  va2stream( rva + @image_base )
end
section_table() click to toggle source

generating PE binary

# File lib/pedump/loader.rb, line 306
def section_table
  @sections.map do |section|
    section.hdr.SizeOfRawData = section.data.size
    section.hdr.PointerToRelocations ||= 0
    section.hdr.PointerToLinenumbers ||= 0
    section.hdr.NumberOfRelocations  ||= 0
    section.hdr.NumberOfLinenumbers  ||= 0
    section.hdr.Characteristics      ||= 0
    section.hdr.pack
  end.join
end
va2section(va) click to toggle source

VA to section

# File lib/pedump/loader.rb, line 105
def va2section va
  @sections.find{ |x| x.range.include?(va) }
end
va2stream(va) click to toggle source
# File lib/pedump/loader.rb, line 114
def va2stream va
  return nil unless section = va2section(va)
  StringIO.new(section.data).tap do |io|
    io.seek va-section.va
  end
end
valid_va?(va) click to toggle source

check if any section has specified VA in its range

# File lib/pedump/loader.rb, line 181
def valid_va? va
  @ranges ||= _merge_ranges
  @ranges.any?{ |range| range.include?(va) }
end