Class: PEdump::Loader

Inherits:
Object show all
Defined in:
lib/pedump/loader.rb,
lib/pedump/loader/section.rb,
lib/pedump/loader/minidump.rb

Overview

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

Defined Under Namespace

Classes: Minidump, Section

Constant Summary collapse

DEFAULT_FIND_LIMIT =
2**64

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(io = nil, params = {}) ⇒ Loader

constructors



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# 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

Instance Attribute Details

#dos_stubObject

Returns the value of attribute dos_stub.



11
12
13
# File 'lib/pedump/loader.rb', line 11

def dos_stub
  @dos_stub
end

#find_limitObject

Returns the value of attribute find_limit.



12
13
14
# File 'lib/pedump/loader.rb', line 12

def find_limit
  @find_limit
end

#image_baseObject

Returns the value of attribute image_base.



11
12
13
# File 'lib/pedump/loader.rb', line 11

def image_base
  @image_base
end

#mz_hdrObject

Returns the value of attribute mz_hdr.



11
12
13
# File 'lib/pedump/loader.rb', line 11

def mz_hdr
  @mz_hdr
end

#pe_hdrObject Also known as: pe

Returns the value of attribute pe_hdr.



11
12
13
# File 'lib/pedump/loader.rb', line 11

def pe_hdr
  @pe_hdr
end

#pedumpObject

Returns the value of attribute pedump.



11
12
13
# File 'lib/pedump/loader.rb', line 11

def pedump
  @pedump
end

#sectionsObject

Returns the value of attribute sections.



11
12
13
# File 'lib/pedump/loader.rb', line 11

def sections
  @sections
end

Class Method Details

.load_minidump(io) ⇒ Object



96
97
98
# File 'lib/pedump/loader.rb', line 96

def self.load_minidump io
  new.tap{ |ldr| ldr.load_minidump io }
end

Instance Method Details

#[](va, size, params = {}) ⇒ Object

read arbitrary string



130
131
132
133
134
135
136
137
138
139
140
141
142
# 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) ⇒ Object

write arbitrary string



145
146
147
148
149
150
151
152
153
154
155
156
# 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) ⇒ Object

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



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# 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_exportsObject



295
296
297
298
299
300
# 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_importsObject



284
285
286
287
288
289
290
291
292
293
# 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

#dw(va, n = nil) ⇒ Object Also known as: dword

read single DWord (4 bytes) if no ‘n’ specified delegate to #dwords otherwise



170
171
172
# File 'lib/pedump/loader.rb', line 170

def dw va, n=nil
  n ? dwords(va,n) : self[va,4].unpack('L')[0]
end

#dwords(va, n) ⇒ Object

read N DWords, returns array



176
177
178
# File 'lib/pedump/loader.rb', line 176

def dwords va, n
  self[va,4*n].unpack('L*')
end

#epObject



19
20
21
22
23
24
25
# 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) ⇒ Object



27
# File 'lib/pedump/loader.rb', line 27

def ep= v; @pe_hdr.ioh.AddressOfEntryPoint=v; end

#export(io) ⇒ Object Also known as: dump

save a new PE file to specified IO



319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
# 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

#find(needle, options = {}) ⇒ Object

find first occurence of string returns VA



215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# 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 = {}) ⇒ Object

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



240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# 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) ⇒ Object

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



160
161
162
163
164
165
166
# 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 = {}) ⇒ Object

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



81
82
83
84
85
86
87
88
89
90
91
92
93
94
# 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) ⇒ Object



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# 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

#namesObject

parsing names



269
270
271
272
273
274
275
276
277
278
279
280
281
282
# 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) ⇒ Object

RVA (Relative VA) to section



110
111
112
# File 'lib/pedump/loader.rb', line 110

def rva2section rva
  va2section( rva + @image_base )
end

#rva2stream(rva) ⇒ Object



121
122
123
# File 'lib/pedump/loader.rb', line 121

def rva2stream rva
  va2stream( rva + @image_base )
end

#section_tableObject

generating PE binary



306
307
308
309
310
311
312
313
314
315
316
# 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) ⇒ Object

VA to section



105
106
107
# File 'lib/pedump/loader.rb', line 105

def va2section va
  @sections.find{ |x| x.range.include?(va) }
end

#va2stream(va) ⇒ Object



114
115
116
117
118
119
# 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) ⇒ Boolean

check if any section has specified VA in its range

Returns:

  • (Boolean)


181
182
183
184
# File 'lib/pedump/loader.rb', line 181

def valid_va? va
  @ranges ||= _merge_ranges
  @ranges.any?{ |range| range.include?(va) }
end