pedump 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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