pedump 0.3.3

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