pedump 0.3.3

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,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