pedump 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1105 @@
1
+ #!/usr/bin/env ruby
2
+ require 'logger'
3
+ require 'pedump/version'
4
+
5
+ # pedump.rb by zed_0xff
6
+ #
7
+ # http://zed.0xff.me
8
+ # http://github.com/zed-0xff
9
+
10
+ class String
11
+ def xor x
12
+ if x.is_a?(String)
13
+ r = ''
14
+ j = 0
15
+ 0.upto(self.size-1) do |i|
16
+ r << (self[i].ord^x[j].ord).chr
17
+ j+=1
18
+ j=0 if j>= x.size
19
+ end
20
+ r
21
+ else
22
+ r = ''
23
+ 0.upto(self.size-1) do |i|
24
+ r << (self[i].ord^x).chr
25
+ end
26
+ r
27
+ end
28
+ end
29
+ end
30
+
31
+ class File
32
+ def checked_seek newpos
33
+ @file_range ||= (0..size)
34
+ @file_range.include?(newpos) && (seek(newpos) || true)
35
+ end
36
+ end
37
+
38
+ class PEdump
39
+ attr_accessor :fname, :logger, :force
40
+
41
+ VERSION = Version::STRING
42
+
43
+ @@logger = nil
44
+
45
+ def initialize fname, params = {}
46
+ @fname = fname
47
+ @force = params[:force]
48
+ @logger = @@logger = params[:logger] || PEdump::Logger.new(STDERR)
49
+ end
50
+
51
+ class Logger < ::Logger
52
+ def initialize *args
53
+ super
54
+ @formatter = proc do |severity,_,_,msg|
55
+ # quick and dirty way to remove duplicate messages
56
+ if @prevmsg == msg && severity != 'DEBUG' && severity != 'INFO'
57
+ ''
58
+ else
59
+ @prevmsg = msg
60
+ "#{msg}\n"
61
+ end
62
+ end
63
+ @level = Logger::WARN
64
+ end
65
+ end
66
+
67
+ module Readable
68
+ def read file, size = nil
69
+ size ||= const_get 'SIZE'
70
+ data = file.read(size).to_s
71
+ if data.size < size && PEdump.logger
72
+ PEdump.logger.error "[!] #{self.to_s} want #{size} bytes, got #{data.size}"
73
+ end
74
+ new(*data.unpack(const_get('FORMAT')))
75
+ end
76
+ end
77
+
78
+ class << self
79
+ def logger; @@logger; end
80
+ def logger= l; @@logger=l; end
81
+
82
+ def create_struct fmt, *args
83
+ size = fmt.scan(/([a-z])(\d*)/i).map do |f,len|
84
+ [len.to_i, 1].max *
85
+ case f
86
+ when /[aAC]/ then 1
87
+ when 'v' then 2
88
+ when 'V' then 4
89
+ when 'Q' then 8
90
+ else raise "unknown fmt #{f.inspect}"
91
+ end
92
+ end.inject(&:+)
93
+
94
+ Struct.new( *args ).tap do |x|
95
+ x.const_set 'FORMAT', fmt
96
+ x.const_set 'SIZE', size
97
+ x.class_eval do
98
+ def pack
99
+ to_a.pack self.class.const_get('FORMAT')
100
+ end
101
+ def empty?
102
+ to_a.all?{ |t| t == 0 || t.nil? || t.to_s.tr("\x00","").empty? }
103
+ end
104
+ end
105
+ x.extend Readable
106
+ end
107
+ end
108
+ end
109
+
110
+
111
+ # http://www.delorie.com/djgpp/doc/exe/
112
+ MZ = create_struct( "a2v13Qv2V6",
113
+ :signature,
114
+ :bytes_in_last_block,
115
+ :blocks_in_file,
116
+ :num_relocs,
117
+ :header_paragraphs,
118
+ :min_extra_paragraphs,
119
+ :max_extra_paragraphs,
120
+ :ss,
121
+ :sp,
122
+ :checksum,
123
+ :ip,
124
+ :cs,
125
+ :reloc_table_offset,
126
+ :overlay_number,
127
+ :reserved0, # 8 reserved bytes
128
+ :oem_id,
129
+ :oem_info,
130
+ :reserved2, # 20 reserved bytes
131
+ :reserved3,
132
+ :reserved4,
133
+ :reserved5,
134
+ :reserved6,
135
+ :lfanew
136
+ )
137
+
138
+ class PE < Struct.new(
139
+ :signature, # "PE\x00\x00"
140
+ :image_file_header,
141
+ :image_optional_header,
142
+ :section_table
143
+ )
144
+ alias :ifh :image_file_header
145
+ alias :ioh :image_optional_header
146
+ def x64?
147
+ ifh && ifh.Machine == 0x8664
148
+ end
149
+ def dll?
150
+ ifh && ifh.flags.include?('DLL')
151
+ end
152
+ end
153
+
154
+ # http://msdn.microsoft.com/en-us/library/ms809762.aspx
155
+ class IMAGE_FILE_HEADER < create_struct( 'v2V3v2',
156
+ :Machine, # w
157
+ :NumberOfSections, # w
158
+ :TimeDateStamp, # dw
159
+ :PointerToSymbolTable, # dw
160
+ :NumberOfSymbols, # dw
161
+ :SizeOfOptionalHeader, # w
162
+ :Characteristics # w
163
+ )
164
+ # Characteristics, http://msdn.microsoft.com/en-us/library/windows/desktop/ms680313(v=VS.85).aspx)
165
+ FLAGS = {
166
+ 0x0001 => 'RELOCS_STRIPPED', # Relocation information was stripped from the file.
167
+ # The file must be loaded at its preferred base address.
168
+ # If the base address is not available, the loader reports an error.
169
+ 0x0002 => 'EXECUTABLE_IMAGE',
170
+ 0x0004 => 'LINE_NUMS_STRIPPED',
171
+ 0x0008 => 'LOCAL_SYMS_STRIPPED',
172
+ 0x0010 => 'AGGRESIVE_WS_TRIM', # Aggressively trim the working set. This value is obsolete as of Windows 2000.
173
+ 0x0020 => 'LARGE_ADDRESS_AWARE', # The application can handle addresses larger than 2 GB.
174
+ 0x0040 => '16BIT_MACHINE',
175
+ 0x0080 => 'BYTES_REVERSED_LO', # The bytes of the word are reversed. This flag is obsolete.
176
+ 0x0100 => '32BIT_MACHINE',
177
+ 0x0200 => 'DEBUG_STRIPPED',
178
+ 0x0400 => 'REMOVABLE_RUN_FROM_SWAP',
179
+ 0x0800 => 'NET_RUN_FROM_SWAP',
180
+ 0x1000 => 'SYSTEM',
181
+ 0x2000 => 'DLL',
182
+ 0x4000 => 'UP_SYSTEM_ONLY', # The file should be run only on a uniprocessor computer.
183
+ 0x8000 => 'BYTES_REVERSED_HI' # The bytes of the word are reversed. This flag is obsolete.
184
+ }
185
+
186
+ def initialize *args
187
+ super
188
+ self.TimeDateStamp = Time.at(self.TimeDateStamp)
189
+ end
190
+ def flags
191
+ FLAGS.find_all{ |k,v| (self.Characteristics & k) != 0 }.map(&:last)
192
+ end
193
+ end
194
+
195
+ module IMAGE_OPTIONAL_HEADER
196
+ # DllCharacteristics, http://msdn.microsoft.com/en-us/library/windows/desktop/ms680339(v=vs.85).aspx)
197
+ FLAGS = {
198
+ 0x0001 => '0x01', # reserved
199
+ 0x0002 => '0x02', # reserved
200
+ 0x0004 => '0x04', # reserved
201
+ 0x0008 => '0x08', # reserved
202
+ 0x0010 => '0x10', # ?
203
+ 0x0020 => '0x20', # ?
204
+ 0x0040 => 'DYNAMIC_BASE',
205
+ 0x0080 => 'FORCE_INTEGRITY',
206
+ 0x0100 => 'NX_COMPAT',
207
+ 0x0200 => 'NO_ISOLATION',
208
+ 0x0400 => 'NO_SEH',
209
+ 0x0800 => 'NO_BIND',
210
+ 0x1000 => '0x1000', # ?
211
+ 0x2000 => 'WDM_DRIVER',
212
+ 0x4000 => '0x4000', # ?
213
+ 0x8000 => 'TERMINAL_SERVER_AWARE'
214
+ }
215
+ def initialize *args
216
+ super
217
+ self.extend InstanceMethods
218
+ end
219
+ def self.included base
220
+ base.extend ClassMethods
221
+ end
222
+ module ClassMethods
223
+ def read file, size = nil
224
+ usual_size = self.const_get('USUAL_SIZE')
225
+ cSIZE = self.const_get 'SIZE'
226
+ cFORMAT = self.const_get 'FORMAT'
227
+ size ||= cSIZE
228
+ PEdump.logger.warn "[?] unusual size of IMAGE_OPTIONAL_HEADER = #{size} (must be #{usual_size})" if size != usual_size
229
+ new(*file.read([size,cSIZE].min).to_s.unpack(cFORMAT)).tap do |ioh|
230
+ ioh.DataDirectory = []
231
+
232
+ # check if "...this address is outside the memory mapped file and is zeroed by the OS"
233
+ # see http://www.phreedom.org/solar/code/tinype/, section "Removing the data directories"
234
+ ioh.each_pair{ |k,v| ioh[k] = 0 if v.nil? }
235
+
236
+ # http://opcode0x90.wordpress.com/2007/04/22/windows-loader-does-it-differently/
237
+ # maximum of 0x10 entries, even if bigger
238
+ [0x10,ioh.NumberOfRvaAndSizes].min.times do |idx|
239
+ ioh.DataDirectory << IMAGE_DATA_DIRECTORY.read(file)
240
+ ioh.DataDirectory.last.type = IMAGE_DATA_DIRECTORY::TYPES[idx]
241
+ end
242
+ #ioh.DataDirectory.pop while ioh.DataDirectory.last.empty?
243
+ end
244
+ end
245
+ end
246
+ module InstanceMethods
247
+ def flags
248
+ FLAGS.find_all{ |k,v| (self.DllCharacteristics & k) != 0 }.map(&:last)
249
+ end
250
+ end
251
+ end
252
+
253
+ # http://msdn.microsoft.com/en-us/library/ms809762.aspx
254
+ class IMAGE_OPTIONAL_HEADER32 < create_struct( 'vC2V9v6V4v2V6',
255
+ :Magic, # w
256
+ :MajorLinkerVersion, :MinorLinkerVersion, # 2b
257
+ :SizeOfCode, :SizeOfInitializedData, :SizeOfUninitializedData, :AddressOfEntryPoint, # 9dw
258
+ :BaseOfCode, :BaseOfData, :ImageBase, :SectionAlignment, :FileAlignment,
259
+ :MajorOperatingSystemVersion, :MinorOperatingSystemVersion, # 6w
260
+ :MajorImageVersion, :MinorImageVersion, :MajorSubsystemVersion, :MinorSubsystemVersion,
261
+ :Reserved1, :SizeOfImage, :SizeOfHeaders, :CheckSum, # 4dw
262
+ :Subsystem, :DllCharacteristics, # 2w
263
+ :SizeOfStackReserve, :SizeOfStackCommit, :SizeOfHeapReserve, :SizeOfHeapCommit, # 6dw
264
+ :LoaderFlags, :NumberOfRvaAndSizes,
265
+ :DataDirectory # readed manually
266
+ )
267
+ USUAL_SIZE = 224
268
+ include IMAGE_OPTIONAL_HEADER
269
+ end
270
+
271
+ # http://msdn.microsoft.com/en-us/library/windows/desktop/ms680339(v=VS.85).aspx)
272
+ class IMAGE_OPTIONAL_HEADER64 < create_struct( 'vC2V5QV2v6V4v2Q4V2',
273
+ :Magic, # w
274
+ :MajorLinkerVersion, :MinorLinkerVersion, # 2b
275
+ :SizeOfCode, :SizeOfInitializedData, :SizeOfUninitializedData, :AddressOfEntryPoint, :BaseOfCode, # 5dw
276
+ :ImageBase, # qw
277
+ :SectionAlignment, :FileAlignment, # 2dw
278
+ :MajorOperatingSystemVersion, :MinorOperatingSystemVersion, # 6w
279
+ :MajorImageVersion, :MinorImageVersion, :MajorSubsystemVersion, :MinorSubsystemVersion,
280
+ :Reserved1, :SizeOfImage, :SizeOfHeaders, :CheckSum, # 4dw
281
+ :Subsystem, :DllCharacteristics, # 2w
282
+ :SizeOfStackReserve, :SizeOfStackCommit, :SizeOfHeapReserve, :SizeOfHeapCommit, # 4qw
283
+ :LoaderFlags, :NumberOfRvaAndSizes, #2dw
284
+ :DataDirectory # readed manually
285
+ )
286
+ USUAL_SIZE = 240
287
+ include IMAGE_OPTIONAL_HEADER
288
+ end
289
+
290
+ IMAGE_DATA_DIRECTORY = create_struct( "VV", :va, :size, :type )
291
+ IMAGE_DATA_DIRECTORY::TYPES =
292
+ %w'EXPORT IMPORT RESOURCE EXCEPTION SECURITY BASERELOC DEBUG ARCHITECTURE GLOBALPTR TLS LOAD_CONFIG
293
+ Bound_IAT IAT Delay_IAT CLR_Header'
294
+ IMAGE_DATA_DIRECTORY::TYPES.each_with_index do |type,idx|
295
+ IMAGE_DATA_DIRECTORY.const_set(type,idx)
296
+ end
297
+
298
+ IMAGE_SECTION_HEADER = create_struct( 'A8V6v2V',
299
+ :Name, # A8 6dw
300
+ :VirtualSize, :VirtualAddress, :SizeOfRawData, :PointerToRawData, :PointerToRelocations, :PointerToLinenumbers,
301
+ :NumberOfRelocations, :NumberOfLinenumbers, # 2w
302
+ :Characteristics # dw
303
+ )
304
+ class IMAGE_SECTION_HEADER
305
+ alias :flags :Characteristics
306
+ def flags_desc
307
+ r = ''
308
+ f = self.flags.to_i
309
+ r << (f & 0x4000_0000 > 0 ? 'R' : '-')
310
+ r << (f & 0x8000_0000 > 0 ? 'W' : '-')
311
+ r << (f & 0x2000_0000 > 0 ? 'X' : '-')
312
+ r << ' CODE' if f & 0x20 > 0
313
+
314
+ # section contains initialized data. Almost all sections except executable and the .bss section have this flag set
315
+ r << ' IDATA' if f & 0x40 > 0
316
+
317
+ # section contains uninitialized data (for example, the .bss section)
318
+ r << ' UDATA' if f & 0x80 > 0
319
+
320
+ r << ' DISCARDABLE' if f & 0x02000000 > 0
321
+ r << ' SHARED' if f & 0x10000000 > 0
322
+ r
323
+ end
324
+ end
325
+
326
+ # http://msdn.microsoft.com/en-us/library/windows/desktop/ms680339(v=VS.85).aspx
327
+ IMAGE_SUBSYSTEMS = %w'UNKNOWN NATIVE WINDOWS_GUI WINDOWS_CUI' + [nil,'OS2_CUI',nil,'POSIX_CUI',nil] +
328
+ %w'WINDOWS_CE_GUI EFI_APPLICATION EFI_BOOT_SERVICE_DRIVER EFI_RUNTIME_DRIVER EFI_ROM XBOX' +
329
+ [nil, 'WINDOWS_BOOT_APPLICATION']
330
+
331
+ # http://ntcore.com/files/richsign.htm
332
+ class RichHdr < String
333
+ attr_accessor :offset, :key # xor key
334
+
335
+ class Entry < Struct.new(:version,:id,:times)
336
+ def inspect
337
+ "<id=#{id}, version=#{version}, times=#{times}>"
338
+ end
339
+ end
340
+
341
+ def self.from_dos_stub stub
342
+ key = stub[stub.index('Rich')+4,4]
343
+ start_idx = stub.index(key.xor('DanS'))
344
+ end_idx = stub.index('Rich')+8
345
+ if stub[end_idx..-1].tr("\x00",'') != ''
346
+ t = stub[end_idx..-1]
347
+ t = "#{t[0,0x100]}..." if t.size > 0x100
348
+ PEdump.logger.error "[!] non-zero dos stub after rich_hdr: #{t.inspect}"
349
+ return nil
350
+ end
351
+ RichHdr.new(stub[start_idx, end_idx-start_idx]).tap do |x|
352
+ x.key = key
353
+ x.offset = stub.offset + start_idx
354
+ end
355
+ end
356
+
357
+ def dexor
358
+ self[4..-9].sub(/\A(#{Regexp::escape(key)}){3}/,'').xor(key)
359
+ end
360
+
361
+ def decode
362
+ x = dexor
363
+ if x.size%8 == 0
364
+ x.unpack('vvV'*(x.size/8)).each_slice(3).map{ |slice| Entry.new(*slice)}
365
+ else
366
+ PEdump.logger.error "[?] #{self.class}: dexored size(#{x.size}) must be a multiple of 8"
367
+ nil
368
+ end
369
+ end
370
+ end
371
+
372
+ class DOSStub < String
373
+ attr_accessor :offset
374
+ end
375
+
376
+ def logger= l
377
+ @logger = @@logger = l
378
+ end
379
+
380
+ def self.dump fname
381
+ new(fname).dump
382
+ end
383
+
384
+ def mz f=nil
385
+ @mz ||= f && MZ.read(f).tap do |mz|
386
+ if mz.signature != 'MZ' && mz.signature != 'ZM'
387
+ if @force
388
+ logger.warn "[?] no MZ signature. want: 'MZ' or 'ZM', got: #{mz.signature.inspect}"
389
+ else
390
+ logger.error "[!] no MZ signature. want: 'MZ' or 'ZM', got: #{mz.signature.inspect}. (not forced)"
391
+ return nil
392
+ end
393
+ end
394
+ end
395
+ end
396
+
397
+ def dos_stub f=nil
398
+ @dos_stub ||=
399
+ begin
400
+ return nil unless mz = mz(f)
401
+ dos_stub_offset = mz.header_paragraphs.to_i * 0x10
402
+ dos_stub_size = mz.lfanew.to_i - dos_stub_offset
403
+ if dos_stub_offset <= 0
404
+ logger.warn "[?] invalid DOS stub offset #{dos_stub_offset}"
405
+ nil
406
+ elsif f && dos_stub_offset > f.size
407
+ logger.warn "[?] DOS stub offset beyond EOF: #{dos_stub_offset}"
408
+ nil
409
+ elsif dos_stub_size < 0
410
+ logger.warn "[?] invalid DOS stub size #{dos_stub_size}"
411
+ nil
412
+ elsif dos_stub_size == 0
413
+ # no DOS stub, it's ok
414
+ nil
415
+ elsif !f
416
+ # no open file, it's ok
417
+ nil
418
+ else
419
+ if dos_stub_size > 0x1000
420
+ logger.warn "[?] DOS stub size too big (#{dos_stub_size}), limiting to 0x1000"
421
+ dos_stub_size = 0x1000
422
+ end
423
+ f.seek dos_stub_offset
424
+ DOSStub.new(f.read(dos_stub_size)).tap do |dos_stub|
425
+ dos_stub.offset = dos_stub_offset
426
+ if dos_stub['Rich']
427
+ if @rich_hdr = RichHdr.from_dos_stub(dos_stub)
428
+ dos_stub[dos_stub.index(@rich_hdr)..-1] = ''
429
+ end
430
+ end
431
+ end
432
+ end
433
+ end
434
+ end
435
+
436
+ def rich_hdr f=nil
437
+ dos_stub(f) && @rich_hdr
438
+ end
439
+ alias :rich_header :rich_hdr
440
+ alias :rich :rich_hdr
441
+
442
+ def pe f=nil
443
+ @pe ||=
444
+ begin
445
+ pe_offset = mz(f) && mz(f).lfanew
446
+ if pe_offset.nil?
447
+ logger.fatal "[!] NULL PE offset (e_lfanew). cannot continue."
448
+ nil
449
+ elsif pe_offset > f.size
450
+ logger.fatal "[!] PE offset beyond EOF. cannot continue."
451
+ nil
452
+ else
453
+ f.seek pe_offset
454
+ pe_sig = f.read 4
455
+ logger.error "[!] 'NE' format is not supported!" if pe_sig == "NE\x00\x00"
456
+ if pe_sig != "PE\x00\x00"
457
+ if @force
458
+ logger.warn "[?] no PE signature (want: 'PE\\x00\\x00', got: #{pe_sig.inspect})"
459
+ else
460
+ logger.error "[?] no PE signature (want: 'PE\\x00\\x00', got: #{pe_sig.inspect}). (not forced)"
461
+ return nil
462
+ end
463
+ end
464
+ PE.new(pe_sig).tap do |pe|
465
+ pe.image_file_header = IMAGE_FILE_HEADER.read(f)
466
+ if pe.ifh.SizeOfOptionalHeader > 0
467
+ if pe.x64?
468
+ pe.image_optional_header = IMAGE_OPTIONAL_HEADER64.read(f, pe.ifh.SizeOfOptionalHeader)
469
+ else
470
+ pe.image_optional_header = IMAGE_OPTIONAL_HEADER32.read(f, pe.ifh.SizeOfOptionalHeader)
471
+ end
472
+ end
473
+
474
+ if (nToRead=pe.ifh.NumberOfSections) > 32
475
+ if @force.is_a?(Numeric) && @force > 1
476
+ logger.warn "[!] too many sections (#{pe.ifh.NumberOfSections}). forced. reading all"
477
+ else
478
+ logger.warn "[!] too many sections (#{pe.ifh.NumberOfSections}). not forced, reading first 32"
479
+ nToRead = 32
480
+ end
481
+ end
482
+ pe.section_table = nToRead.times.map do
483
+ IMAGE_SECTION_HEADER.read(f)
484
+ end
485
+ end
486
+ end
487
+ end
488
+ end
489
+
490
+ def resource_directory f=nil
491
+ @resource_directory ||= _read_resource_directory_tree(f)
492
+ end
493
+
494
+ # OPTIONAL: assigns @mz, @rich_hdr, @pe, etc
495
+ def dump f=nil
496
+ f ? _dump_handle(f) : File.open(@fname,'rb'){ |f| _dump_handle(f) }
497
+ self
498
+ end
499
+
500
+ def _dump_handle h
501
+ rich_hdr(h) # includes mz(h)
502
+ resources(h) # includes pe(h)
503
+ imports h
504
+ exports h
505
+ packer h
506
+ end
507
+
508
+ def data_directory f=nil
509
+ pe(f) && pe.ioh && pe.ioh.DataDirectory
510
+ end
511
+
512
+ def sections f=nil
513
+ pe(f) && pe.section_table
514
+ end
515
+ alias :section_table :sections
516
+
517
+ ##############################################################################
518
+ # imports
519
+ ##############################################################################
520
+
521
+ # http://sandsprite.com/CodeStuff/Understanding_imports.html
522
+ # http://stackoverflow.com/questions/5631317/import-table-it-vs-import-address-table-iat
523
+ IMAGE_IMPORT_DESCRIPTOR = create_struct 'V5',
524
+ :OriginalFirstThunk,
525
+ :TimeDateStamp,
526
+ :ForwarderChain,
527
+ :Name,
528
+ :FirstThunk,
529
+ # manual:
530
+ :module_name,
531
+ :original_first_thunk,
532
+ :first_thunk
533
+
534
+ ImportedFunction = Struct.new(:hint, :name, :ordinal)
535
+
536
+ def imports f=nil
537
+ return @imports if @imports
538
+ return nil unless pe(f) && pe(f).ioh && f
539
+ dir = @pe.ioh.DataDirectory[IMAGE_DATA_DIRECTORY::IMPORT]
540
+ return [] if !dir || (dir.va == 0 && dir.size == 0)
541
+ va = @pe.ioh.DataDirectory[IMAGE_DATA_DIRECTORY::IMPORT].va
542
+ file_offset = va2file(va)
543
+ return nil unless file_offset
544
+ f.seek file_offset
545
+ r = []
546
+ until (t=IMAGE_IMPORT_DESCRIPTOR.read(f)).empty?
547
+ r << t
548
+ end
549
+ @imports = r.each do |x|
550
+ if x.Name.to_i != 0 && (va = va2file(x.Name))
551
+ f.seek va
552
+ x.module_name = f.gets("\x00").chop
553
+ end
554
+ [:original_first_thunk, :first_thunk].each do |tbl|
555
+ camel = tbl.capitalize.to_s.gsub(/_./){ |char| char[1..-1].upcase}
556
+ if x[camel].to_i != 0 && (va = va2file(x[camel]))
557
+ f.seek va
558
+ x[tbl] ||= []
559
+ if pe.x64?
560
+ x[tbl] << t while (t = f.read(8).unpack('Q').first) != 0
561
+ else
562
+ x[tbl] << t while (t = f.read(4).unpack('V').first) != 0
563
+ end
564
+ end
565
+ cache = {}
566
+ bits = pe.x64? ? 64 : 32
567
+ x[tbl] && x[tbl].map! do |t|
568
+ cache[t] ||=
569
+ if t & (2**(bits-1)) > 0 # 0x8000_0000(_0000_0000)
570
+ ImportedFunction.new(nil,nil,t & (2**(bits-1)-1)) # 0x7fff_ffff(_ffff_ffff)
571
+ elsif va=va2file(t)
572
+ f.seek va
573
+ ImportedFunction.new(f.read(2).unpack('v').first, f.gets("\x00").chop)
574
+ else
575
+ nil
576
+ end
577
+ end
578
+ x[tbl] && x[tbl].compact!
579
+ end
580
+ if x.original_first_thunk && !x.first_thunk
581
+ logger.warn "[?] import table: empty FirstThunk of #{x.module_name}"
582
+ elsif !x.original_first_thunk && x.first_thunk
583
+ logger.warn "[?] import table: empty OriginalFirstThunk of #{x.module_name}"
584
+ elsif x.original_first_thunk != x.first_thunk
585
+ logger.warn "[?] import table: OriginalFirstThunk != FirstThunk of #{x.module_name}"
586
+ end
587
+ end
588
+ end
589
+
590
+ ##############################################################################
591
+ # exports
592
+ ##############################################################################
593
+
594
+ #http://msdn.microsoft.com/en-us/library/ms809762.aspx
595
+ IMAGE_EXPORT_DIRECTORY = create_struct 'V2v2V7',
596
+ :Characteristics,
597
+ :TimeDateStamp,
598
+ :MajorVersion, # These fields appear to be unused and are set to 0.
599
+ :MinorVersion, # These fields appear to be unused and are set to 0.
600
+ :Name,
601
+ :Base, # The starting ordinal number for exported functions
602
+ :NumberOfFunctions,
603
+ :NumberOfNames,
604
+ :AddressOfFunctions,
605
+ :AddressOfNames,
606
+ :AddressOfNameOrdinals,
607
+ # manual:
608
+ :name, :entry_points, :names, :name_ordinals
609
+
610
+ def exports f=nil
611
+ return @exports if @exports
612
+ return nil unless pe(f) && pe(f).ioh && f
613
+ dir = @pe.ioh.DataDirectory[IMAGE_DATA_DIRECTORY::EXPORT]
614
+ return [] if !dir || (dir.va == 0 && dir.size == 0)
615
+ va = @pe.ioh.DataDirectory[IMAGE_DATA_DIRECTORY::EXPORT].va
616
+ file_offset = va2file(va)
617
+ return nil unless file_offset
618
+ f.seek file_offset
619
+ @exports = IMAGE_EXPORT_DIRECTORY.read(f).tap do |x|
620
+ x.entry_points = []
621
+ x.name_ordinals = []
622
+ x.names = []
623
+ if x.Name.to_i != 0 && (va = va2file(x.Name))
624
+ f.seek va
625
+ x.name = f.gets("\x00").chop
626
+ end
627
+ if x.NumberOfFunctions.to_i != 0
628
+ if x.AddressOfFunctions.to_i !=0 && (va = va2file(x.AddressOfFunctions))
629
+ f.seek va
630
+ x.entry_points = f.read(x.NumberOfFunctions*4).unpack('V*')
631
+ end
632
+ if x.AddressOfNameOrdinals.to_i !=0 && (va = va2file(x.AddressOfNameOrdinals))
633
+ f.seek va
634
+ x.name_ordinals = f.read(x.NumberOfNames*2).unpack('v*').map{ |o| o+x.Base }
635
+ end
636
+ end
637
+ if x.NumberOfNames.to_i != 0 && x.AddressOfNames.to_i !=0 && (va = va2file(x.AddressOfNames))
638
+ f.seek va
639
+ x.names = f.read(x.NumberOfNames*4).unpack('V*').map do |va|
640
+ f.seek va2file(va)
641
+ f.gets("\x00").chop
642
+ end
643
+ end
644
+ end
645
+ end
646
+
647
+ ##############################################################################
648
+ # resources
649
+ ##############################################################################
650
+
651
+ IMAGE_RESOURCE_DIRECTORY = create_struct 'V2v4',
652
+ :Characteristics, :TimeDateStamp, # 2dw
653
+ :MajorVersion, :MinorVersion, :NumberOfNamedEntries, :NumberOfIdEntries, # 4w
654
+ :entries # manual
655
+ class IMAGE_RESOURCE_DIRECTORY
656
+ class << self
657
+ attr_accessor :base
658
+ alias :read_without_children :read
659
+ def read f, root=true
660
+ if root
661
+ @@loopchk1 = Hash.new(0)
662
+ @@loopchk2 = Hash.new(0)
663
+ @@loopchk3 = Hash.new(0)
664
+ elsif (@@loopchk1[f.tell] += 1) > 1
665
+ PEdump.logger.error "[!] #{self}: loop1 detected at file pos #{f.tell}" if @@loopchk1[f.tell] < 2
666
+ return nil
667
+ end
668
+ read_without_children(f).tap do |r|
669
+ nToRead = r.NumberOfNamedEntries.to_i + r.NumberOfIdEntries.to_i
670
+ r.entries = []
671
+ nToRead.times do |i|
672
+ if f.eof?
673
+ PEdump.logger.error "[!] #{self}: #{nToRead} entries in directory, but got EOF on #{i}-th."
674
+ break
675
+ end
676
+ if (@@loopchk2[f.tell] += 1) > 1
677
+ PEdump.logger.error "[!] #{self}: loop2 detected at file pos #{f.tell}" if @@loopchk2[f.tell] < 2
678
+ next
679
+ end
680
+ r.entries << IMAGE_RESOURCE_DIRECTORY_ENTRY.read(f)
681
+ end
682
+ #r.entries.uniq!
683
+ r.entries.each do |entry|
684
+ entry.name =
685
+ if entry.Name.to_i & 0x8000_0000 > 0
686
+ # Name is an address of unicode string
687
+ f.seek base + entry.Name & 0x7fff_ffff
688
+ nChars = f.read(2).to_s.unpack("v").first.to_i
689
+ begin
690
+ f.read(nChars*2).force_encoding('UTF-16LE').encode!('UTF-8')
691
+ rescue
692
+ PEdump.logger.error "[!] #{self} failed to read entry name: #{$!}"
693
+ "???"
694
+ end
695
+ else
696
+ # Name is a numeric id
697
+ "##{entry.Name}"
698
+ end
699
+ if entry.OffsetToData && f.checked_seek(base + entry.OffsetToData & 0x7fff_ffff)
700
+ if (@@loopchk3[f.tell] += 1) > 1
701
+ PEdump.logger.error "[!] #{self}: loop3 detected at file pos #{f.tell}" if @@loopchk3[f.tell] < 2
702
+ next
703
+ end
704
+ entry.data =
705
+ if entry.OffsetToData & 0x8000_0000 > 0
706
+ # child is a directory
707
+ IMAGE_RESOURCE_DIRECTORY.read(f,false)
708
+ else
709
+ # child is a resource
710
+ IMAGE_RESOURCE_DATA_ENTRY.read(f)
711
+ end
712
+ end
713
+ end
714
+ @@loopchk1 = @@loopchk2 = @@loopchk3 = nil if root # save some memory
715
+ end
716
+ end
717
+ end
718
+ end
719
+
720
+ IMAGE_RESOURCE_DIRECTORY_ENTRY = create_struct 'V2',
721
+ :Name, :OffsetToData,
722
+ :name, :data
723
+
724
+ IMAGE_RESOURCE_DATA_ENTRY = create_struct 'V4',
725
+ :OffsetToData, :Size, :CodePage, :Reserved
726
+
727
+ def va2file va
728
+ sections.each do |s|
729
+ if (s.VirtualAddress...(s.VirtualAddress+s.VirtualSize)).include?(va)
730
+ return va - s.VirtualAddress + s.PointerToRawData
731
+ end
732
+ end
733
+ # not found with regular search. assume any of VirtualSize was 0, and try with RawSize
734
+ sections.each do |s|
735
+ if (s.VirtualAddress...(s.VirtualAddress+s.SizeOfRawData)).include?(va)
736
+ return va - s.VirtualAddress + s.PointerToRawData
737
+ end
738
+ end
739
+ logger.error "[?] can't find file_offset of VA 0x#{va.to_i.to_s(16)}"
740
+ nil
741
+ end
742
+
743
+ def _read_resource_directory_tree f
744
+ return nil unless pe(f) && pe(f).ioh && f
745
+ res_dir = @pe.ioh.DataDirectory[IMAGE_DATA_DIRECTORY::RESOURCE]
746
+ return [] if !res_dir || (res_dir.va == 0 && res_dir.size == 0)
747
+ res_va = @pe.ioh.DataDirectory[IMAGE_DATA_DIRECTORY::RESOURCE].va
748
+ res_section = @pe.section_table.find{ |t| t.VirtualAddress == res_va }
749
+ unless res_section
750
+ logger.warn "[?] can't find resource section for va=0x#{res_va.to_s(16)}"
751
+ return []
752
+ end
753
+ f.seek res_section.PointerToRawData
754
+ IMAGE_RESOURCE_DIRECTORY.base = res_section.PointerToRawData
755
+ #@resource_data_base = res_section.PointerToRawData - res_section.VirtualAddress
756
+ IMAGE_RESOURCE_DIRECTORY.read(f)
757
+ end
758
+
759
+ class Resource < Struct.new(:type, :name, :id, :lang, :file_offset, :size, :cp, :reserved, :data, :valid)
760
+ def bitmap_hdr
761
+ bmp_info_hdr = data.find{ |x| x.is_a?(BITMAPINFOHEADER) }
762
+ raise "no BITMAPINFOHEADER for #{self.type} #{self.name}" unless bmp_info_hdr
763
+
764
+ bmp_info_hdr.biHeight/=2 if %w'ICON CURSOR'.include?(type)
765
+
766
+ colors_used = bmp_info_hdr.biClrUsed
767
+ colors_used = 2**bmp_info_hdr.biBitCount if colors_used == 0 && bmp_info_hdr.biBitCount < 16
768
+
769
+ # XXX: one byte in each color is unused!
770
+ @palette_size = colors_used * 4 # each color takes 4 bytes
771
+
772
+ # scanlines are DWORD-aligned and padded to DWORD-align with zeroes
773
+ # XXX: some data may be hidden in padding bytes!
774
+ scanline_size = bmp_info_hdr.biWidth * bmp_info_hdr.biBitCount / 8
775
+ scanline_size += (4-scanline_size%4) if scanline_size % 4 > 0
776
+
777
+ @imgdata_size = scanline_size * bmp_info_hdr.biHeight
778
+ "BM" + [
779
+ BITMAPINFOHEADER::SIZE + 14 + @palette_size + @imgdata_size,
780
+ 0,
781
+ BITMAPINFOHEADER::SIZE + 14 + @palette_size
782
+ ].pack("V3") + bmp_info_hdr.pack
783
+ ensure
784
+ bmp_info_hdr.biHeight*=2 if %w'ICON CURSOR'.include?(type)
785
+ end
786
+
787
+ # only valid for types BITMAP, ICON & CURSOR
788
+ def restore_bitmap src_fname
789
+ File.open(src_fname, "rb") do |f|
790
+ parse f
791
+ if data.first == "PNG"
792
+ "\x89PNG" +f.read(self.size-4)
793
+ else
794
+ bitmap_hdr + f.read(@palette_size + @imgdata_size)
795
+ end
796
+ end
797
+ end
798
+
799
+ def bitmap_mask src_fname
800
+ File.open(src_fname, "rb") do |f|
801
+ parse f
802
+ bmp_info_hdr = bitmap_hdr
803
+ bitmap_size = BITMAPINFOHEADER::SIZE + @palette_size + @imgdata_size
804
+ return nil if bitmap_size >= self.size
805
+
806
+ mask_size = self.size - bitmap_size
807
+ f.seek file_offset + bitmap_size
808
+
809
+ bmp_info_hdr = BITMAPINFOHEADER.new(*bmp_info_hdr[14..-1].unpack(BITMAPINFOHEADER::FORMAT))
810
+ bmp_info_hdr.biBitCount = 1
811
+ bmp_info_hdr.biCompression = bmp_info_hdr.biSizeImage = 0
812
+ bmp_info_hdr.biClrUsed = bmp_info_hdr.biClrImportant = 2
813
+
814
+ palette = [0,0xffffff].pack('V2')
815
+ @palette_size = palette.size
816
+
817
+ "BM" + [
818
+ BITMAPINFOHEADER::SIZE + 14 + @palette_size + mask_size,
819
+ 0,
820
+ BITMAPINFOHEADER::SIZE + 14 + @palette_size
821
+ ].pack("V3") + bmp_info_hdr.pack + palette + f.read(mask_size)
822
+ end
823
+ end
824
+
825
+ # also sets the file position for restore_bitmap next call
826
+ def parse f
827
+ raise "called parse with type not set" unless self.type
828
+ #return if self.data
829
+
830
+ self.data = []
831
+ case type
832
+ when 'BITMAP','ICON'
833
+ f.seek file_offset
834
+ if f.read(4) == "\x89PNG"
835
+ data << 'PNG'
836
+ else
837
+ f.seek file_offset
838
+ data << BITMAPINFOHEADER.read(f)
839
+ end
840
+ when 'CURSOR'
841
+ f.seek file_offset
842
+ data << CURSOR_HOTSPOT.read(f)
843
+ data << BITMAPINFOHEADER.read(f)
844
+ when 'GROUP_CURSOR'
845
+ f.seek file_offset
846
+ data << CUR_ICO_HEADER.read(f)
847
+ nRead = CUR_ICO_HEADER::SIZE
848
+ data.last.wNumImages.to_i.times do
849
+ if nRead >= self.size
850
+ PEdump.logger.error "[!] refusing to read CURDIRENTRY beyond resource size"
851
+ break
852
+ end
853
+ data << CURDIRENTRY.read(f)
854
+ nRead += CURDIRENTRY::SIZE
855
+ end
856
+ when 'GROUP_ICON'
857
+ f.seek file_offset
858
+ data << CUR_ICO_HEADER.read(f)
859
+ nRead = CUR_ICO_HEADER::SIZE
860
+ data.last.wNumImages.to_i.times do
861
+ if nRead >= self.size
862
+ PEdump.logger.error "[!] refusing to read ICODIRENTRY beyond resource size"
863
+ break
864
+ end
865
+ data << ICODIRENTRY.read(f)
866
+ nRead += ICODIRENTRY::SIZE
867
+ end
868
+ when 'STRING'
869
+ f.seek file_offset
870
+ 16.times do
871
+ break if f.tell >= file_offset+self.size
872
+ nChars = f.read(2).to_s.unpack('v').first.to_i
873
+ t =
874
+ if nChars*2 + 1 > self.size
875
+ # TODO: if it's not 1st string in table then truncated size must be less
876
+ PEdump.logger.error "[!] string size(#{nChars*2}) > stringtable size(#{self.size}). truncated to #{self.size-2}"
877
+ f.read(self.size-2)
878
+ else
879
+ f.read(nChars*2)
880
+ end
881
+ data <<
882
+ begin
883
+ t.force_encoding('UTF-16LE').encode!('UTF-8')
884
+ rescue
885
+ t.force_encoding('ASCII')
886
+ tt = t.size > 0x10 ? t[0,0x10].inspect+'...' : t.inspect
887
+ PEdump.logger.error "[!] cannot convert #{tt} to UTF-16"
888
+ [nChars,t].pack('va*')
889
+ end
890
+ end
891
+ # XXX: check if readed strings summary length is less than resource data length
892
+ when 'VERSION'
893
+ require 'pedump/version_info'
894
+ f.seek file_offset
895
+ data << PEdump::VS_VERSIONINFO.read(f)
896
+ end
897
+
898
+ data.delete_if do |x|
899
+ valid = !x.respond_to?(:valid?) || x.valid?
900
+ PEdump.logger.warn "[?] ignoring invalid #{x.class}" unless valid
901
+ !valid
902
+ end
903
+ ensure
904
+ validate
905
+ end
906
+
907
+ def validate
908
+ self.valid =
909
+ case type
910
+ when 'BITMAP','ICON','CURSOR'
911
+ data.any?{ |x| x.is_a?(BITMAPINFOHEADER) && x.valid? } || data.first == 'PNG'
912
+ else
913
+ true
914
+ end
915
+ end
916
+ end
917
+
918
+ STRING = Struct.new(:id, :lang, :value)
919
+
920
+ def strings f=nil
921
+ r = []
922
+ Array(resources(f)).find_all{ |x| x.type == 'STRING'}.each do |res|
923
+ res.data.each_with_index do |string,idx|
924
+ r << STRING.new( ((res.id-1)<<4) + idx, res.lang, string ) unless string.empty?
925
+ end
926
+ end
927
+ r
928
+ end
929
+
930
+ # see also http://www.informit.com/articles/article.aspx?p=1186882 about icons format
931
+
932
+ class BITMAPINFOHEADER < create_struct 'V3v2V6',
933
+ :biSize, # BITMAPINFOHEADER::SIZE
934
+ :biWidth,
935
+ :biHeight,
936
+ :biPlanes,
937
+ :biBitCount,
938
+ :biCompression,
939
+ :biSizeImage,
940
+ :biXPelsPerMeter,
941
+ :biYPelsPerMeter,
942
+ :biClrUsed,
943
+ :biClrImportant
944
+
945
+ def valid?
946
+ self.biSize == 40
947
+ end
948
+ end
949
+
950
+ # http://www.devsource.com/c/a/Architecture/Resources-From-PE-I/2/
951
+ CUR_ICO_HEADER = create_struct('v3',
952
+ :wReserved, # always 0
953
+ :wResID, # always 2
954
+ :wNumImages # Number of cursor images/directory entries
955
+ )
956
+
957
+ CURDIRENTRY = create_struct 'v4Vv',
958
+ :wWidth,
959
+ :wHeight, # Divide by 2 to get the actual height.
960
+ :wPlanes,
961
+ :wBitCount,
962
+ :dwBytesInImage,
963
+ :wID
964
+
965
+ CURSOR_HOTSPOT = create_struct 'v2', :x, :y
966
+
967
+ ICODIRENTRY = create_struct 'C4v2Vv',
968
+ :bWidth,
969
+ :bHeight,
970
+ :bColors,
971
+ :bReserved,
972
+ :wPlanes,
973
+ :wBitCount,
974
+ :dwBytesInImage,
975
+ :wID
976
+
977
+ ROOT_RES_NAMES = [nil] + # numeration is started from 1
978
+ %w'CURSOR BITMAP ICON MENU DIALOG STRING FONTDIR FONT ACCELERATORS RCDATA' +
979
+ %w'MESSAGETABLE GROUP_CURSOR' + [nil] + %w'GROUP_ICON' + [nil] +
980
+ %w'VERSION DLGINCLUDE' + [nil] + %w'PLUGPLAY VXD ANICURSOR ANIICON HTML MANIFEST'
981
+
982
+ def resources f=nil
983
+ @resources ||= _scan_resources(f)
984
+ end
985
+
986
+ def version_info f=nil
987
+ resources(f) && resources(f).find_all{ |res| res.type == 'VERSION' }.map(&:data).flatten
988
+ end
989
+
990
+ def _scan_resources f=nil, dir=nil
991
+ dir ||= resource_directory(f)
992
+ return nil unless dir
993
+ dir.entries.map do |entry|
994
+ case entry.data
995
+ when IMAGE_RESOURCE_DIRECTORY
996
+ if dir == @resource_directory # root resource directory
997
+ entry_type =
998
+ if entry.Name & 0x8000_0000 == 0
999
+ # root resource directory & entry name is a number
1000
+ ROOT_RES_NAMES[entry.Name] || entry.name
1001
+ else
1002
+ entry.name
1003
+ end
1004
+ _scan_resources(f,entry.data).each do |res|
1005
+ res.type = entry_type
1006
+ res.parse f
1007
+ end
1008
+ else
1009
+ _scan_resources(f,entry.data).each do |res|
1010
+ res.name = res.name == "##{res.lang}" ? entry.name : "#{entry.name} / #{res.name}"
1011
+ res.id ||= entry.Name if entry.Name.is_a?(Numeric) && entry.Name < 0x8000_0000
1012
+ end
1013
+ end
1014
+ when IMAGE_RESOURCE_DATA_ENTRY
1015
+ Resource.new(
1016
+ nil, # type
1017
+ entry.name,
1018
+ nil, # id
1019
+ entry.Name, # lang
1020
+ #entry.data.OffsetToData + @resource_data_base,
1021
+ va2file(entry.data.OffsetToData),
1022
+ entry.data.Size,
1023
+ entry.data.CodePage,
1024
+ entry.data.Reserved
1025
+ )
1026
+ else
1027
+ logger.error "[!] invalid resource entry: #{entry.data.inspect}"
1028
+ nil
1029
+ end
1030
+ end.flatten.compact
1031
+ end
1032
+
1033
+ def packer f = nil
1034
+ @packer ||= pe(f) && @pe.ioh &&
1035
+ begin
1036
+ if !(va=@pe.ioh.AddressOfEntryPoint)
1037
+ logger.error "[?] can't find EntryPoint RVA"
1038
+ nil
1039
+ elsif va == 0 && @pe.dll?
1040
+ logger.debug "[.] it's a DLL with no EntryPoint"
1041
+ nil
1042
+ elsif !(ofs = va2file(va))
1043
+ logger.error "[?] can't find EntryPoint RVA (0x#{va.to_s(16)}) file offset"
1044
+ nil
1045
+ else
1046
+ require 'pedump/packer'
1047
+ if PEdump::Packer.all.size == 0
1048
+ logger.error "[?] no packer definitions found"
1049
+ nil
1050
+ else
1051
+ Packer.of f, :ep_offset => ofs
1052
+ end
1053
+ end
1054
+ end
1055
+ end
1056
+ alias :packers :packer
1057
+ end
1058
+
1059
+ ####################################################################################
1060
+
1061
+ if $0 == __FILE__
1062
+ require 'pp'
1063
+ dump = PEdump.new(ARGV.shift).dump
1064
+ if ARGV.any?
1065
+ ARGV.each do |arg|
1066
+ if dump.respond_to?(arg)
1067
+ pp dump.send(arg)
1068
+ elsif arg == 'restore_bitmaps'
1069
+ File.open(dump.fname,"rb") do |fi|
1070
+ r = dump.resources.
1071
+ find_all{ |r| %w'ICON BITMAP CURSOR'.include?(r.type) }.
1072
+ each do |r|
1073
+ fname = r.name.tr("/# ",'_')+".bmp"
1074
+ puts "[.] #{fname}"
1075
+ File.open(fname,"wb"){ |fo| fo << r.restore_bitmap(fi) }
1076
+ if mask = r.bitmap_mask(fi)
1077
+ fname.sub! '.bmp', '.mask.bmp'
1078
+ puts "[.] #{fname}"
1079
+ File.open(fname,"wb"){ |fo| fo << r.bitmap_mask(fi) }
1080
+ end
1081
+ end
1082
+ end
1083
+ exit
1084
+ else
1085
+ puts "[?] invalid arg #{arg.inspect}"
1086
+ end
1087
+ end
1088
+ exit
1089
+ end
1090
+ p dump.mz
1091
+ require './lib/hexdump_helper' if File.exist?("lib/hexdump_helper.rb")
1092
+ if defined?(HexdumpHelper)
1093
+ include HexdumpHelper
1094
+ puts hexdump(dump.dos_stub) if dump.dos_stub
1095
+ puts
1096
+ if dump.rich_hdr
1097
+ puts hexdump(dump.rich_hdr)
1098
+ puts
1099
+ p(dump.rich_hdr.decode)
1100
+ puts hexdump(dump.rich_hdr.dexor)
1101
+ end
1102
+ end
1103
+ pp dump.pe
1104
+ pp dump.resources
1105
+ end